使用 ReactJS 實現一個簡易的輪播圖 (carousel) 組件。
carousel-stage-2.gif
Task 1:在相框中展示圖片,左右按鈕切換當前圖片
實現思路;把圖片橫向排列成組(image row),放置在相框(frame)中,隱藏超出相框的部分。利用圖片組左側和相框左側的距離(margin-left)改變當前展示在相框中的內容,點擊左右按鈕可以改變這個距離。
// How to make use of this component
<Carousel width={400} height={400}>
{images.map(image => <img src={image} alt="" key={image}/>)}
</Carousel>
// Carousel component
import React, { Component } from 'react';
export default class Carousel extends Component {
constructor(props) {
super(props);
this.state = { currentIndex: 0 };
this.renderChildren = this.renderChildren.bind(this);
this.setIndex = this.setIndex.bind(this);
}
renderChildren() {
const { children, width, height } = this.props;
const childStyle = {
width: width,
height: height
};
return React.Children.map(children, child => {
const childClone = React.cloneElement(child, { style: childStyle });
return (
<div
style={{
display: 'inline-block'
}}
>
{childClone}
</div>
);
});
}
setIndex(index) {
const len = this.props.children.length;
const nextIndex = (index + len) % len;
this.setState({ currentIndex: nextIndex });
}
render() {
const { width, height } = this.props;
const { currentIndex } = this.state;
const offset = -currentIndex * width;
const frameStyle = {
width: width,
height: height,
whiteSpace: 'nowrap',
overflow: 'hidden',
position: 'relative'
};
const imageRowStyle = {
marginLeft: offset,
transition: '.2s'
};
const buttonStyle = {
position: 'absolute',
top: '40%',
bottom: '40%',
width: '10%',
background: 'rgba(0,0,0,0.2)',
outline: 'none',
border: 'none'
};
const leftButtonStyle = {
...buttonStyle,
left: 0
};
const rightButtonStyle = {
...buttonStyle,
right: 0
};
return (
<div className="carousel">
<div className="frame" style={frameStyle}>
<button
onClick={() => this.setIndex(currentIndex - 1)}
style={leftButtonStyle}
>
<
</button>
<div style={imageRowStyle}>{this.renderChildren()}</div>
<button
onClick={() => this.setIndex(currentIndex + 1)}
style={rightButtonStyle}
>
>
</button>
</div>
</div>
);
}
}
實現思路
-
如何顯示block-level的div在同一行
Solution 1:
For parent element
white space: nowrap
For children elements
display: inline block
Solution 2:
對同一行的div設置float屬性
float:left
-
隱藏超出相框的圖片部分
overflow: hidden
-
通過圖片組和相框左側的距離(margin-left)控制顯示在相框中的內容,設置動畫時間為0.2秒。
const offset = -(currentIndex * width) const imageRowStyle = { marginLeft: offset, transition: '.2s' };
-
通過按鈕來控制margin-left, 改變當前相框中內容
setIndex(index) { const len = this.props.children.length; const nextIndex = (index + len) % len; this.setState({ currentIndex: nextIndex }); } ... // in render function const {width, height} = this.props; const {currrentIndex} = this.state; const offset = - currentIndex * wid th
-
放置按鈕在相框中,設置合適大小,背景半透明,取消邊框顯示。
const buttonStyle = { position: 'absolute', top: '40%', bottom: '40%', width: '10%', background: 'rgba(0,0,0,0.2)', outline: 'none', border: 'none' }; const leftButtonStyle = { ...buttonStyle, left: 0 }; const rightButtonStyle = { ...buttonStyle, right: 0 };
Task #2 平滑無限循環
在之前的步驟中,我們使用了transiion來實現滑動動畫。但當圖片從最后一幅去到第一幅或者反之的時候,相框中會經過所有的圖片,造成不連貫的體驗。為了解決這個問題,我們可以在一頭一尾多加上一副圖片,當carousel處于最后一幅圖片并點擊next按鈕的時候,首先把下一幅圖片移動到相框,在動畫結束的時候,再把相框中的圖片切換為第一幅圖片。
這里需要使用requestAnimationFrame,來保證當動畫結束的時候,立即改變component state。
import React, { Component } from 'react';
export default class Carouse extends Component {
constructor(props) {
super(props);
this.state = { currentIndex: 1, offset: -this.props.width };
this.renderChildren = this.renderChildren.bind(this);
this.setIndex = this.setIndex.bind(this);
}
renderChildren() {
const { children, width, height } = this.props;
const childStyle = {
width: width,
height: height
};
if (!children) {
return;
}
const appendedChildren = [
children[children.length - 1],
...children,
children[0]
];
return React.Children.map(appendedChildren, (child, index) => {
const childClone = React.cloneElement(child, { style: childStyle });
return (
<div
style={{
display: 'inline-block'
}}
key={index}
>
{childClone}
</div>
);
});
}
setIndex(index) {
let nextIndex = index;
const len = this.props.children.length;
const { width } = this.props;
this.setState({ currentIndex: nextIndex });
const currentOffset = this.state.offset;
const nextOffset = -nextIndex * width;
let start = null;
const move = timestamp => {
if (!start) {
start = timestamp;
}
const progress = timestamp - start;
this.setState({
offset: currentOffset + (nextOffset - currentOffset) * progress / 100
});
if (progress < 100) {
requestAnimationFrame(move);
} else {
if (nextIndex === 0) {
nextIndex = len;
} else if (nextIndex === len + 1) {
nextIndex = 1;
}
this.setState({ currentIndex: nextIndex, offset: -nextIndex * width });
}
};
requestAnimationFrame(move);
}
render() {
const { width, height } = this.props;
const { currentIndex, offset } = this.state;
const frameStyle = {
width: width,
height: height,
whiteSpace: 'nowrap',
overflow: 'hidden',
position: 'relative'
};
const imageRowStyle = {
marginLeft: offset
};
const buttonStyle = {
position: 'absolute',
top: '40%',
bottom: '40%',
width: '10%',
background: 'rgba(0,0,0,0.2)',
outline: 'none',
border: 'none'
};
const leftButtonStyle = {
...buttonStyle,
left: 0
};
const rightButtonStyle = {
...buttonStyle,
right: 0
};
return (
<div className="carousel">
<div className="frame" style={frameStyle}>
<button
onClick={() => this.setIndex(currentIndex - 1)}
style={leftButtonStyle}
>
<
</button>
<div style={imageRowStyle}>{this.renderChildren()}</div>
<button
onClick={() => this.setIndex(currentIndex + 1)}
style={rightButtonStyle}
>
>
</button>
</div>
</div>
);
}
}