28、Flutter之组件过度动画,物理动画,隐式动画,显式动画,交织动画

3,321 阅读6分钟

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

概述

HeroAnimatedWidget外,还有很多动画效果,如组件过度动画,物理动画,隐式动画,显式动画,交织动画等。但本质都是在一段时间内不断改变屏幕上显示的内容,从而产生视觉暂留现象。

动画一般可分为两类:

[补间动画]:补间动画是一种预先定义物体运动的起点和终点,物体的运动方式,运动时间,时间曲线,然后从起点过渡到终点的动画。

「基于物理的动画」:基于物理的动画是一种模拟现实世界运动的动画,通过建立运动模型来实现。例如一个篮球 从高处落下,需要根据其下落高度,重力加速度,地面反弹力等影响因素来建立运动模型。

隐式动画

隐式动画使用 Flutter 框架内置的动画部件创建,通过设置动画的起始值和最终值来触发。当使用 setState 方法改变部件的动画属性值时,框架会自动计算出一个从旧值过渡到新值的动画。

比如 AnimatedOpacity 部件,改变它的 opacity 值就可以触发动画。

111.gif

代码:

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

  @override
  State<OpacityChangePage> createState() => _OpacityChangePageState();
}

class _OpacityChangePageState extends State<OpacityChangePage> {
  double _opacity = 1.0;
  //改变目标值
  void _toggle(){
    _opacity = _opacity >0 ?0.0:1.0;
    setState(() {

    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("隐式动画"),
      body: Center(
        child: AnimatedOpacity(opacity: _opacity, duration: Duration(seconds: 2),
          child: Container(width: 200,height: 200,color: Colors.yellow,),
        )
      ),
      floatingActionButton: FloatingActionButton(onPressed: _toggle,child: Icon(Icons.play_arrow),),
    );
  }
}

显式动画

显式动画指的是需要手动设置动画的时间,运动曲线,取值范围的动画。将值传递给动画部件如: RotationTransition,最后使用一个AnimationController 控制动画的开始和结束。

111.gif 代码:

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

/// @Author wywinstonwy
/// @Date 2022/10/25 09:03
/// @Description: 

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

  @override
  State<RotationAnimationPage> createState() => _RotationAnimationPageState();
}

class _RotationAnimationPageState extends State<RotationAnimationPage> with SingleTickerProviderStateMixin{
  late AnimationController _controller;
  late Animation<double> _turns;
  bool _playing = false;
  //控制动画状态
  void _toggle(){
    if(_playing){
      _playing = false;
      _controller.stop();
    }else{
      _controller.forward()..whenComplete(() => _controller.reverse());
      _playing = true;
    }
    setState(() {
      
    });
  }
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //初始化动画控制器,设置动画时间
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 10));
    //设置动画取值范围和时间曲线
    _turns = Tween(begin: 0.0, end: pi * 2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeIn),
    );
  }
  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _controller.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("显示动画"),
      body: Center(
        child: RotationTransition(
          turns: _turns,
          child:  Container(
            width: 200,
            height: 200,
            child: Image.asset("images/室内_风扇02.png",fit: BoxFit.cover,),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: _toggle,
      child: Icon(_playing ? Icons.pause : Icons.play_arrow),
      ),
    );
  }
}

除了 RotationTransition 外,还有其他的显示动画部件如:FadeTransition, ScaleTransition, SizeTransition, SlideTransition 等。

交织动画

交织动画是由一系列的小动画组成的动画。每个小动画可以是连续或间断的,也可以相互重叠。其关键点在于使用 Interval 部件给每个小动画设置一个时间间隔,以及为每个动画的设置一个取值范围 Tween,最后使用一个 AnimationController 控制总体的动画状态。

Interval 继承至 Curve 类,通过设置属性 beginend 来确定这个小动画的运行范围。

class Interval extends Curve {
  /// 动画起始点
  final double begin;
  /// 动画结束点
  final double end;
  /// 动画缓动曲线

  final Curve curve;
  /// ...
}

111.gif

这是一个由 5 个小动画组成的交织动画,宽度,高度,颜色,圆角,边框,每个动画都有自己的动画区间。

图片.png 代码:

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

/// @Author wywinstonwy
/// @Date 2022/10/25 09:53
/// @Description: 

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

  @override
  State<StaggeredAnimationPage> createState() => _StaggeredAnimationPageState();
}

