使用 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>
);
}
}