likes
comments
collection
share

Flutter 的自定义 UI 系列(三)--画布 Canvas

作者站长头像
站长
· 阅读数 21

canvas 的绘制能力

  • Flutter 系统提供的 Canvas 可以绘制常规的几何图形和图形的变换操作

常规的几何图形

点 drawPoints

void drawPoints(PointMode pointMode, List<Offset> points, Paint paint)
  • pointMode: 绘制方式
    • PointMode.points 点,单独绘制每个点
    • PointMode.lines 线段,每两个点连成一条线段
    • PointMode.polygon 线,所有的点连成一条线
  • points: 点的集合
  • paint: 画笔
_paint.strokeCap=StrokeCap.round; 

设置画笔的 strokeCap 的属性为 round 时画出来的是圆点

void drawRawPoints(PointMode pointMode, Float32List points, Paint paint)

drawRawPoints()方法和 drawPoints() 基本类似,传入的点的集合是 Float32List

  • 示例代码
void paint(Canvas canvas, Size size) {

  canvas.drawPoints(PointMode.points, [Offset(0, 0), Offset(30, 0), Offset(60, 0), Offset(90, 0)], _paint);
  
  _paint.strokeCap=StrokeCap.round;
  canvas.drawPoints(PointMode.points, [Offset(0, 50), Offset(30, 50), Offset(60, 50),Offset(90,50)], _paint);
  
  canvas.drawPoints(PointMode.lines,  [Offset(0, 100), Offset(30, 100), Offset(60, 100),Offset(90, 100)], _paint);
  
  canvas.drawPoints(PointMode.polygon,  [Offset(0, 150), Offset(30, 150), Offset(60, 150),Offset(90, 150)], _paint);
  
  Float32List points = Float32List.fromList([0, 200, 30,200,60,200,90,200]);
  canvas.drawRawPoints(PointMode.points, points, _paint);
}
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

线 drawLine

void drawLine(Offset p1, Offset p2, Paint paint)
  • p1:起始点
  • p2:终止点

设置 paintstrokeWidth 可以改变线宽

  • 示例代码
void paint(Canvas canvas, Size size) {

  canvas.drawLine(Offset.zero, Offset(100,0), _paint);

  _paint.strokeWidth=2;  
  canvas.drawLine(Offset(0,70), Offset(100,70), _paint);
}
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

矩形

/// 矩形
void drawRect(Rect rect, Paint paint) 
/// 圆角矩形
void drawRRect(RRect rrect, Paint paint) 
/// 两个矩形裁剪后的圆环
void drawDRRect(RRect outer, RRect inner, Paint paint)

设置 paintstyle 属性,绘制边框还是填充模式

Rect 有多个命名构造函数

  • 以四个边的位置来确定矩形的位置
Rect.fromLTRB(this.left, this.top, this.right, this.bottom) 
  • 从它的左边缘和上边缘、它的宽度和它的高度构造一个矩形
Rect.fromLTWH(double left, double top, double width, double height) 
  • 从其中心点、宽度和高度构造一个矩形
Rect.fromCenter({ required Offset center, required double width, required double height }) 
  • 构造一个包围给定圆的矩形
Rect.fromCircle({ required Offset center, required double radius })  
  • 以左上点和右下点构造一个矩形
Rect.fromPoints(Offset a, Offset b)
  • 示例代码
  @override
  void paint(Canvas canvas, Size size) {

    canvas.drawRect(Rect.fromCenter(center: Offset(30, 50), width: 60, height: 60), _paint);
    
    _paint.style = PaintingStyle.stroke;
    canvas.drawRect(Rect.fromCenter(center: Offset(100, 50), width: 60, height: 60), _paint);

    canvas.drawRRect(RRect.fromRectXY(Rect.fromCenter(center: Offset(170, 50), width: 60, height: 60), 8, 8), _paint);
    
    _paint.style = PaintingStyle.fill;
    var outer = RRect.fromRectXY(Rect.fromCenter(center: Offset(240, 50), width: 60, height: 60), 0, 0);
    var inner = RRect.fromRectXY(Rect.fromCenter(center: Offset(240, 50), width: 50, height: 50), 30, 30);
    canvas.drawDRRect(outer, inner, _paint);
  }
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

圆和弧

void drawCircle(Offset centre, double radius, Paint paint)

drawCircle 绘制圆

  • centre:圆心
  • radius:半径
void drawOval(Rect rect, Paint paint)

绘制椭圆

  • rect 椭圆的范围,椭圆是这个矩形的内切椭圆
void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

