30、Flutter动画开发全解析

Flutter动画开发全解析

1. 引言

在Flutter开发中,动画是提升用户体验的重要手段。Flutter框架提供了丰富的动画支持,内置了多种可直接使用的动画,同时也允许开发者通过组合和扩展这些动画来满足不同的需求。在深入学习动画之前,我们需要了解一些重要的概念和相关类。

2. 动画基础概念
2.1 Animation 类

在Flutter中,动画由状态(status)和类型为T的值(value)组成。状态表示动画的运行状态,如正在运行或已完成;值则是动画当前的值,并且在动画执行过程中会发生变化。Animation 类不仅保存这些信息,还提供了回调函数,让其他类可以了解动画的运行状态和当前值。

需要注意的是,Animation 类实例仅负责保存和暴露这些值,它不涉及视觉反馈、屏幕绘制或绘制方式(即build()函数)。常见的动画类型是Animation ,因为双精度值可以方便地在比例空间中操作各种值。

Animation类会在指定的最小值和最大值之间生成一个值序列(不一定是线性的),这个过程称为插值(interpolation)。插值可以是线性的,也可以定义为阶跃函数或曲线。Flutter提供了多个用于操作动画的函数和工具,如下所示:
- AnimationController:尽管名字中有“Controller”,但它并非用于控制动画对象,而是帮助自身进行控制,因为它继承自Animation类,本身也是一个动画。
- CurvedAnimation:将曲线应用于另一个动画的动画。
- Tween:用于在起始值和结束值之间创建线性插值。

Animation类还提供了在运行周期内访问其状态和值的方法。通过状态监听器(status listeners),我们可以知道动画何时开始、结束或反向运行。使用addStatusListener()方法,我们可以根据动画的开始或结束事件来操作我们的小部件。同样,使用addListener()方法添加值监听器,每当动画值发生变化时,我们都会收到通知,并可以使用setState() {}方法重建小部件。

2.2 AnimationController

AnimationController是Flutter中最常用的动画类之一,它派生自Animation 类,并添加了一些用于操作动画的基本方法。Animation类是Flutter动画的基础,但它没有与动画控制相关的方法,而AnimationController则为动画概念添加了这些控制,例如:
- 播放和停止控制:AnimationController可以让动画向前播放、向后播放或停止。
- 持续时间:真实的动画有有限的播放时间,即它们会播放一段时间后结束或重复。
- 设置动画当前值:这会导致动画停止,并通知状态和值监听器。
- 定义动画的上下限:这样我们可以在播放动画前后知道预期的值。

下面是AnimationController的构造函数及其主要属性的分析:

AnimationController({
    double value,
    Duration duration,
    String debugLabel,
    double lowerBound: 0.0,
    double upperBound: 1.0,
    AnimationBehavior animationBehavior: AnimationBehavior.normal,
    @required TickerProvider vsync
})

各属性含义如下:
| 属性 | 含义 |
| ---- | ---- |
| value | 动画的初始值,如果未指定则默认为lowerBound。 |
| duration | 动画的持续时间。 |
| debugLabel | 用于调试的字符串,在调试输出中标识控制器。 |
| lowerBound | 动画被视为取消的最小值,通常是运行时的起始值,不能为null。 |
| upperBound | 动画被视为完成的最大值,通常是运行时的结束值,不能为null。 |
| animationBehavior | 配置AnimationController在动画禁用时的行为。如果是AnimationBehavior.normal,动画持续时间将减少;如果是AnimationBehavior.preserve,AnimationController将保留其行为。 |
| vsync | 控制器用于在帧触发时获取信号的TickerProvider实例。 |

你可以查看 AnimationController类的文档 以了解所有可用的运行动画方法。

2.3 TickerProvider和Ticker

TickerProvider接口描述了能够提供Ticker对象的对象。Ticker用于需要知道下一帧何时构建的类,通常通过AnimationControllers间接使用。当使用State类时,我们可以通过扩展TickerProviderStateMixin或SingleTickerProviderStateMixin来拥有TickerProvider,并将其与AnimationController对象一起使用。

2.4 CurvedAnimation

CurvedAnimation类用于将Animation类的进度定义为非线性曲线。我们可以使用它来修改现有动画的插值方法。当我们希望在动画向前播放和反向播放时使用不同的曲线时,它也很有用,分别通过其curve和reverseCurve属性实现。

