绘制相关FlutterCanvasDart

Flutter Canvas学习会作为一个系列,原文传送门:

前言

在一般UI框架中,文字与图片的绘制都较为复杂,所以单独一篇来讲解。

Paragraph

void drawParagraph(Paragraph paragraph, Offset offset)

Paragraph是Flutter中用于文字绘制的类,Flutter中所有的文字,最后都是通过它来绘制的,连输入框也都是通过它来实现的,由此可见它的强大之处。

Paragraph是一个没有构造函数的类,它只是提供一个宿主,用于最后的渲染。我们真正需要处理的则是ParagraphBuilder这个类。

ParagraphBuilder类接收一个参数,是一个ParagraphStyle类,用于设置字体基本样式,例如字体方向、对齐方向、字体粗细等,下面我们分几个步骤来绘制文字

第一步,生成ParagraphStyle

import 'dart:ui' as ui;
final paragraphStyle = ui.ParagraphStyle(
  // 字体方向,有些国家语言是从右往左排版的
  textDirection: TextDirection.ltr,
  // 字体对齐方式
  textAlign: TextAlign.justify,
  fontSize: 14,
  maxLines: 2,
  // 字体超出大小时显示的提示
  ellipsis: '...',
  fontWeight: FontWeight.bold,
  fontStyle: FontStyle.italic,
  height: 5,
  // 当我们设置[TextStyle.height]时 这个高度是否应用到字体顶部和底部
  textHeightBehavior:
      TextHeightBehavior(applyHeightToFirstAscent: true,applyHeightToLastDescent: true));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第二步,根据ParagraphStyle生成ParagraphBuilder

final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle);
1

第三步,添加文字。ParagraphBuilder类有个addText方法专门用于接收文字

paragraphBuilder.addText('JSShou学习Canvas');
1

第四步,通过build取到Paragraph

var paragraph = paragraphBuilder.build();
1

第五步,根据宽高进行布局(layout)

paragraph.layout(ui.ParagraphConstraints(width: 300));
1

第六步,绘制(paint)

canvas.drawParagraph(paragraph, Offset(50, 50));
1

全部代码如下

// 第一步
final paragraphStyle = ui.ParagraphStyle(
    // 字体方向,有些国家语言是从右往左排版的
    textDirection: TextDirection.ltr,
    // 字体对齐方式
    textAlign: TextAlign.justify,
    fontSize: 14,
    maxLines: 2,
    // 字体超出大小时显示的提示
    ellipsis: '...',
    fontWeight: FontWeight.bold,
    fontStyle: FontStyle.italic,
    height: 5,
    // 当我们设置[TextStyle.height]时 这个高度是否应用到字体顶部和底部
    textHeightBehavior:
        TextHeightBehavior(applyHeightToFirstAscent: true,applyHeightToLastDescent: true));
// 第二步 与第三步
final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle)
  ..addText('ParagraphBuilder类接收一个参数,是一个ParagraphStyle类,用于设置字体基本样式,例如字体方向、对齐方向、字体粗细等,下面我们分几个步骤来绘制文字');
// 第四步
var paragraph = paragraphBuilder.build();
// 第五步
paragraph.layout(ui.ParagraphConstraints(width: 300));
// 画一个辅助矩形(可以通过paragraph.width和paragraph.height来获取绘制文字的宽高)
canvas.drawRect(
    Rect.fromLTRB(50, 50, 50 + paragraph.width, 50 + paragraph.height),
    paint);
// 第六步
canvas.drawParagraph(paragraph, Offset(50, 50));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

效果如下:

paragraph

TextPainter

说了Paragraph,就不得不提TextPainterTextPainter是通过Paragraph封装而成,相比Paragraph它提供了更加强大的能力

  • 通过传入TextSpan,实现多种不同效果的字体来支持富文本
  • 不像Paragraph必须设置一个宽度,它可以不用初始化宽度,可以用过TextPainter.width来获取实际渲染宽度(实际上Paragraph也能实现,不过它封装了一层更贴近我们的开发思维)

使用它也很简单

var textPainter = TextPainter(
  text: TextSpan(
      text:
          "可多种不同效果的字体来支持富文本可多种不同效果的字体来支持富文本",
      style: TextStyle(color: Colors.white,fontSize: 20)),
  textDirection: TextDirection.rtl,
  textWidthBasis: TextWidthBasis.longestLine,
  maxLines: 2,
)

// 可以传入minWidth,maxWidth来限制它的宽度,如不传,文字会绘制在一行
..layout();
var startOffset = 50.0;
// 绘制辅助矩形框,在文字绘制前即可通过textPainter.width和textPainter.height来获取文字绘制的宽度
canvas.drawRect(
    Rect.fromLTRB(startOffset, startOffset, startOffset + textPainter.width,
        startOffset + textPainter.height),
    paint);
