前言
以前写过移动端的无线轮播图,可是前端的还没有写过,突然想写一个前端的无线轮播图,然后本篇从普通轮播图到无限轮播图,介绍一下实现思路
ps
:编写过程中发现,这里的无限轮播图不能像移动端那样使用三张图片无限连播,因为基本很少缓存的缘故,为了避免图片资源频繁销毁重建下载,因此还是要以获取全部资源的前提下,实现无限轮播,只是看着内容多一些一些,实际上也没有太繁琐
ps2
:轮播图的实现思路很多,不要被本文限制住思路,本文只是提供一种方案思路罢了
以前都是是用 react 编写,本文就是用 vue3 来编写吧
基础轮播图
基础轮播图,也就是点击左箭头右箭头实现切换,同时也支持跳转到任意一幅图
先看看实现效果吧,虽抽,但是可用哈😂
如上所示,自己用css编写了两个切换的按钮三角形,中间区域这是用来显示轮播图的,主要实现思路如下
- 通过编写基本布局,其中轮播图的父布局(
item_container
)不固定长度,轮播图平铺父布局不换行即可,这样item_container
的父布局就是一个大长条 - 祖布局固定一个轮播图长度,并设置
overflow:hidden
超出隐藏,因此默认只会显示第一个,其他超出布局被隐藏 - 根据索引设置 父布局(
item_container
) left 偏移,每增加一个则向左偏移一个轮播图长度,并且是反向,需要注意的是,需要设置 transition 过渡动画,不然不好看transition: left 0.4s ease-in-out
- 需要注意边界
js
//背景
<div class="bkg">
//轮播图内容父布局,并设置过渡动画 transition
<div class="item_container" :style="{ left: data.left }">
//轮播图
<div v-for="(item, index) in data.list" class="item_view" v-bind:key="index">
{{ item }}
</div>
</div>
//外面的两个切换按钮
<div class="left_arrow" @click="onBaseLeft"></div>
<div class="right_arrow" @click="onBaseRight"></div>
</div>
js 的实现逻辑如下所示,核心逻辑就是 layout 了,主要控制 item_container 的 left 即可,由于是根据索引跳转,因此天然支持跳转到某一个,并且轮播图本身就是平铺有序的,过渡动画也很自然
js
import { shallowReactive } from 'vue'
const data = shallowReactive({
list: [1, 2, 3, 4, 5, 6, 7, 8],
index: 0,
left: 'none',
})
//根据索引进行跳转
const Layout = (index: number) => {
//超出边界的不处理
if (index < 0 || index > data.list.length - 1) return
//根据索引设置 left 偏移,记得反向偏移
data.left = `-${index * 600}px`
//更新索引
data.index = index
}
//向左索引 - 1
const onLeft = () => {
Layout(data.index - 1)
}
//向右索引 + 1
const onRight = () => {
Layout(data.index + 1)
}
//跳转指定索引
const jumpToIndex = (index: number) => {
Layout(index)
}
无限轮播图
长得跟基础轮播图一模一样,唯一的区别就是没有边界,可以无限切换,首尾连贯
这个实现逻辑就和基础轮播图不一样了,基础轮播图可以使用父布局取巧,可是这个无线轮播图就行了,只能处理好子图才能更好实现无缝衔接,甚至为了避免连续切换过快还要加上节流函数
布局类似,但由于不能取巧,去掉了父布局,留下了祖布局,过程略复杂,简述一下实现逻辑
- 设置父布局超出隐藏,并设置一个 position,避免内部绝对布局相对位置问题
- 子布局通过绝对布局,设置好切换箭头位置和轮播图的基础位置,轮播图默认全部平铺重叠到整个父布局,后续通过重新布局来实现平铺
- 根据索引将子布局实现平铺,这里直接使用 transform 效果,不使用left了,也减少设置其他属性了,通过设置 translateX 将子图全部平铺完毕,同时设置好 transition 以生成动画
- 平铺过程中需要完成无线轮播,因此,中间图的左右两侧必然需要子图,因此我们只需要知道
中前后三张图
即可,设置调整好好他们的位置,并处理好边界即可 - 由于没有取巧,也没有额外增加元素,仅仅通过内部元素调换,因此动画切换过程可能会出现覆盖问题,因此设置一下层级即可,这里通过设置
z-index
来改变层级(需要在同一个层叠上下文
),因此父布局也需要设置好 - 由于这里的变换并不是纯平铺,算是动态变换,因此需要避免切换过快,否则可能会出现动画过渡差,出现白屏等不自然现象,可以通过节流函数 throttle 来解决(为什么不使用防抖,可以自行思考他们的特色)
ps
:为何要要给所有元素设置 transition,而不是切换动画的那两三个设置动画,因为从一个跳转到另外一个时,如果中间的没有设置动画,则会出现白屏或者其他不自然的过渡现象(element的轮播图就有这个问题,优化过渡有时候也会出现问题)
js
//父布局超出隐藏
<div class="bkg">
//轮播图
<div
v-for="(item, index) in data.list"
class="item_view"
:class="{
is_active: data.activeIndex.includes(index),
}"
:style="{ transform: data.transforms[index] }"
v-bind:key="index">
<div class="flex w-full h-full justify-center items-center">{{ item }}</div>
</div>
//切换箭头
<div class="left_arrow" @click="onLeft"></div>
<div class="right_arrow" @click="onRight"></div>
</div>
js实现逻辑如下所示,onLayout
是核心变换函数,throttle
是简易的节流函数(足够使用),onMounted
中调用 onLayout 是为了处理刚开始的重叠布局问题,以便于显示正常和后续切换动画问题
js
import { onMounted, ref, shallowReactive } from 'vue'
//声明对象用于渲染内容、更新轮播图
const data = shallowReactive<{
list: unknown[]
index: number
activeIndex: number[]
transforms: string[]
}>({
list: [1, 2, 3, 4, 5, 6, 7, 8],
index: -1,
activeIndex: [0],
transforms: [],
})
//节流函数专属
const timeout = ref<number>()
const throttle = (fn: () => void) => {
if (timeout.value !== undefined) return
fn()
timeout.value = setTimeout(() => {
timeout.value = undefined
}, 400)
}
//核心布局逻辑,通过传递切换索引,实现切换
const onLayout = (current: number) => {
//数量为1时无需布局和切换,能够正常动画的最少布局数量则是3
//数量为2时需要增加非常多的逻辑,但只需要利用面向对象思想,将其转化为大与3的数量即可
//转化过程为了避免衔接不畅,只需要在后面拼接自己即可,这样相当于保存了同一个指针,内容也指向同一个对象,更新内容对象也会同时变化
let len = data.list.length
if (len < 2) {
return
} else if (len === 2) {
data.list = data.list.concat(data.list)
len = 4
}
//获取前中后的索引,能够让当前和下一次切换更为自然
const pre = (current - 1 + len) % len
current = (current + len) % len
const next = (current + 1 + len) % len
const transform: string[] = []
//设置好当前三个的transfrom,同时其他的也平铺下去,这样过渡也更加自然
for (let idx = 0; idx < len; idx++) {
let index = idx
if (idx === pre) {
index = -1
} else if (idx === current) {
index = 0
} else if (idx === next) {
index = 1
} else {
index = idx - current
}
//其他的也根据当前索引平铺,这样跳转切换时也更加自然
transform.push(`translateX(${index * 100}%)`)
}
//设置活跃索引用于设置z-index,这样动画切换更加自然,不会出现其他元素从上面飞过的问题
data.activeIndex = data.index >= 0 ? [current, data.index] : [current]
data.transforms = transform
data.index = current
}
const onLeft = () => {
throttle(() => onLayout(data.index - 1))
}
const onRight = () => {
throttle(() => onLayout(data.index + 1))
}
//跳转指定索引
const jumpToIndex = (index: number) => {
throttle(() => onLayout(index))
}
//开始布局,避免重叠和动画问题
onMounted(() => {
onLayout(0)
})
3d无限轮播图
3d无线轮播图和正常无线轮播图类似,只不过中间的不占满屏幕,两边的略小,当然实际效果可以根据自己需求更新,这里只做一个参考(不好看的换个图,也许就好看了🤣)
实现效果如下所示
看到上面的图可以看到和普通的无线轮播图的区别
- 多了两张图,需要再额外准备两张图,方便两侧过渡(自己想准备一张自行处理也没事)
- 中间的图和两边图大小、透明度不一样,因此还需要设置好偏移、大小、透明度
话不多说直接上代码
js
<div class="bkg">
//这里多设置了一个父布局,位的就是子布局好居中,直接铺满,两侧越界展示就行了,祖父节点超出隐藏完事
<div class="item_container">
//轮播图子节点仍然是通过设置transfrom、opacity实现动画,同时 is_active 避免动画效果不好
<div
v-for="(item, index) in data.list"
class="item_view"
:class="{
is_active: data.activeIndex === index,
}"
:style="{ transform: data.transforms[index], opacity: data.opacitys[index] }"
v-bind:key="index"
>
<div class="flex w-full h-full justify-center items-center">{{ item }}</div>
</div>
</div>
<div class="left_arrow" @click="onLeft"></div>
<div class="right_arrow" @click="onRight"></div>
</div>
js代码仍然是核心的 onLayout
逻辑,其他的和 无线轮播图一样,这里就只介绍 onLayout
了
js
import { onMounted, ref, shallowReactive } from 'vue'
const timeout = ref<number>()
const data = shallowReactive<{
list: unknown[]
index: number
activeIndex: number
transforms: string[]
opacitys: string[]
}>({
list: [1, 2, 3, 4, 5, 6, 7, 8],
index: -1,
activeIndex: 0,
transforms: [],
opacitys: [],
})
const throttle = (fn: () => void) => {
if (timeout.value !== undefined) return
fn()
timeout.value = setTimeout(() => {
timeout.value = undefined
}, 400)
}
const onLayout = (current: number) => {
//需要注意的是需要的节点从三个增加到了5个,因此也需要额外处理
let len = data.list.length
if (len < 2) {
return
} else if (len < 5) {
//小于5个就填充,直到大于等于五个,只能一组一组填充,否则会不连贯(当首尾出现一样的是不是很难受)
//一般用到了3d轮播图的,应该内容不会太少,无需考虑两个变6个问题🤣
const list = [...data.list]
for (let idx = len; idx < 5; idx += len) {
data.list.push(...list)
}
len = data.list.length
}
//额外增加两个索引,并处理好越界问题
const prepre = (current - 2 + len) % len
const pre = (current - 1 + len) % len
current = (current + len) % len
const next = (current + 1 + len) % len
const nextnext = (current + 2 + len) % len
//需要同时设置 transform 和 透明度 opacity
const transforms: string[] = []
const opacitys: string[] = []
for (let idx = 0; idx < len; idx++) {
const index = idx
let transform = ''
let opacity = '0'
//设置偏移透明度,越近越大不透明度越高
if (index === prepre) {
transform = `translateX(-100%) scale(0.6)`
opacity = '0'
} else if (idx === pre) {
transform = `translateX(-60%) scale(0.8)`
opacity = '0.7'
} else if (idx === current) {
transform = `translateX(0) scale(1)`
opacity = '1'
} else if (idx === next) {
transform = `translateX(60%) scale(0.8)`
opacity = '0.7'
} else if (idx === nextnext) {
transform = `translateX(100%) scale(0.6)`
opacity = '0'
} else {
transform = `translateX(${(idx - current) * 100}%)`
opacity = '0'
}
transforms.push(transform)
opacitys.push(opacity)
}
data.activeIndex = current
data.transforms = transforms
data.opacitys = opacitys
data.index = current
}
const onLeft = () => {
throttle(() => onLayout(data.index - 1))
}
const onRight = () => {
throttle(() => onLayout(data.index + 1))
}
const jumpToIndex = (index: number) => {
throttle(() => onLayout(index))
}
onMounted(() => {
onLayout(0)
})
最后
轮播图就介绍到这里了,如果想作出更好看的 3d 效果,实际上只需要设置好 transform 就行了,不必要的话,甚至不做无限滚动的也行,只需要设置一下旋转阴影,那样 3d 效果就更好看了🤔