很多网站都会有轮播图的需求,而简单的轮播图实现通常会在展示完最后一个子项后停止轮播,或者跳回到第一个子项重复轮播过程,这样的交互效果往往是存在断层的。接下来介绍如何实现一个无缝的轮播图,达到这样的效果:
预览地址:https://jsfiddle.net/JunreyCen/qxogapws/
核心思想其实非常简单:
-
当轮播到边界子项(
Item 3
),并继续进行横移时,把即将要展示的子项(Item 1
)挪到紧挨着Item 3
的位置,执行横移,如下图Step 1
; -
由于此时活跃子项的索引(
index > 2
)已经超出范围,在下一次横移进行前,需要把索引调整到合理范围内,并重置子项的位置,如下图Step 2
。注意,这一步需要把transition
关闭,不然 “偷梁换柱” 的过程会被一览无遗。
这里提供一份完整的代码实现。我稍微做了点优化,支持
- 左、右两个方向轮播
- 一次切换多个子项
原理上无非是支持多个子项的同时 “偷梁换柱” 罢了,详细的可以关注代码中的 next
函数。
<html>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
<head>
<style>
ul {
list-style: none;
}
.swipe {
position: absolute;
left: 0;
right: 0;
margin: 40px auto;
width: 90%;
max-width: 375px;
height: 200px;
overflow: hidden;
}
.swipe-group {
display: flex;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
.swipe-item {
flex: 0 0 100%;
height: 100%;
line-height: 200px;
text-align: center;
font-size: 40px;
font-weight: 600;
color: #fff;
}
.swipe-item:nth-child(1) {
background-color: aquamarine;
}
.swipe-item:nth-child(2) {
background-color: chocolate;
}
.swipe-item:nth-child(3) {
background-color: darksalmon;
}
</style>
</head>
<body>
<div id="app"></div>
<template id="tpl">
<div class="swipe">
<ul class="swipe-group"
:style="groupStyle">
<li
class="swipe-item"
v-for="item in items"
ref="item">
{{item}}
</li>
</ul>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
new Vue({
el: '#app',
template: '#tpl',
computed: {
groupStyle() {
return {
'transform': `translate3d(${this.offset}px, 0, 0)`,
'transition-duration': `${this.duration}ms`,
};
},
},
data() {
return {
items: [1, 2, 3],
index: 0, // 当前轮播项索引
offset: 0, // swipe组的偏移量
duration: 0, // 过渡动画时长
itemWidth: 0, // 轮播项宽度
};
},
mounted() {
if (this.$el) {
this.itemWidth = this.$el.getBoundingClientRect().width;
}
this.autoplay();
},
methods: {
// index 超出范围时调整
correctIndex() {
this.duration = 0;
const total = this.items.length;
if (this.index < 0) {
this.next(total, true);
} else if (this.index > total - 1) {
this.next(-total, true);
}
},
// 移动到达目标 index 途中的所有 swipe-item
moveItems(indexOffset) {
const targetIndex = this.index + indexOffset;
if (this.index < targetIndex) {
// 向左
for (let i = this.index; i < targetIndex; i++) {
this.moveItem(i + 1);
}
} else {
// 向右
for (let i = targetIndex; i < this.index; i++) {
this.moveItem(i);
}
}
},
// 移动 swipe-item
moveItem(index) {
const total = this.items.length;
const itemIndex = index % 3 < 0 ? index % 3 + 3 : index % 3;
// 目标 index 超出范围时调整对应 swipe-item 的偏移值
if (index > total - 1) {
this.$refs.item[itemIndex].style.transform = `translateX(${total * this.itemWidth}px)`;
} else if (index < 0) {
this.$refs.item[itemIndex].style.transform = `translateX(${-total * this.itemWidth}px)`;
} else {
this.$refs.item[itemIndex].style.transform = 'translateX(0px)';
}
},
resetItems() {
this.$refs.item.forEach($item => {
$item.style.transform = 'translateX(0px)';
});
},
// 向左/右方向切换 indexOffset 个 swipe-item
next(indexOffset, isCorrect) {
isCorrect ? this.resetItems() : this.moveItems(indexOffset);
this.index += indexOffset;
this.offset = -this.index * this.itemWidth;
},
autoplay() {
this.player = setInterval(() => {
this.duration = 0;
this.correctIndex();
// 30ms延时是为了屏蔽 reset 过程中的过渡动画
setTimeout(() => {
this.duration = 500;
this.next(1);
}, 30);
}, 1000);
},
},
});
</script>
</body>
</html>
代码已发布在 github 上,欢迎大家提 issue 交流。
网友评论