Flutter系列八:Flutter动画的使用

动画能提高用户的使用体验,使APP更流畅。那么在Flutter中如何实现动画以及选择使用什么样的动画呢?

开门见山,我们直接上图:

动画概览

绘制依赖的动画

绘制依赖的动画是指我们用动画库没法直接实现的动画,这时候有两种实现方式:

  1. Canvas不断绘制形成动画(CustomPainter或者自定义RenderObjectWidget);
  2. 使用设计师提供的动画文件,然后结合三方库来使用(Lottie或者Flare等)。

Lottie

lottie-flutter借鉴自Lottie,使用方法很也很简单。

  • 添加依赖
dependencies:
  lottie: ^1.0.1
  • 引入库文件
import 'package:lottie/lottie.dart';
  • 使用
Lottie.network('https://raw.githubusercontent.com/xvrh/lottie-flutter/master/example/assets/Mobilo/A.json')
lottie_animation

CustomPainter

CustomPainter是系统提供的一个能够绘制内容的底层API。

  • CustomPainter绘制
class SquarePainter extends CustomPainter {
  final double radians;
  SquarePainter(this.radians);

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.pink
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    var path = Path();

    var angle = (math.pi * 2) / 4.0;

    Offset center = Offset(size.width / 2, size.height / 2);
    Offset startPoint =
        Offset(100 * math.cos(radians), 100 * math.sin(radians));

    path.moveTo(startPoint.dx + center.dx, startPoint.dy + center.dy);

    for (int i = 1; i <= 4; i++) {
      double x = 100 * math.cos(radians + angle * i) + center.dx;
      double y = 100 * math.sin(radians + angle * i) + center.dy;
      path.lineTo(x, y);
    }
    path.close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
  
}
  • CustomPaint这个Widget封装CustomPainter的绘制内容
CustomPaint(
    painter: SquarePainter(animation.value),
    child: Container(),
);
  • AnimationController驱动动画

全部相关代码

提示: CustomPainter可以实现很复杂的绘制,本案例仅仅用CustomPainter简单绘制了一个正方形,然后进行不断的旋转动画。

CustomPaint

隐性动画

隐性动画的WidgetImplicitlyAnimatedWidget,它的特点是在属性发生改变后会自动进行动画,不需对动画进行控制,所以叫做隐性动画。

Flutter中的隐性动画和iOS中的隐性动画的概念类似。

abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
    const ImplicitlyAnimatedWidget({
        Key? key,
        this.curve = Curves.linear,
        required this.duration,
        this.onEnd,
    })
}

ImplicitlyAnimatedWidget可以设置动画的速率曲线curve(可以设置的类型),动画时间duration和动画结束后的回调函数onEnd。属性发生变化后,动画就依据这些参数自动进行。

官方提供了很多的隐性动画Widget,他们被命名为Animated**。接下来我们就来一个个看下这些Widget的动画效果。

AnimatedAlign

Align的可动画版本,alignment的发生变化引发动画效果。

AnimatedAlign
class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with TickerProviderStateMixin {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('CustomPainter'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            selected = !selected;
          });
        },
        child: Center(
          child: Container(
            width: 250.0,
            height: 250.0,
            color: Colors.red,
            child: AnimatedAlign(
              alignment: selected ? Alignment.topRight : Alignment.bottomLeft,
              duration: const Duration(seconds: 1),
              curve: Curves.fastOutSlowIn,
              child: Logo(),
            ),
          ),
        ),
      ),
    );
  }
}

AnimatedContainer

Container的可动画版本,AnimatedContainer的各种属性发生变化后有动画效果。

AnimatedContainer
AnimatedContainer(
    width: selected ? 300.0 : 150.0,
    height: selected ? 300.0 : 150.0,
    decoration: BoxDecoration(
        color: selected ? Colors.amber[500] : Colors.amber[200],
        borderRadius: BorderRadius.circular(selected ? 20 : 0)),
        alignment: selected
            ? AlignmentDirectional.bottomCenter
            : AlignmentDirectional.topCenter,
    duration: Duration(seconds: 1),
    curve: Curves.fastOutSlowIn,
    child: Logo(),
)

AnimatedDefaultTextStyle

TextStyle的可动画版本,TextStyle发生变化引发动画效果。

AnimatedDefaultTextStyle
AnimatedDefaultTextStyle(
    duration: Duration(seconds: 1),
    curve: Curves.bounceOut,
    style: TextStyle(
        fontSize: selected ? 20 : 16,
        color: selected ? Colors.amber : Colors.red,
        fontWeight: FontWeight.bold,
    ),
    child: Text("Animated DefaultTextStyle"),
)

