我什么时候应该使用AnimatedBuilder或AnimatedWidget

我们知道当你乘坐飞机飞行的时候你有很多选择,这里我想表达的是在Flutter中选择动画,首先感谢你选择使用AnimatedBuilderAnimatedWidget,等等,什么,还没有使用?Flutter有很多不同的动画widget,但是与商业航空公司不一样的是,flutter中的每种类型的widget都有自己的适用场景。当然,你可以使用两种不同的方式来完成一样的动画,但是使用适当的animation widget来完成这项工作,将会更加轻松。

这篇文章介绍了和其他动画widget对比,你为什么可能需要使用AnimatedBuilderAnimatedWidget,以及如何使用它们,假设你想向你的APP中添加动画。本文是该系列文章的一部分,逐步介绍了可能希望使用的各种类型的动画widget。你想要特定动画重复执行几次,或者想要暂停、开始以响应某些事件,比如手指点击,由于您的动画需要重复或停止、开始,因此你将需要使用显式动画。

顺便说一下,Flutter有两大类型动画:显式和隐式。对于显式动画,你需要一个animation controller,对于隐式动画则不需要。在上篇关于使用内置显示动画的文章,我们介绍了animation controller,假如你想要了解更多关于此的内容,请先查看那篇文章。

到此,如果你确定使用显式动画,有很多显式动画供您选择,这些类通常命名为FooTransitionFoo是您想要设置的动画的属性名称,我建议先了解一下是否可以使用其中的一个widget来实现你的需求,然后再深入了解AnimatedBuilderAnimatedWidget。有很多效果很棒的widget供您选择,包括旋转、位移、对齐、淡入淡出、文本样式等,另外你可以组合这些Widget,这样就可以同时进行旋转和淡入淡出效果。但是,如果这些内置的Widget不能满足你的需求,那么就是时机使用AnimatedBuilderAnimatedWidget了。

whichanimation.png

这是用于了解使用哪种动画的流程图,本文重点介绍底部的两个蓝色部分,AnimatedBuilder and AnimatedWidget。

特别的例子

为了使以上内容更加具体,让我们来看一个具体的场景:我想编写一个带有外星飞船的APP,这个飞船有一个光柱动画。


spaceship.gif

我绘制了一个渐变色的飞船光束,渐变色从正中心向外逐步黄色变为透明,然后,我使用路径裁剪(path clipper)从该渐变创建了一个光束的形状。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
      body: MyHomePage(),
    ));
  }
}

class MyHomePage extends StatelessWidget {
  final Image starsBackground = Image.asset(
    'assets/milky-way.jpg',
  );
  final Image ufo = Image.asset('assets/ufo.png');
  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        starsBackground,
        ClipPath(
          clipper: const BeamClipper(),
          child: Container(
            height: 1000,
            decoration: BoxDecoration(
              gradient: RadialGradient(
                radius: 1.5,
                colors: [
                  Colors.yellow,
                  Colors.transparent,
                ],
              ),
            ),
          ),
        ),
        ufo,
      ],
    );
  }
}

class BeamClipper extends CustomClipper<Path> {
  const BeamClipper();

  @override
  getClip(Size size) {
    return Path()
      ..lineTo(size.width / 2, size.height / 2)
      ..lineTo(size.width, size.height)
      ..lineTo(0, size.height)
      ..lineTo(size.width / 2, size.height / 2)
      ..close();
  }

  /// Return false always because we always clip the same area.
  @override
  bool shouldReclip(CustomClipper oldClipper) => false;
}

我想要创建一个光束降落的动画,从该渐变的中心开始,并使其重复。这意味着我需要创建显式动画,不幸的是,没有内置的显式动画来为漏斗形渐变设置动画,但是你知道我们有...AnimatedBuilderAnimatedWidget可以解决这个问题!

AnimatedBuilder

为了制作光束动画,我将把这段渐变代码包裹在AnimatedBuilder widget中。当AnimatedBuilder被调用的时候,包含在builder函数中渐变代码也将被调用。

接下来我需要添加一个controller来驱动动画,controller将会提供AnimatedBuilder用来逐帧绘制所需要的值。如你在之前的文章里看到的,我混入(mix in)了SingleTickerProviderStateMixin类,并在initState而不是build方法中初始化了controller实例对象,因为我不想多次创建controller--我想要它为动画的每一帧提供新的值!因为我在initState中创建了一个新的对象,所以我也添加了一个dispose方法,用来告知Flutter,当不再有父节点widget显示在屏幕上的时候,可以销毁controller。

