美文网首页
使用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