Flutter canvas

参考资料

使用canvas会使用CustomPaint

const CustomPaint({
  Key? key,
  this.painter,
  this.foregroundPainter,
  this.size = Size.zero,
  this.isComplex = false,
  this.willChange = false,
  Widget? child,
})
  • painter:绘制的对象,是一个CustomPainter。它的绘制是在child之前。如果设置了child,该painter绘制的内容会被覆盖。
  • foregroundPainter:绘制的对象,是一个CustomPainter。它的绘制是在child之后。如果设置了child,该painter绘制的内容会覆盖child。
  • size: 画板大小,如果定义了child,则会以child的尺寸为准
  • isComplex: 默认值是false,定义绘制内容是否复杂,如果为true,会对canvas的绘制进行一些必要的缓存来优化性能
  • willChange: 默认值是false,配合isComplex使用,控制组件是否在下一帧需要重绘
  • child: 子节点,可以不设置

CustomPainter是一个抽象类,其构造函数如下

const CustomPainter({ Listenable? repaint })
  • repaint: 是一个Listenable,一般用于动画时,传入一个监听来控制canvas组件的重绘

下面就是传入一个AnimationController,数值改动时就会调用paint方法,使用CustomPain做动画时这是固定格式

class CountdownButtonPainter extends CustomPainter {
  CountdownButtonPainter({
    required this.controller,
  }) : super(repaint: controller);

  final AnimationController controller;
@override
  void paint(Canvas canvas, Size size) {
    //...其他代码省略 动画这里会用到controller参数,如倒计时,后面会讲
    
    }
 @override
  bool shouldRepaint(oldDelegate) {
//...其他代码省略,后面会讲
  }
}

Paint方法

Paint paint = Paint()
    ..isAntiAlias = true
    ..color = Colors.pink
    ..blendMode = BlendMode.colorDodge
    ..strokeWidth = 10
    ..style = PaintingStyle.fill;

Paint类提供了很多属性,上面只是一些常用属性,

  • isAntiAlias: 是否抗锯齿
  • color: 画笔颜色
  • strokeWidth: 画笔宽度
  • style: 样式
    • PaintingStyle.fill 默认 填充
    • PaintingStyle.stroke 线
  • strokeCap: 定义画笔端点形状
    • StrokeCap.butt 无形状(默认)
    • StrokeCap.round 圆形
    • StrokeCap.square 正方形

  角度Angle问题

Flutter中的角度是用double表示的,Flutter里面提供了pi来计算角度,公式是:

1度 = pi / 180
90度 = 90 * (pi /180)

在Flutter中,一般传入参数叫startAngle的表示开始的位置,位置表示如下

Canvas与绘制无关API

save

save操作会保存此前的所有绘制内容和canvas状态。在调用该函数之后的绘制操作和变换操作,会重新记录。当你调用restore()之后,会把saverestore之间所进行的操作与之前的内容进行合并。 下面看一个例子

Paint paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 10;
Path generatePath(double x, double y) {
  Path path = new Path();
  path.moveTo(x, y);
  path.lineTo(x + 100, y + 100);
  path.lineTo(x + 150, y + 80);
  path.lineTo(x + 100, y + 200);
  path.lineTo(x, y + 100);
  return path;
}

canvas.drawPath(generatePath(100, 100), paint);
canvas.rotate(10 * pi / 180);
canvas.drawPath(generatePath(100, 150), paint);
canvas.drawPath(generatePath(100, 500), paint);

我使用一个函数画了三个形状一样的图形,只是它们的位置不一样,然后在第一个图形后面使用了rotate进行旋转,当没有使用save时,下面两个图形都会发生旋转 

 当我使用save时

Paint paint = Paint()
  ..color = Colors.red
  ..style = PaintingStyle.stroke
  ..strokeWidth = 10;
Path generatePath(double x, double y) {
  Path path = new Path();
  path.moveTo(x, y);
  path.lineTo(x + 100, y + 100);
  path.lineTo(x + 150, y + 80);
  path.lineTo(x + 100, y + 200);
  path.lineTo(x, y + 100);
  return path;
}

