Flutter 动画

动画控制器

AnimationController
AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()、重置动画reset()等方法。
AnimationController会在动画的每一帧,就会生成一个新的值。
默认情况下,AnimationController在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字。属于Animation<double>类型。
scala 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {

  String _controllerValue = "0";
  late AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        duration: const Duration(milliseconds: 200),// 动画执行周期
        lowerBound: 0, // 动画的最小值
        upperBound: 1, // 设置动画的最大值
        vsync: this)
      ..addListener(() { // Animation添加帧监听器 改变状态后调用setState()来触发UI重建
        setState(() {
          _controllerValue = "${_controller.value}"; // 获取每一帧动画的值
          debugPrint("打印动画执行过程中控制器的值:$_controllerValue");
        });
      });
  }

  void _changeCurveValue() {
    // 重置动画
    _controller.reset();
    //启动动画(正向执行)
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Text(
          _controllerValue,
          style: Theme.of(context).textTheme.headlineMedium,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _changeCurveValue,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

执行结果

I/flutter 复制代码
I/flutter (32467): 打印动画执行过程中控制器的值:0.0
I/flutter (32467): 打印动画执行过程中控制器的值:1.3320699999999999
I/flutter (32467): 打印动画执行过程中控制器的值:2.1646099999999997
I/flutter (32467): 打印动画执行过程中控制器的值:2.6766499999999995
I/flutter (32467): 打印动画执行过程中控制器的值:3.1763999999999997
I/flutter (32467): 打印动画执行过程中控制器的值:3.67885
I/flutter (32467): 打印动画执行过程中控制器的值:4.0

停止动画

scss 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  String _controllerValue = "0";
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        duration: const Duration(milliseconds: 1400), // 动画执行周期 14000
        lowerBound: 1, // 动画的最小值
        upperBound: 10, // 设置动画的最大值
        vsync: this)
      ..addListener(() {
        // Animation添加帧监听器 改变状态后调用setState()来触发UI重建
        setState(() {
          _controllerValue = "${_controller.value}"; // 获取每一帧动画的值
          debugPrint("打印动画执行过程中控制器的值:$_controllerValue");
        });
      });
  }

  // 开始动画
  void _startAnimation() {
    // 重置动画
    _controller.reset();
    //启动动画(正向执行)
    _controller.forward();
  }

  // 停止动画
  void _stopAnimation() {
    //启动动画(正向执行)
    _controller.stop();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            GestureDetector(
              onTap: () {
                _startAnimation();
              },
              child: const Text('开始动画\n'),
            ),
            GestureDetector(
              onTap: () {
                _stopAnimation();
              },
              child: const Text('停止动画\n'),
            ),
          ],
        ),
      ),
    );
  }
}

监听动画执行状态 (动画通知) addStatusListener

动画执行状态
动态从开始到结束中间未间断 AnimationStatus.completed
动态执行过程被停止 AnimationStatus.dismissed
动画开始执行 AnimationStatus.forward
scss 复制代码
@override
void initState() {
  super.initState();
  _controller = AnimationController(
      duration: const Duration(milliseconds: 1400), // 动画执行周期 14000
      lowerBound: 1, // 动画的最小值
      upperBound: 10, // 设置动画的最大值
      vsync: this)
    ..addListener(() {
      // Animation添加帧监听器 改变状态后调用setState()来触发UI重建
      setState(() {
        _controllerValue = "${_controller.value}"; // 获取每一帧动画的值
        debugPrint("打印动画执行过程中控制器的值:$_controllerValue");
      });
    })
    ..addStatusListener((status) {
      if (status == AnimationStatus.forward) {
        debugPrint("打印动画执行过程中控制器的值 开始动画");
      } else if (status == AnimationStatus.completed) {
        debugPrint("打印动画执行过程中控制器的值 动画完成");
      } else if (status == AnimationStatus.dismissed) {
        debugPrint("打印动画执行过程中控制器的值 动画结束");
      }
      debugPrint("打印动画执行过程中控制器的 动画状态 $status ");
    });
}

按下手机物理键停止动画

scss 复制代码
// 停止动画
// 退出应用
void exitApp(){
  _controller.stop();
  _controller.dispose();
  // 用户尝试返回时执行的操作
  // 返回true允许返回,返回false阻止返回
  Navigator.of(context).pop();
  SystemNavigator.pop();
}

@override
Widget build(BuildContext context) {
  return PopScope(
    canPop: false,
      onPopInvoked: (didPop){
        debugPrint("打印动画执行过程中 点击手机物理返回键 $didPop");
        if(!didPop) {
          exitApp();
        }
      },
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
        ),
        body: Center(
          ......
        ),
      ));
}
bash 复制代码
I/flutter (19731): 打印动画执行过程中 点击手机物理返回键 false
I/flutter (19731): 打印动画执行过程中 点击手机物理返回键 true
I/flutter (19731): 打印动画执行过程中 执行 dispose函数

Curve

Flutter Curves 动画曲线合辑

动画过渡的速率
动画过程可以是匀速的、匀加速的或者先加速后减速等。
Flutter中通过Curve(曲线)来描述动画过程,我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的。
CurvedAnimationAnimationController都是Animation<double>类型
CurvedAnimation可以通过包装AnimationControllerCurve生成一个新的动画对象 ,我们正是通过这种方式来将动画和动画执行的曲线关联起来的。

Curves 类是一个预置的枚举类,定义了许多常用的曲线,下面列几种常用的: Curves.linear、Curves.decelerate、Curves.ease、Curves.easeIn、Curves.easeOut、Curves.easeInOut

scss 复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, this.title}) : super(key: key);

  final String? title;

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

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 900),
      vsync: this,
    )..addListener(() => setState(() {}));
    _animationTween(Curves.easeInOut);
  }

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

  void _animationTween(Cubic cubic){
    _animation = Tween(begin: 0.0, end: 1.0)
        .animate(CurvedAnimation(parent: _controller, curve: cubic));
  }

  void _startAnimation() {
    _controller.reset(); // 重置动画
    _animationTween(Curves.easeInToLinear);
    _controller.forward(); //开始动画
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Container(
            color: Colors.blue.withOpacity(0.2),
            child: CustomPaint(
              size: Size(MediaQuery.of(context).size.width,
                  MediaQuery.of(context).size.width),
              painter: CharLinePainter(_animation),
            ),
          ),
          GestureDetector(
            onTap: () {
              _startAnimation();
            },
            child: const Text('\n开始动画'),
          )
        ],
      ),
    );
  }
}
ini 复制代码
import 'dart:ui';

import 'package:flutter/material.dart';

class CharLinePainter extends CustomPainter {
  final Animation _animation;

  CharLinePainter(this._animation);

  static const double basePadding = 24; //基础边界

  double? startX, endX; //相对于原点x轴方向最小和最大偏移量(相对于原点的偏移量)
  double? startY, endY; //相对于原点y轴方向最大和最小偏移量(相对于原点的偏移量)
  double? _fixedWidth; //x轴方向:最大偏移量-最小偏移量(相对于原点的偏移量)
  double? _fixedHeight; //y轴方向:最大偏移量-最小偏移量(相对于原点的偏移量)
  final Path _path = Path();
  final List<Offset> _pointList = []; // 保存要绘制的点的集合

  @override
  void paint(Canvas canvas, Size size) {
    _pointList.clear();
    _initBorder(size);
    _drawXy(canvas);
    _drawXYRulerText(canvas);
    _drawLine(canvas);
    _drawPoint(canvas);
  }

  /// 初始化边界
  void _initBorder(Size size) {
    startX = basePadding * 2;
    endX = size.width - basePadding * 2;
    startY = size.height - basePadding * 2;
    endY = basePadding * 2;
    _fixedWidth = endX! - startX!;
    _fixedHeight = (startY! - endY!);
  }

  ///绘制xy轴
  ///绘制x轴-y轴偏移量不变(y轴坐标点不变)
  ///绘制y轴-x轴偏移量不变(x轴坐标点不变)
  void _drawXy(Canvas canvas) {
    var paint = Paint()
      ..isAntiAlias = true
      ..strokeWidth = 1.0
      ..strokeCap = StrokeCap.square
      ..color = Colors.white
      ..style = PaintingStyle.stroke;
    canvas.drawLine(
        Offset(startX!, startY!), Offset(endX!, startY!), paint); //x轴
    canvas.drawLine(
        Offset(startX!, startY!), Offset(startX!, endY!), paint); //y轴
  }

  ///绘制xy轴刻度+文本
  void _drawXYRulerText(Canvas canvas) {
    ///x、y轴方向每个刻度的间距
    double xRulerW = _fixedWidth! / 4; //x方向两个点之间的距离(刻度长)
    double yRulerH = _fixedHeight! / 12; //y轴方向亮点之间的距离(刻度高)
    for (int i = 1; i <= 4; i++) {
      _initCurvePath(i - 1, xRulerW, yRulerH, canvas);
    }
  }

