一、前情概要
业务逻辑早早写完,动画效果却迟迟不能交付,这其中的缘由听者伤心、闻者流泪,只留下开发与设计慢慢沟通、慢慢核对。
😭 这一切都是因 Swiper 而起......
P.S.嫌麻烦可直接跳至最后,结尾处罗列了本文提到的问题及处理方法,但最好别吧 :)
Swiper:一款免费以及轻量级的移动设备触控滑块的 js 框架(摘自官方中文网站)
基本概念
- Swiper 组件: 业务代码中引入的 "swiper/react"
- Swiper:代码中 Swiper 是划动部分的容器,即
swiper-container
,代码中的<Swiper> - Slide:swiper 容器中的划动单元(即单个推荐商品),代码中的<SwiperSlide>
业务背景
商品卡片自动轮播效果如下
业务需求
组件列表页的单张卡片中推荐商品部分,可看作一个swiper-container
,swiper 功能的需求如下:
- slide 循环 (对应 loop 参数)
- 支持触摸滑动
- 划动后 slide 不能停留在过渡位置
- 只有一个商品时,swiper 不划动 (noSwiping 参数)
- 匀速自动轮播(重点,要考)
- 首次加载页面时,第一张卡片自动轮播
- 屏幕滚动后,处在中间的卡片自动轮播
- 当改变轮播卡片时,上一个轮播的卡片应立即停止轮播
二、主体内容
知晓业务需求后,评估一番选择使用 swiper 组件来完成工作(swiper 组件具备丰富的 Api、较为完善的官方中文说明等优势)。
写完绝大部分交互代码后,开始折腾自动轮播功能,大体的实现思路如下:
- 父组件设置页面滚动监听,将变量
activeIndex
设置成当前处在屏幕中间卡片的index
- 通过 props 给到轮播子组件
activeIndex
,即每个轮播子组件都能拿到同样的activeIndex
- 每个子组件判断
activeIndex === index?
,true 代表当前应自动轮播 - 开启自动轮播开关时,更改当前轮播组件的相关配置参数;而上一个自动轮播组件则关闭自动轮播,调整相关配置参数
前面非常简单,精准获取并给到子组件当前屏幕中间卡片的activeIndex
,并如下配置参数,预想能够动态控制自动播放。
/**
* 动态改变配置的逻辑
* 在实际代码中肯定不会如此不优雅
* 此处这样写是为关注问题本身
*/
autoplay: (activeIndex === index) && {
delay: 0,
stopOnLastSlide: false,
disableOnInteraction: false,
},
复制代码
但是当activeIndex === index
结果改变的时候,发现并不会引起autoplay
参数的变化,即 swiper 组件并不能动态更改配置。
这也抛出了第一个问题,如何动态控制自动轮播的开/关?
问题 1:如何动态控制自动轮播
查阅官网发现:官方提供了autoplay.start()
、autoplay.stop()
两个方法。
既然不能偷懒动态改变 swiper 的参数,那就老老实实按照官方给的方法来控制 swiper 的自动轮播开/关吧。
代码如下:
useEffect(() => {
const mySwiper = mySwiperRef.current;
if (activeIndex === index) {
mySwiper.autoplay.start();
} else {
mySwiper.autoplay.stop();
}
}, [activeIndex]);
复制代码
一番操作成功实现了动态开启/关闭自动轮播的功能,再把activeIndex
的初始值设为 0,即可满足首次加载页面时第一个卡片自动轮播的需求。
但......下一个问题又出现了。
问题 2:无法立即停止
功能强大的 swiper 组件总会在“奇怪”的地方给你出道题 🤷♂️
虽然上面成功搞定了“动态控制自动轮播”的问题,但是在调用autoplay.stop()
后,slide 并不会立即停止,而是过渡完成后再停止。
原因猜想:slide 的滚动是从一个坐标(即 mySwiper.translate)过渡到下一个坐标,autoplay.stop()
在 slide 抵达坐标后才会生效。
继续查阅资料,可以通过 swiper 提供的自定义位移方法来实现立即停止。
「 你看,我 swiper 功能就是强大 」
「 对对对... 你说的都对,你知道为什么吗(捏紧拳头 👊)」
将问题 1 中的代码优化一(亿)点点,得到 👇
useEffect(() => {
const mySwiper = mySwiperRef.current;
if (activeIndex === index) {
if (!firstFlag) {
mySwiper.slideTo(mySwiper.activeIndex, slideSpeed);
autoplayFlag.current = setTimeout(() => {
mySwiper.autoplay.start();
}, lastNeedSwiperSpeed);
} else {
mySwiper.autoplay.start();
setFirstFlag(false);
}
} else {
const newSpeed =
(Math.abs(
Math.abs(mySwiper.translate) - Math.abs(mySwiper.getTranslate())
) /
168) *
5000; // 168是每个slider的宽度,5000是匀速滚动时间
if (Math.floor(newSpeed * 100) / 100) {
setSlideSpeed(Math.floor(newSpeed * 100) / 100);
}
mySwiper.setTranslate(mySwiper.getTranslate());
mySwiper.autoplay.stop();
}
}, [activeIndex]);
复制代码
* getTranslate()
获取 slide 实时位移、translate
是 slide 过渡完成的位移、setTranslate()
设置 slide 的 translate
整个动态自动播放的逻辑条件是分为:
activeIndex === index
即当前 自动轮播 开启else
即当前 自动轮播 关闭
先谈else
部分的改动:
- 通过
translate
和getTranslate()
先计算出 slider 从当前暂停轮播的位置到过渡完成后的位置需要的时间(按照原本设定的速度) - 将
newSpeed
保留两位小数,如果newSpeed
不存在(测试中发现会出现 0 的情况,原因暂不知)则不改变slideSpeed
- 通过
setTranslate()
方法,将 slide 过渡完成的位移设置为当前实时位移,从而实现在中间停顿,然后设置自动轮播autoplay.stop()
再来是activeIndex === index
的部分:
- 如果是从中间停顿后再次开启自动轮播,需要先通过
slideTo
将 slide 移动到暂停前“原本的位移”,当 slide 经过slideSpeed
时间抵达“原本的位移”后,再开启自动轮播(否则slideTo
和autoplay.start()
会有冲突,运动速度相当诡异) - 如果当前
swiper
首次自动轮播,是不需要划动
这样就实现了自动轮播立即停止,并且能够重新从停顿位置开启自动轮播功能,这样看上去就大功告成了,但是在测试中发现,还差一小步。。。
问题 3:用户缓慢划动会造成轮播停止
实现卡片自动轮播效果的基础上仍然需要支持用户手动划动,测试中发现当用户缓慢划动卡片后,卡片会出现无法自动轮播的问题。
产生这个问题的原因暂不明,但可以通过官方提供的 swiper事件解决这个问题,代码如下:
on:{
touchStart:function(){
this.autoplay.stop();
},
touchEnd:function(){
setTimeout(()=>{
this.autoplay.start();
},500)
}
}
复制代码
结尾
解决这三个问题就基本实现了 swiper 自动轮播效果,总结下三个问题:
- 如何动态控制自动轮播?
答:获取到 swiper 实例对象,调用其mySwiper.autoplay.start()
,mySwiper.autoplay.stop()
方法来动态控制自动轮播。 - 无法立即停止?
答:
- 立即停止:利用
mySwiper.getTranslate()
获取到实时位移;再利用mySwiper.setTranslate()
将过渡完成后的位移设置为当前实时位移。 - 重新开启:利用
mySwiper.slideTo()
将 slide 划动到立即停止前本应过渡到的位置;等 slide 划动完成后,再调用mySwiper.autoplay.start()
。
- 立即停止:利用
- 用户缓慢划动会造成轮播停止?
答:用户开始划动时调用
mySwiper.autoplay.stop()
暂停轮播,而当用户停止划动后调用mySwiper.autoplay.start()
重新开启轮播。
P.S. 如果轮播组件有跳转逻辑,跳转后回到轮播页面时,可能会导致自动轮播功能失效(mySwiper.autoplay.running
为false
的情况),查阅了很多帖子博客发现有一个神奇的解决方法,只需在需要开启自动轮播的位置加上如下代码:
if (!mySwiper.autoplay.running) {
mySwiper.animating = false;
mySwiper.autoplay.timeout = undefined;
mySwiper.autoplay.running = false;
mySwiper.autoplay.start();
}
复制代码
经过了大量调试最后得出的解决方案,至于为什么这样写,我也不知道 🤷♂️
如果文中有任何错误或者有歧义的地方,还请多指教