class _StaggeredAnimationPageState extends State<StaggeredAnimationPage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _width;
  late Animation<double> _height;
  late Animation<Color> _color;
  late Animation<double> _border;
  late Animation<BorderRadius> _borderRadius;

 void _play(){
   if(_controller.isCompleted){
     _controller.reverse();
   }else{
     _controller.forward();
   }
 }
 @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _controller = AnimationController(vsync: this,
    duration: Duration(seconds:5)
    );
    //宽度变化
    _width = Tween<double>(begin: 100,end: 300).animate(CurvedAnimation(
      parent: _controller,
      curve:Interval(0.0, 0.2,curve: Curves.ease),
    ));
    //高度变化
   _height = Tween<double>(begin: 100,end: 300).animate(
     CurvedAnimation(
       parent: _controller,
       curve: Interval(0.2,0.4,curve: Curves.ease)
     )
   );
   //颜色变化
    _color = Tween(begin: Colors.blue,end: Colors.yellow).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.4,0.6,curve: Curves.ease)
      )
    ) ;

    _borderRadius = Tween(
      begin: BorderRadius.circular(0.0),
      end: BorderRadius.circular(150.0),
    ).animate(CurvedAnimation(parent: _controller,
        curve: Interval(0.6,0.8,curve: Curves.ease)
    )) ;

    _border = Tween<double>(
      begin: 0,
      end: 25,
    ).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.8, 1.0),
      ),
    );




  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('交织动画')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            return Container(
              width: _width.value,
              height: _height.value,
              decoration: BoxDecoration(
                color: _color.value,
                borderRadius: _borderRadius.value,
                border: Border.all(
                  width: _border.value,
                  color: Colors.cyanAccent
                )
              ),
            );
          },

        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _play,
        child: Icon(Icons.refresh),
      ),
    );
  }
}

执行过程报错:Cannot lerp between "MaterialColor(primary value: Color(0xff2196f3))" and "MaterialColor(primary value: Color(0xffffeb3b))".

To lerp colors, consider ColorTween instead.

物理动画

物理动画是一种模拟现实世界物体运动的动画。需要建立物体的运动模型,以一个物体下落为例,这个运动受到物体的下落高度,重力加速度,地面的反作用力等因素的影响。

111.gif 代码:

import 'package:demo202112/utils/common_appbar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// @Author wywinstonwy
/// @Date 2022/10/25 11:16
/// @Description:

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

  @override
  State<ThrowAnimationPage> createState() => _ThrowAnimationPageState();
}

class _ThrowAnimationPageState extends State<ThrowAnimationPage> {
  // 球心高度
  double y = 70.0;
  // Y 轴速度
  double vy = -10.0;
  // 重力
  double gravity = 0.1;
  // 地面反弹力
  double bounce = -0.5;
  // 球的半径
  double radius = 50.0;
  // 地面高度
  final double height = 700;

  void _fall(_) {
    y += vy;
    vy += gravity;
    //如果球体触及地面,根据地面反弹力改变球体的 Y 轴速度
    if (y + radius > height) {
      y = height - radius;
      vy *= bounce;
    } else if (y - radius < 0) {
      y = 0 + radius;
      vy *= bounce;
    }
    setState(() {});
  }
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // 使用一个 Ticker 在每次更新界面时运行球体下落方法
    Ticker(_fall)..start();

  }
  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;

    return Scaffold(
      appBar: AppBar(title: Text('物理动画')),
      body: Column(
        children: <Widget>[
          Container(
            height: height,
            child: Stack(
              children: <Widget>[
                Positioned(
                  top: y - radius,
                  left: screenWidth / 2 - radius,
                  child: Container(
                    width: radius * 2,
                    height: radius * 2,
                    decoration: const BoxDecoration(
                      color: Colors.blue,
                      shape: BoxShape.circle,
                    ),
                  ),
                ),
              ],
            ),
          ),
          Expanded(child: Container(color: Colors.blue)),
        ],
      ),
    );
  }
}

组件过度动画

Widget属性发生变化时会执行过渡动画的组件统称为”动画过渡组件“,而动画过渡组件最明显的一个特征就是它会在内部自管理AnimationController。我们知道,为了方便使用者可以自定义动画的曲线、执行时长、方向等,在前面介绍过的动画封装方法中,通常都需要使用者自己提供一个AnimationController对象来自定义这些属性值。但是,如此一来,使用者就必须得手动管理AnimationController,这又会增加使用的复杂性。因此,如果也能将AnimationController进行封装,则会大大提高动画组件的易用性。