然后,我将controller传递给AnimatedBuilder,动画按照预期运行啦!

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  final Image starsBackground = Image.asset(
    'assets/milky-way.jpg',
  );
  final Image ufo = Image.asset('assets/ufo.png');
  AnimationController _animation;

  @override
  void initState() {
    super.initState();
    _animation = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    )..repeat();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        starsBackground,
        AnimatedBuilder(
          animation: _animation,
          builder: (_, __) {
            return ClipPath(
              clipper: const BeamClipper(),
              child: Container(
                height: 1000,
                decoration: BoxDecoration(
                  gradient: RadialGradient(
                    radius: 1.5,
                    colors: [
                      Colors.yellow,
                      Colors.transparent,
                    ],
                    stops: [0, _animation.value],
                  ),
                ),
              ),
            );
          },
        ),
        ufo,
      ],
    );
  }

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

class BeamClipper extends CustomClipper<Path> {
  const BeamClipper();

  @override
  getClip(Size size) {
    return Path()
      ..lineTo(size.width / 2, size.height / 2)
      ..lineTo(size.width, size.height)
      ..lineTo(0, size.height)
      ..lineTo(size.width / 2, size.height / 2)
      ..close();
  }

  /// Return false always because we always clip the same area.
  @override
  bool shouldReclip(CustomClipper oldClipper) => false;
}

你可能还记得在TweenAnimationBuilder一文中,我们提到使用child 参数来进行性能优化,我们在AnimatedBuilder中也可以这样做。基本上,如果我们在动画中有从来没改变过的对象,则可以提前构建他们,然后将它传递到AnimatedBuilder中。

在这个例子中,有一种更好的实现方式来做同样的事情:给BeamClipper设置一个const构造函数,并且仅仅设置了const。这样只需要少量的代码,这个对象将会在编译期创建,使构建更快速。当然,有时你会编写一些没有const构造函数的代码,这种情况对与使用可选child参数来说是个很好的应用场景。

AnimatedWidget

到此,我们创建了自己的动画,但是包含AnimatedBuilder的构建函数代码量有点大,假如你的构建方法开始变的有点难以阅读,是时候重构代码了。

你可以将AnimatedBuilder代码提取到单独的Widget中,但是这样的话,你的构建方法中将会嵌套另一个构建方法,看起来有点丑陋。取而代之的是,你可以通过继承自AnimatedWidget创建一个新的Widget来完成相同的动画。我将我的Widget命名为BeamTransition,与FooTransition显示动画的命名习惯一致。我将animation controller传递给BeamTransition,并重用了AnimatedBuilder构造函数的主体代码。

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  final Image starsBackground = Image.asset(
    'assets/milky-way.jpg',
  );
  final Image ufo = Image.asset('assets/ufo.png');
  AnimationController _animation;

  @override
  void initState() {
    super.initState();
    _animation = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    )..repeat();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: <Widget>[
        starsBackground,
        BeamTransition(animation: _animation),
        ufo,
      ],
    );
  }

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

class BeamTransition extends AnimatedWidget {
  BeamTransition({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);
  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return ClipPath(
      clipper: const BeamClipper(),
      child: Container(
        height: 1000,
        decoration: BoxDecoration(
          gradient: RadialGradient(
            radius: 1.5,
            colors: [
              Colors.yellow,
              Colors.transparent,
            ],
            stops: [0, animation.value],
          ),
        ),
      ),
    );
  }
}

就像AnimatedBuilder一样,如果可能的话,我将添加child参数到我的widget中,以便进行性能优化,因为它可以提前而不是每次进行动画时进行构建。顺带提醒一下,在此例子中,将BeamClipper采用const构造声明是最好的方式。

那么,我到底该用哪个呐?

我们刚刚看到了,当你无法找到内置显式动画想要实现你想要的效果时,AnimatedBuilderAnimatedWidget都可以用来实现相同效果的显式动画,那么,你该用哪一个呐?这是一个个人偏好问题,一般来说我建议制作独立的widget,每个widget负责单独的功能--在这个例子中是动画。

绝大多数时,我都赞成使用AnimatedWidget,但是如果你创建animation controller的父节点Widget非常简单,那么为你的动画创建一个独立的Widget可能会引入太多额外的代码,这种情况,AnimatedBuilder是你的首选。

这里有这篇文章的视频版本,如果你更喜欢视频,点击观看

系列文章:

视频 对应文章(英文原文) 对应文章(中文翻译)
如何在 Flutter 中选择合适的动画 Widget     在 Flutter中使用动画的正确选择 How to Choose Which Flutter Animation Widget is Right for You? 【已翻译】链接
隐式动画基础 Flutter animation basics with implicit animations 【已翻译】链接
使用 TweenAnimationBuilder 创建独特的隐式动画 Custom Implicit Animations in Flutter…with TweenAnimationBuilder 【已翻译】链接
使用内置显式动画 Directional animations with built-in explicit animations 【已翻译】链接
通过 AnimatedBuilder 和 AnimatedWidget 创建一个自定义动画 When should I useAnimatedBuilder or AnimatedWidget? 【已翻译】链接
深入理解动画 Animation deep dive 【已翻译】链接
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容