Flutter 动画(显式动画、隐式动画、Hero动画、页面转场动画、交错动画)

前言

当前案例 Flutter SDK版本:3.13.2

显式动画

Tween({this.begin,this.end}) 两个构造参数,分别是 开始值 和 结束值,根据这两个值,提供了控制动画的方法,以下是常用的;

  • controller.forward() : 向前,执行 begin 到 end 的动画,执行结束后,处于end状态;
  • controller.reverse() : 反向,当动画已经完成,进行还原动画;
  • controller.reset() : 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画;

    使用方式一

    使用 addListener() 和 setState();

    import 'package:flutter/material.dart';
    class TweenAnimation extends StatefulWidget {
      const TweenAnimation({super.key});
      @override
      State createState() => _TweenAnimationState();
    }
    /// 使用 addListener() 和 setState()
    class _TweenAnimationState extends State with SingleTickerProviderStateMixin {
      late Animation animation;
      late AnimationController controller;
      @override
      void initState() {
        super.initState();
        controller = AnimationController(
            duration: const Duration(milliseconds: 2000), vsync: this);
        animation = Tween(begin: 50, end: 100).animate(controller)
          ..addListener(() {
            setState(() {}); // 更新UI
          })..addStatusListener((status) {
            debugPrint('status:$status'); // 监听动画执行状态
          });
      }
      @override
      void dispose() {
        controller.dispose();
        super.dispose();
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
              title: const Text(
            '显式动画',
            style: TextStyle(fontSize: 20),
          )),
          body: SizedBox(
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    SizedBox(
                      width: animation.value,
                      height: animation.value,
                      child: const FlutterLogo(),
                    ),
                    ElevatedButton(
                      onPressed: () {
                        if (controller.isCompleted) {
                          controller.reverse();
                        } else {
                          controller.forward();
                        }
                        // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                        // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                        // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                      },
                      child: const Text('缩放'),
                    )
                  ],
                )
              ],
            ),
          ),
        );
      }
    }
    

    使用方式二

    AnimatedWidget,解决痛点:不需要再使用 addListener() 和 setState();

    import 'package:flutter/material.dart';
    class TweenAnimation extends StatefulWidget {
      const TweenAnimation({super.key});
      @override
      State createState() => _TweenAnimationState();
    }
    /// 测试 AnimatedWidget
    class _TweenAnimationState extends State with SingleTickerProviderStateMixin {
      late Animation animation;
      late AnimationController controller;
      @override
      void initState() {
        super.initState();
        controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
        animation = Tween(begin: 50, end: 100).animate(controller)
          ..addStatusListener((status) {
            debugPrint('status:$status'); // 监听动画执行状态
          });
      }
      @override
      void dispose() {
        controller.dispose();
        super.dispose();
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
              title: const Text(
            '显式动画',
            style: TextStyle(fontSize: 20),
          )),
          body: SizedBox(
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    AnimatedLogo(animation: animation),
                    ElevatedButton(
                      onPressed: () {
                        if (controller.isCompleted) {
                          controller.reverse();
                        } else {
                          controller.forward();
                        }
                        // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                        // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                        // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                      },
                      child: const Text('缩放'),
                    )
                  ],
                )
              ],
            ),
          ),
        );
      }
    }
    /// 使用 AnimatedWidget,创建显式动画
    /// 解决痛点:不需要再使用 addListener() 和 setState()
    class AnimatedLogo extends AnimatedWidget {
      const AnimatedLogo({super.key, required Animation animation})
          : super(listenable: animation);
      @override
      Widget build(BuildContext context) {
        final animation = listenable as Animation;
        return Center(
          child: Container(
            margin: const EdgeInsets.symmetric(vertical: 10),
            width: animation.value,
            height: animation.value,
            child: const FlutterLogo(),
          ),
        );
      }
    }
    

    使用方式三

    使用 内置的显式动画 widget;

    后缀是 Transition 的组件,几乎都是 显式动画 widget;

    import 'package:flutter/material.dart';
    class TweenAnimation extends StatefulWidget {
      const TweenAnimation({super.key});
      @override
      State createState() => _TweenAnimationState();
    }
    /// 使用 内置的显式动画Widget
    class _TweenAnimationState extends State with SingleTickerProviderStateMixin {
      late Animation animation;
      late AnimationController controller;
      @override
      void initState() {
        super.initState();
        controller = AnimationController(
            duration: const Duration(milliseconds: 1000), vsync: this);
        animation = Tween(begin: 0.1, end: 1.0).animate(controller)
          ..addListener(() {
            setState(() {}); // 更新UI
          })
          ..addStatusListener((status) {
            debugPrint('status:$status'); // 监听动画执行状态
          });
      }
      @override
      void dispose() {
        controller.dispose();
        super.dispose();
      }
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
              title: const Text(
            '显式动画',
            style: TextStyle(fontSize: 20),
          )),
          body: SizedBox(
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    /// 单个显示动画
                    FadeTransition(
                      opacity: animation,
                      child: const SizedBox(
                        width: 100,
                        height: 100,
                        child: FlutterLogo(),
                      ),
                    ),
                    /// 多个显示动画 配合使用
                    // FadeTransition( // 淡入淡出
                    //   opacity: animation,
                    //   child: RotationTransition( // 旋转
                    //     turns: animation,
                    //     child: ScaleTransition( // 更替
                    //       scale: animation,
                    //       child: const SizedBox(
                    //         width: 100,
                    //         height: 100,
                    //         child: FlutterLogo(),
                    //       ),
                    //     ),
                    //   ),
                    // ),
                    ElevatedButton(
                      onPressed: () {
                        if (controller.isCompleted) {
                          controller.reverse();
                        } else {
                          controller.forward();
                        }
                        // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                        // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                        // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                      },
                      child: const Text('淡入淡出'),
                    )
                  ],
                )
              ],
            ),
          ),
        );
      }
    }
    

    使用方式四

    AnimatedBuilder,这种方式感觉是 通过逻辑 动态选择 Widget,比如 flag ? widgetA : widgetB;

    官方解释:

    • AnimatedBuilder 知道如何渲染过渡效果
    • 但 AnimatedBuilder 不会渲染 widget,也不会控制动画对象。
    • 使用 AnimatedBuilder 描述一个动画是其他 widget 构建方法的一部分。
    • 如果只是单纯需要用可重复使用的动画定义一个 widget,可参考文档:简单使用 AnimatedWidget。
      import 'package:flutter/material.dart';
      class TweenAnimation extends StatefulWidget {
        const TweenAnimation({super.key});
        @override
        State createState() => _TweenAnimationState();
      }
      /// 测试 AnimatedBuilder
      class _TweenAnimationState extends State with SingleTickerProviderStateMixin {
        late Animation animation;
        late AnimationController controller;
        @override
        void initState() {
          super.initState();
          controller = AnimationController(
              duration: const Duration(milliseconds: 2000), vsync: this);
          animation = Tween(begin: 50, end: 100).animate(controller)
            ..addStatusListener((status) {
              debugPrint('status:$status'); // 监听动画执行状态
            });
        }
        @override
        void dispose() {
          controller.dispose();
          super.dispose();
        }
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
                title: const Text(
                  '显式动画',
                  style: TextStyle(fontSize: 20),
                )),
            body: SizedBox(
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      GrowTransition(
                          animation: animation,
                          child: const FlutterLogo()),
                      ElevatedButton(
                        onPressed: () {
                          if (controller.isCompleted) {
                            controller.reverse();
                          } else {
                            controller.forward();
                          }
                          // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                          // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                          // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                        },
                        child: const Text('缩放'),
                      )
                    ],
                  )
                ],
              ),
            ),
          );
        }
      }
      class GrowTransition extends StatelessWidget {
        final Widget child;
        final Animation animation;
        const GrowTransition(
            {required this.child, required this.animation, super.key});
        @override
        Widget build(BuildContext context) {
          return Center(
            child: AnimatedBuilder(
              animation: animation,
              builder: (context, child) {
                return SizedBox(
                  width: animation.value,
                  height: animation.value,
                  child: child,
                );
              },
              child: child,
            ),
          );
        }
      }
      

      使用方式五

      CurvedAnimation 曲线动画,一个Widget,同时使用多个动画;

      import 'package:flutter/material.dart';
      class TweenAnimation extends StatefulWidget {
        const TweenAnimation({super.key});
        @override
        State createState() => _TweenAnimationState();
      }
      /// 测试 动画同步使用
      class _TweenAnimationState extends State with SingleTickerProviderStateMixin {
        late Animation animation;
        late AnimationController controller;
        @override
        void initState() {
          super.initState();
          controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
          animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
            ..addStatusListener((status) {
              debugPrint('status:$status'); // 监听动画执行状态
            });
        }
        @override
        void dispose() {
          controller.dispose();
          super.dispose();
        }
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
                title: const Text(
              '显式动画',
              style: TextStyle(fontSize: 20),
            )),
            body: SizedBox(
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      AnimatedLogoSync(animation: animation),
                      ElevatedButton(
                        onPressed: () {
                          if (controller.isCompleted) {
                            controller.reverse();
                          } else {
                            controller.forward();
                          }
                          // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                          // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                          // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                        },
                        child: const Text('缩放 + 淡入淡出'),
                      )
                    ],
                  )
                ],
              ),
            ),
          );
        }
      }
      /// 动画同步使用
      class AnimatedLogoSync extends AnimatedWidget {
        AnimatedLogoSync({super.key, required Animation animation})
            : super(listenable: animation);
        final Tween _opacityTween = Tween(begin: 0.1, end: 1);
        final Tween _sizeTween = Tween(begin: 50, end: 100);
        @override
        Widget build(BuildContext context) {
          final animation = listenable as Animation;
          return Center(
            child: Opacity(
              opacity: _opacityTween.evaluate(animation),
              child: SizedBox(
                width: _sizeTween.evaluate(animation),
                height: _sizeTween.evaluate(animation),
                child: const FlutterLogo(),
              ),
            ),
          );
        }
      }

       隐式动画

      • 根据属性值变化,为 UI 中的 widget 添加动作并创造视觉效果,有些库包含各种各样可以帮你管理动画的widget,这些widgets被统称为 隐式动画 或 隐式动画 widget。
      • 前缀是 Animated 的组件,几乎都是 隐式动画 widget;      

        import 'dart:math';
        import 'package:flutter/material.dart';
        class ImplicitAnimation extends StatefulWidget {
          const ImplicitAnimation({super.key});
          @override
          State createState() => _ImplicitAnimationState();
        }
        class _ImplicitAnimationState extends State {
          double opacity = 0;
          late Color color;
          late double borderRadius;
          late double margin;
          double randomBorderRadius() {
            return Random().nextDouble() * 64;
          }
          double randomMargin() {
            return Random().nextDouble() * 32;
          }
          Color randomColor() {
            return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
          }
          @override
          void initState() {
            super.initState();
            color = randomColor();
            borderRadius = randomBorderRadius();
            margin = randomMargin();
          }
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                  title: const Text(
                '隐式动画',
                style: TextStyle(fontSize: 20),
              )),
              body: SizedBox(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        AnimatedOpacity(
                          opacity: opacity,
                          curve: Curves.easeInOutBack,
                          duration: const Duration(milliseconds: 1000),
                          child: Container(
                            width: 50,
                            height: 50,
                            margin: const EdgeInsets.only(right: 12),
                            color: Colors.primaries[2],
                          ),
                        ),
                        ElevatedButton(
                          onPressed: () {
                            if(opacity == 0) {
                              opacity = 1;
                            } else {
                              opacity = 0;
                            }
                            setState(() {});
                          },
                          child: const Text('淡入或淡出'),
                        )
                      ],
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        AnimatedContainer(
                          width: 50,
                          height: 50,
                          margin: EdgeInsets.all(margin),
                          decoration: BoxDecoration(
                            color: color,
                            borderRadius: BorderRadius.circular(borderRadius)
                          ),
                          curve: Curves.easeInBack,
                          duration: const Duration(milliseconds: 1000),
                        ),
                        ElevatedButton(
                          onPressed: () {
                            color = randomColor();
                            borderRadius = randomBorderRadius();
                            margin = randomMargin();
                            setState(() {});
                          },
                          child: const Text('形状变化'),
                        )
                      ],
                    )
                  ],
                ),
              ),
            );
          }
        }
        

        显示和隐式的区别

        看图,隐式动画 就是 显示动画 封装后的产物,是不是很蒙,这有什么意义?

        应用场景不同:如果想 控制动画,使用 显示动画,controller.forward()、controller.reverse()、controller.reset(),反之只是在Widget属性值发生改变,进行UI过渡这种简单操作,使用 隐式动画;

        误区

        Flutter显式动画的关键对象 Tween,翻译过来 补间,联想到 Android原生的补间动画,就会有一个问题,Android原生的补间动画,只是视觉上的UI变化,对象属性并非真正改变,那么Flutter是否也是如此?

        答案:非也,是真的改变了,和Android原生补间动画不同,看图:

        以下偏移动画,在Flutter中的,点击偏移后的矩形位置,可以触发提示,反之Android原生不可以,只能在矩形原来的位置,才能触发;

        Flutte 提示库 以及 封装相关 的代码

        fluttertoast: ^8.2.4

        toast_util.dart

        import 'package:flutter/material.dart';
        import 'package:fluttertoast/fluttertoast.dart';
        class ToastUtil {
          static FToast fToast = FToast();
          static void init(BuildContext context) {
            fToast.init(context);
          }
          static void showToast(String msg) {
            Widget toast = Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
                  decoration: BoxDecoration(
                    borderRadius: BorderRadius.circular(25.0),
                    color: Colors.greenAccent,
                  ),
                  alignment: Alignment.center,
                  child: Text(msg),
                )
              ],
            );
            fToast.showToast(
              child: toast,
              gravity: ToastGravity.BOTTOM,
              toastDuration: const Duration(seconds: 2),
            );
          }
        }
        

        在Flutter主入口 初始化Toast配置

        import 'package:flutter/material.dart';
        import 'package:flutter_animation/util/toast_util.dart';
        import 'package:fluttertoast/fluttertoast.dart';
        void main() {
          runApp(const MyApp());
        }
        GlobalKey navigatorKey = GlobalKey(); // 配置一
        class MyApp extends StatelessWidget {
          const MyApp({super.key});
          @override
          Widget build(BuildContext context) {
            return MaterialApp(
              builder: FToastBuilder(), // 配置二
              navigatorKey: navigatorKey, // 配置三
              ... ...
              home: const MyHomePage(title: 'Flutter Demo Home Page'),
            );
          }
        }
        class MyHomePage extends StatefulWidget {
          const MyHomePage({super.key, required this.title});
          final String title;
          @override
          State createState() => _MyHomePageState();
        }
        class _MyHomePageState extends State {
          @override
          void initState() {
            super.initState();
            ToastUtil.init(context); // 配置四
          }
          
          ... ...
        } 

        Flutter显示动画 代码

        import 'package:flutter/material.dart';
        import 'package:flutter_animation/util/toast_util.dart';
        class TweenAnimation extends StatefulWidget {
          const TweenAnimation({super.key});
          @override
          State createState() => _TweenAnimationState();
        }
        /// 测试显式动画,属性是否真的改变了
        class _TweenAnimationState extends State with SingleTickerProviderStateMixin {
          late Animation animation;
          late AnimationController controller;
          @override
          void initState() {
            super.initState();
            controller = AnimationController(
                duration: const Duration(milliseconds: 500), vsync: this);
            animation =
                Tween(begin: const Offset(0, 0), end: const Offset(1.5, 0))
                    .animate(controller)
                  ..addStatusListener((status) {
                    debugPrint('status:$status'); // 监听动画执行状态
                  });
          }
          @override
          void dispose() {
            controller.dispose();
            super.dispose();
          }
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                  title: const Text(
                'Flutter 显式动画',
                style: TextStyle(fontSize: 20),
              )),
              body: Container(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                color: Colors.primaries[5],
                child: Stack(
                  children: [
                    Align(
                      alignment: Alignment.center,
                      child: Container(
                        width: 80,
                        height: 80,
                        decoration: BoxDecoration(
                          border: Border.all(color: Colors.white, width: 1.0),
                        ),
                      ),
                    ),
                    Align(
                      alignment: Alignment.center,
                      child: SlideTransition(
                        position: animation,
                        child: InkWell(
                          onTap: () {
                            ToastUtil.showToast('点击了');
                          },
                          child: Container(
                            width: 80,
                            height: 80,
                            color: Colors.primaries[2],
                          ),
                        ),
                      ),
                    ),
                    Positioned(
                      left: (MediaQuery.of(context).size.width / 2) - 35,
                      top: 200,
                      child: ElevatedButton(
                        onPressed: () {
                          if (controller.isCompleted) {
                            controller.reverse();
                          } else {
                            controller.forward();
                          }
                          // controller.forward(); // 向前,执行 begin 到 end 的动画,执行结束后,处于end状态
                          // controller.reverse(); // 反向,当动画已经完成,进行还原动画
                          // controller.reset(); // 重置,当动画已经完成,进行还原,注意这个是直接还原,没有动画
                        },
                        child: const Text('偏移'),
                      ),
                    )
                  ],
                ),
              ),
            );
          }
        }

        Flutter隐式动画 代码

        import 'package:flutter/material.dart';
        import 'package:flutter_animation/util/toast_util.dart';
        class ImplicitAnimation extends StatefulWidget {
          const ImplicitAnimation({super.key});
          @override
          State createState() => _ImplicitAnimationState();
        }
        /// 测试隐式动画,属性是否真的改变了
        class _ImplicitAnimationState extends State {
          late double offsetX = 0;
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                  title: const Text(
                'Flutter 隐式动画',
                style: TextStyle(fontSize: 20),
              )),
              body: Container(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                color: Colors.primaries[5],
                child: Stack(
                  children: [
                    Align(
                      alignment: Alignment.center,
                      child: Container(
                        width: 80,
                        height: 80,
                        decoration: BoxDecoration(
                          border: Border.all(color: Colors.white, width: 1.0),
                        ),
                      ),
                    ),
                    Align(
                      alignment: Alignment.center,
                      child: AnimatedSlide(
                        offset: Offset(offsetX, 0),
                        duration: const Duration(milliseconds: 500),
                        child: InkWell(
                          onTap: () {
                            ToastUtil.showToast('点击了');
                          },
                          child: Container(
                            width: 80,
                            height: 80,
                            color: Colors.primaries[2],
                          ),
                        ),
                      ),
                    ),
                    Positioned(
                      left: (MediaQuery.of(context).size.width / 2) - 35,
                      top: 200,
                      child: ElevatedButton(
                        onPressed: () {
                          if (offsetX == 0) {
                            offsetX = 1.5;
                          } else {
                            offsetX = 0;
                          }
                          setState(() {});
                        },
                        child: const Text('偏移'),
                      ),
                    )
                  ],
                ),
              ),
            );
          }
        }

        Android原生补间动画 代码

            
        import android.app.Activity
        import android.os.Bundle
        import android.view.View
        import android.view.animation.TranslateAnimation
        import android.widget.Toast
        import com.example.flutter_animation.databinding.ActivityMainBinding
        class MainActivity : Activity(), View.OnClickListener {
            private lateinit var bind: ActivityMainBinding
            override fun onCreate(savedInstanceState: Bundle?) {
                super.onCreate(savedInstanceState)
                bind = ActivityMainBinding.inflate(layoutInflater)
                setContentView(bind.root)
                bind.offsetX.setOnClickListener(this)
                bind.offsetBox.setOnClickListener(this)
            }
            private fun offsetAnimation() {
                val translateAnimation = TranslateAnimation(0f, 200f, 0f, 0f)
                translateAnimation.duration = 800
                translateAnimation.fillAfter = true
                bind.offsetBox.startAnimation(translateAnimation)
            }
            override fun onClick(v: View?) {
                if (bind.offsetX == v) {
                    offsetAnimation()
                } else if (bind.offsetBox == v) {
                    Toast.makeText(this,"点击了",Toast.LENGTH_SHORT).show()
                }
            }
        }

        Hero动画

        应用于 元素共享 的动画。

        下面这三个图片详情案例的使用方式,将 Widget 从 A页面 共享到 B页面 后,改变Widget大小,被称为 标准 hero 动画;

        图片详情案例一:本地图片

        import 'package:flutter/cupertino.dart';
        import 'package:flutter/material.dart';
        import 'package:flutter/scheduler.dart' show timeDilation;
        class HeroAnimation extends StatefulWidget {
          const HeroAnimation({super.key});
          @override
          State createState() => _HeroAnimationState();
        }
        /// 将 Widget 从 A页面 共享到 B页面 后,改变Widget大小
        class _HeroAnimationState extends State {
          /// 测试本地图片
          final List images = [
            'assets/images/01.jpg',
            'assets/images/02.jpg',
            'assets/images/03.jpg',
            'assets/images/04.jpg',
          ];
          @override
          Widget build(BuildContext context) {
            // 减慢动画速度,可以通过此值帮助开发,
            // 注意这个值是针对所有动画,所以路由动画也会受影响
            // timeDilation = 10.0;
            return Scaffold(
              appBar: AppBar(
                title: const Text('Photo List Page'),
              ),
              body: Container(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                alignment: Alignment.topLeft,
                child: GridView.count(
                  padding: const EdgeInsets.all(10),
                  crossAxisCount: 2,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                  children: List.generate(
                      images.length,
                      (index) => PhotoHero(
                            photo: images[index],
                            size: 100,
                            onTap: () {
                              Navigator.of(context).push(CupertinoPageRoute(
                                builder: (context) => PhotoDetail(
                                    size: MediaQuery.of(context).size.width,
                                    photo: images[index]),
                              ));
                            },
                          )),
                ),
              ),
            );
          }
        }
        class PhotoHero extends StatelessWidget {
          const PhotoHero({
            super.key,
            required this.photo,
            this.onTap,
            required this.size,
          });
          final String photo;
          final VoidCallback? onTap;
          final double size;
          @override
          Widget build(BuildContext context) {
            return SizedBox(
              width: size,
              height: size,
              child: Hero(
                tag: photo,
                child: Material(
                  color: Colors.transparent,
                  child: InkWell(
                    onTap: onTap,
                    /// 测试本地图片
                    child: Image.asset(
                                  photo,
                                  fit: BoxFit.cover,
                                ),
                  ),
                ),
              ),
            );
          }
        }
        class PhotoDetail extends StatelessWidget {
          const PhotoDetail({
            super.key,
            required this.photo,
            required this.size,
          });
          final String photo;
          final double size;
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                title: const Text('Photo Detail Page'),
              ),
              body: Column(
                children: [
                  Container(
                    color: Colors.lightBlueAccent,
                    padding: const EdgeInsets.all(16),
                    alignment: Alignment.topCenter,
                    child: PhotoHero(
                      photo: photo,
                      size: size,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                  const Text(
                    '详情xxx',
                    style: TextStyle(fontSize: 20),
                  )
                ],
              ),
            );
          }
        }
        

        图片详情案例二:网络图片

        可以看出,在有延迟的情况下,效果没有本地图片好;

        import 'package:flutter/cupertino.dart';
        import 'package:flutter/material.dart';
        import 'package:flutter/scheduler.dart' show timeDilation;
        class HeroAnimation extends StatefulWidget {
          const HeroAnimation({super.key});
          @override
          State createState() => _HeroAnimationState();
        }
        /// 将 Widget 从 A页面 共享到 B页面 后,改变Widget大小
        class _HeroAnimationState extends State {
          /// 测试网络图片
          final List images = [
            'https://img1.baidu.com/it/u=1161835547,3275770506&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500',
            'https://p9.toutiaoimg.com/origin/pgc-image/6d817289d3b44d53bb6e55aa81e41bd2?from=pc',
            'https://img0.baidu.com/it/u=102503057,4196586556&fm=253&fmt=auto&app=138&f=BMP?w=500&h=724',
            'https://lmg.jj20.com/up/allimg/1114/041421115008/210414115008-3-1200.jpg',
          ];
          @override
          Widget build(BuildContext context) {
            // 减慢动画速度,可以通过此值帮助开发,
            // 注意这个值是针对所有动画,所以路由动画也会受影响
            // timeDilation = 10.0;
            return Scaffold(
              appBar: AppBar(
                title: const Text('Photo List Page'),
              ),
              body: Container(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                alignment: Alignment.topLeft,
                child: GridView.count(
                  padding: const EdgeInsets.all(10),
                  crossAxisCount: 2,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                  children: List.generate(
                      images.length,
                      (index) => PhotoHero(
                            photo: images[index],
                            size: 100,
                            onTap: () {
                              Navigator.of(context).push(CupertinoPageRoute(
                                builder: (context) => PhotoDetail(
                                    size: MediaQuery.of(context).size.width,
                                    photo: images[index]),
                              ));
                            },
                          )),
                ),
              ),
            );
          }
        }
        class PhotoHero extends StatelessWidget {
          const PhotoHero({
            super.key,
            required this.photo,
            this.onTap,
            required this.size,
          });
          final String photo;
          final VoidCallback? onTap;
          final double size;
          @override
          Widget build(BuildContext context) {
            return SizedBox(
              width: size,
              height: size,
              child: Hero(
                tag: photo,
                child: Material(
                  color: Colors.transparent,
                  child: InkWell(
                    onTap: onTap,
                    /// 测试网络图片
                    child: Image.network(
                      photo,
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
              ),
            );
          }
        }
        class PhotoDetail extends StatelessWidget {
          const PhotoDetail({
            super.key,
            required this.photo,
            required this.size,
          });
          final String photo;
          final double size;
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                title: const Text('Photo Detail Page'),
              ),
              body: Column(
                children: [
                  Container(
                    color: Colors.lightBlueAccent,
                    padding: const EdgeInsets.all(16),
                    alignment: Alignment.topCenter,
                    child: PhotoHero(
                      photo: photo,
                      size: size,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                  const Text(
                    '详情xxx',
                    style: TextStyle(fontSize: 20),
                  )
                ],
              ),
            );
          }
        }
        

        图片详情案例三:背景透明

        import 'package:flutter/material.dart';
        import 'package:flutter/scheduler.dart' show timeDilation;
        class HeroAnimation extends StatefulWidget {
          const HeroAnimation({super.key});
          @override
          State createState() => _HeroAnimationState();
        }
        /// 测试 新页面背景透明色 的图片详情
        class _HeroAnimationState extends State {
          /// 测试本地图片
          final List images = [
            'assets/images/01.jpg',
            'assets/images/02.jpg',
            'assets/images/03.jpg',
            'assets/images/04.jpg',
          ];
          @override
          Widget build(BuildContext context) {
            // 减慢动画速度,可以通过此值帮助开发,
            // 注意这个值是针对所有动画,所以路由动画也会受影响
            // timeDilation = 10.0;
            return Scaffold(
              appBar: AppBar(
                title: const Text('Photo List Page'),
              ),
              body: Container(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                alignment: Alignment.topLeft,
                child: GridView.count(
                  padding: const EdgeInsets.all(10),
                  crossAxisCount: 2,
                  mainAxisSpacing: 10,
                  crossAxisSpacing: 10,
                  children: List.generate(
                      images.length,
                          (index) => PhotoHero(
                        photo: images[index],
                        size: 100,
                        onTap: () {
                          Navigator.of(context).push(
                            PageRouteBuilder(
                              opaque: false, // 新页面,背景色不透明度
                              pageBuilder: (context, animation, secondaryAnimation) {
                                return PhotoDetail(
                                    size: MediaQuery.of(context).size.width,
                                    photo: images[index]);
                              },
                            ),
                          );
                        },
                      )),
                ),
              ),
            );
          }
        }
        class PhotoHero extends StatelessWidget {
          const PhotoHero({
            super.key,
            required this.photo,
            this.onTap,
            required this.size,
          });
          final String photo;
          final VoidCallback? onTap;
          final double size;
          @override
          Widget build(BuildContext context) {
            return SizedBox(
              width: size,
              height: size,
              child: Hero(
                tag: photo,
                child: Material(
                  color: Colors.transparent,
                  child: InkWell(
                    onTap: onTap,
                    /// 测试本地图片
                    child: Image.asset(
                      photo,
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
              ),
            );
          }
        }
        class PhotoDetail extends StatelessWidget {
          const PhotoDetail({
            super.key,
            required this.photo,
            required this.size,
          });
          final String photo;
          final double size;
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              backgroundColor: Colors.transparent,
              // backgroundColor: const Color(0x66000000),
              body: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Container(
                    padding: const EdgeInsets.all(16),
                    alignment: Alignment.center,
                    child: PhotoHero(
                      photo: photo,
                      size: size,
                      onTap: () {
                        Navigator.of(context).pop();
                      },
                    ),
                  ),
                  const Text(
                    '详情xxx',
                    style: TextStyle(fontSize: 20,color: Colors.white),
                  )
                ],
              ),
            );
          }
        }

        图片形状转换案例:圆形 转 矩形

        这个案例的使用方式,被称为 径向hero动画;

        • 径向hero动画的 径 是半径距离,圆形状 向 矩形状转换,矩形状的对角半径距离 = 圆形状半径距离 * 2;
        • 这个是官方模版代码,我也没改什么;
        • 官方代码地址:https://github.com/cfug/flutter.cn/blob/main/examples/_animation/radial_hero_animation/lib/main.dart
        • 问题:这种官方代码是 初始化为 圆形 点击向 矩形改变的方式,我尝试反向操作:初始化为 矩形 点击向 圆形改变,但没有成功,如果有哪位同学找到实现方式,麻烦评论区留言;

          我是这样修改的:

          class RadialExpansion extends StatelessWidget {
            ... ... 
            @override
            Widget build(BuildContext context) {
              /// 原来的代码
              // 控制形状变化的核心代码
              // return ClipOval( // 圆形
              //   child: Center(
              //     child: SizedBox(
              //       width: clipRectSize,
              //       height: clipRectSize,
              //       child: ClipRect( // 矩形
              //         child: child,
              //       ),
              //     ),
              //   ),
              // );
              /// 尝试修改 形状顺序
              return ClipRect( // 矩形
                child: Center(
                  child: SizedBox(
                    width: clipRectSize,
                    height: clipRectSize,
                    child: ClipOval( // 圆形
                      child: child,
                    ),
                  ),
                ),
              );
            }
          }

          官方代码演示 

          import 'package:flutter/material.dart';
          import 'dart:math' as math;
          import 'package:flutter/scheduler.dart' show timeDilation;
          class HeroAnimation extends StatefulWidget {
            const HeroAnimation({super.key});
            @override
            State createState() => _HeroAnimationState();
          }
          /// 将 Widget 从 A页面 共享到 B页面 后,改变Widget形状
          class _HeroAnimationState extends State {
            static double kMinRadius = 32.0;
            static double kMaxRadius = 128.0;
            static Interval opacityCurve =
            const Interval(0.0, 0.75, curve: Curves.fastOutSlowIn);
            static RectTween _createRectTween(Rect? begin, Rect? end) {
              return MaterialRectCenterArcTween(begin: begin, end: end);
            }
            static Widget _buildPage(
                BuildContext context, String imageName, String description) {
              return Container(
                color: Theme.of(context).canvasColor,
                child: Center(
                  child: Card(
                    elevation: 8,
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        SizedBox(
                          width: kMaxRadius * 2.0,
                          height: kMaxRadius * 2.0,
                          child: Hero(
                            createRectTween: _createRectTween,
                            tag: imageName,
                            child: RadialExpansion(
                              maxRadius: kMaxRadius,
                              child: Photo(
                                photo: imageName,
                                onTap: () {
                                  Navigator.of(context).pop();
                                },
                              ),
                            ),
                          ),
                        ),
                        Text(
                          description,
                          style: const TextStyle(fontWeight: FontWeight.bold),
                          textScaleFactor: 3,
                        ),
                        const SizedBox(height: 16),
                      ],
                    ),
                  ),
                ),
              );
            }
            Widget _buildHero(
                BuildContext context,
                String imageName,
                String description,
                ) {
              return SizedBox(
                width: kMinRadius * 2.0,
                height: kMinRadius * 2.0,
                child: Hero(
                  createRectTween: _createRectTween,
                  tag: imageName,
                  child: RadialExpansion(
                    maxRadius: kMaxRadius,
                    child: Photo(
                      photo: imageName,
                      onTap: () {
                        Navigator.of(context).push(
                          PageRouteBuilder(
                            pageBuilder: (context, animation, secondaryAnimation) {
                              return AnimatedBuilder(
                                animation: animation,
                                builder: (context, child) {
                                  return Opacity(
                                    opacity: opacityCurve.transform(animation.value),
                                    child: _buildPage(context, imageName, description),
                                  );
                                },
                              );
                            },
                          ),
                        );
                      },
                    ),
                  ),
                ),
              );
            }
            @override
            Widget build(BuildContext context) {
              timeDilation = 5.0; // 1.0 is normal animation speed.
              return Scaffold(
                appBar: AppBar(
                  title: const Text('Radial Transition Demo'),
                ),
                body: Container(
                  padding: const EdgeInsets.all(32),
                  alignment: FractionalOffset.bottomLeft,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      _buildHero(context, 'assets/images/01.jpg', 'Chair'),
                      _buildHero(context, 'assets/images/02.jpg', 'Binoculars'),
                      _buildHero(context, 'assets/images/03.jpg', 'Beach ball'),
                      _buildHero(context, 'assets/images/04.jpg', 'Beach ball'),
                    ],
                  ),
                ),
              );
            }
          }
          class Photo extends StatelessWidget {
            const Photo({super.key, required this.photo, this.onTap});
            final String photo;
            final VoidCallback? onTap;
            @override
            Widget build(BuildContext context) {
              return Material(
                // Slightly opaque color appears where the image has transparency.
                color: Theme.of(context).primaryColor.withOpacity(0.25),
                child: InkWell(
                  onTap: onTap,
                  child: LayoutBuilder(
                    builder: (context, size) {
                      return Image.asset(
                        photo,
                        fit: BoxFit.contain,
                      );
                    },
                  ),
                ),
              );
            }
          }
          class RadialExpansion extends StatelessWidget {
            const RadialExpansion({
              super.key,
              required this.maxRadius,
              this.child,
            }) : clipRectSize = 2.0 * (maxRadius / math.sqrt2);
            final double maxRadius;
            final double clipRectSize;
            final Widget? child;
            @override
            Widget build(BuildContext context) {
              // 控制形状变化的核心代码
              return ClipOval( // 圆形
                child: Center(
                  child: SizedBox(
                    width: clipRectSize,
                    height: clipRectSize,
                    child: ClipRect( // 矩形
                      child: child,
                    ),
                  ),
                ),
              );
            }
          }

          页面转场动画

          在自定义路由时,添加动画,自定义路由需要用到PageRouteBuilder

          import 'package:flutter/material.dart';
          /// 为页面切换加入动画效果
          class PageAnimation extends StatefulWidget {
            const PageAnimation({super.key});
            @override
            State createState() => _PageAnimationState();
          }
          class _PageAnimationState extends State {
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                    title: const Text(
                  '为页面切换加入动画效果',
                  style: TextStyle(fontSize: 20),
                )),
                body: SizedBox(
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      ElevatedButton(
                          onPressed: () {
                            Navigator.of(context).push(_createRouteX());
                          },
                          child: const Text(
                            'X轴偏移',
                            style: TextStyle(fontSize: 20),
                          )),
                      ElevatedButton(
                          onPressed: () {
                            Navigator.of(context).push(_createRouteY());
                          },
                          child: const Text(
                            'Y轴偏移',
                            style: TextStyle(fontSize: 20),
                          )),
                      ElevatedButton(
                          onPressed: () {
                            Navigator.of(context).push(_createRouteMix());
                          },
                          child: const Text(
                            '混合动画',
                            style: TextStyle(fontSize: 20),
                          )),
                    ],
                  ),
                ),
              );
            }
            /// X轴 平移动画,切换页面
            Route _createRouteX() {
              return PageRouteBuilder(
                  // opaque: false, // 新页面,背景色不透明度
                  pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
                  transitionsBuilder: (context, animation, secondaryAnimation, child) {
                    const begin = Offset(1.0, 0.0); // 将 dx 参数设为 1,这代表在水平方向左切换整个页面的宽度
                    const end = Offset.zero;
                    const curve = Curves.ease;
                    var tween =
                        Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
                    return SlideTransition(
                      position: animation.drive(tween),
                      child: child,
                    );
                  });
            }
            /// Y轴 平移动画,切换页面
            Route _createRouteY() {
              return PageRouteBuilder(
                  // opaque: false, // 新页面,背景色不透明度
                  pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
                  transitionsBuilder: (context, animation, secondaryAnimation, child) {
                    const begin = Offset(0.0, 1.0); // 将 dy 参数设为 1,这代表在竖直方向上切换整个页面的高度
                    const end = Offset.zero;
                    const curve = Curves.ease;
                    var tween =
                        Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
                    return SlideTransition(
                      position: animation.drive(tween),
                      child: child,
                    );
                  });
            }
            /// 多个动画配合,切换页面
            Route _createRouteMix() {
              return PageRouteBuilder(
                  // opaque: false, // 新页面,背景色不透明度
                  pageBuilder: (context, animation, secondaryAnimation) => const TestPage01(),
                  transitionsBuilder: (context, animation, secondaryAnimation, child) {
                    var tween = Tween(begin: 0.1, end: 1.0)
                        .chain(CurveTween(curve: Curves.ease));
                    return FadeTransition(
                      // 淡入淡出
                      opacity: animation.drive(tween),
                      child: RotationTransition(
                        // 旋转
                        turns: animation.drive(tween),
                        child: ScaleTransition(
                          // 更替
                          scale: animation.drive(tween),
                          child: child,
                        ),
                      ),
                    );
                  });
            }
          }
          class TestPage01 extends StatelessWidget {
            const TestPage01({super.key});
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                backgroundColor: Colors.lightBlue,
                appBar: AppBar(
                  title: const Text('TestPage01'),
                ),
              );
            }
          }
          

          交错动画

          多个动画配合使用

          这个案例是官方的,原汁原味;

          import 'package:flutter/material.dart';
          import 'package:flutter/scheduler.dart' show timeDilation;
          class IntertwinedAnimation extends StatefulWidget {
            const IntertwinedAnimation({super.key});
            @override
            State createState() => _IntertwinedAnimationState();
          }
          class _IntertwinedAnimationState extends State with SingleTickerProviderStateMixin {
            late AnimationController _controller;
            @override
            void initState() {
              super.initState();
              _controller = AnimationController(
                  duration: const Duration(milliseconds: 2000), vsync: this);
            }
            @override
            void dispose() {
              _controller.dispose();
              super.dispose();
            }
            Future _playAnimation() async {
              try {
                await _controller.forward().orCancel;
                await _controller.reverse().orCancel;
              } on TickerCanceled {}
            }
            @override
            Widget build(BuildContext context) {
              // timeDilation = 10.0;
              return Scaffold(
                appBar: AppBar(
                    title: const Text(
                  '交错动画',
                  style: TextStyle(fontSize: 20),
                )),
                body: GestureDetector(
                  behavior: HitTestBehavior.opaque,
                  onTap: () {
                    _playAnimation();
                  },
                  child: Center(
                    child: Container(
                      width: 300,
                      height: 300,
                      decoration: BoxDecoration(
                          color: Colors.black.withOpacity(0.1),
                          border: Border.all(
                            color: Colors.black.withOpacity(0.5),
                          )),
                      child: StaggerAnimation(controller: _controller),
                    ),
                  ),
                ),
              );
            }
          }
          class StaggerAnimation extends StatelessWidget {
            final Animation controller;
            final Animation opacity;
            final Animation width;
            final Animation height;
            final Animation padding;
            final Animation borderRadius;
            final Animation color;
            StaggerAnimation({super.key, required this.controller})
                : opacity = Tween(
                    begin: 0.0,
                    end: 1.0,
                  ).animate(CurvedAnimation(
                      parent: controller,
                      curve: const Interval(
                        0.0,
                        0.100,
                        curve: Curves.ease,
                      ))),
                  width = Tween(
                    begin: 50.0,
                    end: 150.0,
                  ).animate(CurvedAnimation(
                      parent: controller,
                      curve: const Interval(
                        0.125,
                        0.250,
                        curve: Curves.ease,
                      ))),
                  height = Tween(begin: 50.0, end: 150.0).animate(CurvedAnimation(
                      parent: controller,
                      curve: const Interval(
                        0.250,
                        0.375,
                        curve: Curves.ease,
                      ))),
                  padding = EdgeInsetsTween(
                    begin: const EdgeInsets.only(bottom: 16),
                    end: const EdgeInsets.only(bottom: 75),
                  ).animate(CurvedAnimation(
                      parent: controller,
                      curve: const Interval(
                        0.250,
                        0.375,
                        curve: Curves.ease,
                      ))),
                  borderRadius = BorderRadiusTween(
                    begin: BorderRadius.circular(4),
                    end: BorderRadius.circular(75),
                  ).animate(CurvedAnimation(
                      parent: controller,
                      curve: const Interval(
                        0.375,
                        0.500,
                        curve: Curves.ease,
                      ))),
                  color = ColorTween(begin: Colors.indigo[100], end: Colors.orange[400])
                      .animate(CurvedAnimation(
                          parent: controller,
                          curve: const Interval(
                            0.500,
                            0.750,
                            curve: Curves.ease,
                          )));
            Widget _buildAnimation(BuildContext context, Widget? child) {
              return Container(
                padding: padding.value,
                alignment: Alignment.bottomCenter,
                child: Opacity(
                  opacity: opacity.value,
                  child: Container(
                    width: width.value,
                    height: height.value,
                    decoration: BoxDecoration(
                        color: color.value,
                        border: Border.all(
                          color: Colors.indigo[300]!,
                          width: 3,
                        ),
                        borderRadius: borderRadius.value),
                  ),
                ),
              );
            }
            @override
            Widget build(BuildContext context) {
              return AnimatedBuilder(
                builder: _buildAnimation,
                animation: controller,
              );
            }
          }

          依次执行动画

          这个案例是根据官方demo改的,它那个太复杂了,不利于新手阅读(个人觉得);

          官方文档:创建一个交错效果的侧边栏菜单 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter

          import 'package:flutter/material.dart';
          class Intertwined02Animation extends StatefulWidget {
            const Intertwined02Animation({super.key});
            @override
            State createState() => _Intertwined02AnimationState();
          }
          class _Intertwined02AnimationState extends State {
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                body: SizedBox(
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height,
                  child: const TableList(),
                  // child: const Column(
                  //   crossAxisAlignment: CrossAxisAlignment.center,
                  //   children: [
                  //     TableList()
                  //   ],
                  // ),
                ),
              );
            }
          }
          class TableList extends StatefulWidget {
            const TableList({super.key});
            @override
            State createState() => _TableListState();
          }
          class _TableListState extends State with SingleTickerProviderStateMixin {
            /// 遍历循环写法
            late AnimationController _controller;
            final Duration _durationTime = const Duration(milliseconds: 3000);
            @override
            initState() {
              super.initState();
              _controller = AnimationController(vsync: this, duration: _durationTime);
              _controller.forward();
            }
            @override
            void dispose() {
              _controller.dispose();
              super.dispose();
            }
            /// 遍历Interval
            List _createInterval() {
              List intervals = [];
              // Interval(0.0,0.5);
              // Interval(0.5,0.75);
              // Interval(0.75,1.0);
              double begin = 0.0;
              double end = 0.5;
              for (int i = 0; i < 3; i++) {
                if (i == 0) {
                  intervals.add(Interval(begin, end));
                } else {
                  begin = end;
                  end = begin + 0.25;
                  intervals.add(Interval(begin, end));
                }
                // debugPrint('begin:$begin --- end:$end');
              }
              return intervals;
            }
            /// 遍历循环组件
            List _createWidget() {
              var intervals = _createInterval();
              List listItems = [];
              for (int i = 0; i < 3; i++) {
                listItems.add(AnimatedBuilder(
                  animation: _controller,
                  builder: (context, child) {
                    var animationPercent = Curves.easeOut.transform(intervals[i].transform(_controller.value));
                    final opacity = animationPercent;
                    final slideDistance = (1.0 - animationPercent) * 150;
                    return Opacity(
                        opacity: i == 2 ? opacity : 1,
                        child: Transform.translate(
                          offset: Offset(slideDistance, 100 + (i * 50)),
                          child: child,
                        ));
                  },
                  child: Container(
                    width: 100,
                    height: 50,
                    color: Colors.lightBlue,
                  ),
                ));
              }
              return listItems;
            }
            @override
            Widget build(BuildContext context) {
              return SizedBox(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                child: Column(
                  children: _createWidget(),
                ),
              );
            }
            /// 非遍历循环写法
          // late AnimationController _controller;
          //
          // final Interval _intervalA = const Interval(0.0, 0.5);
          // final Interval _intervalB = const Interval(0.5, 0.8);
          // final Interval _intervalC = const Interval(0.8, 1.0);
          //
          // final Duration _durationTime = const Duration(milliseconds: 3000);
          //
          // @override
          // void initState() {
          //   super.initState();
          //   _controller = AnimationController(vsync: this, duration: _durationTime);
          //   _controller.forward();
          // }
          //
          // @override
          // void dispose() {
          //   _controller.dispose();
          //   super.dispose();
          // }
          //
          // @override
          // Widget build(BuildContext context) {
          //   return SizedBox(
          //     width: MediaQuery.of(context).size.width,
          //     height: MediaQuery.of(context).size.height,
          //     child: Column(
          //       children: [
          //         AnimatedBuilder(
          //           animation: _controller,
          //           builder: (context,child) {
          //             var animationPercent = Curves.easeOut.transform(_intervalA.transform(_controller.value));
          //             final slideDistance = (1.0 - animationPercent) * 150;
          //             return Transform.translate(
          //               offset: Offset(slideDistance,100),
          //               child: child
          //             );
          //           },
          //           child: Container(
          //             width: 100,
          //             height: 50,
          //             color: Colors.lightBlue,
          //           ),
          //         ),
          //         AnimatedBuilder(
          //           animation: _controller,
          //           builder: (context,child) {
          //             var animationPercent = Curves.easeOut.transform(_intervalB.transform(_controller.value));
          //             final slideDistance = (1.0 - animationPercent) * 150;
          //             return Transform.translate(
          //                 offset: Offset(slideDistance,150),
          //                 child: child
          //             );
          //           },
          //           child: Container(
          //             width: 100,
          //             height: 50,
          //             color: Colors.lightBlue,
          //           ),
          //         ),
          //         AnimatedBuilder(
          //           animation: _controller,
          //           builder: (context,child) {
          //             var animationPercent = Curves.easeOut.transform(_intervalC.transform(_controller.value));
          //             final opacity = animationPercent;
          //             final slideDistance = (1.0 - animationPercent) * 150;
          //             return Opacity(
          //               opacity: opacity,
          //               child: Transform.translate(
          //                   offset: Offset(slideDistance,200),
          //                   child: child
          //               ),
          //             );
          //           },
          //           child: Container(
          //             width: 100,
          //             height: 50,
          //             color: Colors.lightBlue,
          //           ),
          //         ),
          //       ],
          //     ),
          //   );
          // }
            /// 基础版本写法
          // late AnimationController _controller;
          // final Duration _durationTime = const Duration(milliseconds: 2000);
          // // 0.0 - 1.0 / 0% - 100%
          // final Interval _interval = const Interval(0.5, 1.0); // 延迟 50% 再开始 启动动画,执行到 100%
          // // final Interval _interval = const Interval(0.5, 0.7); // 延迟 50% 再开始 启动动画,后期的执行速度,增加 30%
          // // final Interval _interval = const Interval(0.0, 0.1); // 不延迟 动画执行速度,增加 90%
          //
          // @override
          // void initState() {
          //   super.initState();
          //   _controller = AnimationController(vsync: this, duration: _durationTime);
          //   _controller.forward();
          // }
          //
          // @override
          // void dispose() {
          //   _controller.dispose();
          //   super.dispose();
          // }
          // @override
          // Widget build(BuildContext context) {
          //   return AnimatedBuilder(
          //       animation: _controller,
          //       builder: (context,child) {
          //         // var animationPercent = Curves.easeOut.transform(_controller.value); // 加动画曲线
          //         // var animationPercent = _interval.transform(_controller.value); // 加动画间隔
          //         var animationPercent = Curves.easeOut.transform(_interval.transform(_controller.value)); // 动画曲线 + 动画间隔
          //
          //         final slideDistance = (1.0 - animationPercent) * 150; // 就是对150 做递减
          //         // debugPrint('animationPercent:$animationPercent --- slideDistance:$slideDistance');
          //         debugPrint('slideDistance:$slideDistance');
          //
          //         return Transform.translate(
          //           offset: Offset(0,slideDistance),
          //           child: child
          //         );
          //       },
          //     child: Container(
          //       width: 100,
          //       height: 50,
          //       color: Colors.lightBlue,
          //     ),
          //   );
          // }
          }
          

          官方文档

          动画效果介绍 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter