flutter画布绘制图片和文字
本节目标:
[1]. 了解如何获取 [ui.Image] 对象。
[2]. 将一张图片使用 Canvas 绘制出来。
[3]. 知道如何从图片中取出部分图片绘制到指定矩形域中。
[4]. 了解 Canvas 绘制图集的操作。
[5]. 如何在 Canvas 中绘制文字,并完善坐标系刻度。
一、图片绘制:
绘制图片需要的是 ui.Image,需要异步加载,这里用 loadImageFromAssets 处理。
PaperPainter 接收 ui.Image 对象。在异步加载完成,刷新传入即可。
0. 如何从 assets
中获取图片数据。
通过
decodeImageFromList
方法可以将一个字节流转换为ui.Image
对象。将assets
的文件读取为字节流可以使用rootBundle.load
方法。
//读取 assets 中的图片
Future<ui.Image> loadImageFromAssets(String path) async {
ByteData data = await rootBundle.load(path);
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
return decodeImageFromList(bytes);
}
1. 基础图片绘制:drawImage
【1】创建 Paper 组件继承自 StatefulWidget
因为读取图片是一个异步操作。在读取完毕后,需要重新渲染界面,也就是可变状态。
现在要有一个概念:画布只承担绘制工作,一切的数据来源由使用者提供
。
也就是将ui.Image
对象作为参数传给在 PaperPainter,画板只专注于绘制操作
。
---->[p04_canvas/14_image/paper.dart]----
class Paper extends StatefulWidget {
@override
_PaperState createState() => _PaperState();
}
class _PaperState extends State<Paper> {
ui.Image _image;
@override
void initState() {
super.initState();
_loadImage();
}
void _loadImage() async {
_image =
await loadImageFromAssets('assets/images/wy_300x200.jpg');
setState(() {});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(painter: PaperPainter(_image)));
}
// 方法在上面已给出,不再重复书写...
Future<ui.Image> loadImageFromAssets()...
}
2. 图片的绘制:drawImage
Canvas#drawImage
方法需要传入ui.Image
对象、偏移量 Offset
和画笔 Paint
。shouldRepaint
方法决定是否重新调用 paint 方法,这里当新旧 image 不同
时允许重绘。
下面图片尺寸 300*200, 直接绘制图片会取图片的原尺寸。
---->[p04_canvas/14_image/paper.dart]----
class PaperPainter extends CustomPainter {
Paint _paint;
final double strokeWidth = 0.5;
final Color color = Colors.blue;
final ui.Image image;
PaperPainter(this.image) {
_paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = strokeWidth
..color = color;
}
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
_drawImage(canvas);
}
@override
bool shouldRepaint(PaperPainter oldDelegate) =>
image != oldDelegate.image;
void _drawImage(Canvas canvas) {
if (image != null) {
canvas.drawImage(
image, Offset(-image.width / 2, -image.height / 2), _paint);
}
}
}
3. 图片域绘制:drawImageRect
drawImageRect
中主要是两个矩形域,src
和dst
。src
表示从资源图片 image
上抠出一块矩形域
,所以原点是图片的左上角
。dst
表示将抠出的图片填充到画布
的哪个矩形域中,所以原点是画布原点
。
---->[p04_canvas/15_image_rect/paper.dart]----
void _drawImageRect(Canvas canvas) {
if (image != null) {
canvas.drawImageRect(
image,
Rect.fromCenter(
center: Offset(image.width/2, image.height/2), width: 60, height: 60),
Rect.fromLTRB(0, 0, 100, 100).translate(200, 0),
_paint);
canvas.drawImageRect(
image,
Rect.fromCenter(
center: Offset(image.width/2, image.height/2-60), width: 60, height: 60),
Rect.fromLTRB(0, 0, 100, 100).translate(-280, -100),
_paint);
canvas.drawImageRect(
image,
Rect.fromCenter(
center: Offset(image.width/2+60, image.height/2), width: 60, height: 60),
Rect.fromLTRB(0, 0, 100, 100).translate(-280, 50),
_paint);
}
}
4. 图片 .9 域绘制:drawImageNine
drawImageNine
中主要是两个矩形域,center
和dst
。center
表示从资源图片image
上一块可缩放的矩形域
,所以原点是图片的左上角
。dst
表示将抠出的图片填充到画布
的哪个矩形域中,所以原点是画布原点
。
这样很容易画出气泡的效果,即指定区域进行缩放,其余不动。
---->[p04_canvas/16_image_rect/paper.dart]----
void _drawImageNine(Canvas canvas) {
if (image != null) {
canvas.drawImageNine(
image,
Rect.fromCenter(center: Offset(image.width/2, image.height-6.0),
width: image.width-20.0, height: 2.0),
Rect.fromCenter(center: Offset(0, 0,), width:300, height: 120),
_paint);
canvas.drawImageNine(
image,
Rect.fromCenter(center: Offset(image.width/2, image.height-6.0),
width: image.width-20.0, height: 2.0),
Rect.fromCenter(center: Offset(0, 0,), width:100, height: 50).translate(250, 0),
_paint);
canvas.drawImageNine(
image,
Rect.fromCenter(center: Offset(image.width/2, image.height-6.0),
width: image.width-20.0, height: 2.0),
Rect.fromCenter(center: Offset(0, 0,), width:80, height: 250).translate(-250, 0),
_paint);
}
}
5.绘制图集:drawAtlas
这个方法有七个参数,用起来比较复杂。主要作用是
在画布上绘制一张图片上的很多部分
,比如雪碧图 (Sprite) 将需要的图片放在一张图里。另外通过drawAtlas
绘制的效率要更高。
【1】定义每个Sprite类封装图片元素数据
---->[p04_canvas/17_image_drawAtlas/paper.dart]----
class Sprite {
Rect position; // 雪碧图 中 图片矩形区域
Offset offset; // 移动偏倚
int alpha; // 透明度
double rotation; // 旋转角度
Sprite({this.offset, this.alpha, this.rotation, this.position});
}
【2】绘制一个图片
绘制时必须传入的参数是
图片 ui.Image
、变换列表List<RSTransform>
、矩形域列表 List<Rect>
下面通过矩形域Rect.fromLTWH(0, 325, 257, 166)
可以绘制出大图中的这张图片:
class PaperPainter extends CustomPainter {
Paint _paint;
final ui.Image image;
final List<Sprite> allSprites = []; // Sprite 列表
PaperPainter(this.image) {
_paint = Paint();
}
@override
void paint(Canvas canvas, Size size) {
if (image == null) {
return;
}
// 添加一个 Sprite
allSprites.add(Sprite(
position: Rect.fromLTWH(0, 325, 257, 166),
offset: Offset(0, 0),
alpha: 255,
rotation: 0));
// 通过 allSprites 创建 RSTransform 集合
final List<RSTransform> transforms = allSprites
.map((sprite) => RSTransform.fromComponents(
rotation: sprite.rotation,
scale: 1.0,
anchorX: 0,
anchorY: 0,
translateX: sprite.offset.dx,
translateY: sprite.offset.dy,
))
.toList();
// 通过 allSprites 创建 Rect 集合
final List<Rect> rects =
allSprites.map((sprite) => sprite.position).toList();
canvas.drawAtlas(image, transforms, rects, null, null, null, _paint);
}
@override
bool shouldRepaint(PaperPainter oldDelegate) => image != oldDelegate.image;
}
【3】图形的变换
我们在定义 Sprite 时,可以将变换的属性放在其中,如
平移
、缩放
、透明度
等。
一般雪碧图制作工具都会给出图片在大图中的坐标位置信息,可以解析添加到allSprites
中。
除此之外的参数还有颜色 color
、混合模式 blendMode
,据此你可以对图片进行更多操作。
5.绘制原始图集:drawRawAtlas
这个方法是
drawAtlas
的底层实现,其中变换列表、矩形域列表
都换为Float32List
,颜色数组换为Int32List
,在使用方式上是一致的。
---->[p04_canvas/18_image_drawRawAtlas/paper.dart]----
allSprites.add(Sprite(
position: Rect.fromLTWH(0, 325, 257, 166),
offset: Offset(0, 0),
alpha: 255,
rotation: 0));
allSprites.add(Sprite(
position: Rect.fromLTWH(0, 325, 257, 166),
offset: Offset(257, 130),
alpha: 255,
rotation: 0));
Float32List rectList = Float32List(allSprites.length * 4);
Float32List transformList = Float32List(allSprites.length * 4);
for (int i = 0; i < allSprites.length; i++) {
final Sprite sprite = allSprites[i];
rectList[i * 4 + 0] = sprite.position.left;
rectList[i * 4 + 1] = sprite.position.top;
rectList[i * 4 + 2] = sprite.position.right;
rectList[i * 4 + 3] = sprite.position.bottom;
final RSTransform transform = RSTransform.fromComponents(
rotation: sprite.rotation,
scale: 1.0,
anchorX: sprite.anchor.dx,
anchorY: sprite.anchor.dy,
translateX: sprite.offset.dx,
translateY: sprite.offset.dy,
);
transformList[i * 4 + 0] = transform.scos;
transformList[i * 4 + 1] = transform.ssin;
transformList[i * 4 + 2] = transform.tx;
transformList[i * 4 + 3] = transform.ty;
}
canvas.drawRawAtlas(image, transformList, rectList, null, null, null, _paint);
二、文字绘制:
Flutter 里的文字绘制要
明显麻烦很多
,但属性多也意味着可定制性高
主要的绘制方式是通过drawParagraph
或TextPaint
。
1. drawParagraph绘制文字
通过
ParagraphBuilder
构造基本样式pushStyle
和添加文字addText
。builder.build()
可以创建Paragraph
对象,之后必须对其排布layout
限制区域。
下面蓝色区域是绘制的辅助, 依次是左对齐
、居中
、右对齐
。
---->[p04_canvas/19_text/paper.dart]----
void _drawTextWithParagraph(Canvas canvas,TextAlign textAlign) {
var builder = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: textAlign,
fontSize: 40,
textDirection: TextDirection.ltr,
maxLines: 1,
));
builder.pushStyle(
ui.TextStyle(
color: Colors.black87, textBaseline: ui.TextBaseline.alphabetic),
);
builder.addText("Flutter Unit");
ui.Paragraph paragraph = builder.build();
paragraph.layout( ui.ParagraphConstraints(width: 300));
canvas.drawParagraph(paragraph, Offset(0, 0));
canvas.drawRect(Rect.fromLTRB(0, 0, 300, 40 ),
_paint..color = Colors.blue.withAlpha(33));
}
2. TextPainter 绘制文字
TextPainter
的绘制基本上就是对drawParagraph
的封装,提供了更多的方法,使用起来简洁一些。所以一般来说都是使用TextPainter
进行文字绘制。绘制先设置TextPainter
,然后执行layout()
方法进行布局,其中可以传入布局区域的最大和最小宽度
。通过paint
方法进行绘制。
---->[p04_canvas/19_text/paper.dart]----
void _drawTextPaint(Canvas canvas) {
var textPainter = TextPainter(
text: TextSpan(
text: 'Flutter Unit',
style: TextStyle(fontSize: 40,color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter.layout(); // 进行布局
textPainter.paint(canvas, Offset.zero); // 进行绘制
}
3. TextPainter 获取文字范围
TextPainter
中可以通过 size 属性获取文字所占区域,注意,获取区域必须在执行layout
方法之后。
一但确定范围后,就容易实现将文字中心绘制在画布原点
,这一个效果是非常重要的。
---->[p04_canvas/19_text/paper.dart]----
void _drawTextPaintShowSize(Canvas canvas) {
TextPainter textPainter = TextPainter(
text: TextSpan(
text: 'Flutter Unit',
style: TextStyle(
fontSize: 40,
color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter.layout(); // 进行布局
Size size = textPainter.size; // 尺寸必须在布局后获取
textPainter.paint(canvas, Offset(-size.width / 2, -size.height / 2));
canvas.drawRect(
Rect.fromLTRB(0, 0, size.width, size.height)
.translate(-size.width / 2, -size.height / 2),
_paint..color = Colors.blue.withAlpha(33));
}
4. 为文字设置画笔样式
比如设置线型的文字,或为文字添加画笔着色器等。可以使用
TextStyle
中的foreground
属性提供一个画笔。注意:此属性和TextStyle#color
属性互斥
。
---->[p04_canvas/19_text/paper.dart]----
void _drawTextPaintWithPaint(Canvas canvas) {
Paint textPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1;
TextPainter textPainter = TextPainter(
text: TextSpan(
text: 'Flutter Unit by 张风捷特烈',
style: TextStyle(
foreground: textPaint, fontSize: 40)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter.layout(maxWidth: 280); // 进行布局
Size size = textPainter.size; // 尺寸必须在布局后获取
textPainter.paint(canvas, Offset(-size.width / 2, -size.height / 2));
canvas.drawRect(
Rect.fromLTRB(0, 0, size.width, size.height)
.translate(-size.width / 2, -size.height / 2),
_paint..color = Colors.blue.withAlpha(33));
}
3.为坐标系添加刻度
这里在之前的坐标系上进行优化。对于 step 过小而导致的刻度密集,这里采取简单的处理方案:
当step < 30
时,第奇数个刻度被忽略。如下是step = 25
时的坐标系。
---->[p04_canvas/20_coo_text/paper.dart]----
// 定义成员变量
final TextPainter _textPainter = TextPainter(textDirection: TextDirection.ltr);
// 绘制方法
void _drawText(Canvas canvas, Size size) {
// y > 0 轴 文字
canvas.save();
for (int i = 0; i < size.height / 2 / step; i++) {
if (step < 30 && i.isOdd || i == 0) {
canvas.translate(0, step);
continue;
} else {
var str = (i * step).toInt().toString();
_drawAxisText(canvas, str, color: Colors.green);
}
canvas.translate(0, step);
}
canvas.restore();
// x > 0 轴 文字
canvas.save();
for (int i = 0; i < size.width / 2 / step; i++) {
if (i == 0) {
_drawAxisText(canvas, "O", color: Colors.black, x: null);
canvas.translate(step, 0);
continue;
}
if (step < 30 && i.isOdd) {
canvas.translate(step, 0);
continue;
} else {
var str = (i * step).toInt().toString();
_drawAxisText(canvas, str, color: Colors.green, x: true);
}
canvas.translate(step, 0);
}
canvas.restore();
// y < 0 轴 文字
canvas.save();
for (int i = 0; i < size.height / 2 / step; i++) {
if (step < 30 && i.isOdd || i == 0) {
canvas.translate(0, -step);
continue;
} else {
var str = (-i * step).toInt().toString();
_drawAxisText(canvas, str, color: Colors.green);
}
canvas.translate(0, -step);
}
canvas.restore();
// x < 0 轴 文字
canvas.save();
for (int i = 0; i < size.width / 2 / step; i++) {
if (step < 30 && i.isOdd || i == 0) {
canvas.translate(-step, 0);
continue;
} else {
var str = (-i * step).toInt().toString();
_drawAxisText(canvas, str, color: Colors.green, x: true);
}
canvas.translate(-step, 0);
}
canvas.restore();
}
void _drawAxisText(Canvas canvas, String str,
{Color color = Colors.black, bool x = false}) {
TextSpan text = TextSpan(
text: str,
style: TextStyle( fontSize: 11, color: color ));
_textPainter.text = text;
_textPainter.layout(); // 进行布局
Size size = _textPainter.size;
Offset offset = Offset.zero;
if (x == null) {
offset = Offset(8, 8);
} else if (x) {
offset = Offset(-size.width / 2, size.height / 2);
} else {
offset = Offset(size.height / 2, -size.height / 2 + 2);
}
_textPainter.paint(canvas, offset);
}
到此为止,
Canvas
的方法基本上都涉及到了,你应该已经知道Canvas
有哪些能力,可以帮我们做什么。下面就将见识一下绘制中最重要,也是最难的一个对象Path
。