canvas.drawPath(generatePath(100, 100), paint);
canvas.save();
canvas.rotate(10 * pi / 180);
canvas.drawPath(generatePath(100, 150), paint);
canvas.restore();
canvas.drawPath(generatePath(100, 500), paint);

看到区别了吧,最后绘制的图形是没有跟随上面部分旋转的。使用save方法后面必须跟一个restore,否则会抛出异常(这也很好理解,save的功能就是划出一块区域进行一些操作,所以这一块区域必须是闭合的)

绘制点-drawPoin

  • pointMode: 设置点、线
    • PointMode.points 设置点
    • PointMode.lines 两个两个点之间连接,如果传入的points是奇数,最后一个点将会被忽略
    • PointMode.polygon 将所有点连接起来
  • points: 一个Offset数组,可以画多个点

import 'dart:ui' as ui;
//...
// 画点
Paint paint = Paint()
  ..color = Colors.red
  ..strokeWidth = 20;
canvas.drawPoints(
    ui.PointMode.points,
    [
      Offset(100, 100),
      Offset(250, 180),
      Offset(200, 300),
    ],
    paint);
// 将端点设置为圆形
paint.strokeCap = StrokeCap.round;
canvas.drawPoints(ui.PointMode.points, [Offset(100, 200)], paint);

绘制线段-drawLine

canvas.drawLine(Offset(100, 100), Offset(250,180), paint);

绘制区域-drawRect

fromLTRB

此方法是所有该类方法的母本,其它方法都是使用此方法实现的

  • left: 矩形左边距离画布左边距离
  • top: 矩形顶部距离画布顶部距离
  • right: 矩形右边距离画布左边边距离
  • bottom: 矩形底部距离画布顶部距离

canvas.drawRect(Rect.fromLTRB(50, 50, 350, 350), paint);

fromCenter

  • center: 长方形方形中心点位置
  • width: 长方形的宽
  • height: 长方形的高

canvas.drawRect(
      Rect.fromCenter(center: Offset(200,300), width: 250, height: 350), paint);

fromCircle

画一个正方形

  • center: 正方形中心点位置
  • radius: 正方形四条边距离中心点距离
canvas.drawRect(
        Rect.fromCircle(center: Offset(200, 300), radius: 150), paint);

fromPoints

使用两个点确定一个矩形

  • a: 矩形左上角的位置
  • b: 矩形右下角的位置

canvas.drawRect(Rect.fromPoints(Offset(100, 200), Offset(300, 400)), paint);

绘制圆角矩形-RRect

RRect用来绘制带圆角的矩形,其绘制的位置原理同Rect.fromLTRB一样,只是多了一个设置圆角的参数。

RRect rRect = RRect.fromLTRBR(100, 100, 350, 350, Radius.circular(30));
canvas.drawRRect(rRect, paint);

绘制圆-drawOval

Rect pRect = Rect.fromLTRB(50, 150, 400, 350);
// 为了区别,先绘制一个矩形区域
canvas.drawRect(pRect, paint);
paint.color = Colors.yellow;
// 绘制椭圆
canvas.drawOval(pRect, paint);

绘制圆弧-drawArc

绘制圆弧,useCenter表示是否绘制中心点到圆弧两边

Rect rect = Rect.fromCircle(
    center: Offset(size.width / 2, size.height / 2), radius: 100);
// 为了方便查看,把区域也绘制出来
canvas.drawRect(rect);
//绘制圆弧
canvas.drawArc(rect, 90 * (pi / 180), 90 * (pi / 180), false, paint);

绘制路径-drawPath  重要!!!!

moveTo

设置画笔开始的位置

lineTo

绘制的下一个位置,传入的是相对于坐标系的具体位置,会按照代码顺序进行移动绘制

relativeLineTo

与lineTo类似,不过传入的是相对于上一个点为原点的位置,比如上一个点是在(100,100),传入的是(150,150),如果用lineTo要达到同样效果应该传入(250,250)。它有很多方法,带了relative的都是同样的原理,所以后续relative-方法就不讲解了。

arcTo

绘制一个弧线

void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)