Curves类定义了许多可直接用于动画的曲线,而不仅仅是Curves.linear。你可以查看 Curves类的文档 以详细了解每条曲线的行为。

2.5 Tween

除了上述类之外,Tween类可以帮助我们处理动画的范围相关的特定任务。默认情况下,动画的简单起始值和结束值分别是0.0和1.0。通过使用Tween,我们可以在不修改AnimationController的情况下更改其范围或类型。Tween可以是任何类型,我们也可以根据需要创建自定义的Tween类。

Tween的作用是在起始值和结束值之间的时间段内返回值,我们可以将这些值作为属性传递给正在动画化的任何内容,从而使其不断更新。例如,我们可以使用特定的Tween来更改小部件的大小、位置、不透明度、颜色等。

此外,还有其他Tween派生类,如CurveTween类可以修改动画曲线,ColorTween类可以在颜色之间创建插值。

3. 使用动画

在处理动画时,我们并不总是创建完全相同的动画对象,但可以发现一些使用场景的相似性。Tween对象对于更改动画的类型和范围很有用。大多数情况下,我们会使用AnimationController、CurvedAnimation和Tween实例来组合动画。

在使用自定义Tween实现之前,让我们通过动画方式重新审视之前的小部件变换,这样可以获得相同的最终效果,但更加平滑和出色。

3.1 旋转动画

我们可以使用AnimationController类使按钮的旋转变得渐进,而不是直接更改按钮的旋转角度。以下是一个示例代码:

_rotationAnimationButton() {
    return Transform.rotate(
      angle: _angle,
      child: RaisedButton(
        child: Text("Rotated button"),
        onPressed: () {
          if (_animation.status == AnimationStatus.completed) {
            _animation.reset();
            _animation.forward();
          }
        },
      ),
    );
  }

需要注意的是:
- 角度值现在通过_angle属性定义,而不是直接赋值给一个字面量。
- 在onPressed属性中,我们检查_animation是否完成,如果完成,则从开始重复动画。

下面是创建和运行AnimationController对象的示例类:

class _RotationAnimationsState extends State<RotationAnimations> with 
SingleTickerProviderStateMixin {
  double _angle = 0.0;
  AnimationController _animation;
  ...
}

在这个类中,有几点需要注意:
- 我们有一个名为RotationAnimations的StatefulWidget对象,使用SingleTickerProviderStateMixin类来提供控制器运行所需的Ticker对象。
- 我们有一个_angle属性,用于定义按钮的当前角度。可以使用setState()方法使小部件以新的角度重建。
- 我们有一个_animation对象,用于保存动画并允许我们管理它。

在State类的initState()函数中设置并启动动画是一个很好的选择:

@override
void initState() {
    super.initState();
    _animation = createRotationAnimation();
    _animation.forward();
  }

下面是定义动画的方法:

createRotationAnimation() {
    var animation = AnimationController(
      vsync: this,
      debugLabel: "animations demo",
      duration: Duration(seconds: 3),
    );
    animation.addListener(() {
      setState(() {
        _angle = (animation.value * 360.0) * _toRadians;
      });
    });
    return animation;
  }

动画的创建可以分为两个重要部分:
1. 动画定义本身,我们设置了动画的debugLabel属性用于调试,设置了vsync以便它可以有一个Ticker并知道何时生成新的动画值,最后设置了动画的持续时间。
2. 监听动画值的变化。每当动画有新值时,我们将其乘以360度,从而得到一个成比例的旋转值。

如果需要,我们可以使用CurveTween为动画添加不同的曲线,例如在createBounceInRotationAnimation()方法中:

createBounceInRotationAnimation() {
    var controller = AnimationController(
      vsync: this,
      debugLabel: "animations demo",
      duration: Duration(seconds: 3),
    );

    var animation = controller.drive(CurveTween(
      curve: Curves.bounceIn,
    ));
    animation.addListener(() {
      setState(() {
        _angle = (animation.value * 360.0) * _toRadians;
      });
    });
    return controller;
  }

这里,我们使用控制器的drive()方法创建另一个Animation实例,并通过CurveTween对象传递所需的曲线。需要注意的是,我们将监听器添加到新的动画对象而不是控制器,因为我们需要相对于曲线的值。

另外,在State类的生命周期结束时,我们必须处理AnimationController类实例,以防止内存泄漏:

@override
void dispose() {
    _animation.dispose();
    super.dispose();
  }
