微信小程序动画实现方案
背景 基于商城业务需求背景下,实现加购动画;具体要求如下:
- 动画落点不准
- 体验优化,动画先行,每点击一次需要触发一次动画
- 减少动画掉帧,卡;避免动画延迟,解决加购卡顿问题
基于以上诉求,对微信小程序各种动画实现方案做了简单的对比分析
动画落点不准确
最开始实现方案,就是在dom ready 的时候去获取元素落点,缓存下来,后续复用改落点即可;
onReady() {
// 获取落点坐标
this.getBubblePos('#end-point');
},
然而在商城当前业务中,这种方案,获取到的坐标频繁出现较大偏差。基于现状,延迟获取落点坐标,出现以下方案(timeout 和 nextTick 都尝试了,依然没能解决问题,于是综合了一下)
onReady() {
// 获取落点坐标
this.$nextTick(() => {
setTimeout(() => {
this.getBubblePos('#end-point');
}, xxxx);
})
},
timeout
方案尝试了不同的延迟时间,发现在1s内,依旧会出现 获取到的位置不准确,初步判断跟加购动画落点所在的结算条的条件渲染有关。timeout 解决不了问题。
最终在x大佬的指导下,参考了其他平台的实现方案,采取了点击时获取落点坐标,解决了该问题;
async startAnimation(e) {
//点击时 获取落点 并 缓存
await this.setStartPos(`#end-point`);
// 开始动画
this.useAnimateApi(0);
},
快速点击下能连续触发,多个动画并存
需要有多个动画元素,保证快速点击的时候,动画不延迟,并且每次点击都能触发一个动画元素执行动画;动画元素要求能购复用,减少冗余dom 元素;
业务现状: 现有加购按钮跟落点所在的结算条位于不同的组件内;点击时获取到点击位置的坐标,然后需要将动画元素从点击位置 移动到落点;
解决方案:
每个页面初始化 n
(n=10)个元素隐藏在 页面内部 css 隐藏到页面某个区域;为了回收复用动画元素 用一个队列来维护当前可使用动画元素;
<!-- css 元素2个元素实现 -->
<block wx:for="{{transition}}" wx:key="id">
<view class="bubblebox bubblebox_{{index}}" style="{{item.parent}}">
<view class="bubble bubble_{{index}}" bind:transitionend="transitionEnd" data-index="{{index}}" style="{{item.child}}"></view>
</view>
</block>
// 维护动画元素队列,动画执行完成之后 回收当前动画元素
transitionEnd(e) {
console.log(e);
// 监听动画结束时间 清除动画
const { index = 0 } = e.currentTarget.dataset || {};
this.data.queue.push(Number(index));
this.data.transition.splice(index, 1, { id: index, parent: '', child: '' });
this.setData({
transition: this.data.transition,
});
},
掉帧 卡顿问题
针对该问题,对各个实现方案做了个对比
Note: 商城加购动画 最终实现方案采用 css transition 实现
wx.createAnimation
useCreateAnimation(index) {
const { x: x1, y: y1 } = this.data.startPos;
const { x: x2, y: y2 } = this.data.dropPos;
// 1. 移动到 起点
// duration 不能为0 最小为1
this.animation.translate(x1, y1).opacity(1).step({ duration: 1, delay: 0 });
// const parent = this.animation.export();
// // 2. 起点移动到 落点
this.animation.translate(x2, y2).step({ duration: 1000, delay: 1 });
// // // 3. 隐藏气泡 回到起点
this.animation.opacity(0).translate(0, 0).step({ duration: 1, delay: 1000 });
const child = this.animation.export();
this.data.transition.splice(index, 1, { id: index, parent: '', child });
this.setData({
transition: this.data.transition,
});
},
this.animate
从小程序基础库 2.9.0 开始
useAnimateApi(index) {
const { x: x1, y: y1 } = this.data.startPos;
const { x: x2, y: y2 } = this.data.dropPos;
console.log(`useAnimateApi`, index, x1, y1, x2, y2);
// 2. 上 下 移动动画
this.animate(`#bubble_${index}`, [{ left: `${x1}px`, top: `${y1}px`, opacity: 1 }, { translate: [`${x2 - x1}px`, `${y2 - y1}px`] }], 500, () => {
console.log('animationEnd');
this.clearAnimation(`#bubble_${index}`, () => {});
this.animationEnd(index);
});
},
css3 transition
useCssAnimate(index) {
const { x: x1, y: y1 } = this.data.startPos;
const { x: x2, y: y2 } = this.data.dropPos;
const parent = `transition: transform 0ms 0ms linear;transform: translate(${x1}px,${y1}px)`;
const child = `transition:opacity 0s 0s,transform 2s 0ms linear;transform: translate(${x2 - x1}px,${y2 - y1}px);opacity:1;`;
this.data.transition.splice(index, 1, { id: index, parent, child });
this.setData({
transition: this.data.transition,
});
},
wxs
相应事件
小程序双引擎设计,setData 是影响性能的关键因素,wxs 相应事件能减少 setData 次数,有助于提高动画性能;
<wxs module="utils">
var transition = function(e, ownerInstance) {
var index = 0
var parent = ownerInstance.selectComponent('#bubblebox_1_'+index) // 返回组件的实例
var child = ownerInstance.selectComponent('#bubble_1_'+index) // 返回组件的实例
var x1 = 300
var y1 =50
var x2 = 50
var y2 = 278
console.log(parent,child)
parent.setStyle({
transition: 'transform 0ms 0ms linear',
transform: 'translate(300px,50px)'
})
child.setStyle({
transition:'opacity 0s 0s,transform 2s 0ms linear',
transform: 'translate(-250px,238px);opacity:1'
})
return false // 不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault
}
module.exports = {
transition:transition
}
</details>
css3 animation
待尝试,动画生成keyframes 如何绑定到 dom 上?
css transition
细节实现
分析当前动画元素过程
- css 元素从隐藏的位置(fixed到左上角,(0,0))移动到起点,并可见;
transition:opacity 0s 0s,top 0s 0s,left 0s 0s,transform 2s 0ms linear;
transform: translate(1px,1px);opacity:1;
top:100px;
left:100px;
通过top,left 移动元素到起点,并且 通过opacity 控制元素展示出来;执行 translate 动画
利用transition
可以同时为不同的 动画属性
指定不同的 duration
timeFunction
delay
- 动画结束 移除动画元素绑定的 style,动画元素回到起点;动画结束,维护可使用的动画元素队列;监听动画结束事件,回收当前元素入队。
<view bind:transitionend="transitionEnd"></view>
transitionEnd(e) {
console.log(e);
const { index = 0 } = e.currentTarget.dataset || {};
// 动画元素 入队
this.data.queue.push(Number(index));
// 监听动画结束时间 清除动画
this.data.transition.splice(index, 1, { id: index, parent: '', child: '' });
this.setData({
transition: this.data.transition,
});
},
可以用1层dom,为何用2层dom ?
虽然元素已经脱离了文档流,top left 并不会触发 重绘 引起性能问题;但是不能充分利用css3 硬件加速的能力。
完美解决???
该方案对 dom性能有较大提高,但是消耗的内存占用也不容小觑。目前商城业务高度复杂,内存占本来就高的情况下,改方案依旧会在内存占用过高的情况下闪现
,过渡态消失的情况。
抛物线方案
该方案 必须使用2层view元素实现
抛物线方案,改方案初步实现后在小程序里面不同机型上 效果差异较大,动画方案回退为直线;
具体步骤:
- 移动到起点
- 父级元素 向左边移动 同时子级元素先向上移动指定距离,再向下(时间函数控制曲线斜率)
- 回收元素
参考:
// 1. 抛物线
const parent = `transition: top 0s 0s, left 0s 0s, opacity 0s 0s, visibility 0s 0s, transform 500ms 0ms ease-in;transform: translateX(${
x2 - x1
}px);left:${x1}px;top:${y1}px;opacity:1;visibility:visible;`
const child = `transition: transform 300ms 200ms ease-in, margin-top 200ms 0ms ease-in-out,opacity 0ms 501ms linear,visibility 0ms 501ms linear;margin-top: -66px;transform: translateY(${
y2 - y1 + 66
}px);opacity:0;visibility:hidden;`