前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 绘制探索 | 绘制中的动画变换

Flutter 绘制探索 | 绘制中的动画变换

作者头像
张风捷特烈
发布2023-04-23 16:12:45
9540
发布2023-04-23 16:12:45
举报
theme: cyanosis
前言:

这篇文章来通过一个有趣的案例,介绍一下 绘制中的动画变换 ,以及如何在当前的变换基础上,叠加变换。如下所示,小车在界面上呈现的任何变动,都是变换矩阵作用的效果: 注: gif 图片为 15fps ,有些卡顿,非实际动画运行效果

94.gif
94.gif

1. 图片的绘制

首先看一下如何在 Flutter 中绘制一张资源图片。如下所示,在 assets/images 中有一张小车的图片:

image.png
image.png

要使用资源,需要在 pubspec.yaml 中配置文件夹的逻辑:

代码语言:javascript
复制
flutter:
  assets:
    - assets/images/

在 Flutter 的 Canvas 绘制中,drawImage 方法可以绘制图片,其中的入参 Image 不是 material包的图片组件,而是 dart:ui 中的 Image 图片数据:

image.png
image.png

可以通过 Flutter 框架中 decodeImageFromList 方法,通过字节数组获取 ui.Image 对象;其中字节数组可以通过文件读取、资源加载、网络下载等形式获取,比如这里获取本地资源中的字节数据可以使用 rootBundle.load 方法:

代码语言:javascript
复制
//读取 assets 中的图片
Future<ui.Image>? loadImageFromAssets(String path) async {
  ByteData data = await rootBundle.load(path);
  return decodeImageFromList(data.buffer.asUint8List());
}

下面 Playground 类继承自 CustomPainter, 表示它是画板的实现类。画板只需要专注于绘制即可,像图片数据加载这种活,画板不应该操心。所以其中持有 ui.Image 对象,并在构造函数中进行初始化。在 paint 方法中使用图像进行绘制。

绘制的内容包括: 画板区域的边线示意矩形框; 小车图像及橙色边线示意框:

image.png
image.png
代码语言:javascript
复制
class Playground extends CustomPainter {
  final ui.Image? image;
  
  Playground(this.image);

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()..style = PaintingStyle.stroke;
    canvas.drawRect(Offset.zero &amp; size, paint);

    if (image != null) {
      drawCarWithRange(canvas, paint);
    }
  }

  void drawCarWithRange(Canvas canvas, Paint paint) {
    Rect zone = Rect.fromLTRB(0, 0, image!.width.toDouble(), image!.width.toDouble());
    paint.color = Colors.orange;
    canvas.drawRect(zone, paint);

    // 绘制图片
    canvas.drawImage(image!, Offset.zero, paint);
  }

  @override
  bool shouldRepaint(covariant Playground oldDelegate) {
    return oldDelegate.image!=image;
  }
}

2.界面中的组件布局

案例中的布局也很简单:左边是画板区域,右侧是三个控制按钮,分别用于 恢复原位顺时针旋转 90°动画移动

image.png
image.png

由于控制按钮的布局相对独立,它与界面其他元素的关系只有回调事件。以后可能会增加其他的按钮,或者修改样式,所以这里将其封装为一个 ControlTools 组件来独立维护,并暴露三个回调给外界来监听事件的触发:

代码语言:javascript
复制
import 'package:flutter/material.dart';

class ControlTools extends StatelessWidget {
  final VoidCallback onReset;
  final VoidCallback onRotate;
  final VoidCallback onMove;

  const ControlTools({
    Key? key,
    required this.onReset,
    required this.onRotate,
    required this.onMove,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24),
      child: Row(
        children: [
          GestureDetector(
            onTap: onReset,
            child: const Icon(Icons.refresh, color: Colors.blue,),
          ),
          const SizedBox(width: 16),
          GestureDetector(
            onTap: onRotate,
            child: const Icon(Icons.rotate_90_degrees_ccw, color: Colors.blue),
          ),
          const SizedBox(width: 16),
          GestureDetector(
            onTap: onMove,
            child: const Icon(Icons.run_circle_outlined, color: Colors.blue),
          )
        ],
      ),
    );
  }
}

这样也能在一定程度上,缓解主布局界面中的代码混乱程度。下面的 RunCar 组件是当前的主界面,在其状态类的 initState 回调中加载图片资源,为 ui.Image 数据赋值和触发更新。Playground 换班可以通过 CustomPaint 组件呈现在界面上,左右通过 Row 组件进行横向布局:

代码语言:javascript
复制
import 'dart:math';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';

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

  @override
  State<RunCar> createState() => _RunCarState();
}

class _RunCarState extends State<RunCar> {
  ui.Image? _image;

