likes
comments
collection
share

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

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

在 Flutter 中,framework 为我们提供了丰富的组件,一些常见的功能和样式都有组件直接提供,比如圆角、颜色、透明度、间距等等。 然而当组件中有许多设计的元素时,就需要我们拿着画笔自定义绘制了。比如下面这样的:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

这个时候我们无法使用既有的组件组装成上面的效果,那我们就需要自己绘制成这样的效果。

本篇文章就告诉大家 Flutter 中怎么绘制自定义的显示内容。

绘制前的准备

绘制组件 CustomPaint

绘制一般考虑三个要素:画布(Canvers)、画笔(Paint)、内容(需求)

在 Android 中,我们需要自定义绘制的话,需要继承自 View,然后重写 onDraw 方法。在 Flutter 中,Widget 只是配置的概念,并没有绘制的功能。

那难道需要自定义一组:Widget、Element、RanderObject 吗? 大家不用担心。Flutter 帮我们做好了前期的准备工作,我们只需要拿着笔在画布上画我们想要显示的内容。这个组件就是 CustomPaint。如下图:

const CustomPaint({
  Key? key,
  this.painter,
  this.foregroundPainter,
  this.size = Size.zero,
  this.isComplex = false,
  this.willChange = false,
  Widget? child,
}) : assert(size != null),
     assert(isComplex != null),
     assert(willChange != null),
     assert(painter != null || foregroundPainter != null || (!isComplex && !willChange)),
     super(key: key, child: child);

CustomPainter? painter : 是背景内容的绘制,会在 child 的下一层 CustomPainter? foregroundPainter : 是前景内容的绘制,会在 child 的上一层 Size size : CustomPaint 组件的尺寸,如果设置了 child 属性,那么 size 属性就会被无视,CustomPaint 的 size 就是 child 的尺寸 bool willChange:绘制是否可能在下一帧中改变

一般情况下,我们只需要设置 painter 和 child 就够了。

绘制舞台 CustomPainter

绘制的舞台是 CustomPainter ,包含了 画布 和 画笔🖌️。CustomPainter 是抽象类,我们需要自定义子类。如下:

class MyPainter extends CustomPainter{
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
  
}

主要是两个方法:

paint() : 真正的绘制方法,提供了画布 shouldRepaint() : 是否重绘

paint() 的入参是:画布(Canvas)尺寸(Size)。画布和其他平台是一样的,都是可以直接用来绘制的实例对象。尺寸 要么是 child 属性的大小,要么是参数 size 的大小。

绘制的画布 Canvas

Canvas 可以记录图形化的操作,比如裁剪、缩放、平移、绘制等等,结合 Canvas 和 转换等 API 可以是画出更加复杂的图形。其实我也不理解,为啥用画布画画~~。

下面就开始画画了~

操作 Canvas

画布状态操作

Canvas 维护了一个状态栈,用于保存和回退状态。比如,先对画布进行保存,然后对画布尽心裁剪,那么后续所有的绘制都是在裁剪之后的画布上进行绘制。这个时候想要在原画布进行绘制,那么就需要回退到裁剪之前的状态

注意一下:保存和回退必须成对出现

save()  保存状态

restore() 回退状态

void paint(Canvas canvas, Size size) {
  canvas.save(); //第一处
  canvas.clipRRect(RRect.fromRectXY(Offset.zero & (size / 2.0), 50.0, 50.0)); //第二处
  canvas.drawPaint(Paint()..color = Colors.white); //第三处
  canvas.restore(); //第四处
  canvas.save(); //到五处
  canvas.clipRRect(RRect.fromRectXY(size.center(Offset.zero) & (size / 2.0), 50.0, 50.0)); //第六处
  canvas.drawPaint(new Paint()..color = Colors.redAccent); //第七处
  canvas.restore(); //第八处
  canvas.drawLine(Offset(0, 0), Offset(100, 100), new Paint()..color = Colors.redAccent);//第九处
 
 }

第一处:对当前的画布状态进行了保存。 当前 状态A

第二处和第三处:对保存的画布进行了裁剪,画布变成了左上角的圆角矩形。在裁剪好的画布上进行了 绘制白色。

第四处:对画布进行了回退,此时画布变成了 状态A

第五处:对画布进行了保存,当前 状态A

