Flutter 之动画

1、原理

1、动画形成

在任何系统的 UI 框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观,由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将 UI 的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率 FPS(Frame Per Second)即每秒的动画帧数。很明显,帧率越高则动画就会越流畅!一般情况下,对于人眼来说,动画帧率超过 16 FPS,就基本能看了,超过 32 FPS 就会感觉相对平滑,而超过 32FPS,大多数人基本上就感受不到差别了。由于动画的每一帧都是要改变 UI 输出,所以在一个时间段内连续的改变 UI 输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在 UI 系统中,动画的平均帧率是重要的性能指标,而在 Flutter 中,理想情况下是可以实现 60FPS 的,这和原生应用能达到的帧率是基本是持平的。

2、Flutter 动画分类

FLutter 中的动画主要分为:隐式动画、显式动画、自定义隐式动画、自定义显式动画和 Hero 动画。

2、隐式动画

通过几行代码就可以实现隐式动画,由于隐式动画背后的实现原理和繁琐的操作细节都被隐去了,所以叫隐式动画。FLutter 中提供的 AnimatedContainer、AnimatedPadding、AnimatedPositioned、AnimatedOpacity、AnimatedDefaultTextStyle、AnimatedSwitcher 都属于隐式动画。隐式动画中可以通过 duration 配置动画时长、可以通过 Curve (曲线)来配置动画过程。

1、AnimatedContainer

AnimatedContainer 的属性和 Container 属性基本是一样的,当 AnimatedContainer 属性改变的时候就会触发动画。

属性

(new) AnimatedContainer AnimatedContainer({
  Key? key,
  AlignmentGeometry? alignment,   //子元素相对于容器的对齐方式
  EdgeInsetsGeometry? padding,    //子元素的内边距
  Color? color,    //容器背景颜色(decoration 也能设置背景色,两个不要同时使用)
  Decoration? decoration,    //容器的边框修饰
  Decoration? foregroundDecoration,    //容器的前景边框修饰(使用时会挡住 color 或 decoration 的颜色)
  double? width,    //容器的宽
  double? height,    //容器的高
  BoxConstraints? constraints,    //容器的大小约束,可以指定最小宽高、最大宽高,width 和 height 即使设置更大的宽高也不会有效果.
  EdgeInsetsGeometry? margin,    //容器的外边距
  Matrix4? transform,    //容器的 Matrix 变换
  AlignmentGeometry? transformAlignment,    //transform 不为空时有效,转换的对齐方式,可以理解为起点位置
  Widget? child,
  Clip clipBehavior = Clip.none,    //在decoration不为空的情况下,才有效果,指定剪切的模式,
  Curve curve = Curves.linear,    //动画运用的曲线
  required Duration duration,    //动画时长
  void Function()? onEnd,    //动画执行结束的回调
})

使用

class AnimatedContainerPage extends StatefulWidget {
  const AnimatedContainerPage({super.key});

  @override
  State<AnimatedContainerPage> createState() => _AnimatedContainerPageState();
}

class _AnimatedContainerPageState extends State<AnimatedContainerPage> {
  bool flag = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            flag = !flag;
          });
        },
        child: const Icon(Icons.animation),
      ),
      appBar: AppBar(
        title: const Text('AnimatedContainer'),
      ),
      body: Center(
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 250),
          width: flag ? 100 : 300,
          height: flag ? 100 : 300,
          color: Colors.green,
        ),
      ),
    );
  }
}
2、AnimatedPadding

属性

(new) AnimatedPadding AnimatedPadding({
  Key? key,
  required EdgeInsetsGeometry padding,     //子元素的内边距
  Widget? child,
  Curve curve = Curves.linear,     //动画运用的曲线
  required Duration duration,    //动画时长
  void Function()? onEnd,     //动画执行结束的回调
})

使用

class AnimatedPaddingPage extends StatefulWidget {
  const AnimatedPaddingPage({super.key});

  @override
  State<AnimatedPaddingPage> createState() => _AnimatedPaddingPageState();
}

class _AnimatedPaddingPageState extends State<AnimatedPaddingPage> {
  bool flag = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.change_circle),
          onPressed: () {
            setState(() {
              flag = !flag;
            });
          }),
      appBar: AppBar(
        title: const Text('AnimatedPadding'),
      ),
      body: AnimatedPadding(
        duration: const Duration(milliseconds: 3000),
        padding: EdgeInsets.fromLTRB(10, flag ? 10 : 500, 0, 0),
        curve: Curves.bounceInOut,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.red,
        ),
      ),
    );
  }
}
3、AnimatedPositioned