弧线和扇形

  • rect:弧线所在椭圆的范围

  • startAngle:开始角度,按弧度单位制,即2*π=360°。 0 为水平最右侧,顺时针方向。

  • sweepAngle:弧线划过的弧度

  • useCenter: 为 true 时是扇形,为 false 时是弧型

  • 示例代码

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawCircle(Offset(50, 50), 50, _paint);
    canvas.drawOval(Rect.fromLTRB(130, 0, 300, 100), _paint);
    canvas.drawArc(Rect.fromLTWH(0, 120, 150, 100), 0, pi/2, false, _paint);
    canvas.drawArc(Rect.fromLTWH(160, 120, 150, 150), 3*pi/2, pi/3, true, _paint);
  }
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

图片 drawImage

void drawImage(Image image, Offset offset, Paint paint)
  • image: 图片源,此 image 并不是Wdiget里Image,而是 dart.ui 下的 Image
  • offset: 图片左上角的位置

获取 ui.Image 的方法

Future<void> loadImage() async {
  image = await _loadImageByProvider(AssetImage("images/coupons.png"));
  setState(() {});
}

Future<ui.Image> _loadImageByProvider(ImageProvider provider, {ImageConfiguration config = ImageConfiguration.empty}) async {
  Completer<ui.Image> completer = Completer<ui.Image>();
  late ImageStreamListener listener;
  ImageStream stream = provider.resolve(config);
  listener = ImageStreamListener((ImageInfo frame, bool sync) {
    final ui.Image image = frame.image;
    completer.complete(image);
    stream.removeListener(listener);
  });
  stream.addListener(listener);
  return completer.future;
}
canvas.drawImageRect(image, src, dst, paint)

选取 image 的一部分到目标区域

  • 示例代码
  @override
  void paint(Canvas canvas, Size size) {
    if (image != null) {
      // 原图片绘制
      canvas.drawImage(image!, Offset.zero, _paint);
      
      // 缩小图片尺寸绘制
      var src = Rect.fromLTWH(0, 0, image?.width.toDouble() ?? size.width, image?.height.toDouble() ?? size.height);
      var dst = Rect.fromLTWH(150, 0, 70, 70);
      canvas.drawImageRect(image!, src, dst, _paint);
    }
  }
  • 示例效果(设置目标区域的大小,可以实现图片缩放绘制)

Flutter 的自定义 UI 系列(三)--画布 Canvas

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

拉伸和压缩部分区域绘制图片 center: 可拉伸或压缩区域 dst : 在画布上的范围

通过绘制两条水平线和两条垂直线来分割图像,将图像绘制成九个部分,其中center参数描述了由这四条线彼此相交的四个点形成的矩形。拉伸或压缩时,四个角保持不变,center对应的横向和纵向可变

  • 示例代码
@override
void paint(Canvas canvas, Size size) {
  if (image != null) {
    var center = Rect.fromLTWH(50, 50, 20, 20);
    var dst = Rect.fromLTWH(0, 0, 128*2, 128);
    canvas.drawImageNine(image!, center, dst, _paint);
    canvas.drawImage(image!, Offset(0,100), _paint);
  }
}
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

颜色与阴影

void drawColor(Color color, BlendMode blendMode)

颜色背景, blendMode:颜色混合模式

void drawShadow(Path path, Color color, double elevation, bool transparentOccluder)
  • path:阴影路径
  • color:阴影颜色
  • elevation:阴影的高度
  • transparentOccluder:官方文档上翻译过来意思是遮挡对象是否透明的。个人理解,因为阴影是一个光源在画布上方被遮挡时产生的,transparentOccluderfalse时,这个遮挡物是不透明的。

Flutter 的自定义 UI 系列(三)--画布 Canvas

  • 示例代码
@override
void paint(Canvas canvas, Size size) {
  Path path = Path();
  var rect = Rect.fromLTWH(10, 10, 150, 45);
  path.addRect(rect);
  canvas.drawShadow(path, Colors.red, 5, false);

  Paint paint = Paint()..color = Colors.red;
  canvas.drawRect(rect, paint);
}
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

Path

Path 路径,是 Canvas 绘制能力中重要的,也是很常用的工具。它是由多个复杂的一维路径组成的,包括点、直线、弧线、贝塞尔曲线,路径可以是开放的也可以是闭合的。Path 可添加子路径的方法和上面的 Canvas 绘制的方法类似,主要还是点、线、矩形、圆、弧这些图形。

一些比较简单的 API

