Flutter动画

251 阅读13分钟

前言

提高用户体验和赋予应用程序更生动的外观,动画无疑是一项强大的工具。如何让你的应用脱颖而出?答案或许就隐藏在精心设计的动画中。通过巧妙运用动画功能,可以创造出令人惊叹的交互效果,让我们一起探索Flutter动画的魅力,为用户带来一场视觉盛宴吧...

在学习Flutter动画之前,先介绍一下flutter_anim_example,它是一个Flutter动画学习项目,涵盖了各种动画案例,让我们一起来深入探索!

example.gif

动画基础

在学习动画之前,我们先来了解一下动画相关的基础类...

Animation

动画的核心类,它代表了在一段时间内生成值的抽象类。Animation类本身并不会直接产生动画效果,而是通过与其他类(如Tween、Curve、AnimationController等)结合来实现动画效果。下面介绍一下该类中一些常规方法:

方法名返回值描述
addListenervoid添加监听,每次动画值改变时调用监听器
removeListenervoid移除监听,每次动画值改变时停止调用监听器
addStatusListenervoid添加状态监听,每次动画状态改变时调用监听器AnimationStatus 动画状态是一个枚举类型,有以下四种状态forward:动画从头到尾运行completed:动画在结尾处停止reverse:动画从尾到头反向运行dismissed:动画在开始处停止
removeStatusListenervoid移除状态监听,每次动画状态改变时停止调用监听器
driveAnimation创建一个新的动画,该动画的值根据给定的 Animatable 对象进行转换,将一个动画对象转换为另一种类型的动画,从而实现不同类型动画之间的转换和组合

AnimationController

AnimationController继承Animation类,并混入了AnimationEagerListenerMixin、AnimationLocalListenersMixin、AnimationLocalStatusListenersMixin类,用于控制动画的播放状态,包括开始、停止、反转等操作。它是管理动画的关键类之一,通常与Animation类结合使用来创建动画效果。

AnimationLocalListenersMixin:提供添加、移除和通知所有监听器的方法。

AnimationLocalStatusListenersMixin:提供添加、移除和通知所有监听器状态的方法。

在AnimationController内部有一个_tick(Duration elapsed)方法,用于处理动画的每一帧更新,更新动画状态、通知监听器、处理动画完成等操作。

下面介绍一下该类中一些常规方法:

方法名描述
resync使用新的 [TickerProvider] 重新创建 [Ticker],并重新与新的TickerProvider进行同步
value这是一个getter方法,表示动画当前的值
reset将动画重置为起点或关闭状态
forward开始从头到尾运行动画。
reverse开始从尾到头运行动画
stop立即停止动画
fling启动一个惯性动画来停止动画
animateTo从当前位置到指定位置运行动画(正向)
animateBack从当前位置到指定位置运行动画(反向)
repeat开始从头到尾运行动画,并在动画完成时重新启动动画。将 [reverse] 设置为 true后将不再是从头到尾运行,起始值将在每次重复时在 [min] 和 [max] 值之间交替
animateWith使用物理模拟(Simulation)来驱动动画
dispose释放资源,调用此方法后,该对象将不再可用

Ticker

Ticker是用于管理动画帧更新时机的类,它依赖于Flutter框架中提供的调度器(SchedulerBinding)来触发动画帧的更新,确保动画在每一帧更新时都能得到正确的处理。

TickerProvider

TickerProvider是一个抽象类,用于提供一个Ticker对象。在创建一个AnimationController对象时,我们必须要传入一个TickerProvider对象。

TickerProviderStateMixin

TickerProviderStateMixin是一个mixin类,用于在State对象中创建和管理多个Ticker对象,适用于需要管理多个动画控制器的情况,通常情况下不需要。

SingleTickerProviderStateMixin

SingleTickerProviderStateMixin是一个mixin类,用于在State对象中创建和管理单个Ticker对象,适用于只需要管理单个动画控制器的情况。

ImplicitlyAnimatedWidget

ImplicitlyAnimatedWidget是一个抽象类,用于创建包含隐式动画的自定义动画组件。它简化了创建动画效果的过程,开发人员只需关注动画的属性变化,而不需要手动处理动画的控制逻辑。

ImplicitlyAnimatedWidgetState

ImplicitlyAnimatedWidgetState是ImplicitlyAnimatedWidget的状态类,负责管理动画的状态和动画控制器。

Animatable

Animatable是一个抽象类,用于定义动画的插值方式。它提供了一个evaluate方法,该方法接受一个double类型的参数(通常是0.0到1.0之间的值),并返回相应的动画值。Animatable通常用作Tween类的基类,用于定义动画的取值范围和插值方式。