AnimatedOpacity

Opacity的可动画版本,透明度改变引发动画效果。

AnimatedOpacity
AnimatedOpacity(
    opacity: selected ? 1.0 : 0.0,
    duration: Duration(seconds: 1),
    curve: Curves.linear,
    child: Logo(),
)

AnimatedPadding

Padding的可动画版本,Padding改变引发动画效果。

AnimatedPadding
AnimatedPadding(
    curve: Curves.easeInOut,
    duration: Duration(seconds: 1),
    child: Container(
        child: Logo(),
    ),
    padding: EdgeInsets.symmetric(
        horizontal: selected ? 10 : 40,
        vertical: selected ? 10 : 30),
)

AnimatedPhysicalModel

PhysicalModel的可动画版本,borderRadiuselevation改变引发动画效果。

AnimatedPhysicalModel
AnimatedPhysicalModel(
    duration: const Duration(milliseconds: 500),
    curve: Curves.fastOutSlowIn,
    elevation: selected ? 0 : 10.0,
    shape: BoxShape.rectangle,
    shadowColor: Colors.red,
    color: Colors.white,
    borderRadius: selected
        ? BorderRadius.all(Radius.circular(0))
        : BorderRadius.all(Radius.circular(10)),
    child: Container(
        color: Colors.blue[100],
        child: Logo(),
    ),
)

AnimatedPositioned

Position的可动画版本,Position改变引发动画效果。

AnimatedPositioned
Stack(
    children: [
        AnimatedPositioned(
            width: selected ? 100.0 : 100.0,
            height: selected ? 100.0 : 100.0,
            top: selected ? 150.0 : 50.0,
            left: selected ? 150.0 : 100.0,
            duration: Duration(seconds: 1),
            curve: Curves.fastOutSlowIn,
                child: Container(
                color: Colors.blue,
            ),
        ),
    ],
),

AnimatedCrossFade

两个子Widget相互切换的动画。

AnimatedCrossFade
AnimatedCrossFade(
    firstChild: Logo(),
    secondChild: FlutterLogo(
        size: 120,
    ),
    crossFadeState: selected
        ? CrossFadeState.showSecond
        : CrossFadeState.showFirst,
    duration: Duration(seconds: 1),
    firstCurve: Curves.easeIn,
    secondCurve: Curves.easeIn,
)

AnimatedSize

子Widget大小发生变化后会发生动画。AnimatedContainer是自身大小发生变化引发动画,这是它们的主要区别。

AnimatedSize

解释:子Widget(Colors.amber)的大小改变没有动画,子Widget的大小改变后AnimatedSize(Colors.red)开始执行动画。

AnimatedSize(
    duration: Duration(seconds: 1),
    reverseDuration: Duration(seconds: 1),
    curve: Curves.fastOutSlowIn,
    child: Container(
        width: selected ? 150 : 100,
        height: selected ? 150 : 100,
    color: Colors.amber,
    ),
    vsync: this,
)

AnimatedSwitcher

这个Widget功能比较全,可以实现

  • 添加、删除Widget的动画;
  • 切换Widget的动画;
  • Widget属性变化的动画;
AnimatedSwitcher
AnimatedSwitcher(
    child: Text(
        "$elapsed",
        key: ValueKey(elapsed),
            style: TextStyle(fontSize: 34),
        ),
        duration: Duration(seconds: 2),
        transitionBuilder:
            (Widget child, Animation<double> animation) {
                final offsetAnimation = TweenSequence([
                      TweenSequenceItem(
                          tween: Tween<Offset>(
                                  begin: Offset(0.0, 1.0),
                                  end: Offset(0.0, 0.0))
                              .chain(CurveTween(curve: Curves.easeInOut)),
                          weight: 1),
                      TweenSequenceItem(
                          tween: ConstantTween(Offset(0.0, 0.0)), weight: 4),
                    ]).animate(animation);
                    return ClipRRect(
                      child: SlideTransition(
                        position: offsetAnimation,
                        child: child,
                ),
            );
        },
        layoutBuilder: (currentChild, previousChildren) {
            return currentChild;
    },
)

前面介绍了一系列官方提供的隐性动画Widget,除了AnimatedCrossFade,AnimatedSizeAnimatedSwitcher外都是ImplicitlyAnimatedWidget的子类。使用这些类能够方便的实现动画,当遇到没法实现的功能又想用隐性动画的需求时,TweenAnimationBuilder就是我们很好的选择。

