26、Flutter之AnimatedWidget相关动画

556 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情

概述

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

为了方便开发者创建动画,不同的UI系统对动画都进行了一些抽象,比如在Android中可以通过XML来描述一个动画然后设置给View。Flutter中也对动画进行了抽象,主要涉及Tween、Animation、Curve、Controller这些角色。

基本的动画概念和类

Animation<double>
CurvedAnimation
AnimationController
Tween
    Tween.animate
动画通知

动画示例

渲染动画
用AnimatedWidget简化
监视动画的过程
用AnimatedBuilder重构
并行动画

动画的重点:

  • Animation对象是Flutter动画库中的一个核心类,它生成指导动画的值。
  • Animation对象知道动画的当前状态(例如,它是开始、停止还是向前或向后移动),但它不知道屏幕上显示的内容。
  • AnimationController管理Animation。
  • CurvedAnimation 将过程抽象为一个非线性曲线.
  • Tween在正在执行动画的对象所使用的数据范围之间生成值。例如,Tween可能会生成从红到蓝之间的色值,或者从0到255。
  • 使用Listeners和StatusListeners监听动画状态改变。

Flutter中的动画系统基于Animation对象的,widget可以在build函数中读取Animation对象的当前值, 并且可以监听动画的状态改变。

Animation

在Flutter中,Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常用的Animation类是Animation。

Flutter中的Animation对象是一个在一段时间内依次生成一个区间之间值的类。Animation对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。 根据Animation对象的控制方式,动画可以反向运行,甚至可以在中间切换方向。

Animation还可以生成除double之外的其他类型值,如:Animation 或 Animation。

Animation对象有状态。可以通过访问其value属性获取动画的当前值。

Animation对象本身和UI渲染没有任何关系。

CurvedAnimation

CurvedAnimation 将动画过程定义为一个非线性曲线.

    final CurvedAnimation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeIn);

基本用法:

class MyCurvedAnimation extends StatefulWidget {
  const MyCurvedAnimation({Key? key}) : super(key: key);
 
  @override
  _MyCurvedAnimationState createState() => _MyCurvedAnimationState();
}
 
class _MyCurvedAnimationState extends State<MyCurvedAnimation> with SingleTickerProviderStateMixin{
  late CurvedAnimation curve;
  late AnimationController controller;
 
  @override
  initState() {
    super.initState();
    controller = AnimationController(duration: Duration(milliseconds: 5000), vsync: this);
    // 创建一个 CurvedAnimation,监听它的 value
    curve = CurvedAnimation(parent: controller, curve: Curves.easeIn)
    ..addListener(() {
      setState(() {
            if(controller.isCompleted){
              // controller.reset();
              controller.reverse();
            }
            else if(controller.isDismissed){
              controller.forward();
            }
      });
    });
    controller.forward();
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar('CurvedAnimation'),
      body: Center(
        child: Container(
          margin: EdgeInsets.symmetric(vertical: 10.0),
          // 使用 CurvedAnimation 对象的 value
          height: 300 * curve.value,
          width: 300 * curve.value,
          child: FlutterLogo(),
        ),
      ),
    );
  }
} 

动画效果:

111.gif

CurvedAnimation和AnimationController都是Animation《double》类型。CurvedAnimation包装它正在修改的对象 - 您不需要子类AnimationController来实现曲线。

AnimationController

AnimationController是一个特殊的Animation对象,在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。 例如,下面代码创建一个Animation对象,但不会启动它运行:

    final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);

AnimationController派生自Animation<double>,因此可以在需要Animation对象的任何地方使用。 但是,AnimationController具有控制动画的其他方法。例如,.forward()方法可以启动动画。数字的产生与屏幕刷新有关,因此每秒钟通常会产生60个数字,在生成每个数字后,每个Animation对象调用添加的Listener对象。

当创建一个AnimationController时,需要传递一个vsync参数,存在vsync时会防止屏幕外动画(译者语:动画的UI不在当前屏幕时)消耗不必要的资源。 通过将SingleTickerProviderStateMixin添加到类定义中,可以将stateful对象作为vsync的值。你可以在GitHub的animate1中看到这个例子。

译者语:vsync对象会绑定动画的定时器到一个可视的widget,所以当widget不显示时,动画定时器将会暂停,当widget再次显示时,动画定时器重新恢复执行,这样就可以避免动画相关UI不在当前屏幕时消耗资源。 如果要使用自定义的State对象作为vsync时,请包含TickerProviderStateMixin。

