通过实例学习Flutter动画+GitHub上的免费画廊应用

166 阅读10分钟

动画是为您的Flutter应用程序增色取悦您的用户的一个好方法。

因此,让我们来了解一下它们!我们将介绍。

  • 隐式动画小部件
  • Tweens & TweenAnimationBuilder
  • AnimationController & AnimatedBuilder
  • 内置的显式过渡小工具

这些只是基础知识。所以我还分享了一个新的画廊应用的源代码。这里面包含了许多动画的例子,你可以从中学习并在你的项目中使用。

Flutter动画库--网络演示。在一个单独的窗口中打开

准备好了吗?让我们开始吧!

隐式动画小部件

Flutter提供了一堆所谓的隐式动画部件,您只需将其放入您的代码中,就可以轻松添加动画。

例如,让我们看一下AnimatedContainer

AnimatedContainer

这里有一个非常无聊的Container ,有一个给定的widthheight ,和color

Container(
  width: 200,
  height: 200,
  color: Colors.red,
)

让我们用AnimatedContainer 替换它,并给它一个duration

AnimatedContainer(
  width: 200,
  height: 200,
  color: Colors.red,
  duration: Duration(milliseconds: 250),
)

为了使其成为动画,我们需要做几件事。

// 1. Convert the parent class to a StatefulWidget
class AnimatedContainerPage extends StatefulWidget {
  @override
  _AnimatedContainerPageState createState() => _AnimatedContainerPageState();
}

class _AnimatedContainerPageState extends State<AnimatedContainerPage> {
  // 2. declare the container properties as state variables
  double _width = 200;
  double _height = 200;
  Color _color = Colors.red;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: AnimatedContainer(
          // 3. pass the state variables as arguments
          width: _width,
          height: _height,
          color: _color,
          duration: Duration(milliseconds: 250),
        ),
      ),
      // 4. add a button with a callback
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: _update,
      ),
    );
  }

  // 5. Update the state variables to rebuild the widget
  void _update() {
    setState(() {
      _width = 300;
      _height = 300;
      _color = Colors.green;
    });
  }
}

有了上面的变化,我们就可以按下按钮,容器就会在给定的持续时间内 动画化为新的数值。

AnimatedContainer动画

但是如果我们再次按下按钮,什么也不会发生,因为状态变量已经被设置为更新的值。

为了让事情变得更有趣,我们可以修改_update 方法,使用一个随机数生成器,这样我们的容器在每次按下按钮时都会变成一组不同的值

final random = Random();

void _update() {
  setState(() {
    _width = random.nextInt(300).toDouble();
    _height = random.nextInt(300).toDouble();
    _color = Color.fromRGBO(
      random.nextInt(128),
      random.nextInt(128),
      random.nextInt(128),
      1,
    );
  });
}

不够好吗?那么我们可以改变动画曲线来修改动画值的变化率

AnimatedContainer(
  width: _width,
  height: _height,
  color: _color,
  duration: Duration(milliseconds: 250),
  // default curve is Curves.linear
  curve: Curves.easeInOutCubic,
)

这样一来,动画的感觉就自然多了。

Flutter自带了大量的曲线,你可以从中选择。而如果内置的曲线都不适合你,你甚至可以定义你自己的Curve 子类。

// a custom sine curve that can be passed to any of the implicitly animated widgets
class SineCurve extends Curve {
  final double count;
 
  SineCurve({this.count = 1});
 
  @override
  double transformInternal(double t) {
    return sin(count * 2 * pi * t) * 0.5 + 0.5;
  }
}

除了AnimatedContainer,Flutter还提供了许多其他隐含的动画部件,如AnimatedAlignAnimatedOpacityAnimatedTheme,以及更多。下面是完整的列表

隐式动画部件是如何工作的?

隐式动画部件有一个或多个可动画的属性,可以被设置为一个目标值。当目标值发生变化时,小组件会在给定的时间内将该属性从旧值动画化到新值

这使得它们很容易使用,因为你所要做的就是更新目标值,而小组件会在引擎盖下处理这些动画。

然而,它们只能用于向前推进的动画。如果你需要一个重复的反向的动画,你需要一个明确的动画(见下文)。

Tweens和TweenAnimationBuilder

Tween是in-between的缩写,表示一个有开始结束范围

Tweens可以应用于不同类型的值

我们可以用tweens来表示该范围内的动画值

而Flutter给了我们一个TweenAnimationBuilder,我们可以用它来定义我们自己的自定义隐式动画。

如果你需要创建一个基本的动画,但内置的隐式动画部件(如AnimatedFoo)都不能满足你的需求,请使用TweenAnimationBuilder

这是如何工作的呢?

好吧,让我们回到无聊的Container

Container(width: 120, height: 120, color: Colors.red)

我们可以用一个TweenAnimationBuilder 来包裹它,并给它一个Duration 和一个Tween