第六处和第七处:对保存的画布 进行了裁剪,画布变成了右下角的圆角矩形。在裁剪好的画布上进行了 绘制红色。

第八处:对画布进行了回退。此时画布变成了 状态 A

第九处:在 状态A 的画布上进行了,绘制直线。

以上代码的实际绘制效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

画布变换操作

平移画布

translate(double dx, double dy) 平移画布

平移就是改变画布的原点,dx 是 x 的值,右为正,左为负。dy 是 y 的值,上为负,下为正

void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 20;
 
  //平移之前
  canvas.drawPoints(PointMode.points, [Offset(0, 0)], _paint);
  canvas.translate(200, 200);
  //平移之后
  canvas.drawPoints(PointMode.points, [Offset(0, 0)], _paint);
}

以上代码的实际绘制效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

我们看到 代码咩有变化,但是实际的原点发生了变化。

有人可能疑问,为什么左上角的小方块比较小?

因为小圆点的大小是 20*20(strokeWidth=20),但是 小圆点的中心点是 原点(0,0)。所以 第一个就是 1/4 的小圆点大小了。

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

缩放画布

scale(double sx, [double sy]) 缩放画布

这里值得注意一下,是将坐标系统的x、y都进行了缩放。 不仅仅是 控件的大小,连控件的位置都会发生变化。

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 20;
 
  //最起初的状态 :长宽20*20 位置在原点(100,100)
  canvas.drawPoints(PointMode.points, [Offset(100, 100)], _paint);
 
  //先扩大两倍 :长宽40*40 位置在(200,200)
  canvas.scale(2, 2);
  canvas.drawPoints(PointMode.points, [Offset(100, 100)], _paint);
 
  //再扩大1.5倍 : 长宽60*60 位置在(300,300)
  canvas.scale(1.5, 1.5);
  canvas.drawPoints(PointMode.points, [Offset(100, 100)], _paint);
}

以上代码的实际绘制效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

想象一下?既然坐标都可以变化,那么是不是轴对称就可以实现了呢?

canvas.scale(1, -1);沿X轴镜像 canvas.scale(-1, 1);沿Y轴镜像 canvas.scale(-1, -1);沿原点镜像

大家可以试试。

旋转画布

rotate(double radians) 旋转画布

注意一下:参数是弧度制

旋转的中心是原点。注意:平移会对原点产生影响。

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 5;
  canvas.translate(200, 200);
  canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint);
  //旋转默认是原点
  //现在的原点是平移影响之后的(200,200)
  canvas.rotate(pi / 2);
  canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint..color = Colors.greenAccent);
}

以上代码的实际绘制效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

斜切画布

skew(double sx, double sy)  斜切

斜切相对来说很复杂,基本公式如下:

现有坐标 (x,y) ,新坐标X,Y, 

X = x + sx * y Y = y+sy * x

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 5;
  canvas.translate(200, 200);
  canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint);
  //斜切
  canvas.skew(0, 1);
  //(0,0) 新坐标 还是(0,0)
  // (100,100) 新坐标 (100,200)
  // x = 100 + 0*100
  // y = 100 + 1*100
  canvas.drawLine(Offset(0, 0), Offset(100, 100), _paint..color = Colors.greenAccent);
}

以上代码的实际绘制效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

基本绘制

限于篇幅,这一篇我们只介绍基本的绘制。更高级的绘制我们后续接着介绍。

绘制点相关

drawPoints(PointMode pointMode, List points, Paint paint)

pointMode: 点的绘制模式

                     PointMode.points: 点模式   绘制出来一个个的点

                     PointMode.lines: 线模式   每两条线绘制成一条线,在线模式下,如果点的个数是奇数个,那么最后一个点不绘制。

                     PointMode.polygon: 线模式   将众多的点连接成一个多边形 points:点的位置 paint:绘制点的画笔,画笔可以设置颜色和样式

示例代码:

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 2;
  canvas.translate(100, 0);
  List<Offset> offsets = [
    Offset(0, 0),
    Offset(10, 10),
    Offset(30, 30),
    Offset(50, 70),
    Offset(30, 70),
  ];
  canvas.drawPoints(PointMode.points, offsets, _paint);
  canvas.translate(0, 200);
  //只有两个折线,最后一个点不绘制
  canvas.drawPoints(PointMode.lines, offsets, _paint);
  canvas.translate(0, 200);
  canvas.drawPoints(PointMode.polygon, offsets, _paint);
}