注意: 在某些情况下,值(position,值动画的当前值)可能会超出AnimationController的0.0-1.0的范围。例如,fling()函数允许您提供速度(velocity)、力量(force)、position(通过Force对象)。位置(position)可以是任何东西,因此可以在0.0到1.0范围之外。 CurvedAnimation生成的值也可以超出0.0到1.0的范围。根据选择的曲线,CurvedAnimation的输出可以具有比输入更大的范围。例如,Curves.elasticIn等弹性曲线会生成大于或小于默认范围的值。

Tween

默认情况下,AnimationController对象的范围从0.0到1.0。如果您需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。例如,以下示例,Tween生成从-200.0到0.0的值:

final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);

Tween是一个无状态(stateless)对象,需要beginend值。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0.0到1.0,但这不是必须的。

Tween继承自Animatable<T>,而不是继承自Animation<T>。Animatable与Animation相似,不是必须输出double值。例如,ColorTween指定两种颜色之间的过渡。

final Tween colorTween =
    new ColorTween(begin: Colors.transparent, end: Colors.black54);   

Tween对象不存储任何状态。相反,它提供了evaluate(Animation<double> animation)方法将映射函数应用于动画当前值。 Animation对象的当前值可以通过value()方法取到。evaluate函数还执行一些其它处理,例如分别确保在动画值为0.0和1.0时返回开始和结束状态。

Tween.animate

要使用Tween对象,请调用其animate()方法,传入一个控制器对象。例如,以下代码在500毫秒内生成从0到255的整数值。

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);    

注意animate()返回的是一个Animation,而不是一个Animatable。

以下示例构建了一个控制器、一条曲线和一个Tween:

class MyTween extends StatefulWidget {
  const MyTween({Key? key}) : super(key: key);
 
  @override
  _MyTweenState createState() => _MyTweenState();
}
 
class _MyTweenState extends State<MyTween> with TickerProviderStateMixin{
  late Animation<double> animation;
  late AnimationController controller;
 
  late Animation<Color?> animationColor;
  late AnimationController controllerColor;
 
  @override
  initState() {
    super.initState();
    controller = AnimationController(duration: Duration(milliseconds: 2000),vsync: this);
    animation = (Tween(begin: 0.0,end: 300.0).animate(controller)
    ..addListener(() {
      setState(() {
        if(controller.isCompleted){
          // controller.reset();
          controller.reverse();
        }
        else if(controller.isDismissed){
          controller.forward();
        }
      });
    }));
    controller.forward();
   //颜色渐变动画
    controllerColor = AnimationController(duration: Duration(milliseconds: 2000),vsync: this);
    animationColor = ColorTween(begin: Colors.transparent,end: Colors.black87).animate(controllerColor)
    ..addListener(() {
      setState(() {
        if(controllerColor.isCompleted){
          // controller.reset();
          controllerColor.reverse();
        }
        else if(controllerColor.isDismissed){
          controllerColor.forward();
        }
      });
    });
 
    controllerColor.forward();
 
 
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar('Teen'),
      body: Column(children: [
        Center(
          child: Container(
            margin: EdgeInsets.symmetric(vertical: 10.0),
            // 使用 CurvedAnimation 对象的 value
            height:  animation.value,
            width: animation.value,
            child: FlutterLogo(),
          ),
        ),
        Center(
          child: Container(
            height: 100,
            width: 100,
            margin: EdgeInsets.symmetric(vertical: 10.0),
            // 使用 CurvedAnimation 对象的 value
           color: animationColor.value!,
            child: FlutterLogo(),
          ),
        )
      ],),
    );
  }
}    

运行效果:

111.gif

大小渐变和颜色代表动画效果,此动画也是并行动画效果

ReverseTween

反转动画

Animation reverseAnim = ReverseTween(IntTween(begin: 0,end: 200)).animate(controller)
      ..addListener(() {
        print("reverse====${reverseAnim.value}");
        setState(() {
          // the state that has changed here is the animation object’s value
        });
      })..addStatusListener((status){
 
      });    

SizeTween

size类型的动画,如

Animation<Size> sizeAnim = SizeTween(begin: Size(100,100),end: Size(200,200)).animate(controller)
      ..addListener(() {
        print("reverse====${sizeAnim.value}");
        setState(() {
          // the state that has changed here is the animation object’s value
        });
      })..addStatusListener((status){
 
      });    