TweenAnimationBuilder<double>(
  // 1. add a Duration
  duration: Duration(milliseconds: 500),
  // 2. add a Tween
  tween: Tween(begin: 0.0, end: 1.0),
  // 3. add a child (optional)
  child: Container(width: 120, height: 120, color: Colors.red),
  // 4. add the buiilder
  builder: (context, value, child) {
    // 5. apply some transform to the given child
    return Transform.translate(
      offset: Offset(value * 200 - 100, 0),
      child: child,
    );
  },
)

child 参数是可选的,可以用于优化目的。欲了解更多信息,请参见。为什么TweenAnimationBuilder和AnimatedBuilder有一个子参数?

上面的builder输入Tween 指定的范围内给我们一个动画值。

在这个例子中,我们用它将子部件在X轴上平移value * 200 - 100 。这将把(0, 1) 之间的动画值映射到(-100, 100) 之间的偏移。

如果我们把上面的代码放在一个新的widget类里面,然后热重新加载,我们就可以看到动画了。

用TweenAnimationBuilder翻译动画

在这个阶段,我们可以将Tween的结束值提取到一个状态变量中,并使用Slider 来更新它。

// 1. use a state variable 
double _value = 0.0;

// 2. pass it to the Tween's end value
TweenAnimationBuilder<double>(
  tween: Tween(begin: 0.0, end: _value),
  ...
)

// 3. Add a slider to update the value
Slider.adaptive(
  value: _value,
  onChanged: (value) => setState(() => _value = value),
)

这样,当我们与Slider 进行交互时,TweenAnimationBuilder 会自动生成新值的动画。

注意滑块和容器转换之间的延迟。这是因为TweenAnimationBuilder在给定的持续时间内对新值进行动画

其他类型的Tweens

在上面的例子中,我们使用了一个类型为doubleTween

但是有几个内置的Tween子类,如ColorTweenSizeTweenFractionalOffsetTween,你可以用它们来在不同的颜色、大小和更多的东西之间做动画。

如果你愿意,你甚至可以定义你自己的Tween 子类。如果你想在你的应用程序中的自定义对象之间制作动画,这很有用。更多相关信息请参见Tween类的文档。

AnimationController

如果我们想创建可以向前、向、甚至永远重复的 显式动画,我们需要一个AnimationController

让我们看看如何使用它。

// 1. Define a StatefulWidget subclass
class RotationTransitionPage extends StatefulWidget {
  const RotationTransitionPage({Key? key}) : super(key: key);

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

class _RotationTransitionPageState extends State<RotationTransitionPage>
    // 2. add SingleTickerProviderStateMixin
    with SingleTickerProviderStateMixin {
  // 3. create the AnimationController
  late final _animationController = AnimationController(
    vsync: this,
    duration: Duration(milliseconds: 500),
  );

  @override
  void dispose() {
    // 4. dispose the AnimationController when no longer needed
    _animationController.dispose();
    super.dispose();
  }
}

最有趣的一行是这样的。

late final _animationController = AnimationController(
  vsync: this,
  duration: Duration(milliseconds: 500),
);

通过将this 传给vsync 参数,我们要求Flutter产生一个新的动画值,我们设备的屏幕刷新率同步(通常为60帧/秒)。关于这一点的更多信息,请参阅。为什么Flutter的动画需要一个vsync/TickerProvider

如果你有很多带有显式动画的部件,每次都设置一个AnimationController 是相当繁琐的。这篇文章提供了两种解决方案。如何减少AnimationController的模板代码。Flutter Hooks vs 扩展State类

AnimatedBuilder

现在我们有了我们的AnimationController ,让我们用它来显示一个旋转的Container

一种方法是在build() 方法中使用一个AnimatedBuilder

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      // 1. use an AnimatedBuilder
      child: AnimatedBuilder(
        // 2. pass our AnimationController as the animation argument
        animation: _animationController,
        // 3. pass the child widget that we will animate
        child: Container(width: 180, height: 180, color: Colors.red),
        // 4. add a builder argument (this will be called when the animation value changes)
        builder: (context, child) {
          // 5. use a Transform widget to apply a rotation
          return Transform.rotate(
            // 6. the angle is a function of the AnimationController's value
            angle: 0.5 * pi * _animationController.value,
            child: child,
          );
        },
      ),
    ),
  );
}

然而,如果我们运行这段代码,什么也不会发生。那是因为我们忘记了启动动画!所以我们可以覆盖。

所以我们可以覆盖initState() 方法并调用forward()

@override
void initState() {
  super.initState();
  _animationController.forward();
}

这将在我们加载页面(或热重启)时 "播放 "一次动画。

如果我们愿意,我们可以通过调用_animationController.repeat() ,使动画永远重复

永远旋转的动画容器

AnimatedBuilder是如何工作的?

让我们再来看看这个问题。

