美文网首页
Flutter动画学习之实践

Flutter动画学习之实践

作者: 愿天深海 | 来源:发表于2023-11-06 20:09 被阅读0次

    在上一篇文章Flutter动画学习之简介中了解了Animation、Curve、Controller、Tween在Flutter中动画中最主要的四个角色。本篇文章就开始实践。

    官方学习文档

    本篇文章Demo下载

    基础匀速版

    创建一个AnimationController,指定时间3秒。使用Tween指定范围100到300。通过controller.forward()启动动画,通过controller.reset()重置动画可重新再次启动动画。

    ///线性缩放大小
    class ScaleAnimationDemo1 extends StatefulWidget {
      const ScaleAnimationDemo1({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _ScaleAnimationDemoState1();
    }
    
    //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
    class _ScaleAnimationDemoState1 extends State<ScaleAnimationDemo1>
        with SingleTickerProviderStateMixin {
      late Animation<double> animation;
      late AnimationController controller;
    
      @override
      void initState() {
        super.initState();
        controller =
            AnimationController(duration: const Duration(seconds: 3), vsync: this);
        //没有指定Curve,过程是线性的,从100变到300
        animation = Tween(begin: 100.0, end: 300.0).animate(controller)
          ..addListener(() {
            setState(() {});
          });
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            const Padding(padding: EdgeInsets.only(top: 120)),
            TextButton(
              child: const Text(
                '线性缩放大小',
                style: TextStyle(color: Colors.blueAccent),
              ),
              onPressed: () => setState(() {
                //重置动画
                controller.reset();
                //启动动画(正向执行)
                controller.forward();
              }),
            ),
            Icon(Icons.access_alarm, size: animation.value)
          ],
        );
      }
    
      @override
      void dispose() {
        //路由销毁时需要释放动画资源
        controller.dispose();
        super.dispose();
      }
    }
    

    addListener()函数调用了setState(),所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用,而在build()中,Icon的size使用的是animation.value ,所以就会逐渐放大。值得注意的是动画完成时要释放控制器(调用dispose()方法)以防止内存泄漏。

    Curve曲线版

    上述例子由于没有指定Curve,所以放大的过程是线性的(匀速),下面指定一个Curve,来实现一个类似于弹簧效果的动画过程。

    需要使用CurvedAnimation包装AnimationController和Curve生成一个新的动画对象。

    ///弹簧效果Curve缩放大小
    class ScaleAnimationDemo2 extends StatefulWidget {
      const ScaleAnimationDemo2({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _ScaleAnimationDemoState2();
    }
    
    //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
    class _ScaleAnimationDemoState2 extends State<ScaleAnimationDemo2>
        with SingleTickerProviderStateMixin {
      late Animation<double> animation;
      late AnimationController controller;
    
      @override
      void initState() {
        super.initState();
        controller =
            AnimationController(duration: const Duration(seconds: 3), vsync: this);
        //指定弹簧效果Curve
        animation = CurvedAnimation(parent: controller, curve: Curves.bounceOut);
        animation = Tween(begin: 100.0, end: 300.0).animate(animation)
          ..addListener(() {
            setState(() {});
          });
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            const Padding(padding: EdgeInsets.only(top: 120)),
            TextButton(
              child: const Text(
                '弹簧效果Curve缩放大小',
                style: TextStyle(color: Colors.blueAccent),
              ),
              onPressed: () => setState(() {
                //重置动画
                controller.reset();
                //启动动画(正向执行)
                controller.forward();
              }),
            ),
            Icon(Icons.access_alarm, size: animation.value)
          ],
        );
      }
    
      @override
      void dispose() {
        //路由销毁时需要释放动画资源
        controller.dispose();
        super.dispose();
      }
    }
    

    使用AnimatedWidget简化

    可以发现更新UI都是通过addListener()和setState(),所有的动画都需要如此属实是重复性工作了。

    AnimatedWidget类封装了调用setState()的细节,并允许将widget分离出来。利用 AnimatedWidget 创建一个可以重复使用运行动画的widget。

    ///AnimatedWidget类封装了调用setState()的细节,并允许将 widget 分离出来
    class ScaleAnimationWidget extends AnimatedWidget {
      const ScaleAnimationWidget({Key? key, required Animation<double> animation})
          : super(key: key, listenable: animation);
    
      @override
      Widget build(BuildContext context) {
        final animation = listenable as Animation<double>;
        return Icon(Icons.access_alarm, size: animation.value);
      }
    }
    
    class ScaleAnimationDemo3 extends StatefulWidget {
      const ScaleAnimationDemo3({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _ScaleAnimationDemoState3();
    }
    
    //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
    class _ScaleAnimationDemoState3 extends State<ScaleAnimationDemo3>
        with SingleTickerProviderStateMixin {
      late Animation<double> animation;
      late AnimationController controller;
    
      @override
      void initState() {
        super.initState();
        controller =
            AnimationController(duration: const Duration(seconds: 3), vsync: this);
        //指定弹簧效果Curve
        animation = CurvedAnimation(parent: controller, curve: Curves.bounceOut);
        animation = Tween(begin: 100.0, end: 300.0).animate(animation);
      }
    
      @override
      Widget build(BuildContext context) {
        return
          Column(
            children: [
              const Padding(padding: EdgeInsets.only(top: 120)),
              TextButton(
                child: const Text(
                  '弹簧效果Curve缩放大小',
                  style: TextStyle(color: Colors.blueAccent),
                ),
                onPressed: () => setState(() {
                  //重置动画
                  controller.reset();
                  //启动动画(正向执行)
                  controller.forward();
                }),
              ),
              ScaleAnimationWidget(animation: animation)
            ],
          );
      }
    
      @override
      void dispose() {
        //路由销毁时需要释放动画资源
        controller.dispose();
        super.dispose();
      }
    }
    

    Flutter API 中的 AnimatedWidget:PositionedTransition, RotationTransition, ScaleTransition, SizeTransition, SlideTransition等。

    动画状态监听

    通过Animation的addStatusListener()方法来添加动画状态改变监听器。
    Flutter中,有四种动画状态,在AnimationStatus枚举类中定义。

    /// The status of an animation.
    enum AnimationStatus {
      /// The animation is stopped at the beginning.
      dismissed,
    
      /// The animation is running from beginning to end.
      forward,
    
      /// The animation is running backwards, from end to beginning.
      reverse,
    
      /// The animation is stopped at the end.
      completed,
    }
    

    在上述例子上,通过监听动画状态,当动画执行结束时反向执行动画,当动画恢复到初始状态时正向执行动画:

    class ScaleAnimationDemo4 extends StatefulWidget {
      const ScaleAnimationDemo4({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _ScaleAnimationDemoState4();
    }
    
    //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
    class _ScaleAnimationDemoState4 extends State<ScaleAnimationDemo4>
        with SingleTickerProviderStateMixin {
      late Animation<double> animation;
      late AnimationController controller;
    
      @override
      void initState() {
        super.initState();
        controller =
            AnimationController(duration: const Duration(seconds: 3), vsync: this);
        animation = Tween(begin: 100.0, end: 300.0).animate(controller)
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              controller.reverse();
            } else if (status == AnimationStatus.dismissed) {
              controller.forward();
            }
          });
      }
    
      @override
      Widget build(BuildContext context) {
        return
          Column(
            children: [
              const Padding(padding: EdgeInsets.only(top: 120)),
              TextButton(
                child: const Text(
                  '缩放大小',
                  style: TextStyle(color: Colors.blueAccent),
                ),
                onPressed: () => setState(() {
                  //启动动画(正向执行)
                  controller.forward();
                }),
              ),
              ScaleAnimationWidget(animation: animation)
            ],
          );
      }
    
      @override
      void dispose() {
        //路由销毁时需要释放动画资源
        controller.dispose();
        super.dispose();
      }
    }
    

    使用AnimatedBuilder重构

    使用AnimatedWidget可以从动画中分离出 widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget 中。
    而AnimatedBuilder正是将渲染逻辑分离出来。AnimatedBuilder知道如何渲染过渡效果,但AnimatedBuilder不会渲染 widget,也不会控制动画对象。使用 AnimatedBuilder描述一个动画是其他 widget 构建方法的一部分。
    AnimatedBuilder 作为渲染树的一个单独类。像 AnimatedWidget,AnimatedBuilder 自动监听动画对象提示,并在必要时在 widget 树中标出,所以这时不需要调用 addListener()。

    class ScaleAnimationDemo5 extends StatefulWidget {
      const ScaleAnimationDemo5({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _ScaleAnimationDemoState5();
    }
    
    //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
    class _ScaleAnimationDemoState5 extends State<ScaleAnimationDemo5>
        with SingleTickerProviderStateMixin {
      late Animation<double> animation;
      late AnimationController controller;
    
      @override
      void initState() {
        super.initState();
        controller =
            AnimationController(duration: const Duration(seconds: 3), vsync: this);
        animation = Tween(begin: 100.0, end: 300.0).animate(controller);
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            const Padding(padding: EdgeInsets.only(top: 120)),
            TextButton(
              child: const Text(
                '缩放大小',
                style: TextStyle(color: Colors.blueAccent),
              ),
              onPressed: () => setState(() {
                //重置动画
                controller.reset();
                //启动动画(正向执行)
                controller.forward();
              }),
            ),
            AnimatedBuilder(
              animation: animation,
              builder: (BuildContext ctx, child) {
                return Icon(Icons.access_alarm, size: animation.value);
              },
            )
          ],
        );
      }
    
      @override
      void dispose() {
        //路由销毁时需要释放动画资源
        controller.dispose();
        super.dispose();
      }
    }
    

    Flutter API 中 AnimatedBuilders:BottomSheet, ExpansionTile, PopupMenu, ProgressIndicator, RefreshIndicator, Scaffold, SnackBar, TabBar, TextField等。

    复合补间动画

    在同一个动画控制器中使用复合补间动画可以达到多个动画效果,每个补间动画控制一个动画的不同效果。
    由于AnimatedWidget和AnimatedBuilder都只能读取单一的 Animation 对象,因此每一个动画效果都创建一个Tween对象并计算确切值Tween.evaluate()。

    class ScaleAnimationDemo6 extends StatefulWidget {
      const ScaleAnimationDemo6({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _ScaleAnimationDemoState6();
    }
    
    //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
    class _ScaleAnimationDemoState6 extends State<ScaleAnimationDemo6>
        with SingleTickerProviderStateMixin {
      late Tween<double> sizeTween;
      late Tween<double> opacityTween;
      late AnimationController controller;
    
      @override
      void initState() {
        super.initState();
        controller =
            AnimationController(duration: const Duration(seconds: 3), vsync: this);
        sizeTween = Tween(begin: 100.0, end: 300.0);
        opacityTween = Tween(begin: 0.1, end: 1.0);
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            const Padding(padding: EdgeInsets.only(top: 120)),
            TextButton(
              child: const Text(
                '缩放大小同时透明度增加',
                style: TextStyle(color: Colors.blueAccent),
              ),
              onPressed: () => setState(() {
                //重置动画
                controller.reset();
                //启动动画(正向执行)
                controller.forward();
              }),
            ),
            AnimatedBuilder(
              animation: controller,
              builder: (BuildContext ctx, child) {
                return Opacity(
                  opacity: opacityTween.evaluate(controller),
                  child: Icon(Icons.access_alarm,
                      size: sizeTween.evaluate(controller)),
                );
              },
            )
          ],
        );
      }
    
      @override
      void dispose() {
        //路由销毁时需要释放动画资源
        controller.dispose();
        super.dispose();
      }
    }
    

    隐式动画

    通过 Flutter 的 动画库,你可以为 UI 中的 widgets 添加动作并创造视觉效果。有些库包含各种各样可以帮你管理动画的 widget。这些 widgets 被统称为 隐式动画隐式动画 widget,其名字来源于它们所实现的 ImplicitlyAnimatedWidget 类。

    使用隐式动画,可以通过设置一个目标值,驱动 widget 的属性进行动画变换;每当目标值发生变化时,属性会从旧值逐渐更新到新值。通过这种方式,隐式动画内部实现了动画控制,从而能够方便地使用隐式动画组件会管理动画效果,用户不需要再进行额外的处理。

    推荐学习隐式动画教程

    AnimatedOpacity

    使用 AnimatedOpacity widget 对 opacity 属性进行动画。
    AnimatedOpacity的构造方法如下

    const AnimatedOpacity({
      Key? key,
      Widget? child,
      required double opacity,
      Curve curve = Curves.linear,
      required Duration duration,
      VoidCallback? onEnd,
      boolean alwaysIncludeSemantics = false,
    }) 
    

    对应的参数:

    • child:要控制透明度的子组件;
    • opacity:最终的透明度值,取值范围从 0.0(不可见)到 1.0(完全可见);
    • curve:动画曲线,默认是线性的Curves.linear,可以使用 Curves 来构建曲线效果;
    • duration:动画时长;
    • onEnd:动画结束回调方法;
    • alwaysIncludeSemantics:是否总是包含语义信息,默认是 false。这个主要是用于辅助访问的,如果是 true,则不管透明度是多少,都会显示语义信息(可以辅助朗读),这对于视障人员来说会更友好。
    class FadeInDemo extends StatefulWidget {
      const FadeInDemo({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _FadeInDemoState();
    }
    
    class _FadeInDemoState extends State<FadeInDemo> {
      double opacity = 0.0;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            const Padding(padding: EdgeInsets.only(top: 120)),
            TextButton(
              child: const Text(
                '透明度变化',
                style: TextStyle(color: Colors.blueAccent),
              ),
              onPressed: () => setState(() {
                if (opacity == 0.0) {
                  opacity = 1;
                } else {
                  opacity = 0.0;
                }
              }),
            ),
            AnimatedOpacity(
              opacity: opacity,
              duration: const Duration(seconds: 2),
              child: const Icon(Icons.access_alarm, size: 200),
            )
          ],
        );
      }
    }
    

    AnimatedContainer

    使用 AnimatedContainer widget 让多个不同类型(doubleColor)的属性(marginborderRadiuscolor)同时进行动画变换。

    double randomBorderRadius() {
      return Random().nextDouble() * 64;
    }
    
    double randomMargin() {
      return Random().nextDouble() * 64;
    }
    
    Color randomColor() {
      return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
    }
    
    class AnimatedContainerDemo extends StatefulWidget {
      const AnimatedContainerDemo({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _AnimatedContainerDemoState();
    }
    
    class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
      late Color color;
      late double borderRadius;
      late double margin;
    
      @override
      void initState() {
        super.initState();
        color = randomColor();
        borderRadius = randomBorderRadius();
        margin = randomMargin();
      }
    
      void change() {
        setState(() {
          color = randomColor();
          borderRadius = randomBorderRadius();
          margin = randomMargin();
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: [
            const Padding(padding: EdgeInsets.only(top: 120)),
            TextButton(
                child: const Text(
                  'change',
                  style: TextStyle(color: Colors.blueAccent),
                ),
                onPressed: () => change()),
            SizedBox(
              width: 128,
              height: 128,
              child: AnimatedContainer(
                margin: EdgeInsets.all(margin),
                duration: const Duration(seconds: 2),
                decoration: BoxDecoration(
                    color: color,
                    borderRadius: BorderRadius.circular(borderRadius)),
                curve: Curves.easeInOutBack,
              ),
            )
          ],
        );
      }
    }
    

    Flutter API 中的 隐式动画:AnimatedAlign, AnimatedRotation, AnimatedScale, AnimatedPositioned, AnimatedSlide。

    交织动画

    交织动画是由一系列的小动画组成的动画。每个小动画可以是连续或间断的,也可以相互重叠。其关键点在于使用 Interval 给每个小动画设置一个时间间隔,以及为每个动画的设置一个取值范围 Tween,最后使用一个 AnimationController 控制总体的动画状态。

    Interval 继承至 Curve 类,通过设置属性 begin 和 end 来确定这个小动画的运行范围。

    class Interval extends Curve {
      /// 动画起始点
      final double begin;
      /// 动画结束点
      final double end;
      /// 动画缓动曲线
      final Curve curve;
    }
    

    下面看一个例子,实现一个柱状图增长的动画:

    1. 开始时高度从0增长到300像素,同时颜色由绿色渐变为红色;这个过程占据整个动画时间的60%。
    2. 高度增长到300后,开始沿X轴向右平移100像素;这个过程占用整个动画时间的40%。
    class StaggerAnimationWidget extends StatelessWidget {
      StaggerAnimationWidget({Key? key, required this.controller})
          : super(key: key) {
        height = Tween(begin: 0.0, end: 300.0).animate(CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.0, 0.6, //间隔,前60%的动画时间
              curve: Curves.ease,
            )));
        padding = Tween(
                begin: const EdgeInsets.only(left: 0.0),
                end: const EdgeInsets.only(left: 100.0))
            .animate(CurvedAnimation(
                parent: controller,
                curve: const Interval(0.6, 1.0, //间隔,后40%的动画时间
                    curve: Curves.ease)));
        color = ColorTween(begin: Colors.green, end: Colors.red).animate(
          CurvedAnimation(
            parent: controller,
            curve: const Interval(
              0.0, 0.6, //间隔,前60%的动画时间
              curve: Curves.ease,
            ),
          ),
        );
      }
    
      late final Animation<double> controller;
      late final Animation<double> height;
      late final Animation<EdgeInsets> padding;
      late final Animation<Color?> color;
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
            animation: controller,
            builder: (BuildContext context, child) {
              return Container(
                  alignment: Alignment.bottomCenter,
                  padding: padding.value,
                  child: Container(
                    color: color.value,
                    width: 50,
                    height: height.value,
                  ));
            });
      }
    }
    
    class StaggerAnimationDemo extends StatefulWidget {
      const StaggerAnimationDemo({Key? key}) : super(key: key);
    
      @override
      State<StatefulWidget> createState() => _StaggerAnimationDemoState();
    }
    
    class _StaggerAnimationDemoState extends State<StaggerAnimationDemo>
        with TickerProviderStateMixin {
      late AnimationController controller;
    
      @override
      void initState() {
        super.initState();
        controller =
            AnimationController(duration: const Duration(seconds: 2), vsync: this);
      }
    
      void _playAnimation() async {
        try {
          //先正向执行动画
          await controller.forward().orCancel;
          //再反向执行动画
          await controller.reverse().orCancel;
        } on TickerCanceled {
          //捕获异常。可能发生在组件销毁时,计时器会被取消。
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Column(
            children: [
              const Padding(padding: EdgeInsets.only(top: 120)),
              ElevatedButton(
                onPressed: () => _playAnimation(),
                child: const Text("start animation"),
              ),
              Container(
                width: 300.0,
                height: 300.0,
                decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.1),
                  border: Border.all(
                    color: Colors.black.withOpacity(0.5),
                  ),
                ),
                //调用我们定义的交错动画Widget
                child: StaggerAnimationWidget(controller: controller),
              ),
            ],
          ),
        );
      }
    }
    

    Hero(跨页面共享元素)动画

    Hero 指的是在页面(路由)间飞跃的 widget。简单来说 Hero 动画就是在路由切换时,有一个共享的widget 可以在新旧路由间切换。由于共享的 widget 在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个 Hero 动画。

    你可能经常遇到 hero 动画。比如,页面上显示的代售商品列表。选择一件商品后,应用会跳转至包含更多细节以及“购买”按钮的新页面。在 Flutter 中,图像从当前页面转到另一个页面称为 hero 动画,相同的动作有时也被称为 共享元素过渡。

    class HeroAnimationRouteA extends StatelessWidget {
      const HeroAnimationRouteA({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Container(
          alignment: Alignment.topCenter,
          child: Column(
            children: [
              const Padding(padding: EdgeInsets.only(top: 120)),
              InkWell(
                child: Hero(
                  tag: "avatar",
                  child: ClipOval(
                    child: Image.asset(
                      "images/cat.jpeg",
                      width: 50,
                    ),
                  ),
                ),
                onTap: () {
                  Navigator.push(
                      context,
                      MaterialPageRoute(
                          builder: (context) => const HeroAnimationRouteB()));
                },
              ),
            ],
          ),
        );
      }
    }
    
    class HeroAnimationRouteB extends StatelessWidget {
      const HeroAnimationRouteB({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Material(
              child: InkWell(
                child: Hero(
                  tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
                  child: Image.asset("images/cat.jpeg"),
                ),
                onTap: () {
                  Navigator.pop(context);
                },
              ),
            ),
          ),
        );
      }
    }
    

    实现 Hero 动画只需要用Hero组件将要共享的 widget 包装起来,并提供一个相同的 tag 即可,中间的过渡帧都是 Flutter 框架自动完成的。必须要注意, 前后路由页的共享Hero的 tag 必须是相同的,Flutter 框架内部正是通过 tag 来确定新旧路由页widget的对应关系的。

    页面转场动画

    在不同路由(页面)之间进行切换的时候,许多设计语言,例如 Material 设计,都定义了一些标准行为。

    Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换。如果在Android上也想使用左右切换风格,一个简单的作法是可以直接使用CupertinoPageRoute。

    但有时自定义路由动画会让 app 看上去更加的独特。为了更好的完成这一点,PageRouteBuilder提供了一个Animation对象,能够通过结合Tween以及Curve对象来自定义路由转换动画。

    PageRouteBuilder中跟页面转场动画相关的参数只要有3个,

    • pageBuilder:创建这个路由的内容
    • transitionsBuilder:创建路由转换器,也就是路由动画
    • transitionDuration:路由转换动画时长,默认是300毫秒

    提示:transitionsBuilder 的 child 参数是通过 pageBuilder 方法来返回一个 transitionsBuilder widget,这个 pageBuilder 方法仅会在第一次构建路由的时候被调用。框架能够自动避免做额外的工作,因为整个过渡期间 child 保存了同一个实例。

    下面看一个例子,使新页面从底部出来

    class Page1 extends StatelessWidget {
      const Page1({super.key});
    
      @override
      Widget build(BuildContext context) {
        return ElevatedButton(
          onPressed: () {
            Navigator.of(context).push(_createRoute());
          },
          child: const Text('Go!'),
        );
      }
    }
    
    Route _createRoute() {
      return PageRouteBuilder(
        pageBuilder: (context, animation, secondaryAnimation) => const Page2(),
        transitionDuration: const Duration(seconds: 3),
        transitionsBuilder: (context, animation, secondaryAnimation, child) {
          //页面从底部出来
          const begin = Offset(0.0, 1.0);
          const end = Offset.zero;
          const curve = Curves.ease;
          //结合两个 tween,请使用 chain()
          var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
          return SlideTransition(
            position: animation.drive(tween), //drive() 来创建一个新的 Animation<Offset>
            child: child,
          );
        },
      );
    }
    
    class Page2 extends StatelessWidget {
      const Page2({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            color: Colors.black26,
            child: const Center(
              child: Text('Page 2'),
            ),
          ),
        );
      }
    }
    

    相关文章

      网友评论

          本文标题:Flutter动画学习之实践

          本文链接:https://www.haomeiwen.com/subject/mfbvidtx.html