  ///计算曲线path
  void _initCurvePath(int i, double xRulerW, double yRulerH, Canvas canvas) {
    if (i == 0) {
      var key = startX!;
      var value = startY;
      _path.moveTo(key, value!);
    } else {
      double preX = startX! + xRulerW * i;
      double preY = (startY! - (i % 2 != 0 ? yRulerH : yRulerH * 6));
      double currentX = startX! + xRulerW * (i + 1);
      double currentY = (startY! - (i % 2 == 0 ? yRulerH : yRulerH * 6));

      _path.cubicTo((preX + currentX) / 2, preY, (preX + currentX) / 2,
          currentY, currentX, currentY);

      // 保存要绘制的点点坐标
      _pointList.add(Offset(currentX, currentY));
    }
  }

  ///绘制直线或曲线
  void _drawLine(Canvas canvas) {
    var paint = Paint()
      ..isAntiAlias = true
      ..strokeWidth = 2.0
      ..strokeCap = StrokeCap.round
      ..color = Colors.red
      ..style = PaintingStyle.stroke;

    var pathMetrics = _path.computeMetrics(forceClosed: false);
    var list = pathMetrics.toList();
    var length = (list.length).toInt();
    Path linePath = Path();
    for (int i = 0; i < length; i++) {

      // 通过动画绘制曲线
      var extractPath = list[i].extractPath(
          0, (list[i].length * _animation.value),
          startWithMoveTo: true);
      linePath.addPath(extractPath, const Offset(0, 0));
    }
    canvas.drawPath(linePath, paint);
  }

  // 绘制点
  void _drawPoint(Canvas canvas) {
    // 绘制点
    var paintPoint = Paint()
      ..color = Colors.greenAccent
      ..strokeCap = StrokeCap.square
      ..strokeWidth = 5; // 设置点的大小
    canvas.drawPoints(
        PointMode.points, _pointList, paintPoint..color = Colors.green);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

运行效果

线性插值lerp函数

动画的原理其实就是每一帧绘制不同的内容,一般都是指定起始和结束状态,然后在一段时间内从起始状态逐渐变为结束状态,而具体某一帧的状态值会根据动画的进度来算出。

lerp 的计算一般遵循: 返回值 = a + (b - a) * t

lerp 是线性 插值,意思是返回值和动画进度t是成一次函数(y = kx + b)关系,因为一次函数的图像是一条直线,所以叫线性插值。

scala 复制代码
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

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

    _animation = Tween<double>(begin: 0, end: 1).animate(_controller);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: AnimatedBuilder(
          animation: _animation,
          builder: (context, child) {
            final color = Color.lerp(Colors.red, Colors.blue, _animation.value);
            return Container(
              width: 100 + 200 * _animation.value,
              height: 100 + 200 * _animation.value,
              color: color,
            );
          },
        ),
      ),
    );
  }
}

实现案例

flutter_animation

相关推荐
SuperHeroWu72 小时前
【HarmonyOS】HarmonyOS 和 Flutter混合开发 (一)之鸿蒙Flutter环境安装
flutter·华为·jdk·harmonyos·鸿蒙·环境安装·混合开发
恋猫de小郭3 小时前
Android 16 Baklava 来了,来看看开发者预览版给我们带来了什么
android·前端·flutter
B.-18 小时前
减少 Flutter 应用体积的常用方法
学习·flutter·android studio·xcode
肥肥呀呀呀20 小时前
mac电脑可以使用的模拟器
flutter·macos
shankss1 天前
网页跳转App,Universal Links(iOS)和 App Links(Android) 如何设置
android·flutter·ios
叫我菜菜就好1 天前
【Flutter_Web】Flutter编译Web第二篇(webview篇):flutter_inappwebview如何改造方法,变成web之后数据如何交互
前端·flutter·交互·inappwebview
zacksleo1 天前
鸿蒙 Flutter 实战:现有 Flutter 项目支持鸿蒙 II
flutter
一个处女座的程序猿O(∩_∩)O1 天前
四大跨平台开发框架深度解析——uniapp、uniapp-X、React Native与Flutter
flutter·react native·uni-app
美了美了1 天前
android、flutter离线推送插件,支持oppo、vivo、小米、华为
android·flutter·离线推送·华为推送·oppo推送·小米推送·vivo推送
sunly_1 天前
Flutter:CustomScrollView自定义滚动使用
android·java·flutter