我们要实现一个AnimatedDecoratedBox,它可以在decoration属性发生变化时,从旧状态变成新状态的过程可以执行一个过渡动画。根据前面所学的知识,我们实现了一个AnimatedDecoratedBox1组件:

111.gif

/// @Author wywinstonwy
/// @Date 2022/10/16 10:15 上午
/// @Description: 
import "package:flutter/material.dart";
 
class AnimatedDecoratedBox1 extends StatefulWidget {
  final BoxDecoration decoration;
  final Widget child;
  final Duration duration;
  final Curve ?curve;
  final Duration? reverseDuration;
 
  const AnimatedDecoratedBox1({
    required this.decoration,
    required this.child,
    required this.duration,
     this.curve,
      this.reverseDuration});
 
 
  @override
  _AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
}
 
class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1> with SingleTickerProviderStateMixin{
  AnimationController get controller=>_controller;
  late AnimationController _controller;
  late Animation<double> _animation;
  late DecorationTween _tween;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      reverseDuration: widget.reverseDuration,
      vsync: this
    );
 
    _tween = DecorationTween(begin: widget.decoration);
    _updateCurve();
  }
  void _updateCurve(){
    _animation = CurvedAnimation(parent: _controller, curve: widget.curve!);
  }
  @override
  void didUpdateWidget(covariant AnimatedDecoratedBox1 oldWidget) {
    super.didUpdateWidget(oldWidget);
    if(widget.curve != oldWidget) _updateCurve();
    _controller.duration = widget.duration;
    _controller.reverseDuration = widget.reverseDuration;
    //正在执行过度动画
    if(widget.decoration !=(_tween.end??_tween.begin)){
      _tween
      ..begin =_tween.evaluate(_animation)
      ..end = widget.decoration;
 
      _controller
      ..value=0.0
      ..forward();
    }
 
 
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: (BuildContext context, Widget? child) {
        return DecoratedBox(
 
            decoration: _tween.animate(_animation).value);
      },
      animation: _animation,
      child:widget.child,
    );
  }
 
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
 
  }
 
}

下面我们来使用AnimatedDecoratedBox1来实现按钮点击后背景色从蓝色过渡到红色的效果:

class MyExcessiveAnimation extends StatefulWidget {
  const MyExcessiveAnimation({Key? key}) : super(key: key);
 
  @override
  _MyExcessiveAnimationState createState() => _MyExcessiveAnimationState();
}
 
class _MyExcessiveAnimationState extends State<MyExcessiveAnimation> {
  Color _decorationColor = Colors.blue;
  var duration = Duration(seconds: 1);
  // Curve curve = CurvedAnimation(parent: ) as Curve;
  // Tween doubleTween =  Tween<double>(begin: -200.0, end: 0.0);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar("组件过度动画"),
      body: Column(children: [
        ElevatedButton(onPressed: (){
          setState(() {
            _decorationColor = Colors.red;
          });
        }, child: const Text('测试')),
        Container(height: 44,
          width: 200,
          child: AnimatedDecoratedBox1(
          decoration: BoxDecoration(color: _decorationColor),
          child: ElevatedButton(onPressed: (){
            setState(() {
              _decorationColor = Colors.red;
            });
          },
              child: const Text('AnimatedDecoratedBox',
                style: TextStyle(color: Colors.black,fontSize: 14),
              )
          ),
          duration: duration,
          curve: Curves.linear,
        ),
        )
 
      ],)
      ,
    );
  }
}

总结

本文介绍了 Flutter 中多种类型的动画,分别是

  • 隐式动画
  • 显式动画
  • 交织动画
  • 基于物理的动画
  • 组件过度动画

Flutter 动画基于类型化的 Animation 对象,Widgets 通过读取动画对象的当前值和监听状态变化重新运行 build 函数,不断变化 UI 形成动画效果。 一个动画的主要因素有

  • Animation 动画对象
  • AnimationController 动画控制器
  • Tween 动画取值范围
  • Curve 动画运动曲线

更多的动画可以参考官方文档:api.flutter.dev/flutter/ani…

图片.png