运行效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

绘制线相关

drawLine(Offset p1, Offset p2, Paint paint)

p1:  起点的坐标 p2: 终点的坐标 paint:绘制线的画笔

示例代码:

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 5;
  canvas.translate(100, 0);
  canvas.drawLine(Offset(10, 10), Offset(50, 70), _paint);
}

运行效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

绘制矩形相关

drawRect(Rect rect, Paint paint)

Rect: 矩形的封装类,包含了矩形的位置、矩形的大小。

Flutter提供了多种生成矩形 Rect 的方式。

根据四个点的坐标 Rect.fromLTRB(this.left, this.top, this.right, this.bottom)

根据左上角顶点坐标和宽高 Rect.fromLTWH(double left, double top, double width, double height)

根据内切圆 ,注意生成的是正方形 Rect.fromCircle({ Offset center, double radius })

根据左上角和右下角对角线 Rect.fromPoints(Offset a, Offset b)

示例代码:

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 5;
  canvas.translate(100, 0);
 
  //根据四个点生成
  canvas.drawRect(Rect.fromLTRB(20, 100, 80, 200), _paint);
 
  //根据定点和宽高
  canvas.drawRect(Rect.fromLTWH(100, 100, 60, 100), _paint);
 
  //根据中心和宽高
  canvas.drawRect(Rect.fromCenter(center: Offset(50, 300), width: 60, height: 100), _paint);
 
  //根据内接圆
  canvas.drawRect(Rect.fromCircle(center: Offset(130, 300), radius: 20), _paint);
 
  //根据对角线坐标
  canvas.drawRect(Rect.fromPoints(Offset(20, 410), Offset(80, 510)), _paint);
}

运行效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

绘制圆相关

drawCircle(Offset c, double radius, Paint paint),

c 是圆心、radius 是半径,这两个元素构成了圆形。

示例代码:

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 5;
  canvas.translate(100, 0);
 
  canvas.drawCircle(Offset(20, 80), 40, _paint);
}

运行效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

绘制椭圆形相关

drawOval(Rect rect, Paint paint)

rect 是椭圆的外接矩形,因此椭圆的长短轴之比就是矩形的宽高比

示例代码:

@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 5;
  canvas.translate(100, 0);
 
  canvas.drawOval(Rect.fromCenter(center: Offset(20, 80), width: 20, height: 40), _paint);
 
  canvas.drawOval(Rect.fromCenter(center: Offset(60, 80), width: 40, height: 20), _paint);
}

运行效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

绘制圆弧

drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

rect :是椭圆的外接矩形,因此椭圆的长短轴之比就是矩形的宽高比。

startAngle :开始绘制圆环的角度,注意: X 轴的角度是0度, 并且必须是弧度制。

sweepAngle :圆环的角度大小,也是弧度制

useCenter :终点是否和圆心连接起来

示例代码:


@override
void paint(Canvas canvas, Size size) {
  Paint _paint = Paint()
    ..color = Colors.redAccent
    ..strokeWidth = 5;
  canvas.translate(100, 0);
 
  canvas.drawArc(
      Rect.fromCenter(center: Offset(60, 80), width: 80, height: 40),
      pi / 2,
      pi / 2 + pi / 4,
      true,
      _paint);
 
  canvas.drawArc(
      Rect.fromCenter(center: Offset(60, 180), width: 80, height: 40),
      pi / 2,
      pi / 2 + pi / 4,
      false,
      _paint);
 
 
  _paint.style = PaintingStyle.stroke;
  canvas.drawArc(
      Rect.fromCenter(center: Offset(60, 260), width: 80, height: 40),
      pi / 2,
      pi / 2 + pi / 4,
      false,
      _paint);
}

运行效果:

Flutter 必知必会系列 —— 随心所欲的自定义绘制 I

总结

上面就是 CustomPaint 组件的基本使用,涉及到绘制的还有贝塞尔曲线,文本、图片、动画。这些内容我们放到后面的章节继续介绍,依靠已经介绍的 API,已经可以画表格咯~。