Tween

Tween类继承了Animatable类,在Flutter中用于定义补间动画的起始值、结束值,以便确定动画变化范围,插值器根据动画的时间值(通常是0.0到1.0之间)来计算出对应的动画值,从而实现动画值的平滑过渡。

Curve

Curve类是一个抽象类,常用于定义动画的时间变化曲线。Flutter提供了许多内置的曲线,请参考: Curves

隐式动画

可直接使用在线 Dart 编辑器运行看效果

淡入淡出(AnimatedOpacity)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  double opacity = 1.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            opacity = opacity == 1.0 ? 0.1 : 1.0;
          });
        },
        child: AnimatedOpacity(
          opacity: opacity,
          curve: Curves.decelerate,
          duration: const Duration(seconds: 1),
          child: const Column(
            children: [
              Text('Age: 28'),
              Text('Sex: Male'),
              Text('Name: Zhang san'),
              Divider(
                height: 16,
                thickness: 1,
                color: Colors.grey,
              ),
            ],
          ),
          onEnd: () {
            print('Animation completed');
          },
        ),
      ),
    );
  }
}

形状变化(AnimatedContainer)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;
  Decoration decoration = BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
            if (change) {
              decoration = BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(100),
              );
            } else {
              decoration = BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(8),
              );
            }
          });
        },
        child: Center(
          child: AnimatedContainer(
            width: 200,
            height: 200,
            margin: const EdgeInsets.all(16),
            padding: const EdgeInsets.all(16),
            alignment: Alignment.center,
            decoration: decoration,
            curve: Curves.easeInOut,
            duration: const Duration(milliseconds: 300),
            onEnd: () {
              print('Animation completed');
            },
            child: const Icon(Icons.add, color: Colors.white, size: 68),
          ),
        ),
      ),
    );
  }
}

文本变化(AnimatedDefaultTextStyle)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;
  TextStyle textStyle = const TextStyle(
    color: Colors.blue,
    fontSize: 16,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
            if (change) {
              textStyle = const TextStyle(
                color: Colors.red,
                fontSize: 48,
                fontWeight: FontWeight.bold,
              );
            } else {
              textStyle = const TextStyle(
                color: Colors.blue,
                fontSize: 16,
              );
            }
          });
        },
        child: Center(
          child: AnimatedDefaultTextStyle(
            curve: Curves.decelerate,
            duration: const Duration(milliseconds: 300),
            style: textStyle,
            child: const Text(
              "Hello World!",
            ),
            onEnd: () {
              print("Animation completed");
            },
          ),
        ),
      ),
    );
  }
}

交叉变化(AnimatedCrossFade)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedCrossFade(
            firstCurve: Curves.fastOutSlowIn,
            crossFadeState:
                change ? CrossFadeState.showFirst : CrossFadeState.showSecond,
            duration: const Duration(milliseconds: 300),
            firstChild: const Icon(
              Icons.radio_button_unchecked,
              size: 68,
              color: Colors.blue,
            ),
            secondChild: const Icon(
              Icons.check_circle,
              size: 68,
              color: Colors.blue,
            ),
            alignment: Alignment.centerLeft,
            sizeCurve: Curves.easeInOut,
          ),
        ),
      ),
    );
  }
}

切换变化(AnimatedSwitcher)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedSwitcher(
            switchInCurve: Curves.easeIn,
            switchOutCurve: Curves.easeInOut,
            duration: const Duration(seconds: 1),
            child: change
                ? const Icon(
                    Icons.radio_button_unchecked,
                    size: 68,
                    color: Colors.blue,
                  )
                : const Icon(
                    Icons.check_circle,
                    size: 68,
                    color: Colors.blue,
                  ),
          ),
        ),
      ),
    );
  }
}

填充变化(AnimatedPadding)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedPadding(
            padding: EdgeInsets.all(change ? 100 : 0.0),
            curve: Curves.linear,
            duration: const Duration(milliseconds: 300),
            child: Container(
              color: Colors.blue,
            ),
            onEnd: () {
              print('Animation completed');
            },
          ),
        ),
      ),
    );
  }
}

定位变化(AnimatedPositioned)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Stack(
          children: [
            AnimatedPositioned(
              width: 200,
              height: 200,
              top: change ? 0 : 500,
              left: change ? 0 : 300,
              curve: Curves.linear,
              duration: const Duration(milliseconds: 300),
              child: Container(
                color: Colors.blue,
              ),
              onEnd: () {
                print('Animation Completed');
              },
            )
          ],
        ),
      ),
    );
  }
}

