以前用的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。初始化的时候会将原数据前后各复制两个,形成新的数组,因为要实现循环,需要一个边界值来判断是否需要重新滚回到第一个或最后一个,大概就酱,单纯作为自己的代码记录
太久没写文章了…开年来好像一直都在忙,也没时间学习…心塞塞…
网友评论