textPainter.paint(canvas, Offset(startOffset, startOffset));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

textPainter

drawImage

void drawImage(Image image, Offset offset, Paint paint)

我们可以通过drawImage来绘制图片,这里的Image不是我们常用的Image,它是dart:ui库中的Image,它保存了图片的一些基本信息并直接与引擎交互。

在使用该方法时,需要先加载一张图片,加载图片的方式有很多,我就介绍一种方式

load('assets/test.png');
// ...
Future<ui.Image> load(String asset) async {
  ByteData data = await rootBundle.load(asset);
  ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),targetWidth: 300,targetHeight: 300);
  ui.FrameInfo fi = await codec.getNextFrame();
  return fi.image;
}
1
2
3
4
5
6
7
8

由于图片加载是异步过程,不能放到CustomPaintpaint方法来加载(因为paint完成之后canvas会dispose掉,如果在异步方法后面再使用canvas,会报错Object has been disposed),这里需要在外部使用一个StatefulWidget,加载完成后将获取到的Image传入CustomPaint中。此时,再使用canvas.drawImage来加载。

drawImage

drawImageRect

void drawImageRect(Image image, Rect src, Rect dst, Paint paint)

  • src: 截取一块图片区域,起始点相对与图片左上角
  • dst: 在canvas上绘制一个区域来绘制上面截取的图片,图片可能会被拉升
// 绘制原图
canvas.drawImage(image, Offset(50, 50), paint);
// 图片区域
Rect rect = Rect.fromCenter(
        center: Offset(image.width / 2, image.height / 2),
        width: image.width / 2,
        height: image.height / 2);
// 绘制辅助线,向下向右平移50,因为原图是相对于Offset(50,50)
canvas.drawRect(rect.shift(Offset(50,50)), paint);
//  go
canvas.drawImageRect(
    image,
    rect,
    Rect.fromLTWH(50, 500, 100, 100),
    paint);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

drawImageRect

drawImageNine

void drawImageNine(Image image, Rect center, Rect dst, Paint paint)

这个方法就像Android中的.9图,在图片拉伸的时候特定的区域不会发生图片失真,而不失真的区域可以由我们自己绘制。

canvas.drawImage(image, Offset(50, 50), paint);
Rect rect = Rect.fromCenter(
        center: Offset(image.width / 2, image.height / 2),
        width: image.width / 2,
        height: image.height / 2);
canvas.drawRect(rect.shift(Offset(50,50)), paint);
Rect dst = Rect.fromLTWH(50, 500, 200, 200);
canvas.drawImageNine(image, rect, dst, paint);
1
2
3
4
5
6
7
8

drawImageNine

如果我们改变dst,也就是图片绘制区域

Rect dst = Rect.fromLTWH(50, 500, 400, 200);
1

drawImageNine1

聪明的你已经发现了,图片被拉伸和压缩都有一定规律,上面原图的蓝色区域是可能被压缩和拉伸的位置,同时蓝色区域上下左右部分也是存在可能被拉伸位置,而四个角会保持原样不会失真。

drawPicture

这个方法是通过传入一个Picture实例来进行绘制的,而Picture需通过PictureRecorder来构造。

// 开始记录Picture
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas canvas = new Canvas(recorder);

// 调用 Canvas 的绘制接口,画一个圆形
canvas.drawCircle(
    Offset(200, 200), 100, Paint()..color = Colors.yellow);

// 绘制结束,生成Picture
Picture picture = recorder.endRecording();
1
2
3
4
5
6
7
8
9
10

这里的Picture并不是指我们传统的图片,图片相关是通过Image来表达的,这里的Picture是之绘制的任何图形(通过drawLine、drawRect等绘制的)。上面只是生成一个Picture的最简单的例子,我们new一个Canvas对象用于记录绘制的数据,当调用recorder.endRecording结束记录并返回一个Picture对象。需要特殊说明的是,这的canvas提供drawPicture方法也并不是希望我们在其中使用PictureRecorder来记录并绘制UI(实际上我们在paint方法中这样做,会报错。还有CustomPaint中提供了canvas对象,不需要new),而是在其它位置得到一个Picture对象时,我们可以此方法来进行绘制。所以上面的代码我们需要放在StatefulWidget中执行并把得到的Picture对象传入CustomPaint中进行绘制。

class MyCanvas extends CustomPainter {
  final ui.Picture picture;

  MyCanvas(this.picture);
  
  void paint(Canvas canvas, Size size) async {
    if (picture != null) canvas.drawPicture(picture);
  }
  //...
}
1
2
3
4
5
6
7
8
9
10

drawPicture

下一篇将介绍Canvas图形变换相关如旋转、平移、缩放等,如有兴趣,请关注

Last Updated: 6/6/2021, 5:51:41 PM