定向变化(AnimatedPositionedDirectional)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Stack(
          children: [
            AnimatedPositionedDirectional(
              width: 200,
              height: 200,
              start: change ? 0 : 300,
              top: change ? 0 : 500,
              curve: Curves.linear,
              duration: const Duration(milliseconds: 300),
              child: Container(
                color: Colors.blue,
              ),
              onEnd: () {
                print('Animation Completed');
              },
            )
          ],
        ),
      ),
    );
  }
}

对齐变化(AnimatedAlign)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Stack(
          children: [
            AnimatedAlign(
              duration: const Duration(milliseconds: 300),
              curve: Curves.easeInOut,
              alignment: change ? Alignment.topCenter : Alignment.bottomCenter,
              child: Container(
                width: 200,
                height: 200,
                color: Colors.blue,
              ),
              onEnd: () {
                print('Animation completed');
              },
            )
          ],
        ),
      ),
    );
  }
}

缩放变化(AnimatedScale)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedScale(
            scale: change ? 2.0 : 1.0,
            alignment: Alignment.center,
            curve: Curves.linear,
            duration: const Duration(milliseconds: 300),
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
            onEnd: () {
              print('Animation completed');
            },
          ),
        ),
      ),
    );
  }
}

旋转变化(AnimatedRotation)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedRotation(
            turns: change ? 1 : 0,
            alignment: Alignment.center,
            curve: Curves.easeInCubic,
            duration: const Duration(milliseconds: 300),
            child: const Icon(
              Icons.refresh,
              size: 100,
              color: Colors.blue,
            ),
            onEnd: () {
              print('Animation completed');
            },
          ),
        ),
      ),
    );
  }
}

滑动变化(AnimatedSlide)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedSlide(
            offset: change ? const Offset(1, 0) : const Offset(0, 0),
            curve: Curves.easeOutSine,
            duration: const Duration(milliseconds: 200),
            child: const Icon(
              Icons.directions_run,
              size: 100,
              color: Colors.blue,
            ),
            onEnd: () {
              print('Animation completed');
            },
          ),
        ),
      ),
    );
  }
}

透明变化(SliverAnimatedOpacity)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: CustomScrollView(
            slivers: [
              SliverAnimatedOpacity(
                opacity: change ? 0.2 : 1.0,
                curve: Curves.easeInOutQuint,
                duration: const Duration(milliseconds: 300),
                sliver: SliverFixedExtentList(
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      return Container(
                        height: 50,
                        color: Colors.blue,
                        alignment: Alignment.center,
                        child: Text(
                          'Item $index',
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 14,
                          ),
                        ),
                      );
                    },
                    childCount: 20,
                  ),
                  itemExtent: 50,
                ),
                onEnd: () {
                  print('Animation completed');
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

物理变化(AnimatedPhysicalModel)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedPhysicalModel(
            curve: Curves.linear,
            shape: BoxShape.rectangle,
            elevation: change ? 0 : 16,
            duration: const Duration(milliseconds: 500),
            color: change ? Colors.yellow : Colors.blue,
            shadowColor: change ? Colors.yellow : Colors.blue,
            borderRadius: BorderRadius.circular(change ? 0 : 16),
            child: Container(
              width: 200,
              height: 200,
              alignment: Alignment.center,
              child: const Text(
                'Hello World!',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 24,
                ),
              ),
            ),
            onEnd: () {
              print('Animation Completed');
            },
          ),
        ),
      ),
    );
  }
}

展开变化(AnimatedFractionallySizedBox)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: AnimatedFractionallySizedBox(
            duration: const Duration(seconds: 1),
            widthFactor: change ? 1.0 : 0.5,
            heightFactor: change ? 1.0 : 0.5,
            alignment: Alignment.center,
            child: Container(
              color: Colors.blue,
              alignment: Alignment.center,
              child: Text(
                change ? '收缩' : '展开',
                style: const TextStyle(color: Colors.white, fontSize: 20),
              ),
            ),
            onEnd: () {},
          ),
        ),
      ),
    );
  }
}

补间变化(TweenAnimationBuilder)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  bool change = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: GestureDetector(
        onTap: () {
          setState(() {
            change = !change;
          });
        },
        child: Center(
          child: TweenAnimationBuilder<Color?>(
            tween: ColorTween(end: change ? Colors.blue : Colors.yellow),
            duration: const Duration(seconds: 1),
            builder: (context, color, child) {
              return TweenAnimationBuilder<double>(
                tween: Tween<double>(end: change ? 200 : 100),
                duration: const Duration(seconds: 1),
                builder: (context, size, child) {
                  return Container(
                    width: size,
                    height: size,
                    color: color,
                    alignment: Alignment.center,
                    child: const Text(
                      'Hello World!',
                      textAlign: TextAlign.center,
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 20,
                      ),
                    ),
                  );
                },
              );
            },
          ),
        ),
      ),
    );
  }
}