AnimatedPositioned 是 Stack 组件中的 Positioned 的动画替换组件。可以通过 AnimatedPositioned 实现组件在 Stack 组件的位置,从而实现相对 Stack 组件的移动效果。需要注意的是横向参数(left、right 和 width)、纵向参数(top、bottom 和 height)只能从3个里面选2个设置,否则会导致布局冲突。
属性

AnimatedPositioned AnimatedPositioned({
  Key? key,
  required Widget child,
  double? left,
  double? top,
  double? right,
  double? bottom,
  double? width,
  double? height,
  Curve curve = Curves.linear,    //动画运用的曲线
  required Duration duration,    //动画时长
  void Function()? onEnd,    //动画结束后回调
})

使用

class AnimatedPositionedPage extends StatefulWidget {
  const AnimatedPositionedPage({super.key});

  @override
  State<AnimatedPositionedPage> createState() => _AnimatedPositionedPageState();
}

class _AnimatedPositionedPageState extends State<AnimatedPositionedPage> {
  bool flag = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AnimatedPositioned'),
      ),
      body: Stack(
        children: [
          AnimatedPositioned(
            duration: const Duration(seconds: 1),
            curve: Curves.easeInOut,
            top: flag ? 10 : 500,
            left: flag ? 10 : 300,
            child: Container(
              width: 60,
              height: 60,
              color: Colors.green,
            ),
          ),
          Align(
            alignment: const Alignment(0, 0.8),
            child: ElevatedButton(
                onPressed: () {
                  setState(() {
                    flag = !flag;
                  });
                },
                child: const Text("Transform")),
          ),
        ],
      ),
    );
  }
}
4、AnimatedOpacity

属性


(new) AnimatedOpacity AnimatedOpacity({
  Key? key,
  Widget? child,
  required double opacity,    //透明度 0~1
  Curve curve = Curves.linear,    //动画运用的曲线
  required Duration duration,    //动画时长
  void Function()? onEnd,    //动画结束后回调
//是否总是包含语义信息,默认是 false。这个主要是用于辅助访问的,如果是 true,则不管透明度是多少,都会显示语义信息(可以辅助朗读),这对于视障人员来说会更友好。
  bool alwaysIncludeSemantics = false,    
})

使用

class AnimatedOpacityPage extends StatefulWidget {
  const AnimatedOpacityPage({super.key});

  @override
  State<AnimatedOpacityPage> createState() => _AnimatedOpacityPageState();
}

class _AnimatedOpacityPageState extends State<AnimatedOpacityPage> {
  bool flag = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            flag = !flag;
          });
        },
        child: const Icon(Icons.opacity),
      ),
      appBar: AppBar(
        title: const Text('AnimatedOpacity'),
      ),
      body: Center(
        child: AnimatedOpacity(
          opacity: flag ? 0 : 1,
          duration: const Duration(seconds: 3),
          curve: Curves.linear,
          child: Container(
            width: 300,
            height: 300,
            color: Colors.green,
          ),
        ),
      ),
    );
  }
}
5、AnimatedDefaultTextStyle

属性

(new) AnimatedDefaultTextStyle AnimatedDefaultTextStyle({
  Key? key,
  required Widget child,
  required TextStyle style,    //子元素的样式,用于动画变化
  TextAlign? textAlign,    //如果文本超过1行时,所有换行的字体的对齐方式,可以是左对齐、右对齐
  bool softWrap = true,    //文本是否应该在软换行符处换行,软换行和硬换行是word用法,具体自阅
  TextOverflow overflow = TextOverflow.clip,    //超过文本行数区域的裁剪方式
  int? maxLines,    //文本最大行数,默认是1
  TextWidthBasis textWidthBasis = TextWidthBasis.parent,    //Text 宽度类型(与父 widget 同宽或最小宽度)
  TextHeightBehavior? textHeightBehavior,    //Text 行高状态
  Curve curve = Curves.linear,    //动画样式
  required Duration duration,    //动画时长
  void Function()? onEnd,    //动画结束后回调
})

使用

class AnimatedDefaultTextStylePage extends StatefulWidget {
  const AnimatedDefaultTextStylePage({super.key});

  @override
  State<AnimatedDefaultTextStylePage> createState() =>
      _AnimatedDefaultTextStylePageState();
}

