Flutter是为了开发人员能够以少且简单的代码来构建精美的UI。所以动画部分也是如此,Flutter提供了多种动画,本文先介绍最基本的隐式动画类型。

隐式动画 Implicit Animations

Flutter提供了一个animation library,通过import package:flutter/animation.dart使用,该库仅依赖Dart核心库和physics.dart库。

该库中的两个核心类: ImplicitlyAnimatedWidgets和AnimatedWidgets。

ImplicitlyAnimatedWidgets

ImplicitlyAnimatedWidgets是继承自StatefulWidget一个抽象类,用于构建Widget,使得该Widget的属性能够产生动画变换。

ImplicitlyAnimatedWidgets(及其子类)每当更改时都会自动为其属性中的更改设置动画。 为此,他们创建并管理自己的内部AnimationController来为动画提供动力。 尽管这些Widgets易于使用,并且不需要您手动管理AnimationController的生命周期,但它们也受到一些限制:
除了动画属性的目标值之外,开发人员只能为动画选择持续时间duration和曲线Curve。 如果您需要对动画进行更多控制(例如,您想将其停在中间的某个位置),请考虑使用AnimatedWidget或其子类之一。 这些Widgets将“Animation”作为自变量来增强动画效果。 这使开发人员可以完全控制动画,但需要您手动管理基础的AnimationController。

使用StreamBuilder和FutureBuilder来进行隐式动画 除了将这些AnimatedFoo的隐式动画Widget放在一个StatefulWidget中,然后调用setState方法来更新属性产生动画效果外,也可以使用StreamBuilder和FutureBuilder来触发一个动画效果。

FutureBuilder(
  future: future,
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
double width;
switch(snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
        width = 0;
break;
case ConnectionState.done:
        width = 500;
break;
    }
return AnimatedContainer(
      width: width,
      child: Image.asset('assets/star.png'),
      duration: Duration(seconds: 1),
    );
  }
),

AnimatedOpacity 不透明度隐式动画Widget的使用

  
import 'package:flutter/material.dart';

const owl_url = 'https://raw.githubusercontent.com/flutter/website/master/src/images/owl.jpg';

class FadeInDemo extends StatefulWidget {
  _FadeInDemoState createState() => _FadeInDemoState();
}

class _FadeInDemoState extends State<FadeInDemo> {
  double opacityLevel = 0.0;

  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Image.network(owl_url),
      MaterialButton(
        child: Text(
          'Show details',
          style: TextStyle(color: Colors.blueAccent),
        ),
        onPressed: () => setState(() {
          opacityLevel = 1.0;
        }),
      ),
      AnimatedOpacity(
        duration: Duration(seconds: 3),
        opacity: opacityLevel,
        child: Column(
          children: <Widget>[
            Text('Type: Owl'),
            Text('Age: 39'),
            Text('Employment: None'),
          ],
        ),
      )
    ]);
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: FadeInDemo(),
        ),
      ),
    );
  }
}

void main() {
  runApp(
    MyApp(),
  );
}
  

AnimatedContainer 使用

import 'dart:math';

import 'package:flutter/material.dart';

const _duration = Duration(milliseconds: 400);

double randomBorderRadius() {
  return Random().nextDouble() * 64;
}

double randomMargin() {
  return Random().nextDouble() * 64;
}

Color randomColor() {
  return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
}

class AnimatedContainerDemo extends StatefulWidget {
  _AnimatedContainerDemoState createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  Color color;
  double borderRadius;
  double margin;

  @override
  void initState() {
    super.initState();
    color = Colors.deepPurple;
    borderRadius = randomBorderRadius();
    margin = randomMargin();
  }

  void change() {
    setState(() {
      color = randomColor();
      borderRadius = randomBorderRadius();
      margin = randomMargin();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          children: <Widget>[
            SizedBox(
              width: 128,
              height: 128,
              child: AnimatedContainer(
                margin: EdgeInsets.all(margin),
                decoration: BoxDecoration(
                  color: color,
                  borderRadius: BorderRadius.circular(borderRadius),
                ),
                duration: _duration,
              ),
            ),
            MaterialButton(
              color: Theme.of(context).primaryColor,
              child: Text(
                'change',
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () => change(),
            ),
          ],
        ),
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: AnimatedContainerDemo(),
    );
  }
}

void main() {
  runApp(
    MyApp(),
  );
}

TweenAnimationBuilder

常用的隐式动画Widget

常用的隐式动画Widget被命名为AnimatedFoo,Foo对应于没有动画效果的那个Widget。