3.2 缩放动画

为了创建缩放动画并获得比直接更改缩放属性更好的效果,我们可以再次使用AnimationController类。以下是构建带有缩放效果的RaisedButton小部件的代码:

_scaleAnimationButton() {
    return Transform.scale(
      scale: _scale,
      child: RaisedButton(
        child: Text("Scaled button"),
        onPressed: () {
          if (_animation.status == AnimationStatus.completed) {
            _animation.reverse();
          } else if (_animation.status == AnimationStatus.dismissed) {
            _animation.forward();
          } 
        },
      ),
    );
  }

在这个示例中,我们使用了_scale属性,并在onPressed方法中进行了更改。如果动画完成,我们使用AnimationController的reverse()函数反向播放动画;如果动画处于初始状态,则向前播放。

动画对象的创建与旋转动画类似,但在控制器的构造上有一些细微的修改:

createScaleAnimation() {
    var animation = AnimationController(
      vsync: this,
      lowerBound: 1.0,
      upperBound: 2.0,
      debugLabel: "animations demo",
      duration: Duration(seconds: 2),
    );
    animation.addListener(() {
      setState(() {
        _scale = animation.value;
      });
    });
    return animation;
  }

这里,我们更改了控制器的lowerBound和upperBound值,以使按钮增长到两倍大小,并且不希望它小于初始大小(scale = 1.0)。此外,我们更改了动画值监听器,直接从动画中获取值,无需进行任何计算。

3.3 平移动画

同样,我们可以使用AnimationController使平移变换看起来更好、更平滑。以下是创建平移动画的代码:

createTranslateAnimation() {
    var controller = AnimationController(
      vsync: this,
      debugLabel: "animations demo",
      duration: Duration(seconds: 2),
    );
    var animation = controller.drive(Tween<Offset>(
      begin: Offset.zero,
      end: Offset(70, 200),
    ));
    animation.addListener(() {
      setState(() {
        _offset = animation.value;
      });
    });
    return controller;
  }

在这个示例中,我们使用了Tween 实例,并通过drive()方法将其传递给AnimationController对象,就像之前使用CurveTween一样。这是因为Offset类重写了减法和加法等数学运算符,使得计算中间偏移量(动画值)成为可能,从而实现两个Offset值之间的插值。

你可以查看 Offset类的源代码 以了解更多细节。需要注意的是,要创建自定义插值,我们通常需要编写自定义的Tween类。

4. 多种变换和自定义Tween

我们可以使用Matrix4类组合多种变换,对于动画也是类似的,我们可以组合动画、依次运行它们并播放它们。为了创建组合动画,我们可以基于单个Animation对象创建多个变换值。

以下是实现组合动画的步骤:
1. 在类中定义多个值:

class _ComposedAnimationsState extends State<ComposedAnimations>
    with SingleTickerProviderStateMixin {
  Offset _offset = Offset.zero;
  double _scale = 1.0;
  double _angle = 0.0;
  ...
}
  1. 每当动画值发生变化时,根据它计算我们的值:
animation.addListener(() {
    setState(() {
        _offset = Offset(animation.value * 70, animation.value * 200);
        _scale = 1.0 + animation.value;
        _angle = 360 * animation.value;
      });
    });
  1. 在build()方法中应用我们在动画执行的每个步骤中计算的值:
_composedAnimationButton() {
    return Transform.translate(
      offset: _offset,
      child: Transform.rotate(
        angle: _angle * _toRadians,
        child: Transform.scale(
          scale: _scale,
          child: RaisedButton(
            child: Text("multiple animation"),
            onPressed: () {
              if (_animation.status == AnimationStatus.completed) {
                _animation.reverse();
              } else if (_animation.status == AnimationStatus.dismissed) {
                _animation.forward();
              }
            },
          ),
        ),
      ),
    );
  }

对于简单的情况,这样做是最好的,因为我们需要处理的对象更少,并且只播放一个动画。然而,为了使代码更易于维护,最好将值的计算与动画本身分离,这就是我们可以使用Tween的地方。

5. 自定义Tween

要创建自定义Tween类,首先需要定义我们的值对象。这里,我们选择将变换值分组:

class ButtonTransformation {
  final double scale;
  final double angle;
  final Offset offset;
  // this none getter returns a initial state of transformation
  // with default scale, no rotation or translation
  static ButtonTransformation get none => ButtonTransformation(
        scale: 1.0,
        angle: 0.0,
        offset: Offset.zero,
      );
}