此方法同drawArc类似,前三个参数都一样,最后一个参数表示是否跟path之前的绘制(如通过lineTo绘制的线段)相连,false表示连接,true表示不相连。

示例

下图是利用最基本的moveTo,lineTo,arctoPoint绘制一个圆角矩形

Path _createPath(Size size) {
    final double _width = size.width;
    final double _height = size.height;

    return Path()
      // 移动到二分之一宽度位置,作为起始点。
      ..moveTo(_width / 2, 0)
      // 右上直线
      ..relativeLineTo(_width / 2 - effectiveRadius, 0)
      // 右上弧
      ..relativeArcToPoint(
        Offset(effectiveRadius, effectiveRadius),
        radius: _circularRadius,
      )
      // 右侧直线
      ..relativeLineTo(0, _height - effectiveRadius * 2)
      // 右下弧
      ..relativeArcToPoint(
        Offset(-effectiveRadius, effectiveRadius),
        radius: _circularRadius,
      )
      // 底部直线
      ..relativeLineTo(-_width + effectiveRadius * 2, 0)
      // 左下弧
      ..relativeArcToPoint(
        Offset(-effectiveRadius, -effectiveRadius),
        radius: _circularRadius,
      )
      // 左侧直线
      ..relativeLineTo(0, -_height + effectiveRadius * 2)
      // 左上弧
      ..relativeArcToPoint(
        Offset(effectiveRadius, -effectiveRadius),
        radius: _circularRadius,
      )
      // 左上直线
      ..relativeLineTo(_width / 2 - effectiveRadius, 0)
      ..close();
  }

addRect

绘制一个矩形区域

path.addRect(Rect.fromLTRB(50, 50, 350, 350));

1

此效果跟上面drawRect中第一个例子一样

addRRect

绘制一个带圆角的矩形

RRect rRect = RRect.fromLTRBR(100, 100, 350, 350, Radius.circular(30));
path.addRRect(rRect);

1
2

上面代码效果跟drawRRect一样

addArc

绘制一个圆弧

Path path = new Path();
// 画一个矩形区域
Rect rect = Rect.fromCircle(
    center: Offset(size.width / 2, size.height / 2), radius: 100);
canvas.drawRect(rect, paint);
// 在矩形区域画圆弧
path.addArc(rect, 90 * (pi / 180), 90 * (pi / 180));
paint.color = Colors.red;
canvas.drawPath(path, paint);

1
2
3
4
5
6
7
8
9

上面代码同drawArc效果一样

canvas.drawArcpath.addArcpath.arcTo(当forceMoveTo为true时),三种方式都能绘制同样的圆弧效果

addOval

绘制一个椭圆

Rect pRect = Rect.fromLTRB(50, 150, 400, 350);
path.addOval(pRect);
canvas.drawPath(path, paint);

1
2
3

代码效果同drawOval

addPolygon

通过点绘制线段

void addPolygon(List points, bool close)

  • point: 传入多个点的位置
  • close: 为true时最后一个点会和第一个点相连
Path path = new Path();
path.addPolygon([
      Offset(100, 100),
      Offset(250, 180),
      Offset(200, 300),
    ], false);
canvas.drawPath(path, paint);

computeMetrics (路径快照,可绘制一部分)

computeMetrics方法用于返回一个之前绘制的路径的一份快照。当我们使用moveTolineToarcToconicTo等绘制路径时,可以使用此来实现只绘制其中一部分。

var path = Path();
path.moveTo(50, 500);
path.cubicTo(50, 200, 300, 400, 350, 150);
// 将完整绘制图形置为红色
paint.color = Colors.red;
canvas.drawPath(path, paint);
ui.PathMetrics pathMetrics = path.computeMetrics();
// 绘制一半
var progress = 0.5;
// 将颜色更改为紫色用于区分
paint.color = Colors.deepPurple;
for (ui.PathMetric pathMetric in pathMetrics) {
  Path extractPath = pathMetric.extractPath(
    0.0,
    pathMetric.length * progress,
  );
  canvas.drawPath(extractPath, paint);
}

combine (路径联合,交集并集补集)