AnimatedBuilder(
  // pass our AnimationController as the animation argument
  animation: _animationController,
  // pass the child widget that we will animate
  child: Container(width: 180, height: 180, color: Colors.red),
  // add a builder argument
  builder: (context, child) {
    // use a Transform widget to apply a rotation
    return Transform.rotate(
      // the angle is a function of the AnimationController's value
      angle: 0.5 * pi * _animationController.value,
      child: child,
    );
  },
)

AnimatedBuilder 需要一个类型为 的参数。由于Animation<double> AnimationController 扩展了 Animation<double> ,我们可以把它作为一个参数传递。

这将导致每次动画值改变时builder 都会被调用。我们可以用它来转换给定的子部件,或者甚至返回一个依赖于动画值的全新的部件。

内置的显式过渡小部件

AnimationController & 是非常强大的,你可以结合它们来创造一些非常自定义的效果。AnimationBuilder

但有时你甚至不需要一个AnimatedBuilder ,因为Flutter已经有了一套内置的过渡小部件,你可以使用。

例如,我们可以用一个RotationTransition 来代替上面的所有代码,并以更少的精力完成同样的事情。

RotationTransition(
  turns: _animationController,
  child: Container(width: 180, height: 180, color: Colors.red),
)

不喜欢旋转?那就用比例过渡怎么样?

ScaleTransition(
  scale: _animationController,
  child: Container(width: 180, height: 180, color: Colors.red),
)

通过这种改变,我们可以得到一个无限期重复的缩放动画。

循环缩放的动画容器

AnimationController监听器

AnimationController 可以做的事情比我们目前看到的多得多。例如,我们可以添加一个状态监听器,使动画在每次完成后都能向前和向后交替进行

@override
void initState() {
  super.initState();
  // add a status listener
  _animationController.addStatusListener((status) {
    if (status == AnimationStatus.completed) {
      _animationController.reverse();
    } else if (status == AnimationStatus.dismissed) {
      _animationController.forward();
    }
  });
  // start the animation when the widget is first loaded
  _animationController.forward();
}

CurvedAnimation

我们可以做的另一件很酷的事情是使用一个Tween ,从父级AnimationController ,生成一个新的Animation 对象。这通常被用来添加一个自定义的动画曲线。

late final _customAnimation = Tween(
  begin: 0.0,
  end: 1.0,
).animate(CurvedAnimation(
  parent: _animationController,
  curve: Curves.easeInOut,
));

一旦我们有了这个,我们就可以把它传递给我们的过渡小部件。

ScaleTransition(
  // use _customAnimation rather than _animationController as an argument
  scale: _customAnimation,
  child: Container(width: 180, height: 180, color: Colors.red),
)

而通过将上面的状态监听器代码与这个CurvedAnimation ,我们得到了这个。

交替比例过渡

隐式与显式动画部件

现在我们已经涵盖了所有的基础知识,我们应该发现Flutter动画API的命名和使用方式的一些共同模式

隐式动画部件

  • 它们被命名为AnimatedFoo (AnimatedContainer,AnimatedAlign 等)
  • 它们接受DurationCurve 参数
  • 它们只能向前做动画
  • 找不到您需要的内置隐式动画部件?使用TweenAnimationBuilder

明确的动画部件

  • 它们被命名为FooTransition (RotationTransition,ScaleTransition 等)
  • 它们接受一个Animation 参数
  • 它们可以向前、向、或永远重复地制作动画
  • 找不到你需要的显式动画部件?使用AnimatedBuilder

这些API还有哪些共同的模式?

事实上,有时候定义一个AnimatedWidget 子类比使用AnimatedBuilder 更方便。

例如,这里是Flutter SDK里面的RotationTransition widget的实现方式。

class RotationTransition extends AnimatedWidget {
  const RotationTransition({
    Key? key,
    required Animation<double> turns,
    this.alignment = Alignment.center,
    this.child,
  }) : super(key: key, listenable: turns);
  // the parent listenable property has type Listenable,
  // so we use a getter variable to cast it back to Animation<double>
  Animation<double> get turns => listenable as Animation<double>;
  final Alignment alignment;
  final Widget? child;

  // This build method is called every time the listenable (animation) value changes.
  // As such, AnimationBuilder is not needed.
  @override
  Widget build(BuildContext context) {
    final double turnsValue = turns.value;
    final Matrix4 transform = Matrix4.rotationZ(turnsValue * math.pi * 2.0);
    return Transform(
      transform: transform,
      alignment: alignment,
      child: child,
    );
  }
}

事实上,查看内置动画小部件的源代码是学习如何制作自己的小部件的一个好方法!

Flutter动画库

动画的内容比我们所介绍的要多得多。比如说。

  • 如何创建交错的动画
  • 如何使用GestureDetector 来驱动完全定制的 UI 小部件的动画?
  • 如何在Flutter中做动画主题设计?

要了解更多,请查看我在GitHub上的Flutter动画库,它展示了所有最常见的动画API。

官方文档

Flutter 文档中有一些关于动画 API 的广泛文档、代码实验和教程。这里是入门的最佳地点。