TweenAnimationBuilder

实现自定义隐性动画。

TweenAnimationBuilder
TweenAnimationBuilder(
    duration: Duration(seconds: 2),
    curve: Curves.easeInOut,
    tween: Tween<double>(begin: 0, end: selected ? 180 : 0),
    builder: (context, value, child) {
        return RotationWidget(rotationY: value);
    },
)

class RotationWidget extends StatelessWidget {
  static const double degrees2Radians = pi / 180;
  final double rotationY;

  const RotationWidget({Key key, this.rotationY = 0}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Transform(
        alignment: FractionalOffset.center,
        transform: Matrix4.identity()
          ..setEntry(3, 2, 0.001)
          ..rotateY(rotationY * degrees2Radians),
        child: rotationY <= 90
            ? Logo(size: 250)
            : Transform(
                alignment: FractionalOffset.center,
                transform: Matrix4.identity()
                  ..setEntry(3, 2, 0.001)
                  ..rotateY(180 * degrees2Radians),
                child: FlutterLogo(size: 200),
              ));
  }
}

显性动画

显性动画是指需要开发者去控制动画,也就是说需要开发者去使用动画Animation相关的类去控制动画过程。

动画(Animation)相关类

  • Animation
abstract class Animation<T> extends Listenable implements ValueListenable<T> {

  void addListener(VoidCallback listener);
  void removeListener(VoidCallback listener);
    
  void addStatusListener(AnimationStatusListener listener);
  void removeStatusListener(AnimationStatusListener listener);

  AnimationStatus get status;
    
  T get value;

  bool get isDismissed => status == AnimationStatus.dismissed;
  bool get isCompleted => status == AnimationStatus.completed;
}

Animation这个类代表了动画的当前值和状态,以及告知监听者这两个值的改变

  1. value代表动画当前的值,addListener()removeListener()可以添加和移除监听者,当value变化后,会通知监听者值的变化;
  2. status代表动画的状态,有初始状态dismissed,正向动画状态forward,反向动画状态reverse动画已完成状态completed四种,addStatusListener()removeStatusListener()可以添加和移除监听者,当status变化后,会通知监听者状态的变化。
  • AnimationController
class AnimationController extends Animation<double> {

    void reset() {}

    TickerFuture forward({ double? from }) {}
    
    TickerFuture reverse({ double? from }) {}
    
    TickerFuture animateTo(double target, { Duration? duration, Curve curve = Curves.linear }) {}
    
    TickerFuture animateBack(double target, { Duration? duration, Curve curve = Curves.linear }) {}
    
    TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period }) {}
    
    void stop({ bool canceled = true }) {}
}

Animation只能代表当前动画值和动画的状态,没法控制动画。AnimationController继承自Animation是对动画进行控制的类。譬如它能重置动画reset(),正向进行动画forward(),反向进行动画reverse(),重复进行动画repeat()和停止动画stop()等。

  • CurvedAnimation
class CurvedAnimation extends Animation<double> {
    
    final Animation<double> parent;
    
    Curve curve;
    
    Curve? reverseCurve;
}

CurvedAnimation的功能是给parent设置一个动画速率变化的曲线。也就是说动画值的变化率可以不是固定的。curve是正向动画的曲线,reverseCurve是反向动画的曲线。可以设置的类型和隐性动画一样

  • Tween
class Tween<T extends dynamic> extends Animatable<T> {

  T? begin;


  T? end;

Tween主要是给``设置一个动画的开始值begin和动画的结束值end

系统提供了一些列的Tween封装类供我们使用:

ColorTween

SizeTween

RectTween

IntTween

StepTween

ConstantTween

CurveTween

  • TweenSequence
class TweenSequence<T> extends Animatable<T> {

  final List<TweenSequenceItem<T>> _items = <TweenSequenceItem<T>>[];
  final List<_Interval> _intervals = <_Interval>[];

}

TweenSequence可以设置一系列的TweenSequenceItem,相当于设置一些关键帧,可以实现类似关键帧动画

动画实现的案例

我们来用上面提到的一些类来实现一个图片放大然后缩小的循环动画,效果如下:

Animation
  • 1.创建AnimationController
// vsync参数值是同步信号,duration参数值是动画的时间
AnimationController _controller = AnimationController(vsync: this, duration: Duration(seconds: 2));
  • 2.给_controller设置Curve
// parent参数值是需要设置Curve的Animation,curve参数值是正向动画的Curve,reverseCurve参数值是反向动画的Curve
Animation _animation = CurvedAnimation(
                        parent: _controller,
                        curve: Curves.bounceOut,
                        reverseCurve: Curves.bounceIn);
  • 3.给_animation设置动画的开始值和结束值
Animation_sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);
  • 4.监听_controller动画值的改变,然后进行界面更新