SizedBox.fromSize(
              child: Container(
                color: Colors.red,
              ),
              size: sizeAnim.value,
            )    

RectTween

Rect 类型动画

rectAnim = RectTween(begin: Rect.fromLTRB(100,100,100,100),end: Rect.fromLTRB(100,100,100,100)).animate(controller)
      ..addListener(() {
        print("reverse====${rectAnim.value}");
        setState(() {
          // the state that has changed here is the animation object’s value
        });
      })..addStatusListener((status){
 
      });    

StepTween

步数动画,比如做个计时器

import 'package:demo202112/utils/common_appbar.dart';
import 'package:flutter/material.dart';

/// @Author wywinstonwy
/// @Date 2022/10/23 11:28
/// @Description: 

class StepTweenPage extends StatefulWidget {
  const StepTweenPage({Key? key}) : super(key: key);

  @override
  State<StepTweenPage> createState() => _StepTweenPageState();
}

class _StepTweenPageState extends State<StepTweenPage>with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<int> stepAnim;
  int _stepNum = 10;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = AnimationController(vsync: this,duration: Duration(seconds: _stepNum));
    controller.addListener(() {
      print("controller=====${controller.value}");
    });
    controller.addStatusListener((status) {
      print("status====$status");
    });
    
    stepAnim = StepTween(begin: _stepNum,end: 0).animate(controller)
    ..addListener(() {
      print("stepAnim====${stepAnim.value}");
      setState(() {
        this._stepNum = stepAnim.value;
      });
    })
    ..addStatusListener((status) { 
      
    });
    controller.forward();
  }
  @override
  void dispose() {
    // TODO: implement dispose
    controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("StemTween动画"),
      body: Column(children: [
        Text(stepAnim.value.toString(),style: TextStyle(fontSize: 150),)
      ],),
    );
  }
}

效果动画:

111.gif

用AnimatedWidget简化

重点是什么?

  • 如何使用AnimatedWidget助手类(而不是addListener()和setState())来给widget添加动画

  • 使用AnimatedWidget创建一个可重用动画的widget。要从widget中分离出动画过渡,请使用AnimatedBuilder。

  • Flutter API提供的关于AnimatedWidget的示例包括:AnimatedBuilder、AnimatedModalBarrier、DecoratedBoxTransition、FadeTransition、PositionedTransition、RelativePositionedTransition、RotationTransition、ScaleTransition、SizeTransition、SlideTransition。

AnimatedWidget类允许您从setState()调用中的动画代码中分离出widget代码。AnimatedWidget不需要维护一个State对象来保存动画。

在下面的重构示例中,LogoApp现在继承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在绘制时使用动画的当前值。LogoApp仍然管理着AnimationController和Tween。

 
import 'package:demo202112/utils/common_appbar.dart';
import 'package:flutter/material.dart';
 
class MyAnimationWidget extends StatefulWidget {
  const MyAnimationWidget({Key? key}) : super(key: key);
 
  @override
  _MyAnimationWidgetState createState() => _MyAnimationWidgetState();
}
 
class _MyAnimationWidgetState extends State<MyAnimationWidget> with SingleTickerProviderStateMixin{
  late AnimationController controller;
  late Animation<double> animation;
 
  initState() {
    super.initState();
    controller =  AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation =  Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }
  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar('AnimationWidget'),
      body: Container(
        height: MediaQuery.of(context).size.height,
        width: MediaQuery.of(context).size.width,
        color: Colors.yellow,
        child: AnimationLogo(animation: animation,),),
    );
  }
}
 
class AnimationLogo extends AnimatedWidget{
  AnimationLogo({Key?key,required Animation<double> animation}) : super(key: key,listenable: animation);
 
  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
 
 
  }
 
}    

运行效果:

111.gif

LogoApp将Animation对象传递给基类并用animation.value设置容器的高度和宽度,因此它的工作原理与之前完全相同。

总结:

Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它用于保存动画的插值和状态。

AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。

Tween构造函数需要beginend两个参数。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0.0到1.0,但这不是必须的,我们可以自定义需要的范围。

Curve动画过程可以是匀速的、加速的或者先加速后减速等。Flutter中通过Curve(曲线)来描述动画过程,Curve可以是线性的(Curves.linear),也可以是非线性的。

还有一些其他的动画效果后续会介绍到。