主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black

贡献主题:https://github.com/xitu/juejin-markdown-themes

theme: condensed-night-purple

highlight:

1.前置知识

对于每个 UI 帧来说,主要依次执行 AnimateBuildLayoutCompositing bitsPaintCompositing。每当界面发生变化时,都是一帧触发会更新的结果。如下每两格代表一帧的UI 时间(左)和 Raster 时间(右)。 当左侧很高时,说明你的界面写的有问题。看下面的两个 UI 帧, 可以看出 Build 占了很大部分,就说明 UI 可能存在某些低效率情况。

image-20201216082750492

image-20201216083822903


你可以向下看整个 Build 遍历的深度,如果树过深表示可能存在问题。这时应该看一下,是否对不必要的部分进行了更新。

33333


但是要注意,对于全局主题、文字等更新,必然会从顶节点进行遍历,这是无法避免的,虽然会让产生一定延迟,但这些都是视觉不敏感操作,操作次数也不是非常频繁。但会动画而言就不同了,掉几帧就会感觉卡卡的,不流畅,另一方面,动画会持续一段时间进行不断渲染,所以要特别注意性能问题。另外不要在 debug 模式看性能不要在 debug 模式看性能不要在 debug 模式看性能!用 profile 模式。

image-20201216092358356


2. 反面教材!!!

动画如下,中间的圆形渐变扩大动画,上下的方块不动。

44444

程序入口

```dart void main() { runApp(MyApp()); }

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: HomePage()); } } ```

_HomePageState 混入 SingleTickerProviderStateMixin,创建动画器 controller,监听动画器,每次触发时调用 _HomePageStatesetState 方法,来使 _HomePageState 中持有的 Element 进行更新。点击中间时进行动画触发。

```dart class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); }

class _HomePageState extends State with SingleTickerProviderStateMixin { AnimationController controller;

@override void initState() { super.initState(); controller = AnimationController( lowerBound: 0.3, upperBound: 1.0, vsync: this, duration: const Duration(milliseconds: 500)); controller.addListener(() { setState(() {}); }); }

@override void dispose() { controller.dispose(); super.dispose(); }

@override Widget build(BuildContext context) { print('---------_HomePageState#build------'); return Scaffold( appBar: AppBar( title: Text("动画测试"), ), body: Column( children: [ Expanded( child: Padding( padding: EdgeInsets.only(top: 20), child: buildBoxes(), ), ), Expanded( child: Center( child: buildCenter(), ), ), Expanded( child: Padding( padding: EdgeInsets.only(bottom: 20), child: buildBoxes(), ), ), ], )); }

Widget buildCenter() => GestureDetector( onTap: () { controller.forward(from: 0.3); }, child: Transform.scale( scale: controller.value, child: Opacity(opacity: controller.value, child: Shower()), ), );

Widget buildBoxes() => Wrap( spacing: 20, runSpacing: 20, children: List.generate( 24, (index) => Container( alignment: Alignment.center, width: 40, height: 40, color: Colors.orange, child: Text('$index',style: TextStyle(color: Colors.white),), )), ); } ```

为了方便测试,这里将中间组件抽离成 Shower。用 StatefulWidget 方便测试动画执行中 _ShowerState 回调函数的情况。

```dart class Shower extends StatefulWidget { @override _ShowerState createState() => _ShowerState(); }

class _ShowerState extends State { @override void initState() { super.initState(); print('-----Shower#initState----------'); }

@override Widget build(BuildContext context) { print('-----Shower#build----------'); return Container( width: 150, height: 150, alignment: Alignment.center, decoration: BoxDecoration(color: Colors.orange, shape: BoxShape.circle), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Container( height: 30, width: 30, decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), ), Container( height: 30, width: 30, decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle), ) ], ), Text( 'Toly', style: TextStyle(fontSize: 40, color: Colors.white), ), ], ), ); } } ```


然后会发现,_HomePageState#buildShower#build 会不断触发。其根本原因是在较高的层级进行了 setState ,导致其下树被遍历,在这种情况下执行动画,是不可取的。我们需要做的是降低更新元素节点层级。Flutter 为我们提供了 AnimatedBuilder

image-20201216091036721

33333


3. 正面面教材 AnimatedBuilder

