问题
在实现走马灯的时候想到了几个问题
- 1、结构如何指定
- 2、走马灯不能只实现图片的轮播,轮播模板需要用户自定义?
- 3、如何实现滚动?
- 4、需要实现无缝滚动,具体可以使用复制首尾项填充和动态偏移距离的方法?如何操作?
- 5、过渡效果如何添加?
- 6、垂直轮播效果?
效果(展示部分)
- 基础
- 无缝
- 垂直
思路(只有难点)
结构如何搭建
<ty-carousel height="300px">
<ty-carousel-item v-for="ele in imgs" :key="ele.id">
<ty-image fit="contain" alt="1234" :src="ele.url" :lazy="true">
</ty-image>
</ty-carousel-item>
</ty-carousel>
也就是说需要创建ty-carousel, ty-carousel-item.
通过插槽用户即可自定义循环模板
ty-carousel-item
<template>
<div ref="item" class='ty-carousel-item' :style="{ 'transform': positionOffset }">
<slot></slot>
</div>
</template>
<style lang='less' scoped>
.ty-carousel-item {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #d3dce6;
}
</style>
ty-carousel
<template>
<div class='ty-carousel' @mouseenter="enterMain" @mouseleave="leaveMain">
<!-- 主体 -->
<div class="main" :style="{ 'height': height }">
<!-- 指示器 -->
<div class="indicator" v-if="!isOutSide && isIndicator">
<div class="dot" v-for="(c, index) in playControl.positionArray" :key="index">
<span :style="{ 'background': index == playControl.currentPage ? 'pink' : '#f0f1f3' }"
@click.stop="pushPage(index)"></span>
</div>
</div>
<transition name="leftBtnAnimation">
<!-- 左侧按钮 -->
<div class="btn_left" v-show="isControlShow || arrowMode == 'show'" @click.stop="withLeft">
<ty-icon type="arrow-double-left"></ty-icon>
</div>
</transition>
<transition name="rightBtnAnimation">
<!-- 右按钮 -->
<div class="btn_right" v-show="isControlShow || arrowMode == 'show'" @click.stop="withRight">
<ty-icon type="arrow-double-right"></ty-icon>
</div>
</transition>
<div class="show">
<slot></slot>
</div>
</div>
<!-- 指示器 -->
<div class="outside" v-if="isOutSide && isIndicator">
<div class="dot" v-for="(c, index) in playControl.positionArray" :key="index">
<span :style="{ 'background': index == playControl.currentPage ? 'pink' : '#f0f1f3' }"
@click.stop="pushPage(index)"></span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ty-carousel',
props: {
// 盒子整体高度
height: {
type: [String],
default: '300px'
},
// 初始化激活页面
initialIndex: {
type: [Number],
default: 0
},
// 是否自动切换
autoPlay: {
type: [Boolean],
default: true
},
// 切换间隔时间
interval: {
type: [Number],
default: 2000
},
// 指示器是否显示在外侧
indicatorOutside: {
type: [Boolean],
default: false
},
// 是否显示指示器
indicator: {
type: [Boolean],
default: true
},
// 箭头展示方式
arrow: {
type: [String],
default: 'none'
},
// 切换时触发的回调
change: {
type: Function
},
// 轮播方向
verticalPlay: {
type: [Boolean],
default: false
}
},
}
</script>
<style lang='less' scoped>
@import '../../../css/carousel.less';
</style>
如何实现滚动与无缝
滚动的方式可以使用定位或者2d偏移等等,这里我使用transform2d偏移,
无缝滚动的实现方式1:
复制首尾项反向填充当对应的节点首尾中,当节点激活到末尾或者首项时,若继续此方向轮播, 则(迅速)跳转到反向的指定页码上,再动画过渡到指定的'最后页码'上, 反之亦如此
无缝实现思路2:
在轮播的时候每个子组件都获取最新的相较于激活页的偏移值,根据偏移值来控制展示, 当激活页左侧没有子组件时,将右侧的最后一个组件偏移到最左边,当激活页右侧没有子组件时,将左侧的第一个组件偏移到最右边
这里我使用的是第二种方法,毕竟无需用户有其他的操作
具体
解决思路: 创建一个数组保存每一个组件距离激活页的距离,下表格的数组每一项表示距离当前展示组件的距离
如: 第一行数组表示在激活页码为0时, 第一个组件距激活组件的距离为0, 第二个组件距激活组件的距离为1
依次类推
这样可以获得每一个组件在不同的激活页下距离激活页的距离, 将数组对应的距离值分配给对应的子组件
子组件根据距离使用transform动态偏移(距离1, 则偏移1*100%, 距离-1 则偏移-1*100%)
无缝:
因为这里每个组件是单独偏移,所以无缝推荐使用左右补充的方式(也方便为card模式做准备),不推荐使用
首张末张复制插入的方式(需要复制组件或vnode节点)
无缝思路
对循环情况分类:
1 左右都有组件 --> 正方向递增, 负方向递减
2 左没有组件 --> 正方向递增, 留数组末尾为距离-1
3 右没有组件 --> 负方向递减, 留数组首项为距离1
具体结合数组思考(其实就是保证激活组件左右都有组件)
// right点击
// --0---1---2---3---4---> 对应组件序号(图片)
// [ 0, 1, 2, 3, -1] 0
// [-1, 0, 1, 2, 3] 1
// [-2, -1, 0, 1, 2] 2
// [-3, -2, -1, 0, 1] 3
// [ 1, -3, -2, -1, 0] 4
// [ 0, 1, 2, 3, -1] 0
// 激活页码
// left点击
// --0---1---2---3---4-
// [ 0, 1, 2, 3, -1] 0
// [ 1, -3, -2, -1, 0] 4
// [-3, -2, -1, 0, 1] 3
// [-2, -1, 0, 1, 2] 2
// [-1, 0, 1, 2, 3] 1
// [ 0, 1, 2, 3, -1] 0
如何添加过渡?
这里不能直接全部子组件添加过渡,因为这样或导致切换的闪烁
当激活页右侧的组件需要直接过渡到最左侧时,会从页面的最上方经过,结合数组就是 距离为正数的组件突然变为了距离负数
可以只根据切换过程中需要展示的组件添加过渡
具体
在偏移时我们需要 激活页和偏移目标页添加动画效果,其他的不需要(避免因为都有动画导致闪烁)
所以需要动态的添加和删除组件动画的方法
实现思路, 创建一个方法接受 两个索引, 根据索引找到对应组件添加动画
这里根据向左,向右两种情况添加动画
因为在距离数组中-1表示的是激活页左边的组件,0表示激活页, 1表示激活页右边的组件
所以我们这里传入对应的距离,在内部方法会找到对应的组件添加过渡
垂直实现?
根据使用者传入的参数来决定子组件是translateX
还是translateY
总体难点攻克方法
每次激活页码更新 --> 得到每个子组件的偏移值 --> 给指定组件预制添加动画 --> 传递子组件对应偏移并启动
代码(逻辑)
ty-carousel
<script>
export default {
name: 'ty-carousel',
props: {
// 盒子整体高度
height: {
type: [String],
default: '300px'
},
// 初始化激活页面
initialIndex: {
type: [Number],
default: 0
},
// 是否自动切换
autoPlay: {
type: [Boolean],
default: true
},
// 切换间隔时间
interval: {
type: [Number],
default: 2000
},
// 指示器是否显示在外侧
indicatorOutside: {
type: [Boolean],
default: false
},
// 是否显示指示器
indicator: {
type: [Boolean],
default: true
},
// 箭头展示方式
arrow: {
type: [String],
default: 'none'
},
// 切换时触发的回调
change: {
type: Function
},
// 轮播方向
verticalPlay: {
type: [Boolean],
default: false
}
},
data() {
return {
playControl: {
// 当前播放的序号
currentPage: -1,
// 循环列表
list: [],
// 位置列表
positionArray: [],
},
// 节流标识
bool: true,
// 方向
direction: 'right',
// control 控件是否展示
isControlShow: false,
// 循环的定时器标识
Timer: null,
// 指示器位置 ouside
isOutSide: false,
// 是否显示指示器
isIndicator: true,
// 箭头展示方式
arrowMode: 'none'
}
},
created() {
// 初始化激活页面
this.playControl.currentPage = this.initialIndex;
this.$nextTick(() => {
// 筛选出符合条件的slot
this.playControl.list = this.$children.filter(child => child.$options.name === 'ty-carousel-item')
if (this.playControl.list.length < 2) {
console.warn('warn 请最少使用2个及以上的数据')
}
})
},
mounted() {
// 初始化各组件位置
this.updateChildItemPosition()
},
methods: {
// 鼠标进入可视区
enterMain() {
// 关闭定时器
this.closeSetInter();
if (this.arrowMode == 'none') {
// 展示控件
this.isControlShow = true
}
},
// 鼠标离开可视区
leaveMain() {
// 开启定时器
if (this.autoPlay) {
this.openSetInter(this.withRight);
}
if (this.arrowMode == 'none') {
// 展示控件
this.isControlShow = false
}
},
// 点击dot到指定位置
pushPage(index) {
// 节流
if (this.bool) {
this.bool = false;
setTimeout(() => {
if (index - this.playControl.currentPage > 0) {
// 相较激活页向右
this.direction = 'right'
this.updateChildAnimation()
this.playControl.currentPage = index
this.updateChildItemPosition()
} else {
// 相较激活页向左
this.direction = 'left'
// 更新需要展示的组件动画
this.updateChildAnimation()
// 更新激活页
this.playControl.currentPage = index
// 更新子组件位置
this.updateChildItemPosition()
}
this.bool = true
}, 100)
}
},
// 点击左侧按钮
withLeft() {
// 节流
if (this.bool) {
this.bool = false
setTimeout(() => {
this.direction = 'left'
// 更新需要加载过渡的组件
this.updateChildAnimation()
// 更新激活页码
this.playControl.currentPage = this.playControl.currentPage <= 0 ? this.playControl.list.length - 1 : this.playControl.currentPage - 1
// 更新子组件位置
this.updateChildItemPosition()
this.bool = true
}, 300)
}
},
// 点击右侧按钮
withRight() {
// 同左击事件
if (this.bool) {
this.bool = false
setTimeout(() => {
this.direction = 'right'
this.updateChildAnimation()
this.playControl.currentPage = this.playControl.currentPage >= this.playControl.list.length - 1 ? 0 : this.playControl.currentPage + 1
this.updateChildItemPosition()
this.bool = true
}, 300)
}
},
// 更新子组件位置的方法
updateChildItemPosition() {
/*
解决思路: 创建一个数组保存每一个组件距离激活页的距离,下表格的数组每一项表示距离当前展示组件的距离
如: 第一行数组表示在激活页码为0时, 第一个组件距激活组件的距离为0, 第二个组件距激活组件的距离为1
依次类推
这样可以获得每一个组件在不同的激活页下距离激活页的距离, 将数组对应的距离值分配给对应的子组件
子组件根据距离使用transform动态偏移(距离1, 则偏移1*100%, 距离-1 则偏移-1*100%)
无缝:
因为这里每个组件是单独偏移,所以无缝推荐使用左右补充的方式(也方便为card模式做准备),不推荐使用
首张末张复制插入的方式(需要复制组件或vnode节点)
无缝思路
对循环情况分类:
1 左右都有组件 --> 正方向递增, 负方向递减
2 左没有组件 --> 正方向递增, 留数组末尾为距离-1
3 右没有组件 --> 负方向递减, 留数组首项为距离1
具体结合数组思考
*/
this.$nextTick(() => {
// right
// --0---1---2---3---4---> 对应组件序号
// [ 0, 1, 2, 3, -1] 0
// [-1, 0, 1, 2, 3] 1
// [-2, -1, 0, 1, 2] 2
// [-3, -2, -1, 0, 1] 3
// [ 1, -3, -2, -1, 0] 4
// [ 0, 1, 2, 3, -1] 0
// 激活页码
// left
// --0---1---2---3---4-
// [ 0, 1, 2, 3, -1] 0
// [ 1, -3, -2, -1, 0] 4
// [-3, -2, -1, 0, 1] 3
// [-2, -1, 0, 1, 2] 2
// [-1, 0, 1, 2, 3] 1
// [ 0, 1, 2, 3, -1] 0
// '当前在最开始的一项'
if (this.playControl.currentPage == 0) {
// console.log('当前在最开始的一项');
this.playControl.positionArray = this.playControl.list.map((item, index) => {
if (index == this.playControl.list.length - 1) {// 当激活页为首组件时, 末项为-1
return -1
} else {
return index - this.playControl.currentPage// 正方向递增
}
})
// '当前在最后一项'
} else if (this.playControl.currentPage == this.playControl.list.length - 1) {
// console.log('当前在最后一项');
this.playControl.positionArray = this.playControl.list.map((item, index) => {
if (index == 0) { // 当激活页为最后的组件时, 首项为1
return 1
} else {// 负方向递减
return index - this.playControl.currentPage
}
})
//'当前在中间项'
} else {
// console.log('当前在中间项');
this.playControl.positionArray = this.playControl.list.map((item, index) => {
if (index == this.playControl.currentPage) {
return 0
} else {
return index - this.playControl.currentPage // 正方向++, 负方向--
}
})
}
// 获得的不同情况下组件距离列表
// console.log(this.playControl.positionArray);
this.playControl.list.forEach((item, index) => {
// 触发每个子组件的更新位置方法, 传入对应组件的距离
item.updatePositionNew(this.playControl.positionArray[index], this.verticalPlay)
})
})
},
// 更新子组件动画效果的方法
updateChildAnimation() {
/*
在偏移时我们需要 激活页和偏移目标页添加动画效果,其他的不需要(避免因为都有动画导致闪烁)
所以需要动态的添加和删除组件动画的方法
实现思路, 创建一个方法接受 两个索引, 根据索引找到对应组件添加动画
这里根据向左,向右两种情况添加动画
因为在距离数组中-1表示的是激活页左边的组件,0表示激活页, 1表示激活页右边的组件
所以我们这里传入对应的距离,在内部方法会找到对应的组件添加过渡
*/
this.direction == 'left' ? this.loadAnimation(-1, 0) : this.loadAnimation(1, 0)
},
// 筛选及加载动画
loadAnimation(start, end) {
// 找到要添加动画的组件
// AnimationEleIndexList ---> 添加过渡组件序号array
// this.playControl.positionArray --> 子组件距离列表
let AnimationEleIndexList = [this.playControl.positionArray.indexOf(start), this.playControl.positionArray.indexOf(end)];
// this.playControl.list ---> 子组件列表
this.playControl.list.forEach((item, index) => {
if (AnimationEleIndexList.includes(index)) { // 对应组件添加过渡
item.$el.style.transition = 'transform .4s'
} else { // 其他组件删除过渡
item.$el.style.transition = ''
}
})
},
// 开启循环定时器
openSetInter(callback) {
// 循环触发向右点击
this.Timer = setInterval(() => {
callback();
}, this.interval)
},
// 关闭循环定时器
closeSetInter() {
if (this.Timer) {
clearInterval(this.Timer);
this.Timer = null;
}
}
},
watch: {
autoPlay: {
handler(newV) {
if (typeof newV == 'boolean' && newV) {
// 开启定时(循环)
this.openSetInter(this.withRight)
} else {
// 关闭定时(循环)
this.closeSetInter()
}
}, immediate: true
},
indicatorOutside: {
handler(newV) {
if (typeof newV == 'boolean') {
this.isOutSide = newV
}
}, immediate: true
},
indicator: {
handler(newV) {
if (typeof newV == 'boolean') {
this.isIndicator = newV
}
}, immediate: true
},
arrow: {
handler(newV) {
if (newV == 'show') {
this.arrowMode = 'show'
} else if ((newV == 'hidden')) {
this.arrowMode = 'hidden'
} else {
this.arrowMode = 'none'
}
}, immediate: true
},
'playControl.currentPage': {
handler(newV) {
this.$emit('change', newV)
}
}
},
// 关闭定时器
beforeDestroy() {
this.closeSetInter()
}
}
</script>
<style lang='less' scoped>
@import '../../../css/carousel.less';
</style>
ty-carousel-item
<template>
<div ref="item" class='ty-carousel-item' :style="{ 'transform': positionOffset }">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ty-carousel-item',
data() {
return {
// 偏移值
positionOffset: 0,
}
},
methods: {
// 更新位置
// params : 偏移值 是否垂直
updatePositionNew(position, vertical) {
this.positionOffset = vertical ? `translateY(${position * 100}%)` : `translateX(${position * 100}%)`
}
},
}
</script>
<style lang='less' scoped>
.ty-carousel-item {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #d3dce6;
}
</style>