美文网首页
使用vue仿某东魔芋导航

使用vue仿某东魔芋导航

作者: 羽晞yose | 来源:发表于2020-05-15 19:16 被阅读0次
    Animation.gif

    以前用的GifCam录制,图片失真严重,现在换了ScreenToGif,交互的时候卡的可以,所以图片录制会出现拖动对不上,但真实结果是对的,瞎比比到此为止

    某东之前活动出现过这种魔芋导航(我也不知道他们叫什么,但是看到他们注释里写moyu),没完全重现他们的实现,某东这块导航总共展示9个,上环4个下环5个,实际业务不需要。没去撸他们的实现是因为他们这玩意有点小问题,所以撸了还得自行修复。压缩过的代码过滤出逻辑,还要跟着他们思维来修复太麻烦,所以自己实现一个


    交互动效分析:

    可无限增加广告图,环形结构,为什么不说是圆形,如果以圆形来做,这个圆形的半径将会成为实现的最大限制,而且以圆形来做还有一个问题,圆形自转的时候,每个小圆必然需要反向自转,那么结果就是每一次交互你都必须对接近20个小圆节点进行操作(20是我直接用css重现排列得出来数字,实际以上图效果为例结果需要17个),因此可以断定这里肯定是模拟了一个环形轮播滚动


    这里的手势操作,我使用的是腾讯手势库 AlloyFinger,还加了一个 gsap 动画库,当然这个动画库可有可无,因为我不想写太多 setAttribute,而且项目时间需要我快速实现,所以能不重复造轮子就直接用吧~

    上代码:

    <template>
        <section class="arc-swiper"
            v-finger:touch-move="pressMoveHandle"
            v-finger:touch-end="touchEndHandle">
            <ul ref="arcSwiper" class="circle-list js-arc">
                <li class="preserve-3d" v-for="(item, index) of copyCircleList" :key="index">
                    <a class="circle" v-href="item.link" v-stat="'IMG_LINK_ARC_SWIPER_'+index">
                        <img class="covimg" :src="tool.addDomain(item.img_src)" />
                    </a>
                </li>
            </ul>
        </section>
    </template>
    
    <script>
    /**
     * @author yose
     * @date 2020/2/19
     * @description 弧形轮播广告图,借助腾讯手势库 + TweenMax动画库实现
     * @description 全部数值均为取整后的数值,所以视觉上可能会有点对不正,但是不取正里面很多计算会有偏差
     **/
    
    import Tool from '@/mod/util/tool/1.0/tool.js'; // 工具函数组件
    import { TweenMax } from 'gsap'; // gsap动画库
    import AlloyFinger from 'alloyfinger'; // 腾讯轻量级手势库
    import AlloyFingerPlugin from 'alloyfinger/vue/alloy_finger_vue';
    
    // 插件注册
    Vue.use(AlloyFingerPlugin, {
        AlloyFinger
    });
    
    export default {
        name: 'arc-swiper',
    
        props: {
            list: {
                type: Array,
                default: () => []
            }
        },
    
        data () {
            return {
                tool: Tool,
                // 前后要复制的数据数量
                copyCount: 2,
                // 前后复制N条后的数据
                copyCircleList: [],
                // 圈圈的节点数组
                circles: [],
                // 每个圈圈的偏移量(offsetLeft)
                circlesOffsetLeft: [],
                // 已滚动距离
                distance: 0,
                // 视窗长度
                winWidth: window.innerWidth,
                // 自动播放间隔时间
                autoPlayTime: 3000,
                // 类似于setInterval的id,用于控制动画是否暂停
                tick: true,
                // setTimeout的id
                delayId: ''
            };
        },
    
        computed: {
            // 起始点
            arriveStart () {
                return ~~(this.circlesOffsetLeft[this.copyCount] - this.winWidth / 2);
            },
            // 终止点
            arriveEnd () {
                return ~~(this.circlesOffsetLeft[this.circlesOffsetLeft.length - this.copyCount] - this.winWidth / 2);
            },
            // 每个圈圈占据的空间大小(自身大小 + 间距)
            circleSpace () {
                return ~~(this.circlesOffsetLeft[1] - this.circlesOffsetLeft[0]);
            },
            // 高度半径,圆弧最大高度值(其实就是视觉稿该区域高度值,-40是为了下沉不会超出该区域)
            radius () {
                return this.$refs.arcSwiper.clientHeight - 40;
            }
        },
    
        methods: {
            // 增加节点,前后各复制多this.copyCount个
            copyVDom () {
                let end = this.list.length;
                let unshiftDom = this.list.slice(end - this.copyCount, end);
                let pushDom = this.list.slice(0, this.copyCount);
    
                this.copyCircleList = unshiftDom.concat(this.list).concat(pushDom);
            },
            // 初始化数据
            initAttribute () {
                // 将节点类数组直接转数组存储,方便后续直接使用
                this.circles = Array.from(document.querySelectorAll('.preserve-3d'));
    
                this.circles.forEach(item => {
                    this.circlesOffsetLeft.push(~~(item.offsetLeft));
                });
    
                this.distance = -this.arriveStart;
            },
            // 重置滚动位置
            resetPosition () {
                // 到达起始点 瞬间移动,需要将原本偏移节点重新归位
                if (-this.distance <= this.arriveStart) {
                    TweenMax.set(this.circles, { y: 0 });
                    this.distance = -this.arriveEnd;
                    return;
                }
    
                // 到达终止点 瞬间移动,需要将原本偏移节点重新归位
                if (-this.distance >= this.arriveEnd) {
                    TweenMax.set(this.circles, { y: 0 });
                    this.distance = -this.arriveStart;
                }
            },
            // 滚动,同时计算每个圈圈的位置(关键函数)
            computedPosition () {
                let activeSliders = [];
                let space = this.circleSpace; // 每个圈圈占据的空间大小(自身大小 + 距离下一个的距离)
                let translate = this.distance; // 已经滚动距离
                let enter = translate + space / 2; // 进入动画序列的距离
                let winWidth = this.winWidth; // 视图窗口长度
    
                this.circlesOffsetLeft.forEach((item, index) => {
                    // 进入动画序列的swiper-slide
                    if (-item + this.winWidth > enter) activeSliders.push(this.circles[index]);
    
                    // 离开动画序列的swiper-slide
                    if (translate + item < -space / 2) activeSliders.shift();
                });
    
                activeSliders.forEach((item, index) => {
                    let itemIndex = this.circles.indexOf(item);
                    let offsetLeft = this.circlesOffsetLeft[itemIndex] - this.winWidth;
                    let dValue = -(translate + offsetLeft + space / 2);
                    let percentage = dValue / winWidth > 0.5 ? (1 - dValue / winWidth).toFixed(2) : (dValue / winWidth).toFixed(2);
                    let translateY = this.radius * percentage;
    
                    TweenMax.set(item, { y: ~~(translateY) });
                });
    
                TweenMax.set(this.$refs.arcSwiper, {
                    x: this.distance
                });
    
                return activeSliders;
            },
            // 获取下一个边界值
            getNextIndex () {
                let nextIndex = 0;
    
                // 如果不循环,是否有更好的办法?获取下一个比目标值大的数,如果圈圈超过中线一半,则认为应该滑到下一个,否则为当前这个
                this.circlesOffsetLeft.every((item, index) => {
                    nextIndex = index;
                    return -this.distance >= ~~(item - this.winWidth / 2) + this.circleSpace / 2;
                });
    
                return nextIndex;
            },
            // 获取目标的边界值
            getCritical (index) {
                return ~~(this.circlesOffsetLeft[index] - this.winWidth / 2);
            },
            ticker (fn) {
                window.requestAnimationFrame(() => {
                    fn();
                    if (this.tick) this.ticker(fn);
                });
            },
            // 自动播放
            autoPlay () {
                this.tick = true;
    
                this.delayId = setTimeout(() => {
                    // 不能用下一个目标的值来算,因为有瞬间移动,将导致动画一直无法到达最后两个目标值
                    let distance = this.circleSpace;
    
                    this.ticker(() => {
                        distance -= 8;
                        this.distance -= 8;
    
                        this.resetPosition();
                        this.computedPosition();
    
                        if (distance <= 0) {
                            this.tick = false;
                            // 这里需要获取下一个目标边界值,否则滚动存在溢出可能(也就是越跑越偏)
                            // 此时已瞬间移动完,所以不会导致陷入无限滚动当中
                            let nextIndex = this.getNextIndex();
                            this.distance = -this.getCritical(nextIndex);
                            this.shake(this.computedPosition());
                        }
                    });
    
                    this.autoPlay();
                    // 抖动
                    // this.shake(this.circles);
                }, this.autoPlayTime);
            },
            pressMoveHandle (evt) {
                this.resetPosition();
                this.distance += ~~(evt.deltaX * 1.4);
                this.computedPosition();
                this.tick = false;
                clearTimeout(this.delayId);
            },
            // 虽然最好的方式是当前视图展示中的四个节点添加动效,但因为有瞬间移动,会导致展示出来的节点没有动效
            // 所以全部节点都跑一次动效,毕竟节点也不是很多
            shake (domList) {
                TweenMax.to(domList, 0.2, {
                    scale: 0.95,
    
                    onComplete: () => {
                        TweenMax.to(domList, 0.2, {
                            scale: 1
                        });
                    }
                });
            },
            touchEndHandle (evt) {
                let nextIndex = this.getNextIndex();
                let end = this.getCritical(nextIndex);
    
                this.ticker(() => {
                    if (end < -this.distance) {
                        this.distance += 8;
                        if (-this.distance <= end) {
                            this.tick = false;
                            this.distance = -end;
                        }
                    } else {
                        this.distance -= 8;
                        if (-this.distance >= end) {
                            this.tick = false;
                            this.distance = -end;
                        }
                    }
    
                    this.resetPosition();
                    this.computedPosition();
                });
    
                this.autoPlay();
            }
        },
    
        created () {
            this.copyVDom();
        },
    
        mounted () {
            this.initAttribute();
            this.computedPosition();
            this.autoPlay();
        }
    };
    </script>
    
    <style lang="less" scoped>
    @import "~@/inc/sales/style/mixin/fn.less";
    
    .arc-swiper {
        position: absolute;
        bottom: 0;
        width: 100%;
        height: 320px/@p;
        overflow: hidden;
    }
    
    .circle-list {
        position: absolute;
        width: 100%;
        height: 100%;
        .flex();
        bottom: 0;
    }
    
    .preserve-3d {
        margin-right: 20px/@p;
    }
    
    .circle {
        display: block;
        .size(200px/@p);
        border-radius: 50%;
        overflow: hidden;
        background-color: #fff;
    }
    
    .covimg {
        display: block;
        .size(100%);
        border-radius: 50%;
    }
    </style>
    

    已封装成一个vue单文件组件,好像也没啥好说的,毕竟注释也写了不少,如果要说思路,那就是根据屏幕宽度计算一下圆形是否进入可视区域,实现是以圆形一半来判断该圆形广告图已进入屏幕,当超过圆形宽度一半则需要添加translateY。初始化的时候会将原数据前后各复制两个,形成新的数组,因为要实现循环,需要一个边界值来判断是否需要重新滚回到第一个或最后一个,大概就酱,单纯作为自己的代码记录

    太久没写文章了…开年来好像一直都在忙,也没时间学习…心塞塞…

    相关文章

      网友评论

          本文标题:使用vue仿某东魔芋导航

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