class _AnimatedDefaultTextStylePageState
    extends State<AnimatedDefaultTextStylePage> {
  bool flag = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.opacity),
          onPressed: () {
            setState(() {
              flag = !flag;
            });
          }),
      appBar: AppBar(
        title: const Text('AnimatedDefaultTextStylePage'),
      ),
      body: Center(
        child: Container(
          width: 300,
          height: 300,
          alignment: Alignment.center,
          color: Colors.green,
          child: AnimatedDefaultTextStyle(
              style: TextStyle(fontSize: flag ? 15 : 20),
              duration: const Duration(milliseconds: 300),
              child: const Text("AnimatedDefaultTextStyle")),
        ),
      ),
    );
  }
}
6、AnimatedSwitcher(切换)

AnimatedContainer、AnimatedPadding、AnimatedPositioned、AnimatedOpacity、
AnimatedDefaultTextStyle 都是在属性改变的时候执行动画,AnimatedSwitcher 则是在子元素改变的时候执行动画。相比上面的动画组件 AnimatedSwitcher 多了 transitionBuilder 参数,可以在 transitionBuilder 中自定义动画。

属性

(new) AnimatedSwitcher AnimatedSwitcher({
  Key? key,
  Widget? child,
  required Duration duration,    // 新child显示动画时长
  Duration? reverseDuration,     // 旧child隐藏的动画时长
  Curve switchInCurve = Curves.linear,    // 新child显示的动画曲线
  Curve switchOutCurve = Curves.linear,    // 旧child隐藏的动画曲线
  Widget Function(Widget, Animation<double>) transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder,    // 动画构建器
  Widget Function(Widget?, List<Widget>) layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder,    //布局构建器
})

使用

class AnimatedSwitcherPage extends StatefulWidget {
  const AnimatedSwitcherPage({super.key});

  @override
  State<AnimatedSwitcherPage> createState() => _AnimatedSwitcherPageState();
}

class _AnimatedSwitcherPageState extends State<AnimatedSwitcherPage> {
  bool flag = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.opacity),
          onPressed: () {
            setState(() {
              flag = !flag;
            });
          }),
      appBar: AppBar(
        title: const Text('AnimatedSwitcherPage'),
      ),
      body: Center(
        child: Container(
          width: 300,
          height: 180,
          alignment: Alignment.center,
          color: Colors.green,
          child: AnimatedSwitcher(
            duration: const Duration(seconds: 3),
            child: flag
                ? const CircularProgressIndicator()
                : Image.network(
                    "https://www.itying.com/images/flutter/2.png",
                    fit: BoxFit.cover,
                  ),
          ),
        ),
      ),
    );
  }
}
//transitionBuilder 自定义动画
//在上述代码的 body 中的 AnimatedSwitcher 中添加 transitionBuilder 属性
body: Center(
        child: Container(
          width: 300,
          height: 180,
          alignment: Alignment.center,
          color: Colors.green,
          child: AnimatedSwitcher(
            //transitionBuilder 自定义动画效果
            transitionBuilder: (child, animation) {
              return ScaleTransition(
                scale: animation,
                child: FadeTransition(
                  opacity: animation,
                  child: child,
                ),
              );
            },
            duration: const Duration(seconds: 3),
            child: flag
                ? const CircularProgressIndicator()
                : Image.network(
                    "https://www.itying.com/images/flutter/2.png",
                    fit: BoxFit.cover,
                  ),
          ),
        ),
      ),
//transitionBuilder 改变子元素执行动画
//在上述代码更改的基础上,更改 AnimatedSwitcher 中添加  child 属性
body: Center(
        child: Container(
          width: 300,
          height: 180,
          alignment: Alignment.center,
          color: Colors.green,
          child: AnimatedSwitcher(
            //transitionBuilder 自定义动画效果
            transitionBuilder: (child, animation) {
              return ScaleTransition(
                scale: animation,
                child: FadeTransition(
                  opacity: animation,
                  child: child,
                ),
              );
            },
            duration: const Duration(seconds: 3),
            child: Text(
              key: UniqueKey(),
              flag ? "你好 Flutter" : "你好啊!",
              style: const TextStyle(fontSize: 30),
            ),
          ),
        ),
      ),

3、显示动画

常见的显式动画有 RotationTransition(旋转)、FadeTransition(透明度)、ScaleTransition(缩放)、SlideTransition(移动)、AnimatedIcon(改变常见图标)。在显示动画中开发者需要创建一个 AnimationController,通过 AnimationController 控制动画的开始、暂停、重置、跳转、倒播等。

1、RotationTransition

属性

