Flutter动画——从踩坑到上线

本文详细介绍了在Flutter中实现复杂动画的过程,特别是奖励二级页入口动画的制作。从Flutter的绘制原理、动画分类(显式和隐式动画)及其原理出发,探讨了动画在业务场景中的应用。文章还分享了在实际开发中遇到的挑战和解决方案,如动画状态管理、掉帧问题等,并提供了动画选型决策树。
摘要由CSDN通过智能技术生成

前言

最近公司在做Flutter改版,在为期3个月左右的改造过程中,将双端整体从native迁移到了flutter平台并上线。

从改版后的视觉效果来看,在本次改造中关键节点多次使用到了动画场景,其中,就包括本次的分享所分享的内容——奖励二级页入口动画。

奖励二级页入口承载了2.0版本中收入的动态展示触达,新奖励触达,并在业务过程中通过一系列复杂的动效来提升了该部分的用户体验。且兼具了奖励二级页入口的工作。

鉴于网上大多讲解flutter的基础动画,对复杂动画和动画实现原理的分享和实现案例较少,本文在讲解原理和flutter动画基础之外,分享了flutter动画原理,分析了本次奖励入口动画的实现过程,既是对flutter动画的一次系统性梳理,同时也是对本次flutter重构部分的总结。

Flutter绘制原理

凡事不离原理,同样,flutter能让widget动起来,背后也离不开优秀的架构设计和组件之间的的精密配合。

我们都知道,之所以能够看到动画,都是由一系列静态画面也就是帧在每1秒内连续不断地进行变换所得到的,当变换的速度足够快时,我们就能够看到一系列流畅的动画效果。

所以,在正式开始了解flutter动画原理之前,我们有必要先来了解一下flutter的渲染和绘制流程。

从下图可以看出,flutter绘制总共分3步:

1.GPU发起VSYNC信号:由GPU发起同步帧信号后,将信号传递至dart引擎进行处理,dart引擎在每个同步帧信号的间隙进行视图的构建、布局和渲染。

2.dart绘制渲染:也是dart的绘制流水线。与android的绘制流程类似,这块也分3步:

  • 构建:在这一步会调用我们所写widget当中的build方法来生成相应的element和renderobject来进行视图的构建工作。
  • layout:布局,通过调用renderobject的performlayout方法来进行视图的布局,也就是说,我们所写的widget并不是会最终被绘制到屏幕上,而是通过renderobject来完成的。
  • 绘制与合成:在这一步会调用renderobject的paint方法来进行元素的绘制。

3.将合成好的图像再交还给dart引擎,并由dart引擎通过数据总线来通知GPU来实现最终的光栅化工作。

Flutter动画分类

flutter动画从概念上分为2大类,显式和隐式。一个通俗的理解就是:如果我们不需要对动画过程进行控制,就使用隐式动画。当我们需要对动画的过程进行控制,比如:开始,倒退,播放到xx进度的时候,我们就需要使用显式动画。flutter中的绝大多数动画都是显式动画

隐式动画

制作隐式动画,我们可以使用flutter自带的隐式动画widget来实现,他们的命名大多以Animatedxxx来命名。一般通过名字就可以找到我们所需要的隐式动画。

Algn→AnimatedAlign
Container→ AnimatedContainer
DefaultTextStyle → AnimatedDefaultTextStyle
Opacity → AnimatedOpacity
Padding → AnimatedPadding
PhysicalModel → AnimatedPhysicalModel
Positioned → AnimatedPositioned
PositionedDirectional → AnimatedPositionedDirectional
Theme → AnimatedThemeSize → AnimatedSize
Fade → AnimatedCrossFade
Switcher- > AnimatedSwitcher

实现的动画效果也比较简单,举个例子(AnimatedCrossFade)

除此之外,我们还可以使用TweenAnimtedBuilder自定义隐式动画来实现自带控件之外的隐式动画,写法也非常简单。我总结出来的套路就是TweenAnimatedBuilder + 每一帧变换的widget。代码如下

这个动画就可以实现一个container移动的效果。无需关心内存和动画开始结束的控制等问题,细节和过程,flutter都帮我们处理好了。

一些简单的动效并且不需要参与整个动画控制过程的动画,我们可以使用隐式动画来实现。但如果设计的动画动作比较复杂,隐式动画无法完成,我们就可以考虑使用显式动画了。

显式动画

与隐式动画类似,显式动画在flutter中也有一些定义好的类型,同时,当内置动画不能满足我们需求的时候,它还支持自定义显式动画,自定义显式动画的好处是我们可以自由控制动画的形式和变换规则,而且对动画的状态也能灵活的进行控制。

