微信小程序动画实现方案
发布于 3 年前 作者 lixiong 2984 次浏览 最后一次编辑是 2 年前 来自 分享

微信小程序动画实现方案

背景 基于商城业务需求背景下,实现加购动画;具体要求如下:

  1. 动画落点不准
  2. 体验优化,动画先行,每点击一次需要触发一次动画
  3. 减少动画掉帧,卡;避免动画延迟,解决加购卡顿问题

基于以上诉求,对微信小程序各种动画实现方案做了简单的对比分析

动画落点不准确

最开始实现方案,就是在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 细节实现

分析当前动画元素过程

  1. 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

  1. 动画结束 移除动画元素绑定的 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. 移动到起点
  2. 父级元素 向左边移动 同时子级元素先向上移动指定距离,再向下(时间函数控制曲线斜率)
  3. 回收元素

参考:

// 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;`

Refer

回到顶部