(new) RotationTransition RotationTransition({
  Key? key,
  required Animation<double> turns,    //动画控制器
  Alignment alignment = Alignment.center,    //设置动画的旋转中心
  FilterQuality? filterQuality,    //在进行图像变换的过程中,图像的取样质量
  Widget? child,    //将要执行动画的子view
})

使用

class RotationTransitionPage extends StatefulWidget {
  const RotationTransitionPage({super.key});

  @override
  State<RotationTransitionPage> createState() => _RotationTransitionPageState();
}

class _RotationTransitionPageState extends State<RotationTransitionPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  @override
  void initState() {
    super.initState();
    //Vsync 机制可以理解为是显卡与显示器的通信桥梁,显卡在渲染每一帧之前会等待垂直同步信号,只有显示器完成了一次刷新时,发出垂直同步信号,
    //显卡才会渲染下一帧,确保刷新率和帧率保持同步,以达到供需平衡的效果,防止卡顿现象。
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));
  }

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

    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('RotationTransition'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          RotationTransition(
            turns: _controller,
            child: const FlutterLogo(
              size: 100,
            ),
          ),
          const SizedBox(
            height: 50,
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
            child: Wrap(
              spacing: 10,
              alignment: WrapAlignment.center,
              children: [
                ElevatedButton(
                    onPressed: () {
                      _controller.forward();
                    },
                    child: const Text("正序播放")),
                ElevatedButton(
                    onPressed: () {
                      _controller.reverse();
                    },
                    child: const Text("倒序播放")),
                ElevatedButton(
                    onPressed: () {
                      _controller.stop();
                    },
                    child: const Text("停止播放")),
                ElevatedButton(
                    onPressed: () {
                      _controller.reset();
                    },
                    child: const Text("重置")),
                ElevatedButton(
                    onPressed: () {
                      _controller.repeat();
                    },
                    child: const Text("重复播放"))
              ],
            ),
          )
        ],
      ),
    );
  }
}

lowerBound & upperBound
AnimationController 用于控制动画,它包含动画的启动 forward() 、停止 stop() 、反向播放reverse() 等方法。 AnimationController 会在动画的每一帧生成一个新的值。默认情况下, AnimationController 在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字 ,我们也可以通过 lowerBound 和 upperBound 来修改 AnimationController 生成数字的区间。

//在上述代码中更改 initState() 方法如下:
void initState() {
    super.initState();
    //Vsync 机制可以理解为是显卡与显示器的通信桥梁,显卡在渲染每一帧之前会等待垂直同步信号,只有显示器完成了一次刷新时,发出垂直同步信号,
    //显卡才会渲染下一帧,确保刷新率和帧率保持同步,以达到供需平衡的效果,防止卡顿现象。
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),

      //第三圈到第五圈
      lowerBound: 3,
      upperBound: 5,
    );

    _controller.addListener(() {
      print(_controller.value);
    });
  }
2、FadeTransition

属性

(new) FadeTransition FadeTransition({
  Key? key,
  required Animation<double> opacity,    //组件的透明度
  //是否总是包含语义信息,默认是 false。这个主要是用于辅助访问的,如果是 true,则不管透明度是多少,都会显示语义信息(可以辅助朗读),这对于视障人员来说会更友好。
  bool alwaysIncludeSemantics = false,
  Widget? child,
})

使用

class FadeTransitionPage extends StatefulWidget {
  const FadeTransitionPage({super.key});

  @override
  State<FadeTransitionPage> createState() => _FadeTransitionPageState();
}

class _FadeTransitionPageState extends State<FadeTransitionPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    //Vsync 机制可以理解为是显卡与显示器的通信桥梁,显卡在渲染每一帧之前会等待垂直同步信号,只有显示器完成了一次刷新时,
    //发出垂直同步信号,显卡才会渲染下一帧,确保刷新率和帧率保持同步,以达到供需平衡的效果,防止卡顿现象。
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 3));
  }

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

    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FadeTransition'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          FadeTransition(
            opacity: _controller,
            child: const FlutterLogo(
              size: 50,
            ),
          ),
          const SizedBox(
            height: 50,
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                    onPressed: () {
                      _controller.forward();
                    },
                    child: const Text("正序播放")),
                ElevatedButton(
                    onPressed: () {
                      _controller.reverse();
                    },
                    child: const Text("倒序播放")),
                ElevatedButton(
                    onPressed: () {
                      _controller.repeat();
                    },
                    child: const Text("重复播放"))
              ],
            ),
          ),
        ],
      ),
    );
  }
}
3、ScaleTransition

