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

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 必知必会系列 —— 随心所欲的自定义绘制相关的知识,希望对你有一定的参考价值。

作者:Time_sun

在 Flutter 中,framework 为我们提供了丰富的组件,一些常见的功能和样式都有组件直接提供,比如圆角、颜色、透明度、间距等等。

然而当组件中有许多设计的元素时,就需要我们拿着画笔自定义绘制了。比如下面这样的:

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

本篇文章就告诉大家 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 的画布上进行了,绘制直线。

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

画布变换操作

平移画布

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

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

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

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

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

缩放画布

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

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

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

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

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

斜切画布

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

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

基本绘制

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

绘制点相关

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

运行效果:

绘制线相关

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

运行效果:

绘制矩形相关

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

运行效果:

绘制圆相关

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

运行效果:

绘制椭圆形相关

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

运行效果:

绘制圆弧

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

运行效果:

总结

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

以上是关于Flutter 必知必会系列 —— 随心所欲的自定义绘制的主要内容,如果未能解决你的问题,请参考以下文章

必知必会的设计原则——合成复用原则

测试必知必会系列- Linux常用命令 - tar

设计模式必知必会系列终章

大数据必知必会系列——面试官问能不能手写一个spark程序?[新星计划]

大数据必知必会系列__面试官问能不能徒手画一下你们的项目架构[新星计划]

大数据必知必会系列——萌新提问怎么定义HiveUDF函数?能否给个示例[新星计划]