小结

隐式动画通常都以Animated+开头,并继承了ImplicitlyAnimatedWidget类,开发者无需手动管理AnimationController的生命周期,通过改变动画属性值,自动产生动画效果,简化了动画的实现过程。但也有一定局限性,除了改变动画属性值之外,开发者只能为动画选择“持续时间”和“曲线”。

显示动画

SizeTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with TickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _sizeFactor;

  late final AnimationController _controller2;
  late final Animation<double> _sizeFactor2;

  bool isVertical = true;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _sizeFactor = CurvedAnimation(
      parent: _controller,
      curve: Curves.decelerate,
    );
    _controller.addStatusListener((status) {
      // 动画开始、结束、正向或反向时回调
      switch (status) {
        case AnimationStatus.forward:
          // 动画开始
          break;
        case AnimationStatus.completed:
          // 动画完成
          break;
        case AnimationStatus.reverse:
          // 动画开始(倒放)
          break;
        case AnimationStatus.dismissed:
          // 动画完成(回到起始位置)
          setState(() {
            isVertical = !isVertical;
          });
          break;
      }
    });

    // 通常情况下,只需要一个动画控制器,这里只是为了示范TickerProviderStateMixin和SingleTickerProviderStateMixin的区别
    _controller2 = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _sizeFactor2 = CurvedAnimation(
      parent: _controller2,
      curve: Curves.decelerate,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        SizeTransition(
          sizeFactor: _sizeFactor,
          axis: isVertical ? Axis.vertical : Axis.horizontal,
          axisAlignment: -1,
          fixedCrossAxisSizeFactor: 1,
          child: Container(
            width: 128,
            height: 128,
            color: Colors.blue,
            margin: const EdgeInsets.symmetric(horizontal: 20),
          ),
        ),
        const SizedBox(height: 16),
        SizeTransition(
          sizeFactor: _sizeFactor2,
          axis: isVertical ? Axis.vertical : Axis.horizontal,
          axisAlignment: -1,
          fixedCrossAxisSizeFactor: 1,
          child: Container(
            width: 128,
            height: 128,
            color: Colors.orange,
            margin: const EdgeInsets.symmetric(horizontal: 20),
          ),
        ),
      ],
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }

    if (_controller2.isCompleted) {
      _controller2.reverse();
    } else {
      _controller2.forward();
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    _controller2.dispose();
    super.dispose();
  }
}

SlideTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<Offset> position;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    position = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(0.5, 0),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeIn,
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return Center(
      child: SlideTransition(
        position: position,
        child: Container(
          width: double.infinity,
          height: double.infinity,
          color: Colors.blue,
        ),
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

ScaleTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _scale;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _scale = CurvedAnimation(
      parent: _controller,
      curve: Curves.fastOutSlowIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return Center(
      child: ScaleTransition(
        scale: _scale,
        child: Container(
          width: 128,
          height: 128,
          color: Colors.blue,
        ),
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

RotationTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> turns;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    turns = Tween<double>(
      begin: 0,
      end: 1,
    ).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return Center(
      child: RotationTransition(
        turns: turns,
        child: Container(
          width: 128,
          height: 128,
          color: Colors.blue,
        ),
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

FadeTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _opacity;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    _opacity = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeIn,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return Center(
      child: FadeTransition(
        opacity: _opacity,
        child: Container(
          width: 128,
          height: 128,
          color: Colors.blue,
          margin: const EdgeInsets.symmetric(horizontal: 20),
        ),
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

PositionedTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    // PositionedTransition是Stack的一个子组件,它使用绝对定位来控制子组件的位置
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        const double iconSize = 100;
        final Size biggest = constraints.biggest;
        Rect begin = Rect.fromLTWH((biggest.width - iconSize) / 2,
            biggest.height - iconSize, iconSize, iconSize);
        Rect end = Rect.fromLTWH(
            (biggest.width - iconSize) / 2, 0, iconSize, iconSize);
        return Stack(
          children: <Widget>[
            PositionedTransition(
              rect: RelativeRectTween(
                begin: RelativeRect.fromSize(
                  begin,
                  biggest,
                ),
                end: RelativeRect.fromSize(
                  end,
                  biggest,
                ),
              ).animate(CurvedAnimation(
                parent: _controller,
                curve: Curves.elasticInOut,
              )),
              child: const Icon(
                Icons.airplanemode_on,
                size: iconSize,
                color: Colors.blue,
              ),
            ),
          ],
        );
      },
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

RelativePositionedTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    // RelativePositionedTransition是Stack的一个子组件,它使用相对定位来控制子组件的位置
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        const double iconSize = 100;
        final Size biggest = constraints.biggest;
        Rect begin = Rect.fromLTWH((biggest.width - iconSize) / 2,
            biggest.height - iconSize, iconSize, iconSize);
        Rect end = Rect.fromLTWH(
            (biggest.width - iconSize) / 2, 0, iconSize, iconSize);
        return Stack(
          children: <Widget>[
            RelativePositionedTransition(
              size: biggest,
              rect: RectTween(
                begin: begin,
                end: end,
              ).animate(CurvedAnimation(
                parent: _controller,
                curve: Curves.elasticInOut,
              )),
              child: const Icon(
                Icons.airplanemode_on,
                size: iconSize,
                color: Colors.blue,
              ),
            ),
          ],
        );
      },
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

DefaultTextStyleTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<TextStyle> _style;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 200),
      vsync: this,
    );
    _style = TextStyleTween(
      begin: const TextStyle(
        color: Colors.black,
        fontSize: 16,
        fontWeight: FontWeight.normal,
      ),
      end: const TextStyle(
        color: Colors.blue,
        fontSize: 32,
        fontWeight: FontWeight.bold,
      ),
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.decelerate,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return Center(
      child: DefaultTextStyleTransition(
        style: _style,
        textAlign: TextAlign.center,
        child: const Text('Hello World!'),
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

DecoratedBoxTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<Decoration> _decoration;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    final DecorationTween decorationTween = DecorationTween(
      begin: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(64.0),
        boxShadow: const <BoxShadow>[
          BoxShadow(
            color: Colors.blue,
            blurRadius: 8.0,
            spreadRadius: 2.0,
            offset: Offset(0, 4.0),
          ),
        ],
      ),
      end: BoxDecoration(
        color: Colors.white,
        boxShadow: const <BoxShadow>[
          BoxShadow(
            color: Colors.blue,
            blurRadius: 8.0,
            spreadRadius: 2.0,
            offset: Offset(0, 4.0),
          ),
        ],
        borderRadius: BorderRadius.circular(8.0),
        // No shadow.
      ),
    );
    _decoration = decorationTween.animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.ease,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return Center(
      child: DecoratedBoxTransition(
        decoration: _decoration,
        position: DecorationPosition.background,
        child: const Icon(
          Icons.add_circle,
          size: 128,
          color: Colors.blue,
        ),
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

AlignTransition

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<AlignmentGeometry> _alignment;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
    _alignment = Tween<Alignment>(
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.ease,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
              child: animWidget(),
            ),
            const Divider(
              indent: 16,
              endIndent: 16,
              height: 32,
              thickness: 0.5,
              color: Colors.grey,
            ),
            ElevatedButton(
              onPressed: startAnimation,
              child: const Padding(
                  padding: EdgeInsets.all(12), child: Text('Start Animation')),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return AlignTransition(
      alignment: _alignment,
      child: Container(
        width: 240,
        height: 48,
        color: Colors.blue,
        margin: const EdgeInsets.symmetric(horizontal: 20),
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

小结

显式动画是一种更灵活、更具定制性的动画实现方式,通过使用Animation和AnimationController等类来手动控制动画的每个阶段,可以实现各种复杂的动画效果。

补间动画

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with TickerProviderStateMixin {
  late List<AnimationController> controllers;
  late List<Animation<double>> animations;

  bool isFlipX = false;
  bool isFlipY = false;

  @override
  void initState() {
    super.initState();
    controllers = List.generate(4, (index) {
      return AnimationController(
        vsync: this,
        duration: const Duration(milliseconds: 300),
      );
    });

    animations = controllers.mapIndexed((index, controller) {
      switch (index) {
        case 0:
          return Tween<double>(begin: 0.0, end: 2 * 3.14).animate(
            CurvedAnimation(
              parent: controller,
              curve: Curves.easeInOut,
            ),
          );
        case 1:
          return Tween<double>(begin: 0, end: 100).animate(
            CurvedAnimation(
              parent: controller,
              curve: Curves.easeInOut,
            ),
          );
        case 2:
          return Tween<double>(begin: 1.0, end: 2.0).animate(
            CurvedAnimation(
              parent: controller,
              curve: Curves.easeInOut,
            ),
          )..addStatusListener((status) {
              if (status == AnimationStatus.completed) {
                controller.reverse();
              }
            });
        case 3:
          return Tween<double>(begin: 100, end: 200).animate(
            CurvedAnimation(
              parent: controller,
              curve: Curves.easeInOut,
            ),
          )..addStatusListener((status) {
              if (status == AnimationStatus.completed) {
                controller.reverse();
              }
            });
      }
      return Tween<double>(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(
          parent: controller,
          curve: Curves.easeInOut,
        ),
      );
    }).toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('补间动画'),
      ),
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: ListView(
          children: <Widget>[
            _buildDescriptionText(),
            _buildDivider(),
            _buildAnimatedWidget(0),
            _buildDivider(),
            _buildAnimatedWidget(1),
            _buildDivider(),
            _buildAnimatedWidget(2),
            _buildDivider(),
            _buildAnimatedWidget(3),
            _buildDivider(),
            _buildFlipWidget(),
          ],
        ),
      ),
    );
  }

  Widget _buildDescriptionText() {
    return const Text('补间动画在两个关键帧之间创建平滑的过渡动画,下面使用Animation和Tween类来实现补间动画');
  }

  Widget _buildDivider() {
    return const Divider(
      height: 32,
      thickness: 1,
      color: Colors.grey,
    );
  }

  Widget _buildAnimatedWidget(int index) {
    return GestureDetector(
      onTap: () {
        startAnimation(index);
      },
      child: AnimatedBuilder(
        animation: animations[index],
        builder: (context, child) {
          if (index == 0) {
            return _buildRotateWidget(
                animations[index], Icons.refresh, Colors.blue);
          } else if (index == 1) {
            return _buildTranslateWidget(
                animations[index], Icons.accessibility, Colors.orange);
          } else if (index == 2) {
            return _buildScaleWidget(
                animations[index], Icons.favorite, Colors.red);
          } else if (index == 3) {
            return _buildIconWidget(
                animations[index], Icons.star, Colors.yellow);
          }
          return Container();
        },
      ),
    );
  }

  Widget _buildRotateWidget(
      Animation<double> animation, IconData icon, Color color) {
    return Transform.rotate(
      angle: animation.value,
      child: Icon(
        icon,
        color: color,
        size: 80,
      ),
    );
  }

  Widget _buildTranslateWidget(
      Animation<double> animation, IconData icon, Color color) {
    return Transform.translate(
      offset: Offset(animation.value, 0),
      child: Icon(
        icon,
        color: color,
        size: 80,
      ),
    );
  }

  Widget _buildScaleWidget(
      Animation<double> animation, IconData icon, Color color) {
    return Transform.scale(
      scale: animation.value,
      child: Icon(
        icon,
        color: color,
        size: 80,
      ),
    );
  }

  Widget _buildIconWidget(
      Animation<double> animation, IconData icon, Color color) {
    return GestureDetector(
      onTap: () {
        startAnimation(3);
      },
      child: Icon(
        icon,
        color: color,
        size: animation.value,
      ),
    );
  }

  Widget _buildFlipWidget() {
    return GestureDetector(
      onTap: () {
        setState(() {
          flip();
        });
      },
      child: Transform.flip(
        flipX: isFlipX,
        flipY: isFlipY,
        child: const Icon(
          Icons.flip,
          color: Colors.green,
          size: 80,
        ),
      ),
    );
  }

  void startAnimation(int index) {
    if (controllers.length > index && index >= 0) {
      if (controllers[index].isDismissed) {
        controllers[index].forward();
      } else {
        controllers[index].reverse();
      }
    }
  }

  void flip() {
    isFlipX = !isFlipX;
    isFlipY = !isFlipY;
  }

  @override
  void dispose() {
    for (var controller in controllers) {
      controller.dispose();
    }
    super.dispose();
  }
}

extension IterableIndexed<E> on Iterable<E> {
  Iterable<T> mapIndexed<T>(T Function(int index, E e) f) sync* {
    var index = 0;
    for (var element in this) {
      yield f(index, element);
      index++;
    }
  }
}

小结

补间动画属于显示动画的范畴,它通过Tween类来定义动画的起始值和结束值,从而确定动画的变化范围,开发人员可以通过补间动画实现各种复杂的动画效果,如缩放、平移、旋转等。

Hero动画

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        padding: const EdgeInsets.all(32),
        alignment: Alignment.topLeft,
        child: HeroWidget(
          tag: 'hero',
          width: 100,
          onTap: () {
            startPageHero();
          },
        ),
      ),
    );
  }

  startPageHero() async {
    Widget child = Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.grey[200],
        alignment: Alignment.center,
        child: HeroWidget(
          tag: 'hero',
          width: 300,
          onTap: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
    await Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) {
          return child;
        },
      ),
    );
  }
}

class HeroWidget extends StatelessWidget {
  final double width;
  final String tag;
  final VoidCallback? onTap;

  const HeroWidget({
    super.key,
    required this.width,
    required this.tag,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: width,
      child: Hero(
        tag: tag,
        child: Material(
          color: Colors.transparent,
          child: InkWell(
            onTap: onTap,
            child: Icon(Icons.person, size: width),
          ),
        ),
      ),
    );
  }
}

小结

Hero动画是一种让应用在切换页面时更加顺畅的特效。它通过将两个页面中相同的元素用Hero组件包裹起来,并用相同的标签来标识它们,实现了元素之间的平滑过渡动画效果。

交织动画

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late List<Animation<double>> animations;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    animations = List.generate(10, (index) {
      return Tween<double>(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Interval(
            index * 0.1,
            (index + 1) * 0.1,
            curve: Curves.easeIn,
          ),
        ),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: <Widget>[
            Expanded(child: animWidget()),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                startAnimation();
              },
              child: const Text('Start Animation'),
            ),
            const SizedBox(height: 24),
          ],
        ),
      ),
    );
  }

  Widget animWidget() {
    return ListView(
      children: List.generate(
        10,
        (index) {
          return AnimatedBuilder(
            animation: _controller,
            builder: (context, child) {
              return Opacity(
                opacity: animations[index].value,
                child: Container(
                  width: double.infinity,
                  margin:
                      const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
                  height: 48,
                  color: Colors.blue[200],
                  alignment: Alignment.center,
                  child: Text(
                    'Item $index',
                    style: const TextStyle(
                      fontSize: 18,
                      color: Colors.black,
                    ),
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }

  startAnimation() {
    if (_controller.isCompleted) {
      _controller.reverse();
    } else {
      _controller.forward();
    }
  }

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

小结

在交织动画中,每个元素都可以有自己的动画开始时间、持续时间、延迟等属性,使得它们可以以错开的时间序列展示动画效果。这种错开的动画效果可以让用户的视觉焦点更加丰富,增加页面的活力和吸引力。

列表动画

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  int? selectIndex;

  final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();

  List<String> listData = List.generate(3, (index) => 'Item $index');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('列表动画'),
      ),
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        padding: const EdgeInsets.symmetric(horizontal: 16),
        child: Stack(
          children: [
            AnimatedList(
              key: listKey,
              initialItemCount: listData.length,
              itemBuilder: (context, index, animation) {
                return buildItem(context, index, animation);
              },
            ),
            Positioned(
              bottom: 20,
              right: 20,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  ElevatedButton(
                    onPressed: () {
                      insert();
                    },
                    child: const Text('+'),
                  ),
                  ElevatedButton(
                    onPressed: () {
                      remove();
                    },
                    child: const Text('-'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget buildItem(
      BuildContext context, int index, Animation<double> animation) {
    return SizeTransition(
      sizeFactor: animation,
      child: Card(
        color: selectIndex == index ? Colors.blue[300] : null,
        child: ListTile(
          title: Text(listData[index]),
          onTap: () {
            setState(() {
              selectIndex = index;
            });
          },
        ),
      ),
    );
  }

  insert() {
    final int index = selectIndex != null ? selectIndex! : listData.length;
    listData.insert(index, 'Item ${listData.length}');
    listKey.currentState!.insertItem(index);
  }

  remove() {
    final int index = selectIndex != null ? selectIndex! : listData.length - 1;
    if (index < 0) return;
    final String text = listData[index];
    listKey.currentState!.removeItem(
      index,
      (context, animation) => SizeTransition(
        sizeFactor: animation,
        child: Card(
          color: selectIndex == index ? Colors.blue[300] : null,
          child: ListTile(
            title: Text(text),
          ),
        ),
      ),
    );
    listData.removeAt(index);
    selectIndex = null;
  }
}

小结

列表动画在列表执行添加、删除等操作时实现平滑的动画效果,可以增强用户体验和提升应用的视觉吸引力。

转场动画

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('页面转场动画'), centerTitle: true),
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () {
                startPagePanUpDown();
              },
              child: const Text('上下平移'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                startPagePanLeftRight();
              },
              child: const Text('左右平移'),
            ),
          ],
        ),
      ),
    );
  }

  void startPagePanUpDown() async {
    PageRouteBuilder routeBuilder = PageRouteBuilder(
      pageBuilder: (context, animation, secondaryAnimation) =>
          const OtherView(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        const begin = Offset(0.0, 1.0);
        const end = Offset.zero;
        final tween = Tween(begin: begin, end: end);
        final curvedAnimation = CurvedAnimation(
          parent: animation,
          curve: Curves.ease,
        );
        return SlideTransition(
          position: tween.animate(curvedAnimation),
          child: child,
        );
      },
    );
    Navigator.of(context).push(routeBuilder);
  }

  void startPagePanLeftRight() async {
    PageRouteBuilder routeBuilder = PageRouteBuilder(
      pageBuilder: (context, animation, secondaryAnimation) =>
          const OtherView(),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        const begin = Offset(1.0, 0.0);
        const end = Offset.zero;
        final tween = Tween(begin: begin, end: end);
        final curvedAnimation = CurvedAnimation(
          parent: animation,
          curve: Curves.ease,
        );
        return SlideTransition(
          position: tween.animate(curvedAnimation),
          child: child,
        );
      },
    );
    Navigator.of(context).push(routeBuilder);
  }
}

class OtherView extends StatelessWidget {
  const OtherView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('其他页面')),
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        alignment: Alignment.center,
        child: const Text(
          'Hello OtherView!',
          style: TextStyle(fontSize: 24, color: Colors.black),
        ),
      ),
    );
  }
}

