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


theme: cyanosis

前言:

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

94.gif


1. 图片的绘制

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

image.png

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

flutter: assets: - assets/images/


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

image.png

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

//读取 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

```dart 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 & 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

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

``` 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.rotate90degreesccw, color: Colors.blue), ), const SizedBox(width: 16), GestureDetector( onTap: onMove, child: const Icon(Icons.runcircle_outlined, color: Colors.blue), ) ], ), ); } } ```


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

```dart 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 createState() => _RunCarState(); }

class _RunCarState extends State { 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 ? 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 矩阵进行变换。而矩阵可以通过乘法进行变换的叠加,下面一个小例子说明一下:

```dart ---->[playground.dart#绘制方法]---- @override void paint(Canvas canvas, Size size) { Paint paint = Paint()..style = PaintingStyle.stroke; canvas.drawRect(Offset.zero & 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


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

image.png

```dart Matrix4 m4 = Matrix4.identity(); Matrix4 rotate90 = Matrix4.rotationZ(pi/2); m4.multiply(rotate90);

// 略同... ```

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

``` 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


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

// 略同... m4.multiply(rotate90); // 叠加旋转变换 m4.multiply(moveMatrix); // 叠加移动变换

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

image.png


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

// 略同... m4.multiply(rotate90); m4.multiply(moveMatrix); m4.multiply(rotate90); m4.multiply(rotate90); m4.multiply(rotate90); m4.multiply(moveMatrix);

image.png


4. 控制矩阵变换

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

image.png

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

93.gif

```dart class _RunCarState extends State with SingleTickerProviderStateMixin {

//... ValueNotifier _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


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

```dart 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)); } ```


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

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

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); }


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

本文正在参加「金石计划」

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值