banner图新思路

Published on
发布于·预估阅读7分钟
    Authors
    • Name
      willson-wang
      Twitter

    一般banner图思路,就是父容器定宽,并且overflow: hidden;然后包裹一个一级子元素,一级子元素需要清除浮动带来的高度塌陷;最后让所有的二级子元素float: left; 轮播的原理就是不断的改变一级子元素的left, top or transformX, transformY来实现轮播动画;这里需要注意的是区分无缝轮播及有缝轮播;

    无缝轮播,在最首复制一个最末尾的元素,在最末复制一张最首的元素,这样就达到来无缝轮播;

    有缝轮播,不对首尾进行复制元素,所有从最末到最首及从最首到最末的时候,会滑过中间的元素;

    现在换一种新的banner实现思路,来实现无缝轮播,而有缝轮播跟一般的banner图思路是一致的;

    使用transition及transitionEnd来实现,通过设置transition的动画时间来控制动画过程;当过渡时间为0的时候,是不会触发transitionEnd动画的;

    目前transitionEnd的兼容性,兼容性加上前缀已经很不错了,可以放心的使用

    image

    两个关键点:轮播图片的处理、动画的设置

    轮播图的处理,不需要复制首尾两个元素,实现思路就是设置每个siwper-item的transformX,按照顺序排列,需要显示在视窗内的元素,位置总是在第二个位置,且transformX为0,其它的分布在左右;如下图所示

    image

    动画的设置分为移动时的动画与切换时的动画

    移动时的动画在touchmove事件内处理,通过pageX的差值来进行设置transformX,此时tansition时间为0 切换时的动画通过每个元素现在的transformX 减 or 加上视图的宽度or高度

    最后切换动画结束之后,在调整轮播元素的位置,始终将需要出现在视窗内的元素展示在第二个位置,并设置transformX为0

    关于自动轮播,一般的实现思路是开一个定时器,然后递归自己,当进入轮播图的时候清除当前的定时器,离开轮播区域之后又重新开启定时器;现在换一个更简单的思路,即切换页面的时候清除当前定时器,结束之后在开启定时器,这样整个逻辑内只有一个定时器存在;

    _auto() {
        this._stop()
        if (this.auto) {
            this.timer = setTimeout(() => {
                this.next()
            }, this.interval)
        }
    }
    
    _stop() {
        this.timer && clearTimeout(this.timer)
    }
    
    next() {
            this.slideDerection = 'nexted'
            this.currentIndex += 1
            this.go(this.currentIndex)
    }
    
    go(idx) {
        // 清除自动轮播定时器
        this._stop()
        if (idx > this.list.length - 1) {
            this.currentIndex = 0
        }
        if (idx < 0) {
            this.currentIndex = this.list.length - 1
        }
        this.forItems((item, key) => {
            this.setTransition(item, this.duration)
            let distance =
                this.slideDerection === 'nexted'
                    ? this._offset[key] - this.width
                    : this._offset[key] + this.width
            if (!this.loop && this.auto && this.currentIndex === 0) {
                distance = key * this.width
            }
            this.setTransform(item, distance)
        })
        // 设置完transform之后在开启定时器,因为transition的动画是小于定时器开启的时间的,所以离开后
        // 又可以自动轮播了,这里还可以放到transitionEnd内执行,跟之前的对比少监听了一些事件,减少了处理逻辑
        this._auto()
    }
    
    swiper.vue
    
    <template>
        <div class="slide-wrap" ref="box">
            <div class="slide-content" ref="slide" :style="{ height }" :currentIndex="currentIndex">
                <div
                    class="slide-item"
                    v-for="(item, index) in newList"
                    :class="[index === 1 && 'active']"
                    :key="index"
                >
                    <div v-if="!isBroadcast" class="slide-item__img" :style="{backgroundImage: `url(${item.img})`}"></div>
                    <div class="swiper-txt" v-else :style="{ height, lineHeight: height }">
                        {{ item.txt }}
                    </div>
                </div>
            </div>
            <div class="swiper-dots" v-if="showDots">
                <span
                    v-for="(item, index) in list"
                    :key="index"
                    :class="[index === currentIndex ? 'active' : '']"
                ></span>
            </div>
        </div>
    </template>
    
    <script>
    /**
     *  2 0 1
     *  0 1 2
     *  1 2 0
     *  2 0 1
     *
     *  2 0 1
     *  1 2 0
     *  0 1 2
     *  2 1 0
     *
     *  -375 -750
     *  0 -375
     *  375 0
     *
     *  -375 0
     *  0 375
     *  375 750
     *
     *  -375 -750
     *  0 -375
     *  375 0
     *  750 375
     *
        -750
        0
        750
        1500
        2250
        3000
    
        0 1 2 3 4 5
    
        5 0 1 2 3 4  cur = 0
        1 2 3 4 5 0
        2 3 4 5 0 1
    
        0 1 2 3 4 5  cur = 1  idx = 4
        3 4 5 0 1 2
    
        3 4 5 0 1 2  cur = 4 idx = 2
        1 2 3 4 5 0  cur = 2 idx = 1
        0 1 2 3 4 5
    
        0 1 2 3
      0 1 2 3
    0 1 2 3
     *  */
    import Swiper from './Swiper'
    
    export default {
        name: 'Swiper',
        props: {
            value: {
                type: Number,
                default: 0
            },
            list: {
                type: Array,
                default: () => {
                    return []
                }
            },
            height: {
                type: [String, Number]
            },
            duration: {
                type: Number,
                default: 300
            },
            auto: {
                type: Boolean,
                default: true
            },
            showDots: {
                type: Boolean,
                default: false
            },
            loop: {
                type: Boolean,
                default: false
            },
            direction: {
                type: String,
                default: 'horizontal'
            },
            isBroadcast: {
                type: Boolean,
                default: false
            },
            minMovingDistance: {
                type: Number,
                default: 30
            },
            interval: {
                type: Number,
                default: 4000
            }
        },
        data() {
            const index = this.value >= this.list.length ? 0 : this.value
            return {
                currentIndex: index || 0,
                newList: []
            }
        },
        computed: {
            newHeight() {
                return this.height
            }
        },
        methods: {
            init(index = 0) {
                const vm = this
                this.swiper && this.swiper.destory()
                this.swiper = new Swiper({
                    wrap: this.$refs.box,
                    currentIndex: index,
                    slideDerection: this.slideDerection,
                    list: this.newList,
                    duration: this.duration,
                    auto: this.auto,
                    loop: this.loop,
                    direction: this.direction,
                    minMovingDistance: this.minMovingDistance
                }).on('swiperEnd', function end(index) {
                    let idx = index
                    if (vm.loop && vm.list.length === 2) {
                        idx = index % 2
                    }
                    vm.currentIndex = idx
                    vm.$emit('swiperEnd', idx)
                })
            },
            reRender() {
                if (!this.$el) return
                this.$nextTick(() => {
                    this.destory()
                    this.currentIndex = this.value
                    this.getNewList()
                    this.init(this.value)
                })
            },
            getNewList() {
                let tempArr = JSON.parse(JSON.stringify(this.list))
                if (this.loop) {
                    if (tempArr.length === 2) {
                        tempArr = [...tempArr, ...tempArr]
                    }
                    // 需要显示的元素永远处于第二个位置
                    tempArr = [...tempArr.slice(this.currentIndex - 1), ...tempArr.slice(0, this.currentIndex - 1)]
                }
                return tempArr
            }
        },
        watch: {
            currentIndex(val) {
                this.$emit('input', val)
            },
            list(val, oldVal) {
                if (JSON.stringify(val) !== JSON.stringify(oldVal)) {
                    this.reRender()
                }
            }
        },
        created() {
            this.newList = this.getNewList()
        },
        mounted() {
            if (this.newList.length) {
                this.init(this.currentIndex)
            }
        },
        beforeDestory() {
            this.swiper && this.swiper.destroy()
        }
    }
    </script>
    

    demo