小结

转场动画是一种用于在页面之间实现平滑过渡效果的动画,可以增强用户体验和提升应用的视觉吸引力。

物理动画

import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: Home(),
    );
  }
}

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

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

class HomeState extends State<Home> with SingleTickerProviderStateMixin {
  /// 动画控制器
  late final AnimationController _controller;

  /// 定义一个新的对齐动画
  late Animation<Alignment> _animation;

  /// left: -1, right: 1, top: -1, bottom: 1, center: 0
  var _dragAlignment = Alignment.center;

  /// 是否垂直方向
  bool isVertical = true;

  /// 动画是否正在运行
  bool running = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController.unbounded(vsync: this)
      ..addListener(() {
        setState(() {
          _dragAlignment = _animation.value;
        });
      });

    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        // 动画完成
        setState(() {
          isVertical = !isVertical;
          running = false;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('弹簧')),
      body: Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.white,
        child: Column(
          children: [
            Expanded(
                child: Align(
              alignment: _dragAlignment,
              child: SizedBox(
                width: 120,
                height: 120,
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    for (int i = 0; i < 12; i++)
                      Transform.rotate(
                        angle: i * 30 * 0.0174533,
                        child: Align(
                          alignment: Alignment.topCenter,
                          child: Container(
                            width: 80,
                            height: 10,
                            color: Colors.red,
                          ),
                        ),
                      ),
                    Container(
                      width: 110,
                      height: 110,
                      decoration: const BoxDecoration(
                        shape: BoxShape.circle,
                        color: Colors.orange,
                      ),
                    ),
                  ],
                ),
              ),
            )),
            const SizedBox(height: 48),
            ElevatedButton(
              onPressed: () {
                startAnimation();
              },
              child: const Text('Start Animation'),
            ),
            const SizedBox(height: 24)
          ],
        ),
      ),
    );
  }

  /// 开始动画
  void startAnimation() {
    if (running) {
      return;
    }
    running = true;
    // 用于创建一个从begin到end的动画
    if (isVertical) {
      _animation = _controller.drive(
        AlignmentTween(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
        ),
      );
    } else {
      _animation = _controller.drive(
        AlignmentTween(
          begin: Alignment.centerLeft,
          end: Alignment.centerRight,
        ),
      );
    }
    // 定义一个弹簧的物理特性,mass:质量,stiffness:刚度,damping:阻尼
    const spring = SpringDescription(
        mass: 10, // 值越大动画的质量越稳定,但是速度越慢,其值通常为正数,但没有严格的上限
        stiffness: 800, // 其值通常为正数,刚度越大弹性越强
        damping: 0.2 // 其值通常为正数,阻尼越大震荡越快平息
        );
    // SpringSimulation: 基于弹簧物理模拟的动画的类
    // spring:弹簧的物理特性,start:动画开始位置,end:动画结束位置,velocity:动画的初始速度
    final simulation = SpringSimulation(spring, 0, 0.5, 0);
    _controller.animateWith(simulation);
  }

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

小结

物理动画是基于物理学原理的一类动画形式,利用物理学的模型来创建动态、自然的效果,而不是基于预定义的关键帧。物理动画通过模拟真实世界中的物理特性,让用户界面更加生动、自然、符合直觉。

预置动画

预置动画是指Flutter官方开源的 animations package,这个 package 包含了以下内置常用模式: Container 变换、共享轴变化、渐变穿透和渐变变换。

总结

Flutter提供了多种动画类型:隐式动画自动处理动画过渡,显示动画直接显示变化,补间动画控制动画过程,Hero动画在页面间传递元素,交织动画同时播放多个动画,列表动画为列表项添加动画效果,转场动画页面切换时动画效果,物理动画模拟真实物理效果,预置动画提供预定义动画效果。