//移动画笔到指定位置
void moveTo(double x, double y)  
//从当前位置连接一条直线到指定位置
void lineTo(double x, double y)
//添加一个矩形到 path
void addRect(Rect rect)
//添加圆角矩形
void addRRect(RRect rrect)
//添加一个椭圆到 path,如果 Rect 是一个正方形的话,那么添加的会是一个正圆
void addOval(Rect oval)
//添加一个弧线到 path,是弧度制单位
void addArc(Rect oval, double startAngle, double sweepAngle)
//添加一些点到 path,这些点将连成一条线段。[close] 是否闭合,如果为true,最后一个点将和第一个点连线
void addPolygon(List<Offset> points, bool close)
//添加其他 path 到当前的 path
void addPath(Path path, Offset offset, {Float64List? matrix4})
//重置 Path,将 path 恢复为最初状态,以当前点作为起始点
void reset()
//闭合图像
void close()
  • 示例代码
@override
void paint(Canvas canvas, Size size) {
  var path = Path();
  path.moveTo(10,10);
  path.lineTo(100, 10);
  path.lineTo(100, 60);
  path.lineTo(10, 60);
  // 最后将从当前位置连线到最开始的点
  path.close();
  canvas.drawPath(path, _paint);
}

弧线

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

画一条弧线,forceMoveTo的意思是画这个弧的时候是拖着笔到起点还是抬下笔到起点

@override
void paint(Canvas canvas, Size size) {
  var path = Path();
  path.moveTo(0,0);
  path.lineTo(100,0);
  path.arcTo(Rect.fromLTWH(0, 0, 100, 100), 0, pi, true);
  canvas.drawPath(path, _paint);
}

Flutter 的自定义 UI 系列(三)--画布 Canvas

贝塞尔曲线

// 二阶曲线
void conicTo(double x1, double y1, double x2, double y2, double weight)

绘制贝塞尔曲线

  • x1y1: 控制点
  • x2y2: 曲线目标定点
  • weight:权重,大于1,则曲线为双曲线; 如果权重等于 1,则为抛物线; 如果小于 1,则为椭圆
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)

三阶贝塞尔曲线

  • x1y1: 控制点1
  • x2y2: 控制点2
  • x3y3: 曲线目标定点
  • 示例代码
  @override
  void paint(Canvas canvas, Size size) {
    var path = Path();
    path.moveTo(80, 40);
    path.conicTo(160, 120, 240, 40,1);
    canvas.drawPath(path, _paint);

    var path2 = Path();
    path2.moveTo(80, 160);
    path2.cubicTo(120, 200, 160, 100, 240, 160);
    canvas.drawPath(path2, _paint);
  }
  • 示例效果 Flutter 的自定义 UI 系列(三)--画布 Canvas

相对坐标

void relativeMoveTo(double dx, double dy)
void relativeLineTo(double dx, double dy)

相对当前位置的移动,还有很多 relative*** 的方法都是相对当前位置的方法。

path的边界

Rect getBounds()

获取 path 的边界范围,返回一个 Rect。

  • 示例代码
@override
void paint(Canvas canvas, Size size) {

  var path2 = Path();
  path2.moveTo(80, 80);
  path2.lineTo(160, 80);
  path2.arcTo(Rect.fromLTWH(160, 80, 80, 80), 0, pi, false);
  path2.lineTo(0, 80);
  path2.close();
  canvas.drawPath(path2, _paint);
  // path 的边界
  var bounds = path2.getBounds();
  _paint.color=Colors.black;
  canvas.drawRect(bounds, _paint);
}
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

PathMetric

PathMetrics computeMetrics({bool forceClosed = false})

返回改path的 PathMetric 对象集合

PathMetric 是测量和提取路径的工具,可以获取path的很多信息

  • PathMetric.length: 当前 path 的总长度
  • PathMetric.isClosed: 当前 path 的是否闭合路径
  • PathMetric.contourIndex: 当前 path 的索引
  • Tangent? getTangentForOffset(double distance): 返回给点长度的点的信息。

    比如,一个 path 的 length =100,getTangentForOffset(50)方法,会计算出长度为50时的点的位置和角度信息,角度是该点切线与x轴正方向之间的夹角。

  • 示例代码
@override
void paint(Canvas canvas, Size size) {
  var path = Path();
  path.moveTo(0, 0);
  path.addOval(Rect.fromLTWH(0, 0, 160, 160));
  canvas.drawPath(path, _paint);
  PathMetric pathMetric = path.computeMetrics().first;

  print(pathMetric.length);//path的长度 501
  print(pathMetric.isClosed);// true
  print(pathMetric.contourIndex);// 0
  var tangentForOffset = pathMetric.getTangentForOffset(100);
  _paint.strokeWidth=10;
  canvas.drawPoints(ui.PointMode.points, [tangentForOffset!.position], _paint);
  print(tangentForOffset.angle);
}
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

