End

Flutter 陈航 22-动画 Animation Hero

本文地址


目录

22 | 如何构造炫酷的动画效果?

原文

Flutter 完全接管了渲染层,除了静态的页面布局之外,对组件动画的支持自然也不在话下。

Animation

  • Animation:根据预定规则,在单位时间内持续输出动画的当前状态
  • AnimationController:用于管理 Animation,可用来设置动画的时长、启动/暂停、反转动画等
  • Listener:是 Animation 的回调函数,用来监听动画的进度变化,进而根据当前值重新渲染组件

Animation 仅提供动画的数据,而不负责动画的渲染,因此我们还需要监听动画执行进度,并在回调中使用 setState 强制刷新界面才能看到动画效果。

案例

class HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  double _width = 50;
  late AnimationController controller;

  @override
  void initState() {
    Duration duration = const Duration(milliseconds: 2000); // 动画周期为 2000 毫秒
    controller = AnimationController(vsync: this, duration: duration); // vsync 用于防止出现不可见动画

    Animation<double> animation = Tween(begin: 50.0, end: 300.0).animate(controller); // 从 50 到 300 线性变化的动画
    animation.addListener(() {
      //flog("进度回调 value = ${animation.value}");
      setState(() => _width = animation.value);
    });
    animation.addStatusListener((status) => flog("status = ${status.name}"));
    super.initState();
  }

  void _onPressed() {
    flog("duration = ${controller.duration?.inMilliseconds}, status = ${controller.status}, "
        "velocity = ${controller.velocity.toStringAsExponential(1)}, animationBehavior = ${controller.animationBehavior}, "
        "animating = ${controller.isAnimating}, complete = ${controller.isCompleted}, dismiss = ${controller.isDismissed}");
    controller.reset(); // 重置到默认状态
    controller.forward().then((value) => flog("动画执行完毕")); //启动动画
  }

  @override
  Widget build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Image.asset("assets/images/monkey.jpg", width: _width),
      floatingActionButton: FloatingActionButton(onPressed: _onPressed));

  @override
  void dispose() {
    controller.dispose(); // 释放资源
    super.dispose();
  }
}

上面在创建 AnimationController 的时候,设置了一个 vsync 属性,设置后会把动画绑定到一个 Widget,当 Widget 不显示时,动画将会暂停,当 Widget 再次显示时,动画会重新恢复执行,这样就可以避免动画组件不可见时消耗资源。

非线性曲线动画

Tween 默认是线性变化的,通过创建 CurvedAnimation 可以实现非线性曲线动画。Curves 提供了很多常用的曲线,比如震荡曲线 elasticOut

CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.elasticOut); // 震荡曲线
Animation<double> animation = Tween(begin: 50.0, end: 300.0).animate(curve);

心跳效果

如果想让动画像心跳一样执行,有两个办法:

  • 使用 controller.repeat(reverse: true) 启动动画,让动画来回重复执行
  • 监听动画状态,在动画结束时,反向执行;在动画反向执行完毕时,重新启动执行
animation.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    controller.reverse(); // 动画结束时反向执行
  } else if (status == AnimationStatus.dismissed) {
    controller.forward(); // 动画反向执行完毕时,重新执行
  }
});
controller.forward();

简化动画代码

Flutter 提供了两个类来简化动画代码,即 AnimatedWidget 与 AnimatedBuilder。

AnimatedWidget

AnimatedWidget 会将 Animation 的状态与其子 Widget 的视觉样式绑定,因此,我们只需把 Animation 对象传入 AnimatedWidget 即可,而不用再通过监听动画的执行进度刷新 UI 了。

class AnimatedImage extends AnimatedWidget {
  final Animation<double> animation;

  const AnimatedImage(this.animation, {super.key}) : super(listenable: animation);

  @override
  Widget build(BuildContext context) => Image.asset("assets/images/monkey.jpg", width: animation.value);
}

AnimatedBuilder

与 AnimatedWidget 类似,AnimatedBuilder 也会自动监听 Animation 对象的变化,并根据需要将该控件树标记为 dirty 以自动刷新 UI。

AnimatedBuilder 也是继承自 AnimatedWidget 的

AnimatedBuilder(
  animation: animation, // 自动监听 Animation 的变化,并通过 builder 重新构建 Widget
  builder: (context, child) => Image.asset("assets/images/monkey.jpg", width: animation.value),
)

Hero 动画

共享元素变换 Shared Element Transition:在两个页面的共享元素之间,做出流畅的页面切换效果

Android 原生提供了对这种动画效果的支持,通过几行代码,就可以实现在两个 Activity 共享的组件之间做出流畅的转场动画。Flutter 也有类似的概念,即 Hero 控件。

为了实现共享元素变换,我们需要将这两个组件分别用 Hero 包裹,并同时为它们设置相同的 tag

class HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  void _onPressed() {
    Route<HomePage> route = MaterialPageRoute(
      builder: (context) => const HomePage(title: "页面2"),
      settings: const RouteSettings(arguments: 300.0),
    );
    Navigator.push(context, route);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Hero(
        tag: 'monkey', // 设置共享 tag
        child: Image.asset("assets/images/monkey.jpg", width: getArguments(context)),
      ),
      floatingActionButton: FloatingActionButton(onPressed: _onPressed));

  double getArguments(BuildContext context) {
    Object? arguments = ModalRoute.of(context)?.settings.arguments;
    return arguments is double ? arguments : 50.0;
  }
}

总结

在 Flutter 中,动画的状态与渲染是分离的。我们通过 Animation 生成动画曲线,使用 AnimationController 控制动画时间、启动动画。而动画的渲染,则需要 addListener 获取动画进度后,主动刷新才能实现动画的更新。

为了简化这一步骤,Flutter 提供了 AnimatedWidgetAnimatedBuilder 这两个组件,省去了状态监听和 UI 刷新的工作。而对于跨页面动画,Flutter 提供了 Hero 组件,只要两个组件有同样的 tag,就能实现元素跨页面过渡的转场效果。

建议尽量使用 AnimatedWidget 或 AnimatedBuilder 来缩小受动画影响的组件范围,只重绘需要做动画的组件。

2023-1-8

posted @ 2023-01-08 15:51  白乾涛  阅读(182)  评论(0编辑  收藏  举报