属性

(new) ScaleTransition ScaleTransition({
  Key? key,
  required Animation<double> scale,    //动画控制器
  Alignment alignment = Alignment.center,    //设置动画的缩放中心
  FilterQuality? filterQuality,     //在进行图像变换的过程中,图像的取样质量
  Widget? child,    //将要执行动画的子view
})

使用

class ScaleTransitionPage extends StatefulWidget {
  const ScaleTransitionPage({super.key});

  @override
  State<ScaleTransitionPage> createState() => _ScaleTransitionPageState();
}

class _ScaleTransitionPageState extends State<ScaleTransitionPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 3));
  }

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

    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ScaleTransitionPage'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          ScaleTransition(
            scale: _controller,
            child: const FlutterLogo(
              size: 100,
            ),
          ),
          const SizedBox(
            height: 50,
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                    onPressed: () {
                      _controller.forward();
                    },
                    child: const Text("正序播放")),
                ElevatedButton(
                    onPressed: () {
                      _controller.reverse();
                    },
                    child: const Text("倒序播放")),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

AnimationController 结合 Tween 控制动画
默认情况下, AnimationController 对象值的范围是[0.0,1.0]。如果我们需要构建 UI 的动画值在不同的范围或不同的数据类型,则可以使用 Tween 来添加映射以生成不同的范围或数据类型的值。

//上述代码中更改 ScaleTransition 如下:
ScaleTransition(
            scale: _controller.drive(Tween(begin: 1,end: 2)),
            child: const FlutterLogo(
              size: 100,
            ),
          ),
4、SlideTransition

这是一负责平移的显示动画组件,使用时需要通过 position 属性传入一个 Animated 表示位移程度,通常借助 Tween 实现。
属性

(new) SlideTransition SlideTransition({
  Key? key,
  required Animation<Offset> position,    //位置偏移系数(如:0.5 表示向右偏移 width 的 50%)
  bool transformHitTests = true,    //默认为 true。作用是否对 pointerEvent 的 position 进行转换
  TextDirection? textDirection,  //文本方向,一般都是从左到右
  Widget? child,
})

使用

class SlideTransitionPage extends StatefulWidget {
  const SlideTransitionPage({super.key});

  @override
  State<SlideTransitionPage> createState() => _SlideTransitionPageState();
}