  @override
  void initState() {
    super.initState();
    _loadImage();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            CustomPaint(
              size: const Size(400, 400),
              painter: Playground(_image),
            ),
            ControlTools(
              onReset: _onReset,
              onMove: _onMove,
              onRotate: _onRotate,
            ),
          ],
        ),
      ),
    );
  }

  //读取 assets 中的图片
  Future<ui.Image>? loadImageFromAssets(String path) async {
    ByteData data = await rootBundle.load(path);
    return decodeImageFromList(data.buffer.asUint8List());
  }

  void _loadImage() async {
    _image = await loadImageFromAssets('assets/images/car.png');
    setState(() {});
  }
}

3.如何对绘制区域进行变换操作

下面来看一下,如何对一部分的绘制内容进行变换,对于移动、平移、缩放等简单的变换 Canvas 中提供了相关的方法。但我们现在要做的,需要基于多个变换进行叠加,比如 移动、旋转、移动、移动,如果每个动作都通过 Canvas 的相关方法进行变换处理,需要很多无谓的计算,也会把过程搞得非常复杂。 Canvas 中有一个 transform 方法,可以通过 Matrix4 矩阵进行变换。而矩阵可以通过乘法进行变换的叠加,下面一个小例子说明一下:

代码语言:javascript
复制
---->[playground.dart#绘制方法]----
@override
void paint(Canvas canvas, Size size) {
  Paint paint = Paint()..style = PaintingStyle.stroke;
  canvas.drawRect(Offset.zero &amp; size, paint);
  if (image != null) {
    // 操作矩阵
    Matrix4 m4 = Matrix4.identity();
    Matrix4 moveMatrix = Matrix4.translationValues(100, 0, 0);
    m4.multiply(moveMatrix);
    
    canvas.save();
    canvas.transform(m4.storage);
    drawCarWithRange(canvas, paint);
    canvas.restore();
  }
}

案例中 m4 矩阵是在绘制图片时施加的变换,moveMatrix 表示移动变换的矩阵。m4.multiply(moveMatrix) 矩阵表示在 m4 上叠加 moveMatrix 变换,本质上是两个 4X4 矩阵的乘法。 触发 multiply 方法后会, m4 矩阵的值会被改变。使用它的数据作为 canvas.transform 的参数,会产生移动的变换效果:

image.png
image.png

下面再来看下旋转变换,默认情况下 Canvas 在进行变换时是以画布左上角为变换中心的。当叠加顺时针 90° 的旋转变换时,效果如下所示:

image.png
image.png
代码语言:javascript
复制
Matrix4 m4 = Matrix4.identity();
Matrix4 rotate90 = Matrix4.rotationZ(pi/2);
m4.multiply(rotate90);

// 略同...

其实对于旋转而言,很多时候我们期望旋转中心是在被变换者的中心,这就要对变换中心进行处理。关于这方面,之前出过一个视频,感兴趣的可以看一下 : 《Flutter 绘制实践 | 路径篇 · 变换中心》 。这里就不卖关子了,平移变换可以影响变换中心, 为了抵消平移变换带来的后果,在旋转之后,反向平移即可。矩阵的 multiplied 方法本质上使用的是 multiply,只不过 multiplied 会生成新的矩阵,不会改变调用者的数据。 代码如下:

代码语言:javascript
复制
Matrix4 m4 = Matrix4.identity();
Matrix4 moveCenter = Matrix4.translationValues(50, 50, 0);
Matrix4 moveBack = Matrix4.translationValues(-50, -50, 0);

Matrix4 rotate90 = Matrix4.rotationZ(pi/2);
rotate90 = moveCenter.multiplied(rotate90).multiplied(moveBack);
m4.multiply(rotate90); 

这样就可以达到以中心为旋转中心,旋转 90° 的效果:

image.png
image.png

最后,来看一下多个矩阵的叠加效果。大家可以先想想一想,如果在上面的旋转变换之后,再叠加 moveMatrix 沿 x 轴移动 100 ,会是什么效果?

代码语言:javascript
复制
// 略同...
m4.multiply(rotate90);   // 叠加旋转变换
m4.multiply(moveMatrix); // 叠加移动变换

答案是向下平移了 100 , 这时可能很多人比较疑惑, moveMatrix 不是沿 x 轴平移的吗,怎么会往下跑。其实矩阵的变换,是图形的相对坐标系统的变换,在当前的视角中,坐标系也被旋转了 90°,在当前变换之下,沿 X 轴移动是下方没有任何问题。

image.png
image.png

这样的话,名称对 m4 叠加一次 rotate90 变换,它就会以图片中心为原点旋转 90°,每次叠加一次 moveMatrix 就会以车头为正方向平移 100。

代码语言:javascript
复制
// 略同...
m4.multiply(rotate90);
m4.multiply(moveMatrix);
m4.multiply(rotate90);
m4.multiply(rotate90);
m4.multiply(rotate90);
m4.multiply(moveMatrix);
image.png
image.png

4. 控制矩阵变换

到这里,变换操作就介绍完了,我们只要在点击按钮时通过 multiply 叠加对应的矩阵,就可以完成转动和移动的效果。比如可以通过构造函数将 Matrix4 矩阵作为入参,有界面的交互来更新数据和重绘。如下所示,在画板构造时通过可监听对象来提供矩阵数据:

image.png
image.png

状态类中维护 _matrix 可监听对象,在点击按钮时,修改变换矩阵值即可。比如移动按钮每点击一次,叠加一个变换移动变换。这样就完成了一个简单版的图像旋转、平移的控制效果。

93.gif
93.gif
代码语言:javascript
复制
class _RunCarState extends State<RunCar> with SingleTickerProviderStateMixin {

  //...
  ValueNotifier<Matrix4> _matrix = ValueNotifier(Matrix4.identity());
  late Matrix4 rotate90;
  late Matrix4 moveMatrix;

  @override
  void initState() {
    super.initState();
    //...
    _initMatrix();
  }

  void _initMatrix() {
    // 初始化变换矩阵
    Matrix4 moveCenter = Matrix4.translationValues(50, 50, 0);
    Matrix4 moveBack = Matrix4.translationValues(-50, -50, 0);
    rotate90 = Matrix4.rotationZ(pi/2);
    rotate90 = moveCenter.multiplied(rotate90).multiplied(moveBack);
    moveMatrix = Matrix4.translationValues(100, 0, 0);
  }

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

  //...
  void _onRotate() {
    _matrix.value = _matrix.value.multiplied(rotate90);
  }

  void _onMove() {
    _matrix.value = _matrix.value.multiplied(moveMatrix);
  }

  void _onReset() {
    _matrix.value = Matrix4.identity();
  }
}

5. 矩阵补间动画

上面是直接叠加矩阵,点一下动一下,接下来看一下如何为矩阵变换添加动画效果。也就是说在一段时间内会不断对矩阵数据进行更新,从起始矩阵到结束矩阵,在界面上就会呈现动画效果。需要获取动画的驱动力,最简单的方式是让状态类混入 SingleTickerProviderStateMixin,让状态类拥有创建动画控制器的能力:

1680656713117.png
1680656713117.png

下面要让动画运动过程中,每帧叠加的矩阵进行动画过渡。矩阵的补间计算可以通过 Matrix4Tween 指定起止矩阵进行计算,下面定义了两个 Matrix4Tween 分别用于处理移动和旋转矩阵的补间:

代码语言:javascript
复制
late Matrix4Tween moveTween;
late Matrix4Tween rotateTween;

void _initTween() {
  rotateTween = Matrix4Tween(begin: Matrix4.rotationZ(0), end: Matrix4.rotationZ(pi/2));
  moveTween = Matrix4Tween(begin: Matrix4.translationValues(0, 0, 0), end: Matrix4.translationValues(100, 0, 0));
}

在移动方法中,监听动画帧的变化,叠加对应的矩阵值即可,如下所示:

代码语言:javascript
复制
void _onMove() {
  Matrix4 start = _matrix.value.clone();
  Animation<Matrix4> m4Anima = moveTween.animate(_controller);
  m4Anima.addListener(() => _matrix.value = start.multiplied(m4Anima.value));
  _controller.forward(from: 0);
}

旋转也是同理:这样就实现了一开始的效果:

94.gif
94.gif
代码语言:javascript
复制
final Matrix4 moveCenter = Matrix4.translationValues(50, 50, 0);
final Matrix4 moveBack = Matrix4.translationValues(-50, -50, 0);
void _onRotate() {
  Matrix4 start  = _matrix.value.clone();
  Animation<Matrix4> m4Tween = rotateTween.animate(_controller);
  m4Tween.addListener(() {
    Matrix4 rotate = moveCenter.multiplied(m4Tween.value).multiplied(moveBack);
    _matrix.value = start.multiplied(rotate);
  });
  _controller.forward(from: 0);
}

到这里,关于绘制中的矩阵变换就介绍的差不多了,也知道了如何对矩阵变换进行动画处理,希望可以对你有所帮助。那本文就到这里,谢谢观看 ~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 1. 图片的绘制
  • 2.界面中的组件布局
  • 3.如何对绘制区域进行变换操作
  • 4. 控制矩阵变换
  • 5. 矩阵补间动画
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档