1.最初的思路
当前窗口由第一个变成第二个:先把第二个放在第一个的后面,然后第一个做左滑动的动画,第二个也做向左滑动的动画,这样第二个就出现在当前窗口了,然后把第一个去掉。
第二个到第三个也是一样的,二和三同时做左滑的动画,三就出现在了当前窗口,然后去掉二。
第三个到第一个:把第一个放在第三个的后面,同时做左滑的动画,然后干掉第三个
1.1. 最初的代码
- sliders.vue
<template>
<div class="lf-sliders">
<div class="lf-sliders-window" ref="window">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "LiFaSliders",
}
</script>
<style scoped>
</style>
- demo.vue
<template>
<div>
<lf-sliders>
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</lf-sliders>
</div>
</template>
<script>
import LfSliders from './slides.vue'
export default {
name: "demo",
components: {
LfSliders
},
data(){
return {
}
},
methods: {
},
created() {
}
}
</script>
<style scoped>
.box{
width: 100px;
height: 100px;
background: red;
border: 1px solid gray;
}
</style>
我们要让它其中的一个显示,但是我们的组件里只有一个slot,我们怎么能知道哪个是第一个?
(1).通过vue的this.$children
来获取(但是他只能获取子组件的,而我们当前组件没有子组件)
(2). dom操作通过this.$refs.window.children
- slides.vue
mounted() {
console.log(this.$children)
console.log(this.$refs.window.children)
}
我们不想用dom操作,所以需要加一个子组件lf-sliders-item
1.2. 改进后的代码
- demo.vue
<template>
<div>
<lf-sliders>
<lf-sliders-item>
<div class="box">1</div>
</lf-sliders-item>
<lf-sliders-item>
<div class="box">2</div>
</lf-sliders-item>
<lf-sliders-item>
<div class="box">3</div>
</lf-sliders-item>
</lf-sliders>
</div>
</template>
- slliders-item.vue
<template>
<div class="lf-sliders-item">
<slot></slot>
</div>
</template>
我们需要给每一个item一个属性visible来控制它是否显示,正常情况下我们应该在sliders里传入一个visible,然后在item中通过props接受这个visible,但是我们sliders里面用的是slot没有lf-sliders-item标签,所以我们没法在item中通过props接收这个visible,只能在item里的data中传入一个visible
- lf-sliders-item
<template>
<div class="lf-sliders-item" v-if="visible">
<slot></slot>
</div>
</template>
<script>
export default {
name: "sliders-item",
data(){
return {
visible: false
}
}
}
</script>
- sliders.vue
<template>
<div class="lf-sliders">
<div class="lf-sliders-window" ref="window">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "LiFaSliders",
mounted() {
let first = this.$children[0]
first.visible = true
}
}
</script>
1.3. 简单的实现从1到2的过程
注意:因为我们把一开始进入前的位置设为了100%,而如果我们不给item绝对定位的话,由于第二张本来就在第一个后面,所以你再加上一开始的100%中间就会有一张的空白,但是如果我们都绝对定位的话,外层就会没有高度,所以我们必须至少保证有一个不绝对定位,又因为轮播是先当前这张离开,然后才后面的进来,所以我们需要设置离开的时候那一张绝对定位
- slides.vue
<template>
<div class="lf-slides">
<div class="lf-slides-window" ref="window">
<div class="lf-slides-wrapper">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LiFaslides",
mounted() {
let first = this.$children[0]
let second = this.$children[1]
first.visible = true
setTimeout(()=>{
first.visible = false
second.visible = true
},3000)
}
}
</script>
<style scoped lang="scss">
.lf-slides{
display: inline-block;
border: 1px solid black;
&-wrapper{
position: relative;
display: flex;
}
}
</style>
- slides-item.vue
<template>
<transition name="fade">
<div class="lf-slides-item" v-if="visible">
<slot></slot>
</div>
</transition>
</template>
<script>
export default {
name: "slides-item",
data() {
return {
visible: false
}
}
}
</script>
<style scoped lang="scss">
.fade-enter-active, .fade-leave-active {
transition: all .3s;
}
.fade-enter {
transform: translateX(100%);
}
//保证有一个不绝对定位,可以让父级有宽高
.fade-leave-active{
position: absolute;
left: 0;
top: 0;
}
.fade-leave-to {
transform: translateX(-100%);
}
</style>
基本实现轮播
给sliders传入一个selected和给item传入一个name,selected的值就是item的name,对应的就是哪个先显示,这个selected还得通知到每个item组件,因此每个item还需要声明一个selected,默认为undefined,在sliders中通过mounted遍历到每个item组件让他们的selected等于sliders里的selected,如果没传就等于item第一个组件的name,之后我们只需要更新selected就可以,但是因为mounted只能执行一次,所以后期我们更新的selected不会通知到item组件,所以我们需要在sliders中通过updated事件再次执行mounted中的更新selected的方法
- demo.vue
<template>
<div>
<lf-sliders :selected="selected">
<lf-sliders-item name="1">
<div class="box">1</div>
</lf-sliders-item>
<lf-sliders-item name="2">
<div class="box">2</div>
</lf-sliders-item>
<lf-sliders-item name="3">
<div class="box">3</div>
</lf-sliders-item>
</lf-sliders>
</div>
</template>
<script>
import LfSliders from './slides.vue'
import LfSlidersItem from './sliders-item.vue'
export default {
name: "demo",
components: {
LfSliders,
LfSlidersItem
},
data(){
return {
selected: '1'
}
},
methods: {
},
created() {
let n = 1
setInterval(()=>{
n++
if(n === 4){
n = 1
}
this.selected = n.toString()
},3000)
}
}
</script>
<style scoped>
.box{
width: 100px;
height: 100px;
background: red;
border: 1px solid gray;
}
</style>
- slides.vue
<template>
<div class="lf-slides">
<div class="lf-slides-window" ref="window">
<div class="lf-slides-wrapper">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LiFaslides",
props: {
selected: {
type: String
}
},
mounted() {
this.updateChildren()
},
updated() {
this.updateChildren()
},
methods: {
updateChildren(){
let first = this.$children[0]
this.$children.forEach((vm)=>{
vm.selected = this.selected || first.$attrs.name
})
}
}
}
</script>
<style scoped lang="scss">
.lf-slides{
display: inline-block;
border: 1px solid black;
&-wrapper{
position: relative;
display: flex;
}
}
</style>
- slides-item.vue
<template>
<transition name="fade">
<div class="lf-slides-item" v-if="visible">
<slot></slot>
</div>
</transition>
</template>
<script>
export default {
name: "slides-item",
data() {
return {
selected: undefined
}
},
props: {
name: {
type: String,
required: true
}
},
computed: {
visible(){
return this.selected === this.name
}
}
}
</script>
<style scoped lang="scss">
.fade-enter-active, .fade-leave-active {
transition: all .3s;
}
.fade-enter {
transform: translateX(100%);
}
.fade-leave-active{
position: absolute;
}
.fade-leave-to {
transform: translateX(-100%);
}
</style>
向左滑动实现
思路:给slides传入一个autoPlay属性,默认为true,然后将上面的setTImeout放到slides组件中,定义一个automaticPlay方法,在这个方法里通过遍历子组件拿到一个names数组,然后从names数组里找当前的selected是第几个,之后触发父组件的update:selected事件,将names[index]传给父组件,然后对index进行++,之后通过setTimout反复的调用
- slides.vue
export default {
name: "LiFaslides",
props: {
selected: {
type: String
},
autoPlay: {
type: Boolean,
default: true
}
},
mounted() {
this.updateChildren()
this.automaticPlay()
},
updated() {
this.updateChildren()
},
methods: {
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.selected = selected
})
},
automaticPlay(){
let names = this.$children.map((vm)=>{
return vm.name
})
let selected = this.getSelected()
//拿到每一次的索引值,下次动画好在基础上累加
let index = names.indexOf(selected)
let run = ()=>{
this.$emit('update:selected',names[index])
index++
if(index > names.length-1){
index = 0
}
setTimeout(()=>{
run()
},3000)
}
run()
},
getSelected(){
let first = this.$children[0]
return this.selected || first.$attrs.name
}
}
}
反向滚动
只需要将index++改成--,但是如果直接用--names[index]
就会出问题,所以需要一开始的时候对index进行判断,让newindex=index-1,如果newindex小于
0就让它等于最后一个也就是names.length-1如果大于等于names.length就让它等于0,然后每次触发update:selected更新的时候都触发一个select,把当前的index传入,然后通过select的时候拿到当前的现在选中的index给lastSelectedIndex属性(这里还包括点击某一个控制点,点击触发select的时候先把当前的selected给lastSelectedIndex,然后再更新selected为你点击的那个点的索引和值),接着触发update把最新的index传进去,通过对比lastSelectedIndex和selectedIndex来确定是否给item添加一个reverse的类
- sides.vue
<ul class="dots">
<li v-for="n in childrenLength" :class="{active: selectedIndex === n-1}"
@click="select(n-1)"
>
{{n-1}}
</li>
</ul>
export default {
name: "LiFaslides",
props: {
selected: {
type: String
},
autoPlay: {
type: Boolean,
default: true
}
},
data(){
return {
childrenLength: 0,
lastSelectedIndex: undefined
}
},
mounted() {
this.childrenLength = this.$children.length
this.updateChildren()
this.automaticPlay()
},
updated() {
this.updateChildren()
},
computed: {
selectedIndex(){
return this.names.indexOf(this.selected) || 0
},
names(){
return this.$children.map(vm=>vm.name)
}
},
methods: {
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.selected = selected
vm.reverse = this.selectedIndex > this.lastSelectedIndex ? false : true
})
},
automaticPlay(){
let selected = this.getSelected()
//拿到每一次的索引值,下次动画好在基础上累加
let index = this.names.indexOf(selected)
let run = ()=>{
let newIndex = index -1
if(newIndex < 0){
newIndex = this.names.length - 1
}
if(newIndex === this.names.length){
newIndex = 0
}
this.select(newIndex)
setTimeout(()=>{
run()
},3000)
}
setTimeout(run, 3000)
},
getSelected(){
let first = this.$children[0]
return this.selected || first.$attrs.name
},
select(index){
//当选中新的index的时候,就把旧的index赋给lastSelectedIndex
this.lastSelectedIndex = this.selectedIndex
//然后把新的index和选中值传给selected
this.$emit('update:selected',this.names[index])
}
}
}
- sides-item.vue
<template>
<transition name="fade">
<div class="lf-slides-item" v-if="visible" :class="{reverse}">
<slot></slot>
</div>
</transition>
</template>
<script>
export default {
name: "slides-item",
data() {
return {
selected: undefined,
reverse: false
}
},
props: {
name: {
type: String,
required: true
}
},
computed: {
visible(){
return this.selected === this.name
}
}
}
</script>
<style scoped lang="scss">
.lf-slides-item{
width: 100%;
}
.fade-enter-active, .fade-leave-active {
transition: all 1s;
}
.fade-enter {
transform: translateX(100%);
}
.fade-enter.reverse{
transform: translateX(-100%);
}
.fade-leave-active{
position: absolute;
}
.fade-leave-to {
transform: translateX(-100%);
}
.fade-leave-to.reverse{
transform: translateX(100%);
}
</style>
解决动画混乱的bug
上次的代码中,会发现不管让它正向还是反向他最后类里都会有一个reverse,所以就会造成正向的时候同时出现正向和反向的动画,之所以会一直有这个reverse的类存在是因为我们在updateChildren中虽然立即把reverse改了,但是不代表它会立即生效在dom上面,他有可能是放在了一个任务队列里。解决办法:在reverse生效后再去更改当前显示的selected,也就是延迟更改(在下一次的时候更改)通过nextTick就可以
- slides.vue
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.reverse = this.selectedIndex > this.lastSelectedIndex ? false : true
this.$nextTick(()=>{
vm.selected = selected
})
})
},
解决每次只滚动显示两张图的bug
在自动播放的方法里通过console发现index每次都是初始值,比如初始值是1,那么每次都是1,所以图片只显示两张,我们需要每次setTimout的时候index的值都要变化,可以在结束的时候让index = newIndex
automaticPlay(){
let selected = this.getSelected()
//拿到初始的索引值
let index = this.names.indexOf(selected)
let run = ()=>{
let newIndex = index -1
if(newIndex < 0){
newIndex = this.names.length - 1
}
if(newIndex === this.names.length){
newIndex = 0
}
index = newIndex
this.select(newIndex)
setTimeout(()=>{
run()
},3000)
}
setTimeout(run, 3000)
},
实现鼠标经过轮播图,轮播暂停,离开继续
思路:给外层加一个mouseenter和mouseleave监听事件,然后加一个timerId属性,让setTimeout等于它,鼠标经过清除setTImoue并把timeId置为null,鼠标离开再次执行自动滚动方法
- slides.vue
<div class="lf-slides-window" ref="window" @mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
methods: {
onMouseEnter(){
this.pause()
},
onMouseLeave(){
this.automaticPlay()
},
pause(){
window.clearTimeout(this.timerId)
this.timerId = null
},
autoMaticPlay(){
+//如果当前正在轮播中就不再次执行这个方法
+ if(this.timerId){
+ return
}
}
}
解决反向的时候从第一张到最后一张动画是正向动画的问题
方法:只需要在上一个选中的index等于0并且当前选中的是最后一个的索引(this.names.length-1)的情况下让item的reverse属性为true就可以
updateChildren(){
let selected = this.getSelected()
this.$children.forEach((vm)=>{
vm.reverse = this.selectedIndex > this.lastSelectedIndex ? false : true
//如果上一张是第一张,当前这张是最后一张(也就是反向动画的时候)就让它依然是反向动画
+ if(this.lastSelectedIndex === 0 && this.selectedIndex === this.names.length-1){
+ vm.reverse = true
+ }
//如果上一张是最后一张,当前这张是第一张(也就是正向动画的时候)就让它依然是正向
+ if(this.lastSelectedIndex === this.names.length-1 && this.selectedIndex === 0){
+ vm.reverse = false
+ }
this.$nextTick(()=>{
vm.selected = selected
})
})
},
点击的时候动画方向不对
针对上面的代码自动滚动的时候动画都是正常的,但是当我们点击的下面的对应点的时候,比如我正向的时候当前显示的是第一个,我点第三个就会发现动画是反向的,这就是因为我们上面做的判断,所以我们要把这个判断加到自动滚动的方法中去
//如果是自动滚动的情况下
+ if(this.timerId){
//如果上一张是第一张,当前这张是最后一张(也就是反向动画的时候)就让它依然是反向动画
if(this.lastSelectedIndex === 0 && this.selectedIndex === this.names.length-1){
vm.reverse = true
}
//如果上一张是最后一张,当前这张是第一张(也就是正向动画的时候)就让它依然是正向
if(this.lastSelectedIndex === this.names.length-1 && this.selectedIndex === 0){
vm.reverse = false
}
+ }
手机上触摸滑动
- 为什么获取滑动点的坐标用的是e.touches[0]而不是e.touch
答:因为存在多点触控e.touches获取的是用户手指的数量,也就是屏幕触摸点的数量,e.touches[0]获取的就是第一个触摸点(也就是第一个手指),所以我们需要判断如果触摸点的个数大于1(有一个以上的手指触摸)就直接return - 如何确定是往左滑动还是往右滑动?
只需通过touchstart时获取e.touches[0]的clientX和touchEnd的时候changedTouches[0]的clientX,将开始点的clientX通过一个属性存下来,然后比较两者的大小,如果结束的时候的距离,大于开始的时候就是右滑了,否则就是左滑,然后分别对应着让当前选中的index加1或减1
- slides.vue
<div class="lf-slides-window" ref="window" @touchstart="onTouchStart"
@touchmove="onTouchMove" @touchend="onTouchEnd"
>
methods: {
onTouchStart(e){
if(e.touches.length > 0){return}
this.touchStart = {clientX:e.touches[0].clientX,clientY:e.touches[0].clientY}
this.pause()
},
onTouchMove(e){
console.log(e)
},
onTouchEnd(e){
let {clientX,clientY} = e.changedTouches[0]
if(clientX > this.touchStart.clientX){
this.select(this.selectedIndex + 1)
}else{
this.select(this.selectedIndex - 1)
}
this.automaticPlay()
},
select(newIndex){
if(newIndex < 0){
newIndex = this.names.length - 1
}
if(newIndex >= this.names.length){
newIndex = 0
}
//让newIndex等于条件内的newIndex
this.newIndex = newIndex
//当选中新的index的时候,就把旧的index赋给lastSelectedIndex
this.lastSelectedIndex = this.selectedIndex
//然后把新的index和选中值传给selected
this.$emit('update:selected',this.names[newIndex])
},
automaticPlay(){
//如果当前正在轮播中就不再次执行这个方法
if(this.timerId){
return
}
let selected = this.getSelected()
//拿到初始的索引值
let index = this.names.indexOf(selected)
let run = ()=>{
this.newIndex = index +1
this.select(this.newIndex)
index = this.newIndex
this.timerId =setTimeout(()=>{
run()
},3000)
}
this.timerId = setTimeout(run, 3000)
},
}
上面的代码虽然已经实现了左右滑动,但是如果我没有左右滑,我是上下滑了(上下翻页),他依然会根据你最后clientX的偏移俩执行左滑或右滑时的函数
比如根据上图我们明显能知道第二张是翻页,第三个是滑动,而第一个我们可以根据我们自己的设定,比如开始和结束后的角度如果小于三十度就是在滑动,否则就是翻页,而角度的确定我们可以根据三十度直角三角形特性,三十度角所对的直角边是斜边的一半,所以针对下图只要是斜边除以垂直的直角边大于2就说明是小于三十度,再让他执行左滑右滑的方法
上图2x那条边的距离就是
垂直的直角边的距离就是|y2-y1|
onTouchEnd(e){
let {clientX,clientY} = e.changedTouches[0]
+ let [x1,y1] = [this.touchStart.clientX,this.touchStart.clientY]
+ let [x2,y2] = [clientX,clientY]
+ let distance = Math.sqrt(Math.pow(x2 - x1,2) + Math.pow(y2 - y1,2))
+ let deltaY = Math.abs(y2-y1)
+ let rate = distance / deltaY
+ if(rate > 2){
if(clientX > this.touchStart && this.touchStart.clientX){
this.select(this.selectedIndex - 1)
}else{
this.select(this.selectedIndex + 1)
}
+ }
this.automaticPlay()
},
网友评论