// 通过path1和path2路径的到新的路径p
Path path = Path.combine(PathOperation operation, Path path1, Path path2);

其中PathOperation是一个枚举类型,一共有5个类型

enum PathOperation {
  /// 得到path1单独区域 也可理解为path1-path2差集
  difference,
  
  /// 得到path2单独区域 同理
  reverseDifference,
  
  /// 得到 path1&path2的交集区域
  intersect,
  
  /// 得到 path1&path2的补集区域
  xor,
  
  /// 得到 path1&path2的并集区域
  union,

}

为了直观体现,我们绘制以下两个path路径的圆形,它们之间有重合部分,也有独立的部分。
两个圆环重叠在一起,那么这时候我们就可以通过上面的五种路径联合方式获取到这两个个圆环中的任意交叉的图。

注:通过联合得到的path路径都是一个闭合路径,原始路径如果是非闭合路径,会默认闭合,再进行联合。
链接:https://juejin.cn/post/7247020241095327805

extendWithPath

用于复制一份之前绘制的路径并平移offset的位置,原路径会和新路径连接。matrix4是对新路径进行一个4D矩阵处理。

var path = Path();
path.moveTo(50, 500);
// 绘制一个三阶贝塞尔曲线
path.cubicTo(50, 200, 300, 400, 350, 150);
// 处理
path.extendWithPath(path, Offset(50, 30),
    matrix4: Float64List.fromList(
        [1, 0, 0, 0, .1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2]));
canvas.drawPath(path, paint);

shift

可用于复制之前绘制的路径并平移offset位置,返回Path。与extendWithPath不同的是,此方法仅仅是复制路径,不会跟原路径相连。

var path = Path();
path.moveTo(50, 500);
path.cubicTo(50, 200, 300, 400, 350, 150);

// 原始图形为红色
paint.color = Colors.red;
canvas.drawPath(path, paint);
// 复制原路径并平移
var path2 = path.shift(Offset(50,20));
paint.color = Colors.yellow;
canvas.drawPath(path2, paint);

close

void close()

用于将路径起点与终点连接起来

绘制圆-drawCircle

canvas.drawCircle(Offset(200,200), 100, paint);

绘制一个环-drawDRRect

void drawDRRect(RRect outer, RRect inner, Paint paint)

  • outer 外部形状,由一个RRect构成(为啥是RRect,因为RRect几乎涵盖了所有的闭合形状,如矩形、正方形、圆角矩形、椭圆、圆)
  • inner 内部形状
// 画个圆角矩形
RRect rrect = RRect.fromRectXY(
    Rect.fromCircle(center: Offset(200, 200), radius: 150), 20.0, 40.0);
// 画个圆
RRect rrect1 = new RRect.fromRectXY(
    Rect.fromCircle(center: Offset(200, 200), radius: 80), 100.0, 100.0);
canvas.drawDRRect(rrect, rrect1, paint);

 绘制文字

Paragraph

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

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

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

// 第一步 生成ParagraphStyle类
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));
// 第二步根据ParagraphStyle生成ParagraphBuilder 

final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle)

//第三步,添加文字。ParagraphBuilder类有个addText方法专门用于接收文字
  ..addText('ParagraphBuilder类接收一个参数,是一个ParagraphStyle类,用于设置字体基本样式,例如字体方向、对齐方向、字体粗细等,下面我们分几个步骤来绘制文字');

// 第四步通过build取到Paragraph类
var paragraph = paragraphBuilder.build();

// 第五步,根据宽高进行布局(layout)
paragraph.layout(ui.ParagraphConstraints(width: 300));
// 画一个辅助矩形(可以通过paragraph.width和paragraph.height来获取绘制文字的宽高)
canvas.drawRect(
    Rect.fromLTRB(50, 50, 50 + paragraph.width, 50 + paragraph.height),
    paint);
// 第六步绘制(paint)
canvas.drawParagraph(paragraph, Offset(50, 50));

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

绘制图片 

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

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

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

这里的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);
  @override
  void paint(Canvas canvas, Size size) async {
    if (picture != null) canvas.drawPicture(picture);
  }
  //...
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值