Flutter学习:使用CustomPaint绘制图片

我请讲解下,Flutter学习:使用CustomPaint绘制图片
最新回答
冷巷

2024-09-05 16:44:22

Flutter学习:认识CustomPaint组件和Paint对象

Flutter学习:使用CustomPaint绘制路径

Flutter学习:使用CustomPaint绘制图形

Flutter学习:使用CustomPaint绘制文字

Flutter学习:使用CustomPaint绘制图片

和CustomPaint绘制图形相比,绘制图片要麻烦一点。

绘制图片的方法有6个:

canvas.drawImage

canvas.drawImageNine

canvas.drawImageRect

canvas.drawAtlas

canvas.drawRawAtlas

canvas.drawPicture

canvas.drawImage

该方法需要传递3个参数:

Imageimage:这里的image对象不是material库里的image对象,而是ui库里的image对象

Offsetoffset:绘制的图片左上角的位置坐标

Paintpaint:绘制的画笔,可以给图片添加其他属性

绘制的image对象在ui库里,所有先引用ui库:

import?'dart:ui'?as?ui;

创建一个空的ui里的image对象:

ui.Image??image;

创建绘制图片的类:

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}

绘制资源图片

资源图片是指在存在assets目录下,并已在pubspec.yaml文件中注册的图片。

先把资源图片变成ui.Image对象:

Future?loadIamge(String?path)?async?{??//?加载资源文件??final?data?=?await?rootBundle.load(path);??//?把资源文件转换成Uint8List类型??final?bytes?=?data.buffer.asUint8List();??//?解析Uint8List类型的数据图片??final?image?=?await?decodeImageFromList(bytes);??this.image?=?image;??setState(()?{});}

使用该方法加载图片:

ElevatedButton(??child:?const?Text('加载资源图片'),??onPressed:?()?{????loadAssetImage('assets/images/sxt.jpg');??},),

要想把图片正确的显示在页面中,需要按如下组件配置:

FittedBox(??child:?SizedBox(????width:?image?.width.toDouble(),????height:?image?.height.toDouble(),????child:?CustomPaint(??????painter:?CustomImagePainter(image!),????),??),),

绘制本地图片

创建一个空的XFile对象,初始化ImagePicker对象:

XFile??localImage;final?ImagePicker?imagePicker?=?ImagePicker();

把图片加载成可绘制的对象:

Future?loadLocalImage(String?path)?async?{??//?通过字节的方式读取本地文件??final?bytes?=?await?File(path).readAsBytes();??//?解析图片资源??final?image?=?await?decodeImageFromList(bytes);??this.image?=?image;??setState(()?{});}

从手机文件中加载图片:

Future?pickLocalImage()?async?{??XFile??xImage?=?await?imagePicker.pickImage(source:?ImageSource.gallery);??if?(xImage?!=?null)?{?????localImage?=?xImage;?????await?loadLocalImage(localImage!.path);???}??setState(()?{});}

使用该方法:

ElevatedButton(??child:?const?Text('加载本地图片'),??onPressed:?()?{????pickLocalImage();??},),

绘制网络图片

解析网络图片地址:

ui.Image??image;0

使用该方法:

ui.Image??image;1

以上方法虽然能顺利绘制处图片,但是还有一个致命的缺点,那就是不能绘制处动态图片。

绘制动态图片TODO

如果想绘制动态图片,需要使用instantiateImageCodec。

instantiateImageCodec方法可以传入4个参数:

Uint8List:一个由图片转换成的Uint8List对象

boolallowUpscaling:通常应避免将图像缩放到大于其固有大小,这会导致图像使用过多不必要的内存。如果必须缩放图像,则allowUpscaling参数必须设置为true

int?targetHeight:指定图片被显示出来的固定高度

int?targetWidth:指定图片被显示出来的固定宽度

这里以资源图片为例,修改代码如下:

ui.Image??image;2

现在代码还不能绘制出动态图片,只会获取动态图片的第一帧,所以这种方法也可以用来绘制静态图片。

思路:要想绘制动态图片,需要不停的调用codec.getNextFrame()方法,来传值给image对象。可以通过循环不停调用该方法,但是动态图片刷新显示的速度会很快,和原始图片不同。具体代码编写目前没有,有知道怎么操作的可以告知一下?。

canvas.drawImageRect

将src参数描述的给定图像的子集绘制到dst参数给定的轴对齐矩形中的画布中。

该方法需要传递4个参数:

Imageimage:传递一个ui.Image对象

Rectsrc:绘制一个矩形,用来裁剪图片的某个位置,再显示在dst所在的矩形中

Rectdst:绘制一个矩形,用来显示图片在屏幕的位置和宽高

Paintpaint:绘制的画笔,可以给图片添加其他属性

ui.Image??image;3

drawImageRect绘制图片一共需要3个步骤:

在一个以图片原始大小的坐标轴上,以图片左上角为原点,绘制src定义的矩形,截取src部分的图片内容(如图中黄色透明部分)

在屏幕上绘制dst定义的矩形(如图中灰色部分)

把src截取的图片以dst定义的矩形的左上角为顶点添加到矩形中显示出来

注意:因代码设置组件SizedBox的宽高为image的原始宽高,再添加一个父组件FittedBox可以让图片以原始宽高通过缩放完整的显示在界面,其实现在界面的宽高已经不是屏幕的边界宽高,而是图片的原始宽高。

当dst绘制的矩形比src截取的图片小会怎么样?

修改如下代码:

ui.Image??image;4

当dst绘制的矩形宽高和图片原始比例不同会怎么样?

修改如下代码:

ui.Image??image;5

当src的截图和图片原始比例不同会怎么样?

修改如下代码:

ui.Image??image;6

上图依次为以上三种情况绘制出来的图片显示的效果。由此可见,不管src截取的图片是多宽多高,总是会在dst绘制的矩形中完整的显示出来,哪怕是是图片宽高变形。

canvas.drawImageNine

把普通图片绘制成点九图片使用。需要传递4个参数:

mageimage:传递一个ui.Image对象

Rectcenter:绘制一个矩形,用来确定两条水平线和两条垂直线分割图像

Rectdst:绘制矩形用来确定图片显示的位置和大小

Paintpaint:绘制的画笔,可以给图片添加其他属性

ui.Image??image;7

上图就是绘制的最终结果。这种结果是怎么来的呢?

先通过dst在页面绘制出图片的位置和大小

通过center绘制的矩形将图片划分为9个区域

上图中,浅蓝色的就是center矩形,黄色的部分和center部分,都会因为外部条件使整体宽高变动而被拉伸,只有dst的4个角的区域的图片会保持大小不变。

drawAtlas

将图像的许多部分(atlas)绘画到画布上。当你想要在画布上绘制图像的许多部分时,例如使用精灵图或缩放时,使用这种方法可以进行优化。它比使用多次调用drawImageRect更有效,并且提供了更多功能,可以通过单独的旋转或缩放来单独转换每个图像部分,并使用纯色混合或调制这些部分。

该方法需要传递7个参数:

Imageatlas:ui.Image图片对象

List<RSTransform>transforms:由平移、旋转和统一比例组成的变换。这是一种比完整矩阵更有效的方式来表示这些简单的变换

List<Rect>rects:矩形数组,用来确认裁剪图片的位置和大小

List<Color>?colors:混合模式时使用的颜色数组

BlendMode?blendMode:混合模式

Rect?cullRect:可选的cullRect参数可以提供由要与剪辑进行比较的图集的所有组件呈现的坐标边界的估计值,以便在不相交时快速拒绝操作

Paintpaint:绘制的画笔,可以给图片添加其他属性

transforms和rects列表的长度必须相等,如果colors参数不为null,则它必须为空或与其他两个列表具有相同的长度。

上面这些参数中,比较陌生的就是RSTransform,我们先来了解了解。

RSTransform

该对象有以下几个属性和方法:

doublescos:旋转的余弦值乘以比例因子。用来操作缩放

doublessin:旋转的正弦值乘以比例因子。用来操作旋转

doubletx:平移的x坐标。后面的文字看不懂可以忽略(减去scos参数乘以旋转点的x坐标,再加上ssin参数乘以旋转点的y坐标)

doublety:平移的y坐标。后面的文字看不懂可以忽略(减去ssin参数乘以旋转点的x坐标,减去scos参数乘以旋转点的y坐标)

fromComponents:最简单实现变形的方法

该对象的难点就在于scos和ssin的值应该怎么填才能符合我们的预期。搞了很久也没弄明白,但如果你只是想计算正弦值和余弦值可以使用以下方法:

ui.Image??image;8

如果你只是想移动位置不旋转放大可以使用以下参数:

ui.Image??image;9

直接说简单的fromComponents方法。

fromComponents

该方法有以下6个参数:

doublerotation:旋转的角度

doublescale:缩放的倍数

doubleanchorX:旋转点的x坐标

doubleanchorY:旋转点的y坐标

doubletranslateX:偏移的x坐标

doubletranslateY:偏移的y坐标

知道了RSTransform的用法,接下来的就简单了。

drawAtlas的使用class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}0

绘制图片的流程如下:

先将准备好的图片绘制在页面中

通过rects中的矩形来裁剪图片中的某个部位

将裁剪的图片通过transforms的变形显示出来

colors和blendMode就不演示了。我们来看一下cullRect这个参数。

cullRect可以用来验证rects中的矩形是否在页面内,如果有1个不在,drawAtlas方法将不会绘制任何内容。因为cullRect只接受1个Rect对象,所以必须一个一个的验证。

drawRawAtlas

假如我们有一张5002000的精灵图,是由4个500500的图片组合而成。由drawAltas中对RSTransform的属性解释可得,精灵图中每个图片的旋转中心点默认是左上角,所以,每张图片的旋转中心坐标分别是(index*500,0)。

我们定义一个精灵图的类来存储这些信息。

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}1

然后我们将那几张图的信息存储在一个数组中:

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}2

使用Float32List组成一个矩形需要四个参数,我们有4张图,所以需要4*4个参数:

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}3

transforms的参数必须和rects的个数一样:

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}4

RSTransform

接下来要做的就是把精灵图中每张图的信息存储在上面定义的两个数组中,这里默认使用的构造Rect的方法是Rect.fromLTRB:

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}5

现在可以直接绘制:

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}6

RSTransform.fromComponentsclass?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}7

效果图和RSTransform的一样。

关于如何使用Int32Listcolors参数,可以查看这里。为了方便我将答案复制到下面。

我们先定义一个用来存储颜色的Int32List:

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}8

然后添加链接中的两种方法:

方法一

class?CustomImagePainter?extends?CustomPainter?{??final?ui.Image?image;??CustomImagePainter(this.image);??@override??void?paint(Canvas?canvas,?Size?size)?{????Paint?paint?=?Paint();????canvas.drawImage(image,?Offset.zero,?paint);??}??@override??bool?shouldRepaint(covariant?CustomPainter?oldDelegate)?=>?this?!=?oldDelegate;}9

方法二

Future?loadIamge(String?path)?async?{??//?加载资源文件??final?data?=?await?rootBundle.load(path);??//?把资源文件转换成Uint8