造轮子-轮播

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
    }
+ }

手机上触摸滑动
  1. 为什么获取滑动点的坐标用的是e.touches[0]而不是e.touch
    答:因为存在多点触控e.touches获取的是用户手指的数量,也就是屏幕触摸点的数量,e.touches[0]获取的就是第一个触摸点(也就是第一个手指),所以我们需要判断如果触摸点的个数大于1(有一个以上的手指触摸)就直接return
  2. 如何确定是往左滑动还是往右滑动?
    只需通过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()
},

添加单元测试

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容