背景:
产品小姐姐提的一个需求,希望在顶部的banner实现商品图的无限轮播,每次轮播只展示三张图片,每张图片停留1.5s后滑动,小于4张则不轮播。
需求拆分
接下来就是拆分需求了,根据小姐姐的需求,这次轮播需要实现三个功能点:
- 图片滚动,且每张图片停留1.5s。
- 无限轮播。所谓无限轮播,也就是最后一张展示完之后,要继续从第一张展示,就像一个环形一样(头咬尾)
- 每次只展示三张照片,小于三张则不滚动。
功能实现
拆分需求之后就好说了,接下来就是一个个来实现了
图片滚动
- 首先,JS要控制元素的移动,则需要通过控制style属性来实现。在这里,我是通过控制marginLeft值。
- 其次,要想实现平滑式的滚动,则需要设置transition来让用户感知滚动的过程,而不是立即跳转。
# 获取需要滚动的元素
const pages = document.querySelector<HTMLElement>('.carousel-contents');
# 设置marginLeft、transition来实现平滑滚动
pages.style.marginLeft = `${nowPositionLeft}px`;
pages.style.transition = 'margin-left 0.7s linear';
- 然后,每次滚动都只滑动一张图片的距离,并且保证图片要停留展示1.5s后再滑动。这里就需要定时器来帮我们解决了,我们只需要一个1.5s的setTimeout,每隔1.5s后再执行下一张图片的滚动(这里可以用一个函数封装下)。
const PAGE_WIDTH = 23; // 每一张图片的宽度
const TRANSLATION_DELAY = 1500; // 轮播每一张图片停留的时长
const translatePage = () => {
const pages = document.querySelector<HTMLElement>('.carousel-contents'); // 获取需要滚动的元素
let nowPositionLeft = 0; // 初始化当前滚动元素的marginLeft
const run = () => {
setTimeout(() => {
# 设置marginLeft、transition来实现平滑滚动
pages.style.marginLeft = `${nowPositionLeft}px`;
pages.style.transition = 'margin-left 0.7s linear';
run(); // 持续滚动
}, TRANSLATION_DELAY);
}
run(); // 执行滚动
}
无限轮播
以上都不难想到,真正的问题在于当元素滚动到最后一张图片时(这里举例向左👈滚动),怎么又切到第一张图片,也就是实现无限轮播 你肯定能想到,当滚动最后一张图片时,通过设置marginLeft: 0px,又可以重新回到第一张,这确实是解决的办法,我们来看效果;
当从最后一张滚回到第一张的时候,用户很明显的看到元素“滚回去”的过程,即向右滑动的过程,这里是我们所不能接受的,我们希望的是能够始终向一个方向滚动。
细心分析的话可以发现,原因在于我们设置marginLeft: 0px的时候,transition依然保持着‘0.7s linear’的动画效果,此时如果我们将transition: 'none',则去掉了动画,瞬间回到第一张,也就让用户感知不到往回滑动的过程。但此时瞬间回到第一张的实现,也没办法让用户感知从4——>1的滑动过程,也是不符合要求的。
想来想去,怎么都不能实现4——>1的平滑滚动,那为什么不在4后面加个1,模拟4——>1的滑动。考虑视口需要展示三张图片,那就需要在4后面加1和2(同理可推,如果只是展示两张照片,只需要在4后面多加一个就好)。既然在后面加了,前面肯定也是要加的,才能切换回去,因此在滚动元素的前面加上最后一张,即4。当4滚动到视口的第一张图片位置时,瞬间切换到元素的头部(用户感知不到什么变化),再接着滚动,这样解决元素瞬间回到第一张,没有持续向一个方向滚动了。
const PAGE_WIDTH = 23; // 每一张图片的宽度
const TRANSLATION_DELAY = 1500; // 轮播每一张图片停留的时长
const translatePage = () => {
const pages = document.querySelector<HTMLElement>('.carousel-contents'); // 获取需要滚动的元素
let nowPositionLeft = 0; // 初始化当前滚动元素的marginLeft
const lastChild = pages.lastElementChild;
const firstChild = pages.firstElementChild;
const secondChild = pages.childNodes[1];
// 在 pages 容器的首尾分别添加第一个和最后两个
lastChild && pages.insertBefore(lastChild.cloneNode(true), firstChild);
firstChild && pages.appendChild(firstChild.cloneNode(true));
secondChild && pages.appendChild(secondChild.cloneNode(true));
const run = () => {
setTimeout(() => {
# 设置marginLeft、transition来实现平滑滚动
if (nowPositionLeft + ((pages.childNodes.length - 3) * PAGE_WIDTH) <= 0) {
nowPositionLeft = 0;
pages.style.transition = 'none'; // 去掉动画,让用户感知不到滚动回至最左端
pages.style.marginLeft = `${nowPositionLeft}px`;
} else {
nowPositionLeft = nowPositionLeft - PAGE_WIDTH;
pages.style.transition = 'margin-left 0.7s linear';
pages.style.marginLeft = `${nowPositionLeft}px`;
}
run();
}, TRANSLATION_DELAY);
}
run(); // 执行滚动
}
走到这里,差不多已经能够实现无限滚动了,但是细致的小伙伴一定能发现,这样实现存在一个问题,你会发现4、1、2在视口停留2个TRANSLATION_DELAY的时长,那是应为当瞬间切换到头部时(这里用户感知不到,以为仍然在4、1、2),定时器还是保持着TRANSLATION_DELAY计时,即等待TRANSLATION_DELAY后再进行下一步操作。因此这里需要优化下,即切换到头部位置时,定时器间隔为0,如下
图片个数不够时限制滚动
综合以上三个功能点,最后实现如下
const PAGE_WIDTH = 23; // 每一张图片的宽度
const TRANSLATION_DELAY = 1500; // 轮播每一张图片停留的时长
const translatePage = () => {
const pages = document.querySelector<HTMLElement>('.carousel-contents'); // 获取需要滚动的元素
// 小于4张则不轮播
if (pages.childNodes.length < 4) return;
let nowPositionLeft = 0; // 初始化当前滚动元素的marginLeft
const lastChild = pages.lastElementChild;
const firstChild = pages.firstElementChild;
const secondChild = pages.childNodes[1];
// 在 pages 容器的首尾分别添加第一个和最后两个
lastChild && pages.insertBefore(lastChild.cloneNode(true), firstChild);
firstChild && pages.appendChild(firstChild.cloneNode(true));
secondChild && pages.appendChild(secondChild.cloneNode(true));
const run = (time = TRANSLATION_DELAY) => {
setTimeout(() => {
# 设置marginLeft、transition来实现平滑滚动
if (nowPositionLeft + ((pages.childNodes.length - 3) * PAGE_WIDTH) <= 0) {
nowPositionLeft = 0;
pages.style.transition = 'none'; // 去掉动画,让用户感知不到滚动回至最左端
pages.style.marginLeft = `${nowPositionLeft}px`;
run(0); // 这里不设计setTime间隔,避免该状态连续出现两次
} else {
nowPositionLeft = nowPositionLeft - PAGE_WIDTH;
pages.style.transition = 'margin-left 0.7s linear';
pages.style.marginLeft = `${nowPositionLeft}px`;
run();
}
}, time);
}
run(); // 执行滚动
}
(注释: 视频没有截全)