美文网首页
造轮子-轮播

造轮子-轮播

作者: sweetBoy_9126 | 来源:发表于2019-02-20 23:08 被阅读0次

    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()
    },
    
    

    添加单元测试

    相关文章

      网友评论

          本文标题:造轮子-轮播

          本文链接:https://www.haomeiwen.com/subject/ilncyqtx.html