然后,我们使用定义的类型扩展Tween类:

class CustomTween extends Tween<ButtonTransformation> {
  CustomTween({ButtonTransformation begin, ButtonTransformation end} ): 
  super(begin: begin, end: end,);
  @override
  lerp(double t) {
    return super.lerp(t);
  }
}

我们需要定义自定义Tween的lerp()方法(lerp代表线性插值),该方法负责根据t值返回begin和end之间的中间ButtonTransformation值。

通过查看默认Tween类的lerp()实现,我们可以看到它非常简单:

// part of tween.dart Tween class
@protected
T lerp(double t) {
  assert(begin != null);
  assert(end != null);
  return begin + (end - begin) * t;
}

它使用类型T对象上的 +、 - 和 * 运算符来计算lerp()值。这意味着我们可以在ButtonTransformation类中实现这些运算符,这样Tween就可以像处理其他类型一样工作:

class ButtonTransformation {
  ...
  ButtonTransformation operator -(ButtonTransformation other) =>
      ButtonTransformation(
        scale: scale - other.scale,
        angle: angle - other.angle,
        offset: offset - other.offset,
      );
  ButtonTransformation operator +(ButtonTransformation other) =>
      ButtonTransformation(
        scale: scale + other.scale,
        angle: angle + other.angle,
        offset: offset + other.offset,
      );
  ButtonTransformation operator *(double t) => ButtonTransformation(
        scale: scale * t,
        angle: angle * t,
        offset: offset * t,
      );
}

现在,Tween类能够生成中间的ButtonTransformation值。我们可以像之前一样使用生成的动画值:

createCustomTweenAnimation() {
    var controller = AnimationController(
      vsync: this,
      debugLabel: "animations demo",
      duration: Duration(seconds: 3),
    );
    var animation = controller.drive(CustomTween(
        begin: ButtonTransformation.none, // initial state of the animation
        end: ButtonTransformation(
          angle: 360.0,
          offset: Offset(70, 200),
          scale: 2.0,
        )));
    animation.addListener(() {
      setState(() {
        _buttonTransformation = animation.value;
      });
    });
    return controller;
  }

这里的主要区别在于使用了CustomTween属性。需要注意的是,我们始终需要定义begin和end值,因为Tween是基于相应插值定义的范围。

通过这些示例,我们了解了如何在Flutter中使用和应用最重要的动画。我们还可以使用单独的Animation对象构建多个同时进行的动画,通常通过将相同的AnimationController设置为它们的父对象。由于我们将使用相同的Ticker对象,因此它们可以保证同步。

6. 替代动画应用方式

除了前面介绍的使用AnimationController、Tween等组合动画的方式,Flutter还提供了其他一些替代方式来应用动画到我们的小部件上。

6.1 AnimatedBuilder

AnimatedBuilder是一个非常有用的小部件,它可以帮助我们将动画逻辑和小部件的构建逻辑分离。使用AnimatedBuilder,我们可以将动画值传递给一个builder函数,在这个函数中构建需要动画化的小部件。

下面是一个使用AnimatedBuilder实现旋转动画的示例:

class RotationAnimationWithBuilder extends StatefulWidget {
  @override
  _RotationAnimationWithBuilderState createState() => _RotationAnimationWithBuilderState();
}

class _RotationAnimationWithBuilderState extends State<RotationAnimationWithBuilder>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 3),
    );
    _animation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(_controller);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (BuildContext context, Widget child) {
        return Transform.rotate(
          angle: _animation.value,
          child: RaisedButton(
            child: Text("Rotated with Builder"),
            onPressed: () {
              if (_controller.status == AnimationStatus.completed) {
                _controller.reset();
                _controller.forward();
              }
            },
          ),
        );
      },
    );
  }
}

在这个示例中,我们创建了一个AnimationController和一个Animation 对象。在build方法中,使用AnimatedBuilder将动画值传递给builder函数,在函数内部根据动画值构建旋转的按钮。这样,动画逻辑和小部件构建逻辑就分离了,代码更加清晰和易于维护。

6.2 AnimatedWidget

AnimatedWidget是一个抽象类,我们可以通过继承它来创建自定义的动画小部件。当动画值发生变化时,AnimatedWidget会自动调用build方法进行重绘。