_controller.addListener(() {
    setState(() {});
});
  • 5.监听听_controller动画的状态改变,然后进行循环动画
_controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
});
  • 6.界面的展示逻辑
Logo(
    size: _sizeAnim.value,
)

通过上面几个步骤,动画的代码就写完了。所有代码如下:

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
  // 创建AnimationController
  AnimationController _controller;
  Animation _animation;
  Animation _sizeAnim;

  @override
  void initState() {
    super.initState();

    // 1.创建AnimationController
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    // 2.设置Curve的值
    _animation = CurvedAnimation(
        parent: _controller,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn);

    // 3. 设置动画的开始值和结束值
    _sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);

    // 4. 监听动画值的改变
    _controller.addListener(() {
      setState(() {});
    });

    // 5. 监听动画的状态改变
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: Logo(
          size: _sizeAnim.value,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }

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

AnimatedWidget

上面的代码我们实现了动画,但是有两个问题:

  1. 我们需要监听_controller的值的变化,然后调用setState(() {});这个方法;
  2. 动画的绘制涉及到了Body的重构ReBuild,其实我们只需要Logo进行重构ReBuild就行。

我们可以将Logo重构为一个AnimatedWidget,这样就可以解决上面两问题了:

class LogoAnimatedWidget extends AnimatedWidget {
  LogoAnimatedWidget(Animation anim) : super(listenable: anim);

  @override
  Widget build(BuildContext context) {
    Animation anim = listenable;
    return Logo(size: anim.value);
  }
}

解释下代码逻辑:

  1. 继承AnimatedWidget的子类的构造函数需要传入一个Animation;
  2. 重写build方法,返回Widget,这里就是我们的Logo

接下来删除_controller.addListener(), 然后使用LogoAnimatedWidget

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> with SingleTickerProviderStateMixin {
  // 创建AnimationController
  AnimationController _controller;
  Animation _animation;
  Animation _sizeAnim;

  @override
  void initState() {
    super.initState();

    // 1.创建AnimationController
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    // 2.设置Curve的值
    _animation = CurvedAnimation(
        parent: _controller,
        curve: Curves.bounceOut,
        reverseCurve: Curves.bounceIn);

    // 3. 设置动画的开始值和结束值
    _sizeAnim = Tween(begin: 100.0, end: 200.0).animate(_animation);

    // 4. 监听动画值的改变
    // _controller.addListener(() {
    //   setState(() {});
    // });

    // 5. 监听动画的状态改变
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: LogoAnimatedWidget(_sizeAnim),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }

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

class LogoAnimatedWidget extends AnimatedWidget {
  LogoAnimatedWidget(Animation anim) : super(listenable: anim);

  @override
  Widget build(BuildContext context) {
    Animation anim = listenable;
    return Logo(size: anim.value);
  }
}

AnimatedBuilder

AnimatedWidget其实也有一些问题:

  1. 需要新建一个AnimatedWidget,代码量增加了;
  2. AnimatedWidget如果动画的Widget含有子Widget,那子Widget也会重构ReBuild

AnimatedBuilder可以解决上面两问题。代码如下:

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
        child: AnimatedBuilder(
            animation: _sizeAnim,
            builder: (ctx, child) {
              return Logo(
                size: _sizeAnim.value,
              );
            }),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          _controller.forward();
        },
      ),
    );
  }

AnimatedBuilder有两个参数,animation参数值是Animation, builder参数值是一个函数,会提供BuildContext子Widget,一个提供Widget的构造环境,一个提供child可以复用,无需重构子Widget

官方提供的AnimatedWidget

官方提供了一些AnimatedWidget,使用方式和使用自定义的AnimatedWidget类似。

由于和隐式动画的版本类似,这里就不一一贴出效果了,只是列出来供大家参阅:

AlignTransition

DecoratedBoxTransition

DefaultTextStyleTransition

PositionedTransition

RelativePositionedTransition

RotationTransition

ScaleTransition

SizeTransition

SlideTransition

FadeTransition

总结

至此,我们将Flutter中的动画实现方式总结完了。

我们知道动画的逻辑就是不断的重绘,那Animation相关的类是如何引发重绘的呢?隐式动画又是如何对开发者屏蔽了Animation类实现动画的呢?

相关的问题,我们在下一节将深入底层去为你揭开面纱!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容