这篇文章来通过一个有趣的案例,介绍一下 绘制中的动画变换 ,以及如何在当前的变换基础上,叠加变换。如下所示,小车在界面上呈现的任何变动,都是变换矩阵作用的效果:
注: gif 图片为 15fps ,有些卡顿,非实际动画运行效果
首先看一下如何在 Flutter 中绘制一张资源图片。如下所示,在 assets/images
中有一张小车的图片:
要使用资源,需要在 pubspec.yaml
中配置文件夹的逻辑:
flutter: assets: - assets/images/
在 Flutter 的 Canvas 绘制中,drawImage 方法可以绘制图片,其中的入参 Image
不是 material
包的图片组件,而是 dart:ui
中的 Image
图片数据:
可以通过 Flutter 框架中 decodeImageFromList
方法,通过字节数组获取 ui.Image
对象;其中字节数组可以通过文件读取、资源加载、网络下载等形式获取,比如这里获取本地资源中的字节数据可以使用 rootBundle.load
方法:
//读取 assets 中的图片 Future
下面 Playground
类继承自 CustomPainter
, 表示它是画板的实现类。画板只需要专注于绘制即可,像图片数据加载这种活,画板不应该操心。所以其中持有 ui.Image
对象,并在构造函数中进行初始化。在 paint
方法中使用图像进行绘制。
绘制的内容包括: 画板区域的边线示意矩形框; 小车图像及橙色边线示意框:
```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; } } ```
案例中的布局也很简单:左边是画板区域,右侧是三个控制按钮,分别用于 恢复原位
、顺时针旋转 90°
;动画移动
。
由于控制按钮的布局相对独立,它与界面其他元素的关系只有回调事件。以后可能会增加其他的按钮,或者修改样式,所以这里将其封装为一个 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(() {}); } } ```
下面来看一下,如何对一部分的绘制内容进行变换,对于移动、平移、缩放等简单的变换 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
的参数,会产生移动的变换效果:
下面再来看下旋转变换,默认情况下 Canvas 在进行变换时是以画布左上角为变换中心的。当叠加顺时针 90° 的旋转变换时,效果如下所示:
```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° 的效果:
最后,来看一下多个矩阵的叠加效果。大家可以先想想一想,如果在上面的旋转变换之后,再叠加 moveMatrix 沿 x 轴移动 100 ,会是什么效果?
// 略同... m4.multiply(rotate90); // 叠加旋转变换 m4.multiply(moveMatrix); // 叠加移动变换
答案是向下平移了 100
, 这时可能很多人比较疑惑, moveMatrix 不是沿 x 轴平移的吗,怎么会往下跑。其实矩阵的变换,是图形的相对坐标系统的变换,在当前的视角中,坐标系也被旋转了 90°,在当前变换之下,沿 X 轴移动是下方没有任何问题。
这样的话,名称对 m4
叠加一次 rotate90
变换,它就会以图片中心为原点旋转 90°,每次叠加一次 moveMatrix
就会以车头为正方向平移 100。
// 略同... m4.multiply(rotate90); m4.multiply(moveMatrix); m4.multiply(rotate90); m4.multiply(rotate90); m4.multiply(rotate90); m4.multiply(moveMatrix);
到这里,变换操作就介绍完了,我们只要在点击按钮时通过 multiply
叠加对应的矩阵,就可以完成转动和移动的效果。比如可以通过构造函数将 Matrix4
矩阵作为入参,有界面的交互来更新数据和重绘。如下所示,在画板构造时通过可监听对象来提供矩阵数据:
状态类中维护 _matrix
可监听对象,在点击按钮时,修改变换矩阵值即可。比如移动按钮每点击一次,叠加一个变换移动变换。这样就完成了一个简单版的图像旋转、平移的控制效果。
```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(); } } ```
上面是直接叠加矩阵,点一下动一下,接下来看一下如何为矩阵变换添加动画效果。也就是说在一段时间内会不断对矩阵数据进行更新,从起始矩阵到结束矩阵,在界面上就会呈现动画效果。需要获取动画的驱动力,最简单的方式是让状态类混入 SingleTickerProviderStateMixin
,让状态类拥有创建动画控制器的能力:
下面要让动画运动过程中,每帧叠加的矩阵进行动画过渡。矩阵的补间计算可以通过 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
旋转也是同理:这样就实现了一开始的效果:
final Matrix4 moveCenter = Matrix4.translationValues(50, 50, 0); final Matrix4 moveBack = Matrix4.translationValues(-50, -50, 0); void _onRotate() { Matrix4 start = _matrix.value.clone(); Animation
到这里,关于绘制中的矩阵变换就介绍的差不多了,也知道了如何对矩阵变换进行动画处理,希望可以对你有所帮助。那本文就到这里,谢谢观看 ~
本文正在参加「金石计划」