下面是一个使用AnimatedWidget实现缩放动画的示例:

class ScalingAnimatedWidget extends AnimatedWidget {
  ScalingAnimatedWidget({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Transform.scale(
      scale: animation.value,
      child: RaisedButton(
        child: Text("Scaled with AnimatedWidget"),
        onPressed: () {},
      ),
    );
  }
}

class ScalingAnimationExample extends StatefulWidget {
  @override
  _ScalingAnimationExampleState createState() => _ScalingAnimationExampleState();
}

class _ScalingAnimationExampleState extends State<ScalingAnimationExample>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
      lowerBound: 1.0,
      upperBound: 2.0,
    );
    _animation = _controller;
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return ScalingAnimatedWidget(animation: _animation);
  }
}

在这个示例中,我们创建了一个ScalingAnimatedWidget类,继承自AnimatedWidget。在build方法中,根据动画值构建缩放的按钮。在ScalingAnimationExample类中,创建AnimationController和Animation对象,并将动画传递给ScalingAnimatedWidget。

7. 动画同步与组合

在实际开发中,我们可能需要同时运行多个动画,并且保证它们之间的同步。通常可以通过将相同的AnimationController作为多个Animation对象的父对象来实现。

下面是一个同时运行旋转和缩放动画的示例:

class CombinedAnimations extends StatefulWidget {
  @override
  _CombinedAnimationsState createState() => _CombinedAnimationsState();
}

class _CombinedAnimationsState extends State<CombinedAnimations>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _rotationAnimation;
  Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 3),
    );
    _rotationAnimation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(_controller);
    _scaleAnimation = Tween<double>(begin: 1.0, end: 2.0).animate(_controller);
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        return Transform.rotate(
          angle: _rotationAnimation.value,
          child: Transform.scale(
            scale: _scaleAnimation.value,
            child: RaisedButton(
              child: Text("Combined Animations"),
              onPressed: () {
                if (_controller.status == AnimationStatus.completed) {
                  _controller.reset();
                  _controller.forward();
                }
              },
            ),
          ),
        );
      },
    );
  }
}

在这个示例中,我们创建了一个AnimationController,并使用它来控制两个动画:旋转动画和缩放动画。在build方法中,使用AnimatedBuilder根据动画值构建同时具有旋转和缩放效果的按钮。由于两个动画都使用同一个AnimationController,它们会保持同步。

8. 总结

通过本文的介绍,我们全面了解了Flutter中动画开发的各种方法和技巧。从动画的基础概念,如Animation 类、AnimationController、TickerProvider等,到具体的动画应用,如旋转动画、缩放动画、平移动画等,再到多种变换的组合和自定义Tween的创建,以及替代的动画应用方式AnimatedBuilder和AnimatedWidget,我们掌握了丰富的动画开发知识。

在实际开发中,我们可以根据具体的需求选择合适的动画实现方式。对于简单的动画,直接使用AnimationController和Tween组合可能就足够了;对于复杂的动画场景,使用AnimatedBuilder和AnimatedWidget可以使代码更加清晰和易于维护。同时,通过将多个动画与同一个AnimationController关联,可以实现动画的同步。

希望这些知识能够帮助你在Flutter开发中创建出更加生动和吸引人的用户界面。

下面是一个简单的动画开发流程总结:
1. 确定动画需求,如旋转、缩放、平移等。
2. 创建AnimationController对象,设置持续时间等属性。
3. 根据需求创建相应的Animation对象,如Tween、CurvedAnimation等。
4. 根据动画类型选择合适的实现方式:
- 简单动画:直接使用AnimationController和Tween组合。
- 分离动画逻辑和构建逻辑:使用AnimatedBuilder。
- 创建自定义动画小部件:继承AnimatedWidget。
5. 在build方法中根据动画值构建小部件。
6. 在dispose方法中释放AnimationController资源,防止内存泄漏。

mermaid流程图如下:

graph TD
    A[确定动画需求] --> B[创建AnimationController]
    B --> C[创建Animation对象]
    C --> D{选择实现方式}
    D -->|简单动画| E[AnimationController + Tween]
    D -->|分离逻辑| F[AnimatedBuilder]
    D -->|自定义小部件| G[AnimatedWidget]
    E --> H[构建小部件]
    F --> H
    G --> H
    H --> I[释放资源]

通过遵循这个流程,你可以更加高效地进行Flutter动画开发。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值