class _SlideTransitionPageState extends State<SlideTransitionPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

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

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

    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('SlideTransitionPage'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          SlideTransition(
            position: _controller.drive(Tween(
                begin: const Offset(-1, -1),
                //表示实际的位置向右移动自身宽度的1.2倍
                end: const Offset(0.5, 0.5))),
            child: const FlutterLogo(
              size: 100,
            ),
          ),
          const SizedBox(
            height: 50,
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(10, 0, 0, 0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                    onPressed: () {
                      _controller.forward();
                    },
                    child: const Text("正序播放")),
                ElevatedButton(
                    onPressed: () {
                      _controller.reverse();
                    },
                    child: const Text("倒序播放")),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Tween.animate 驱动动画

//如上代码更改 SlideTransition 代码块如下
SlideTransition(
            // position: _controller.drive(Tween(
            //     begin: const Offset(-1, -1),
            //     //表示实际的位置向右移动自身宽度的1.2倍
            //     end: const Offset(0.5, 0.5))),

            //Tween.animate 驱动动画
            position:
                Tween(begin: const Offset(-1, -1), end: const Offset(0.5, 0.5))
                    .animate(_controller),
            child: const FlutterLogo(
              size: 100,
            ),
          ),

链式操作修改动画效果

//如上代码更改 SlideTransition 代码块如下
SlideTransition(
            // position: _controller.drive(Tween(
            //     begin: const Offset(-1, -1),
            //     //表示实际的位置向右移动自身宽度的1.2倍
            //     end: const Offset(0.5, 0.5))),
            
            // Tween.animate 驱动动画
            // position:
            //     Tween(begin: const Offset(-1, -1), end: const Offset(0.5, 0.5))
            //         .animate(_controller),

            //链式操作修改动画效果
            position:
                Tween(begin: const Offset(0, -1), end: const Offset(0, 0.8))
                    .chain(CurveTween(curve: Curves.bounceIn))
                    .animate(_controller),
            child: const FlutterLogo(
              size: 100,
            ),
          ),

链式操作修改动动画执行时间

//如上代码更改 SlideTransition 代码块如下
SlideTransition(
            // position: _controller.drive(Tween(
            //     begin: const Offset(-1, -1),
            //     //表示实际的位置向右移动自身宽度的1.2倍
            //     end: const Offset(0.5, 0.5))),

            //Tween.animate 驱动动画
            // position:
            //     Tween(begin: const Offset(-1, -1), end: const Offset(0.5, 0.5))
            //         .animate(_controller),

            //链式操作修改动画效果
            // position:
            //     Tween(begin: const Offset(0, -1), end: const Offset(0, 0.8))
            //         .chain(CurveTween(curve: Curves.bounceIn))
            //         .animate(_controller),

            // 链式操作修改动动画执行时间
            position:
                Tween(begin: const Offset(0, -1), end: const Offset(0, 0.8))
                    .chain(CurveTween(curve: Curves.bounceIn))
                    //最后百分之 30 的时间完成动画
                    .chain(CurveTween(curve: const Interval(0.7, 1.0)))
                    .animate(_controller),
            child: const FlutterLogo(
              size: 100,
            ),
          ),
5、AnimatedIcon

AnimatedIcon 顾名思义,是一个用于提供动画图标的组件,它的名字虽然是以 Animated 开头,但是他是一个显式动画组件,需要通过 progress 属性传入动画控制器,另外需要由 Icon 属性传入动画图标数据。

属性

(new) AnimatedIcon AnimatedIcon({
  Key? key,
  required AnimatedIconData icon,    //图标
  required Animation<double> progress,    //动画的进度,值0-1
  Color? color,    //icon 的颜色
  double? size,    //icon的大小
  String? semanticLabel,    //语义标签,不在UI中显示,在辅助功能模式下有用
  TextDirection? textDirection,    //图标的方向
})

方法

class AnimatedIconPage extends StatefulWidget {
  const AnimatedIconPage({super.key});

  @override
  State<AnimatedIconPage> createState() => _AnimatedIconPageState();
}

class _AnimatedIconPageState extends State<AnimatedIconPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

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

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

    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller.forward();
        },
        child: const Icon(Icons.add),
      ),
      appBar: AppBar(
        title: const Text('AnimatedIcon'),
      ),
      body: Center(
        child: AnimatedIcon(
          icon: AnimatedIcons.menu_close,
          progress: _controller,
          size: 50,
        ),
      ),
    );
  }
}

4、交错动画

class SlideTransition2Page extends StatefulWidget {
  const SlideTransition2Page({super.key});

  @override
  State<SlideTransition2Page> createState() => _SlideTransition2PageState();
}

class _SlideTransition2PageState extends State<SlideTransition2Page>
    with SingleTickerProviderStateMixin {
  bool flag = true;
  late AnimationController _controller;
  @override
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 6))
          ..repeat(reverse: true);
  }

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

    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          flag ? _controller.forward() : _controller.reverse();
          flag = !flag;
        },
        child: const Icon(Icons.refresh),
      ),
      appBar: AppBar(
        title: const Text('交错动画'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            SlideringBox(
                controller: _controller,
                color: Colors.blue[200],
                curve: const Interval(0, 0.2)),
            SlideringBox(
                controller: _controller,
                color: Colors.blue[400],
                curve: const Interval(0.2, 0.4)),
            SlideringBox(
                controller: _controller,
                color: Colors.blue[600],
                curve: const Interval(0.4, 0.6)),
            SlideringBox(
                controller: _controller,
                color: Colors.blue[800],
                curve: const Interval(0.6, 0.8)),
            SlideringBox(
                controller: _controller,
                color: Colors.blue[900],
                curve: const Interval(0.8, 1.0)),
          ],
        ),
      ),
    );
  }
}

class SlideringBox extends StatelessWidget {
  final AnimationController controller;
  final Color? color;
  final Curve curve;
  const SlideringBox(
      {super.key,
      required this.controller,
      required this.color,
      required this.curve});

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: Tween(begin: const Offset(0, 0), end: const Offset(0.7, 0))
          .chain(CurveTween(curve: Curves.bounceIn)
              .chain(CurveTween(curve: curve)))
          .animate(controller),
      child: Container(
        width: 220,
        height: 60,
        color: color,
      ),
    );
  }
}

5、自定义动画

1、TweenAnimationBuilder 自定义隐式动画

每当 Tween 的 end 发生变化的时候就会触发动画。

属性