  • TweenAnimationBuilder: 可以对任意属性进行动画,通过使用Tween来表示指定的目标值。
  • AnimatedAlign: Align 的隐式动画版本
  • AnimatedContainer: Container的隐式动画版本
  • AnimatedDefaultTextStyle: DefaultTextStyle的隐式动画版本
  • AnimatedOpacity: Opacity的隐式动画版本
  • AnimatedPadding: Padding的隐式动画版本
  • AnimatedPhysicalModel: PhysicalModel的隐式动画版本
  • AnimatedPositioned: Positioned的隐式动画版本
  • AnimatedPositionedDirectional: PositionedDirectional的隐式动画版本
  • AnimatedTheme: Theme的隐式动画版本
  • AnimatedCrossFade: 在两个给定的子Widget之间交叉淡入淡出,并在其大小之间进行动画设置。
  • AnimatedSize: 在给定的时间内自动变化其大小
  • AnimatedSwitcher: 从一个Widget消失到另一个Widget

使用 TweenAnimationBuilder 创建自定义隐式动画

如果我们需要的一个基础动画效果不在上面的内置范围内,那么可以通过使用 TweenAnimationBuilder 创建自定义隐式动画。
而且使用TweenAnimationBuilder构建一些简单的动画时,可以不需要使用StatefulWidget。

/// This is an EXTREMELY bare-bones illustration of using TweenAnimationBuilder.
/// See the rest of the article for optimizations. 
/// Also note that this example is for illusration use only -- an implicit rotation
/// animation can be accomplished with AnimatedContainer.
class SuperBasic extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        starsBackground,
        Center(
          child: TweenAnimationBuilder<double>(
            tween: Tween<double>(begin: 0, end: 2 * math.pi),
            duration: Duration(seconds: 2),
            builder: (BuildContext context, double angle, Widget child) {
              return Transform.rotate(
                angle: angle,
                child: Image.asset('assets/Earth.png'),
              );
            },
          ),
        ),
      ],
    );
  }
}

TweenAnimationBuilder中的builder参数里,包含了一个Tween<T>中的类型T变量,该值基本上告诉Flutter在给定时刻当前的动画值是多少。该值通过Tween的lerp方法来进行计算得出。

如果是一直在相同的起始点数据集上做动画,那么可以把Tween变量改成static finial类型的,这样可以防止在rebuild时,每次都要重新创建一个新的Tween对象。
class ColorAnimationWithStaticFinal extends StatelessWidget {
  static final colorTween = ColorTween(begin: Colors.white, end: Colors.red);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        starsBackground,
        Center(
          child: TweenAnimationBuilder<Color>(
            tween: colorTween,
            duration: Duration(seconds: 2),
            builder: (_, Color color, __) {
              return ColorFiltered(
                child: Image.asset('assets/sun.png'),
                colorFilter: ColorFilter.mode(color, BlendMode.modulate),
              );
            },
          ),
        ),
      ],
    );
  }
}

除了使用static finial类型的变量,如果我们要动画一个更加复杂的组件,性能优化可能变得更加重要。这时可以使用child参数来控制动画过程中,不发生改变的对象,防止每一帧都重新创建所有Widget对象。

下面的例子里,由于只对ColorFiltered的起始颜色做动画,而图片widget不需要改变,所以通过child参数指定Image,如下:

class ChildParameter extends StatelessWidget {
  static final colorTween = ColorTween(begin: Colors.white, end: Colors.red);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        starsBackground,
        Center(
          child: TweenAnimationBuilder<Color>(
            tween: colorTween,
            child: Image.asset('assets/sun.png'),
            duration: Duration(seconds: 2),
            builder: (_, Color color, Widget myChild) {
              return ColorFiltered(
                child: myChild,
                colorFilter: ColorFilter.mode(color, BlendMode.modulate),
              );
            },
          ),
        ),
      ],
    );
  }
}

显式动画