flutter自带的显式动画命名通常为xxxTransition。通过这些transision可以实现一些简单的显式动画。

除此之外,显式动画也支持自定义。

列举其中几个比较重要的类AnimatedBuilder,AnimatedWidget,AnimationController,Tween,SingleTickerProviderStateMixin

AnimatedBuilder,AnimatedWidget用来构建动画视图。

AnimationController用来实现对动画的控制和状态监听。

Tween用来提供动画需要的数值,并可以将这个数值范围转换为动画所需类型数值。

SingleTickerProviderStateMixin用来提供ticker,并防止屏幕外动画。

AnimatedBuilder的用法如下:

@override
Widget build(BuildContext context) {
  _logger.info("build forward");
  _transmissionController.forward();
  return Container(
    alignment: Alignment.topRight,
    padding: EdgeInsets.only(top :_paddingTop + RFDimens.rf_dimen_10,right: RewardEntranceComponentModel.rewardEntranceXFromRight),
      child: Consumer<BusinessPattern>(
          builder: (BuildContext context, BusinessPattern state, Widget child) {
        return Offstage(
          offstage: !(state.currentState == BusinessPatternState.normal ||
              state.currentState == BusinessPatternState.excitation),
          child: AnimatedBuilder(
            animation: _transmissionController,
            builder: (BuildContext context, Widget child) {
              return Opacity(
                opacity: _transmissionController.value,
                child: SlideTransition(
                  position: Tween(begin: Offset(1.5, 0), end: Offset(0, 0))
                      .animate(_transmissionController),
                  child: child,
                ),
              );
            },
            child: _getCurrentWidgetByState(state),
          ),
        );
      }),
  );
}

可以看出,AnimatedBuilder的写法相对简单,只需要用AnimatedBuilder套上需要做动画动作的widget即可,但同时,代码略显臃肿。

基于这个原因,flutter还为我们提供了AnimatedWidget类,和AnimatedBuilder的使用方法类似,写法如下

class SpinningContainer extends AnimatedWidget {
   const SpinningContainer({Key key, AnimationController controller})
       : super(key: key, listenable: controller);

   Animation<double> get _progress => listenable;

   @override
   Widget build(BuildContext context) {
     return Transform.rotate(
       angle: _progress.value * 2.0 * math.pi,
       child: Container(width: 200.0, height: 200.0, color: Colors.green),
     );
   }
 }

Flutter动画原理

flutter动画的显示和隐式动画原理基本一致,不同点在于隐式动画内部集成了controller,不用过多参与动画的控制和状态监听。

以一个显式动画为例

SizeTransition外部传入一个类型为Animation的sizeFactor,他定义了sizeFactor的动作方式,可以看到这个animation传递到了super中

并在animatedState中对listenable添加了监听回调

最终在每一次animation发生值变化的时候进行handlechange的回调

Flutter动画在业务上的落地场景

奖励二级页入口widget贯穿于整个外卖配送流程,并在流程中通过api请求和push推拉的技术方案将收入信息、奖励信息及时传递给用户,并在事件产生时播放flutter动画和tts

方案踩坑过程

方案1

使用.9图实现+Lottie实现

第一次接到动画开发任务时想找到一种最简单合适的方案来实现,所以当时考虑使用.9图来制作收入部分的底图,但是在实际开发过程中发现了2个问题。

1.数字跳动时的底图部分动作比较生硬,底图虽然实现了长度变化,但是动效不美观,不能直接对底图进行动效控制。

2.Lottie动画不支持金币转动动作。原因是在金币转动动效中有3d变换动作,但是lottie的json文件对这种动作不支持。

这个问题的发现是在对动文件效进行测试时发现的,静态盒子展示和动态盒子展示都可以支持lottie播放,但唯独在金币转动时不能播放,后来使用flutter的全局异常捕获功能断点发现在播放这个json动效时会报错。

后来和设计一起对这个动效进行了调研发现,播放失败是因为金币转动时有3D动效。后续经过大量经过调研,采用了svga播放序列帧的方式进行实现,svga在一些直播和电商app中也有应用,一般用来播放一些复杂动效。

方案实现原理

方案2

使用收入部分字体测宽对底图进行控制CustomPainter + flutter animation + svga

收入部分:方案1的实现方案中的一个问题是底图的视觉效果比较生硬,原因是底图的长度不能随字体宽度进行缩放,不能直接控制。

所以在方案2中采用了字体测宽的方式,估算出字体在底图中所占宽度大小,并对底图按比例进行缩放。