(new) TweenAnimationBuilder<Object?> TweenAnimationBuilder({
  Key? key,
  required Tween<Object?> tween,    //动画值
  required Duration duration,    //动画时长
  Curve curve = Curves.linear,    //动效
  //有三个参数,
  //第一个是BuildContext,
  //第二个是value用于接收上面两个参数定义的动画时间与动画值(类型取决于自己要做动画的数据类型),
  //第三个是TweenAnimationBuilder的子组件,用于优化;
  required Widget Function(BuildContext, Object?, Widget?) builder,
  void Function()? onEnd,  //动画结束后的回调
  Widget? child,
})

使用

//大小变化
class TweenAnimationBuilderPage extends StatefulWidget {
  const TweenAnimationBuilderPage({super.key});

  @override
  State<TweenAnimationBuilderPage> createState() =>
      _TweenAnimationBuilderPageState();
}

class _TweenAnimationBuilderPageState extends State<TweenAnimationBuilderPage> {
  bool flag = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.refresh_sharp),
          onPressed: () {
            setState(() {
              flag = !flag;
            });
          }),
      appBar: AppBar(
        title: const Text('TweenAnimationBuilder'),
      ),
      body: Center(
        child: TweenAnimationBuilder(
            tween: Tween(
                begin: 100.0, end: flag ? 100.0 : 200.0), //此处的数据必须为 double 类型数据
            duration: const Duration(milliseconds: 100),
            builder: ((context, value, child) {
              return Icon(
                Icons.star,
                size: value.toDouble(),
              );
            })),
      ),
    );
  }
}
//透明度变化
//修改上述 body 中的代码,如下
      //大小变化
      // body: Center(
      //   child: TweenAnimationBuilder(
      //       tween: Tween(
      //           begin: 100.0, end: flag ? 100.0 : 200.0), //此处的数据必须为 double 类型数据
      //       duration: const Duration(milliseconds: 100),
      //       builder: ((context, value, child) {
      //         return Icon(
      //           Icons.star,
      //           size: value.toDouble(),
      //         );
      //       })),
      // ),

      //透明度变化
      body: Center(
        child: TweenAnimationBuilder(
            tween: Tween(begin: 0.0, end: flag ? 0.2 : 1.0),
            duration: const Duration(milliseconds: 100),
            builder: ((context, value, child) {
              return Opacity(
                opacity: value,
                child: Container(
                  color: Colors.red,
                  width: 200,
                  height: 200,
                ),
              );
            })),
      ),
2、AnimatedBuilder 自定义显式动画

属性

(new) AnimatedBuilder AnimatedBuilder({
  Key? key,
  required Listenable animation,    //用于监听该动画,然后通知更新UI
  required Widget Function(BuildContext, Widget?) builder,    //构建动画
  Widget? child,
})

使用
透明度动画

class AnimatedBuilderPage extends StatefulWidget {
  const AnimatedBuilderPage({super.key});

  @override
  State<AnimatedBuilderPage> createState() => _AnimatedBuilderPageState();
}

class _AnimatedBuilderPageState extends State<AnimatedBuilderPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1))
          ..repeat(reverse: true);
  }

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

    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AnimatedBuilder'),
      ),
      body: Center(
          child: AnimatedBuilder(
              animation: _controller,
              builder: (BuildContext context, Widget? child) {
                return Opacity(
                  opacity: _controller.value,
                  child: Container(
                    width: 200,
                    height: 200,
                    color: Colors.red,
                    child: const Center(
                      child: Text("AnimatedBuilder"),
                    )
                  ),
                );
              })),
    );
  }
}

自定义变化范围

//修改上述 body 中的代码,如下
      body: Center(
          child: AnimatedBuilder(
              animation: _controller,
              builder: (BuildContext context, Widget? child) {
                ///透明度动画
                // return Opacity(
                //   opacity: _controller.value,
                //   child: Container(
                //     width: 200,
                //     height: 200,
                //     color: Colors.red,
                //     child: const Center(
                //       child: Text("AnimatedBuilder"),
                //     )
                //   ),
                // );

                ///自定义变化范围
                return Opacity(
                  opacity:
                      Tween(begin: 0.4, end: 1.0).animate(_controller).value,
                  child: Container(
                      width: 200,
                      height: 200,
                      color: Colors.red,
                      child: const Center(
                        child: Text("AnimatedBuilder"),
                      )),
                );
              })),

位置变化