AnimatedWidget

当给定的Listenable值发生改变时,使得这个Widget发生重建。

// Flutter code sample for AnimatedWidget

// This code defines a widget called `Spinner` that spins a green square
// continually. It is built with an [AnimatedWidget].

import 'package:flutter/material.dart';

import 'dart:math' as math;

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

/// This Widget is the main application widget.
class MyApp extends StatelessWidget {
  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

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),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  MyStatefulWidget({Key key}) : super(key: key);

  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget>
    with TickerProviderStateMixin {
  AnimationController _controller;

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

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

  @override
  Widget build(BuildContext context) {
    return SpinningContainer(controller: _controller);
  }
}

常用的animated widgets

在framework中有很多animated widgets,它们通常命名为FooTransition,而Foo对应于没有动画效果的Widget。官方文档:Animation and motion widgets

  • AnimatedBuilder: 对于复杂的动画用例很有用,并且是AnimatedWidget子类的命名方案的显着例外
  • AlignTransition: 这是Align的动画版本。
  • DecoratedBoxTransition: 这是DecoratedBox的动画版本。
  • DefaultTextStyleTransition: 它是DefaultTextStyle的动画版本。
  • PositionedTransition: 这是Positioned的动画版本。
  • RelativePositionedTransition: 这是Positioned的动画版本。
  • RotationTransition: 可动画化小部件的旋转。
  • ScaleTransition: 可动画化小部件的比例。
  • SizeTransition: 动画自己的大小。
  • SlideTransition: 可动画化小部件相对于其正常位置的位置。
  • FadeTransition: 这是不透明度的动画版本。
  • AnimatedModalBarrier: 它是ModalBarrier的动画版本。

AnimationController

ImplicitlyAnimatedWidgets和其子类自动管理其内部的AnimationController,而AnimatedWidget和其子类需要手动管理AnimationController的生命周期。

AnimationController是一种特殊的Animation,它在运行应用程序的设备准备显示新帧时提高其动画值(通常,此速率约为每秒60个值)。可以在需要动画的任何地方使用AnimationController。顾名思义,AnimationController还提供了对其Animation的控制:它实现了一些方法,可以随时停止动画,并使其向前和向后运行。

默认情况下,当向前运行时,AnimationController在给定的持续时间内从0.0到1.0线性增加其动画值。对于许多用例,您可能希望该值具有不同的类型,更改动画值的范围或更改动画在值之间的移动方式。这可以通过包装动画来实现:将其包装在Animatable中(请参见下文)会将动画值的范围更改为不同的范围或类型(例如,对Colors或Rects进行动画处理)。此外,可以通过将Curve包装在CurvedAnimation中来将其应用于动画。代替线性增加动画值,弯曲动画会根据提供的曲线更改其值。该框架附带许多内置曲线(请参见“曲线”)。例如,Curves.easeOutCubic在动画开始时迅速增加动画值,然后减慢直到达到目标值:

Animatable 在不同的类型上做动画

Animatable <T>是一个对象,它接受Animation <double>作为输入并产生类型T的值。这些类型的对象可用于转换AnimationController(或其他double类型的Animation)的动画值范围到不同的范围。该新范围甚至不必再为double类型。借助诸如Tween或TweenSequence之类的Animatable(请参见以下部分),AnimationController可用于在给定的时间内将Colors,Rects,Sizes和许多其他类型从一个值平滑过渡到另一个值。

Tween 和 TweenSequences 补间动画

Tween可以将0.0至1.0的double类型的Animation值映射到其他类型的一个范围内,当为补间动画提供动力的动画的动画值从0.0变为1.0时,它将在其开始值和结束值之间生成插值。当补间动画的动画值接近1.0时,补间生成的值通常会越来越接近其最终值。

tweens.gif(https://hellocyc.com/usr/uploads/2020/07/1075814223.gif)
一个 AnimationController可以同时为多个Tween(比如SizeTween,ColorTween)提供支持。

TweenSequences可以在内部定义多段Item,用以分段控制动画

参考视频和博文:
Animation basics with implicit animations

Flutter animation basics with implicit animations
Custom Implicit Animations in Flutter…with TweenAnimationBuilder

如果觉得我的文章对你有用,请随意赞赏