但这个方案在实测过程中发现,在android设备上可以实现预期的动效效果,但ios设备上会出现货币符号挤压和重叠问题。

问题原因是因为在android和ios设备上的尺寸兼容性问题,所以导致计算出的收入部分货币数字尺寸在双端设备上有实现差异。

奖励动效部分:

奖励动效涉及礼盒静态、动态图的切换以及金币转动动效的切换。由于采用了svga的方案,并没有提供很好的动效播放状态回调,所以在方案2中将金币转动切换到礼盒的过程改为了整体动效进行播放,从而减轻了动画切换的控制成本。

同时还对第三方的svgaplayer进行了一定的改造,让业务层也能监听到动画的相关状态。

方案3

收入部分底图作为一个容器,包裹货币符号、收入部分 + svga

方案3所要解决的问题主要是针对在ios和android设备上出现的货币符号和收入数字重叠问题,与方案2不同,方案3由于采用了货币符号+收入部分的整体设计方式,底图作为层级较高的container来实现,不需要对底图的尺寸进行单独控制,从而实现了底图尺寸的自适应效果。

最终实现架构

遇到的问题及解决方案经验分享

奖励动画部分难点在于外部状态对动画动作的影响和播放过程中的掉帧现象,并且在实现该部分动画的过程中进行了比较多的探索。

1.动效的播放状态受状态影响比较严重。

这个问题主要是由于动画中监听了一些Provider状态,每次状态变更时都会引起动效的一次重新build,而且随着项目复杂度的增加,状态问题引起动效的不规则播放越来越严重,且因为调用notifylistener的位置较多,排查比较困难。

解决办法:

  1. 减少Provider的监听,尽量改为注册Listener,控制影响范围。
  2. 将动画widget的层级提高,减少低层级setstate对动画状态的影响。
  3. 对外部的setstate或notifylistener进行简化,减少不必要的刷新动作,或采用局部刷新动作。
  4. 对动画的ui刷新部分由setstate改为AnimatedBuilder,尽量减小刷新范围。

2.奖励部分的动效播放切换过程中的掉帧现象。

问题原因:问题出现的原因和svga的播放原理有关

虽然为了解决动画切换过程中的掉帧现象,采用了预加载方式进行处理,处理后从资源加载到播放过程从之前的100ms+下降到了60~40ms左右,但从主流的android手机的16ms刷新机制来看,仍然有3~5帧左右的掉帧现象。

通过追踪源码可以发现,内存资源加载后仍然会走prepareResources这个方法。

该方法中嵌套了2层异步方法,主要工作是对加载进来的资源进行解码操作,也是比较耗时的操作。

解决办法:

从业务层的角度来看,我们已经充分利用了内存来进行资源加载,但经过缓存处理后的动画切换过程由于解码操作,仍会耗费较多时长。

设计对动画的实现质量要求也比较高。

因此,改造svga在当时的排期条件下是不允许的,所以我们就使用了一种覆盖方法,奇淫巧技来实现视觉上的动画无缝切换。

观察问题发生的节点可知,掉帧现象的发生是在收入增加动画之后和静态礼盒展示之前发生的,而且前者的动画终态和后者的起始态画面基本一致,那么是否可以将两者进行合并,再加上状态控制来解决这个问题呢?

经过验证,答案是肯定的。

原理上来讲简单说就是:在动态收入增加动画播放的时候让SlientRewardAnim暂时隐藏,让收入增加动画正常播放,播放完之后由于会涉及widget状态变更,再将之前隐藏的静态widget进行展示。

实现方案如下:

Flutter动画选型决策树

最后奉上一张flutter官方的动画选型决策树给大家。

结语

flutter动画部分到此分享完毕,但回顾整个过程来看,flutter动画的难点在于

1.动画选型:flutter的动画有很多种,包括但不局限于以上的几种常规方式,一个合适的动画选型能让实现过程事半功倍。

2.动画的状态管理:动画的状态管理是一个令人头痛的问题,由于动画的特殊性,在他的动作依附于其他状态的时候经常会受到外部状态的影响。但此类问题的解法因项目架构不同而不同。需要结合实际情况来解决。

3.动画实现过程中的坑:对于一些需要第三方空间实现的动画过程往往受限于实现方式,这个问题对于初学者来说也是一个需要探索和不断调整的过程。

4.动画的实现方式:基于flutter的动画本身其实难度并不大,而实现他的方式往往有很多种,如何选择一种最合适的,其实是一项非常考验动画实现功底的事情,在这方面,经验还是很重要的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值