//将上述 body 中的代码更改如下
      body: Center(
        child: AnimatedBuilder(
            animation: _controller,
            builder: (context, child) {
              return Container(
                width: 200,
                height: 200,
                color: Colors.red,
                transform: Matrix4.translationValues(
                    Tween(begin: -100.0, end: 100.0)
                        .chain(CurveTween(curve: Curves.bounceIn))
                        .chain(CurveTween(curve: const Interval(0.2, 0.8)))
                        .animate(_controller)
                        .value,
                    0,
                    0),
                child: const Text("AnimatedBuilder"),
              );
            }),
      ),

chilid优化

//将上述 body 中的代码更改如下
    body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Container(
              width: 200,
              height: 200,
              color: Colors.red,
              transform: Matrix4.translationValues(
                  Tween(begin: -100.0, end: 100.0)
                      .chain(CurveTween(curve: Curves.bounceIn))
                      .chain(CurveTween(curve: const Interval(0.2, 0.8)))
                      .animate(_controller)
                      .value,
                  0,
                  0),
              child: child,
            );
          },
          child: const Text("AnimatedBuilder"),
        ),
      ),

6、Hero 动画

1、介绍

微信朋友圈点击小图片的时候会有一个动画效果到大图预览,这个动画效果就可以使用 Hero 动画实现。Hero 指的是可以在路由(页面)之间飞行的 widget,简单来说 Hero 动画就是在路由切换时,有一个共享的 widget 可以在新旧路由间切换。

2、属性
(new) Hero Hero({
  Key? key,
  //用于关联两个两个界面的 Hero 组件,两个 Hero 组件有关联关系 , 则设置相同的 tag 字符串
  required Object tag,    
  //用于定义 Hero 组件的边界 , 以及定义 Hero 组件在界面切换时,从源界面的起始位置到目的界面的最终位置 , 动画执行的变化过程 
  Tween<Rect?> Function(Rect?, Rect?)? createRectTween,    
  //动画过程组件
  Widget Function(BuildContext, Animation<double>, HeroFlightDirection, BuildContext, BuildContext)? flightShuttleBuilder,
  //占位符组件
  Widget Function(BuildContext, Size, Widget)? placeholderBuilder,
  //使用手势进行转场时,是否显示动画
  bool transitionOnUserGestures = false,
  //普通的 Widget 组件 , Hero 动画作用的组件
  required Widget child, 
})
3、使用
//HomePage
import 'package:flutter/material.dart';
import 'package:terminalflutter01/listData.dart';
import './HeroPage.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  ///数据源
  final List<Widget> _getListData = [];

  @override
  void initState() {
    super.initState();

    for (var i = 0; i < listData.length; i++) {
      _getListData.add(
        ///手势
        GestureDetector(
          onTap: () {
            //添加舔砖图片链接
            Navigator.push(context, MaterialPageRoute(builder: (context) {
              return HeroPage(arguments: {
                "imageUrl": listData[i]['imageUrl'],
                "description": "$i",
              });
            }));
          },
          child: Container(
            decoration: BoxDecoration(
                border: Border.all(
                    color: const Color.fromRGBO(233, 233, 233, 0.9), width: 1)),
            child: Column(
              children: [
                Hero(
                    tag: listData[i]['imageUrl'],
                    child: Image.network(listData[i]['imageUrl'])),
                const SizedBox(height: 12),
                Text(
                  listData[i]['title'],
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontSize: 15),
                )
              ],
            ),
          ),
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('HomePage'),
      ),
      body: GridView.count(
        crossAxisSpacing: 10, //子组件水平间距
        mainAxisSpacing: 10, //子组件垂直间距
        crossAxisCount: 2, //子组件水平数量
        padding: const EdgeInsets.all(10),
        children: _getListData,
      ),
    );
  }
}
//HeroPage
import 'package:flutter/material.dart';

class HeroPage extends StatefulWidget {
  final Map arguments;
  const HeroPage({super.key, required this.arguments});

  @override
  State<HeroPage> createState() => _HeroPageState();
}

class _HeroPageState extends State<HeroPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('HeroPage'),
      ),
      body: ListView(
        children:[
          Hero(
            tag: widget.arguments["imageUrl"], 
            child: Image.network(widget.arguments["imageUrl"])
            ),
         const SizedBox(height: 20), 
         Padding( 
          padding: const EdgeInsets.all(5), 
          child: Text(
            widget.arguments["description"], 
            style: const TextStyle(fontSize: 22)
            ), 
        )
        ],
      ),
    );
  }
}
注:可以通过第三方 photo_view 实现单张图片、多张图片预览

补充:Curves 曲线值

参考:
Curves 效果视图 >>>
Curves 效果视图(背用1) >>>
Curves 效果视图(背用2) >>>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容