1. 轮播图基本原理
轮播图(Carousel),在 Antd 中被称为走马灯,可能是前端开发者最常见的组件之一了,不管是在 PC 端还是在移动端我们总能见到他的身影.
那么我们通常是如何使用轮播图的呢?Antd 的代码如下:
<Carousel>
<div><h3>1</h3></div>
<div><h3>2</h3></div>
<div><h3>3</h3></div>
<div><h3>4</h3></div>
</Carousel>
问题是我们在Carousel中放入了四组div为什么一次只显示一组呢?
图中被红框圈住的为可视区域,可视区域的位置是固定的,我们只需要移动后面div的位置就可以做到1 2 3 4四个子组件轮播的效果,那么子组件2目前在可视区域是可以被看到的,1 3 4应该被隐藏,这就需要我们设置overflow 属性为 hidden来隐藏非可视区域的子组件
复制查看动图: https://images2015.cnblogs.com/
因此就比较明显了,我们设计一个可视窗口组件Frame,然后将四个 div共同放入幻灯片组合组件SlideList中,并用SlideItem分别将 div包裹起来,实际代码应该是这样的:
<Frame>
<SlideList>
<SlideItem>
<div><h3>1</h3></div>
</SlideItem>
<SlideItem>
<div><h3>2</h3></div>
</SlideItem>
<SlideItem>
<div><h3>3</h3></div>
</SlideItem>
<SlideItem>
<div><h3>4</h3></div>
</SlideItem>
</SlideList>
</Frame>
我们不断利用translateX来改变SlideList的位置来达到轮播效果,如下图所示,每次轮播的触发都是通过改变transform: translateX()来操作的
2. 轮播图基础实现
搞清楚基本原理那么实现起来相对容易了,我们以移动端的实现为例,来实现一个基础的移动端轮播图.
首先我们要确定可视窗口的宽度,因为我们需要这个宽度来计算出SlideList的长度(SlideList的长度通常是可视窗口的倍数,比如要放三张图片,那么SlideList应该为可视窗口的至少3倍),不然我们无法通过translateX来移动它.
我们通过getBoundingClientRect来获取可视区域真实的长度,SlideList的长度那么为:
slideListWidth = (len + 2) * width(len 为传入子组件的数量,width 为可视区域宽度)
至于为什么要+2后面会提到.
/**
* 设置轮播区域尺寸
* @param x
*/
private setSize(x?: number) {
const { width } = this.frameRef.current!.getBoundingClientRect()
const len = React.Children.count(this.props.children)
const total = len + 2
this.setState({
slideItemWidth: width,
slideListWidth: total * width,
total,
translateX: -width * this.state.currentIndex,
startPositionX: x !== undefined ? x : 0,
})
}
获取到了总长度之后如何实现轮播呢?我们需要根据用户反馈来触发轮播,在移动端通常是通过手指滑动来触发轮播,这就需要三个事件onTouchStart onTouchMove onTouchEnd.
onTouchStart顾名思义是在手指触摸到屏幕时触发的事件,在这个事件里我们只需要记录下手指触摸屏幕的横轴坐标 x 即可,因为我们会通过其横向滑动的距离大小来判断是否触发轮播
/**
* 处理触摸起始时的事件
*
* @private
* @param {React.TouchEvent} e
* @memberof Carousel
*/
private onTouchStart(e: React.TouchEvent) {
clearInterval(this.autoPlayTimer)
// 获取起始的横轴坐标
const { x } = getPosition(e)
this.setSize(x)
this.setState({
startPositionX: x,
})
}
onTouchMove顾名思义是处于滑动状态下的事件,此事件在onTouchStart触发后,onTouchEnd触发前,在这个事件中我们主要做两件事,一件事是判断滑动方向,因为用户可能向左或者向右滑动,另一件事是让轮播图跟随手指移动,这是必要的用户反馈.
/**
* 当触摸滑动时处理事件
*
* @private
* @param {React.TouchEvent} e
* @memberof Carousel
*/
private onTouchMove(e: React.TouchEvent) {
const { slideItemWidth, currentIndex, startPositionX } = this.state
const { x } = getPosition(e)
const deltaX = x - startPositionX
// 判断滑动方向
const direction = deltaX > 0 ? 'right' : 'left'
this.setState({
direction,
moveDeltaX: deltaX,
// 改变translateX来达到轮播组件跟随手指移动的效果
translateX: -(slideItemWidth * currentIndex) + deltaX,
})
}
onTouchEnd顾名思义是滑动完毕时触发的事件,在此事件中我们主要做一个件事情,就是判断是否触发轮播,我们会设置一个阈值threshold,当滑动距离超过这个阈值时才会触发轮播,毕竟没有阈值的话用户稍微触碰轮播图就造成轮播,误操作会造成很差的用户体验.
1 /**
2 * 滑动结束处理的事件
3 *
4 * @private
5 * @memberof Carousel
6 */
7 private onTouchEnd() {
8 this.autoPlay()
9 const { moveDeltaX, slideItemWidth, direction } = this.state
10 const threshold = slideItemWidth * THRESHOLD_PERCENTAGE
11 // 判断是否轮播
12 const moveToNext = Math.abs(moveDeltaX) > threshold
13
14 if (moveToNext) {
15 // 如果轮播触发那么进行轮播操作
16 this.handleSwipe(direction!)
17 } else {
18 // 轮播不触发,那么轮播图回到原位
19 this.handleMisoperation()
20 }
21 }
3. 轮播图的动画效果
我们常见的轮播图肯定不是生硬的切换,一般在轮播中会有一个渐变或者缓动的动画,这就需要我们加入动画效果.
我们制作动画通常有两个选择,一个是用 css3自带的动画效果,另一个是用浏览器提供的requestAnimationFrame API
孰优孰劣?css3简单易用上手快,兼容性好,requestAnimationFrame 灵活性更高,能实现 css3实现不了的动画,比如众多缓动动画 css3都束手无策,因此我们毫无疑问地选择了requestAnimationFrame.
双方对比请看张鑫旭大神的CSS3动画那么强,requestAnimationFrame还有毛线用?
想用requestAnimationFrame实现缓动效果就需要特定的缓动函数,下面就是典型的缓动函数
type tweenFunction = (t: number, b: number, _c: number, d: number) => number
const easeInOutQuad: tweenFunction = (t, b, _c, d) => {
const c = _c - b;
if ((t /= d / 2) < 1) {
return c / 2 * t * t + b;
} else {
return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
}
缓动函数接收四个参数,分别是:
- t: 时间
- b:初始位置
- _c:结束的位置
- d:速度
通过这个函数我们能算出每一帧轮播图所在的位置, 如下:
在获取每一帧对应的位置后,我们需要用requestAnimationFrame不断递归调用依次移动位置,我们不断调用animation函数是其触发函数体内的this.setState({ translateX: tweenQueue[0], })来达到移动轮播图位置的目的,此时将这数组内的30个位置依次快速执行就是一个缓动动画效果.
1 /**
2 * 递归调用,根据轨迹运动
3 *
4 * @private
5 * @param {number[]} tweenQueue
6 * @param {number} newIndex
7 * @memberof Carousel
8 */
9 private animation(tweenQueue: number[], newIndex: number) {
10 if (tweenQueue.length < 1) {
11 this.handleOperationEnd(newIndex)
12 return
13 }
14 this.setState({
15 translateX: tweenQueue[0],
16 })
17 tweenQueue.shift()
18 this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex))
19 }
但是我们发现了一个问题,当我们移动轮播图到最后的时候,动画出现了问题,当我们向左滑动最后一个轮播图div4时,这种情况下应该是图片向左滑动,然后第一张轮播图div1进入可视区域,但是反常的是图片快速向右滑动div1出现在可是区域…
因为我们此时将位置4设置为了位置1,这样才能达到不断循环的目的,但是也造成了这个副作用,图片行为与用户行为产生了相悖的情况(用户向左划动,图片向右走).
目前业界的普遍做法是将图片首尾相连,例如图片1前面连接一个图片4,图片4后跟着一个图片1,这就是为什么之前计算长度时要+2
slideListWidth = (len + 2) * width(len 为传入子组件的数量,width 为可视区域宽度)
当我们移动图片4时就不会出现上述向左滑图片却向右滑的情况,因为真实情况是:
图片4 -- 滑动为 -> 伪图片1 也就是位置 5 变成了位置 6
当动画结束之后,我们迅速把伪图片1的位置设置为真图片1,这其实是个障眼法,也就是说动画执行过程中实际上是图片4到伪图片1的过程,当结束后我们偷偷把伪图片1换成真图片1,因为两个图一模一样,所以这个转换的过程用户根本看不出来…
如此一来我们就可以实现无缝切换的轮播图了
4. 改进方向
我们实现了轮播图的基本功能,但是其通用性依然存在缺陷:
- 提示点的自定义: 我的实现是一个小点,而 antd 是用的条,这个地方完全可以将 dom 结构的决定权交给开发者.
- 方向的自定义: 本轮播图只有水平方向的实现,其实也可以有纵向轮播
- 多张轮播:除了单张轮播也可以多张轮播
以上都是可以对轮播图进行拓展的方向,相关的还有性能优化方面
我们的具体代码中有一个相关实现,我们的轮播图其实是有自动轮播功能的,但是很多时候页面并不在用户的可视页面中,我们可以根据是否页面被隐藏来取消定时器终止自动播放.
网友评论