使用 ReactJS 实现一个简易的轮播图 (carousel) 组件。
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, 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, height: height, whiteSpace: 'nowrap', overflow: 'hidden', position: 'relative' }; const imageRowStyle = { marginLeft: offset, transition: '.2s' }; const buttonStyle = { position: 'absolute', top: '40%', bottom: '40%', '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%', '10%', background: 'rgba(0,0,0,0.2)', outline: 'none', border: 'none' }; const leftButtonStyle = { ...buttonStyle, left: 0 }; const rightButtonStyle = { ...buttonStyle, right: 0 };
实现思路市通过改变
marginLeft,改变左侧间距,
marginLeft的使用肯定市需要浮动定位和固定定位的结合,还有就是溢出隐藏,
如果想这个过程中有动画效果,一个动火过度,就需要增加
transition: '.2s'
Task #2 平滑无限循环
在之前的步骤中,我们使用了transiion来实现滑动动画。但当图片从最后一幅去到第一幅或者反之的时候,相框中会经过所有的图片,造成不连贯的体验。为了解决这个问题,我们可以在一头一尾多加上一副图片,当carousel处于最后一幅图片并点击next按钮的时候,首先把下一幅图片移动到相框,在动画结束的时候,再把相框中的图片切换为第一幅图片。
这里需要使用requestAnimationFrame,来保证当动画结束的时候,立即改变component state。
作者:一拾五
链接:https://www.jianshu.com/p/07f36235eb2e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
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, 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, height: height, whiteSpace: 'nowrap', overflow: 'hidden', position: 'relative' }; const imageRowStyle = { marginLeft: offset }; const buttonStyle = { position: 'absolute', top: '40%', bottom: '40%', '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> ); } }