Canvas 的裁剪

void clipPath(Path path, {bool doAntiAlias = true})
void clipRect(Rect rect, { ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true })
void clipRRect(RRect rrect, {bool doAntiAlias = true})

画布裁剪,裁剪指定的部分,在剩下的部分绘制

  • clipOp: ClipOp.difference,减去目标区域;ClipOp.intersect 保留目标区域

  • doAntiAlias: 是否抗锯齿效果

  • 示例代码

@override
void paint(Canvas canvas, Size size) {
  canvas.clipRect(Rect.fromLTWH(50, 50, 50, 50), clipOp: ui.ClipOp.difference);
  canvas.drawImageRect(
    image!,
    Rect.fromLTWH(0, 0, image!.width.toDouble(), image!.height.toDouble()),
    Rect.fromLTWH(0, 0, 200, 200),
    _paint,
  );
}
  • 示例效果 Flutter 的自定义 UI 系列(三)--画布 Canvas

Canvas 的变换

Canvas 提供了4种变换操作,位移、旋转、缩放、斜切,其效果类似于系统提供的 Transform 组件,在熟悉这四种变换操作前,要先了解 Flutter 中的坐标系。

坐标系

Flutter 中坐标系是当前画布的左上角为原点,向左为 x 轴正向,向下为 y 轴正向。角度是弧度制,以距离原点水平位置右侧为 0 度,顺顺时针方向。即 pi 为原点左侧水平方向。

Flutter 的自定义 UI 系列(三)--画布 Canvas

位移 translate

void translate(double dx, double dy)

移动画布原点到指定的位置,dxdy可以为负数

Flutter 的自定义 UI 系列(三)--画布 Canvas

缩放 scale

void scale(double sx, [double? sy])

缩放操作,sxsy分别是x轴和y轴方向上的缩放比例 ,如果未指定sy ,则sx将用于两个方向的比例。sxsy可以为负数,为负数时会产生对向旋转的效果。

如图所示,黑色为原坐标系,红色为变换后的坐标系。

Flutter 的自定义 UI 系列(三)--画布 Canvas

旋转 rotate

void rotate(double radians)

以原点为中心旋转画布,radians:旋转角度,弧度制,即 pi=180º

如图所示,黑色为原坐标系,红色为变换后的坐标系。 Flutter 的自定义 UI 系列(三)--画布 Canvas

斜切 skew

void skew(double sx, double sy)

延x或y轴方向做斜切的变换,sxsy 为x轴和y轴方向切度值,值为三角函数中的 tan 值,即 45 度时 tan 值为 1

如图所示,黑色为原坐标系,红色为变换后的坐标系。 Flutter 的自定义 UI 系列(三)--画布 Canvas

save 和 restore

原文档:Saves a copy of the current transform and clip on the save stack. 即有个保存画布状态的栈,当调用save时,会把当前画布保存到栈中。调用restore时,会把从栈中取出画布状态。因为用栈保存,所以 Canvas 的 saverestore,有如下特性

  • 成对出现的,即有一个 save,就需要有一个 restore
  • 因为保存在栈中,所以有栈的特点-后进先出,即有多个 save ,调用 restore,会从最后一个save的开始。
  • 示例代码
@override
void paint(Canvas canvas, Size size) {
  // 画个直角坐标系
  _canvasCoordinates(canvas);
  //画个矩形
  canvas.drawRect(Offset.zero & Size(80, 50), _paint);
  //保存状态 1
  canvas.save();
  canvas.rotate(pi);
  canvas.translate(0, 20);
  canvas.skew(0, 1);
  _paint.color = Colors.red;
  canvas.drawRect(Offset.zero & Size(80, 50), _paint);
  // 恢复状态1
  canvas.restore();
  
  // 再次做变换操作时
  // 因为上面已经做了 restore 操作,所以这里的坐标原点还是最初始的原点
  canvas.translate(-100, 0);
  _paint.color = Colors.green;
  canvas.drawRect(Offset.zero & Size(80, 50), _paint);
}
  • 示例效果

Flutter 的自定义 UI 系列(三)--画布 Canvas

总结

Canvas 的功能非常强大,其中还有些api是我比较模糊的,待以后慢慢补充。下一篇文章将会用做一些我们开发中常见的 UI 需求,用实际的代码来展示 Canvas 强大的功能。

转载自:https://juejin.cn/post/7022914592779010055
评论
请登录