需要做的改变: 1、移除监听动画器 2、使用 AnimatedBuilder

```dart @override void initState() { super.initState(); controller = AnimationController( vsync: this, lowerBound: 0.3, upperBound: 1.0, duration: const Duration(milliseconds: 500)); // 1、移除监听动画器 }

Widget buildCenter() => GestureDetector( onTap: () { controller.forward(from: 0); }, child: AnimatedBuilder( // 2、使用 AnimatedBuilder animation: controller, builder: (ctx, child) { return Transform.scale( scale: controller.value, child: Opacity(opacity: controller.value, child: child), ); }, child: Shower()), ); ```

仅此而已,让我们看一下效果,动画执行正常

44444

控制台什么都没有,是很过分呢?这不是啪啪啪打我 setState 的脸吗?

从下面的 UI 帧中 可以看出,同样的情景,使用 AnimatedBuilder 进行动画可以很有效地使 Build 过程缩短。

image-20201216092703510

image-20201216092810767


4.AnimatedBuilder 源码解析

首先,AnimatedBuilder 继承自 AnimatedWidget,成员有构造器 builder 和子组件 child,对象创建时还需要 Listenable 对象 animation

```dart class AnimatedBuilder extends AnimatedWidget { const AnimatedBuilder({ Key key, @required Listenable animation, @required this.builder, this.child, }) : assert(animation != null), assert(builder != null), super(key: key, listenable: animation);

final TransitionBuilder builder; final Widget child;

@override Widget build(BuildContext context) { return builder(context, child); } }

typedef TransitionBuilder = Widget Function(BuildContext context, Widget child); ```

AnimatedBuilder 很简单,使用核心应该都在 AnimatedWidget 中。可以看出 AnimatedWidget 是一个 StatefulWidget 有更改状态的需要。

```dart abstract class AnimatedWidget extends StatefulWidget { const AnimatedWidget({ Key key, @required this.listenable, }) : assert(listenable != null), super(key: key);

final Listenable listenable;

@protected Widget build(BuildContext context);

@override _AnimatedState createState() => _AnimatedState(); } ```

_AnimatedState 中处理也非常简单,监听传入的 listenable,执行 _handleChange, 而 _handleChange 执行的是.....,没错:你大爷终究还是你大爷。更新还是要靠 setState。但比起上面的那个setState ,这里的 setState 的影响就小很多。

```dart class AnimatedState extends State { @override void initState() { super.initState(); widget.listenable.addListener( handleChange); }

@override void didUpdateWidget(AnimatedWidget oldWidget) { super.didUpdateWidget(oldWidget); if (widget.listenable != oldWidget.listenable) { oldWidget.listenable.removeListener(handleChange); widget.listenable.addListener(handleChange); } }

@override void dispose() { widget.listenable.removeListener(_handleChange); super.dispose(); }

void _handleChange() { setState(() { // The listenable's state is our build state, and it changed already. }); }

@override Widget build(BuildContext context) => widget.build(context); } ```

当执行 build 时,执行的是widget.build(context) ,也就是将当前的上下文回调给widget.build方法,而widget.build 方法执行的是 : builder (context, child) 也就是我们写的那个 builder (下图),可以看出回调的这个 child 仍是传入的 child,这样不会构建新的 Shower 组件,也不会触发 Shower 组件对应 State 的 build 方法,一切动画需要的都在 builder 方法中进行,刷新的东西也被 AnimatedBuilder 包在了局部。就这样,岁月静好,波澜不惊

dart @override Widget build(BuildContext context) { return builder(context, child); }

image-20201216100725103


这样来看,AnimatedBuilder 似乎也没有什么神秘的,了解了这些,再去看 Flutter 框架中的封装的各种动画组件,你就会豁然开朗,这便是知一而通百。总结一下,并不是说 setState 不好,而是用的时机对不对。AnimatedBuilder 本质上也是使用 setState 进行触发更新的,所以看待问题不要片面和激进。对于应界面 UI 来说,我们需要关注的是如何将 Build 过程的消耗降到最低,特别是对于动画、滑动这样会持续跟新渲染的场景。

@张风捷特烈 2020.12.16 未允禁转 我的公众号:编程之王 联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328 ~ END ~

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