Skip to content

Flutter绘制基础

豆浆油条 edited this page Sep 23, 2019 · 18 revisions

有两个类做这件事情:

  • CustomPaint :会在绘制阶段提供一个 Canvas 画布
  • CustomPainter : 具体的画笔, 可配置画笔的颜色,路径等

CustomPaint

构造方法:

const CustomPaint({
    Key key,
    this.painter,
    this.foregroundPainter,
    this.size = Size.zero,
    this.isComplex = false,
    this.willChange = false,
    Widget child,
  }) :super(key: key, child: child);
  我们只需要关心三个参数,painter,foregroundPainter 和 child  , 这里需要说明一下,painter 是绘制的 backgroud 层,而child 是在backgroud之上绘制,foregroundPainter 是在 child 之上绘制,所以这里就有了个层级关系,这跟android里面的backgroud与foreground是一个意思,那这两个painter的应用场景是什么呢?假如你只是单纯的想绘制一个图形,只用painter就可以了,但是如果你想给绘制区域添加一个背景(颜色,图片,等等),这时候如果使用 painter是会有问题的,painter的绘制会被child 层覆盖掉,此时你只需要将painter替换成foregroundPainter,然会颜色或者图片传递给child即可。
  如果是Android绘制几何图形,应该是重写View的onLayout() 和 onDraw方法,但是Flutter实现绘制,必须继承CustomPainter并重写 paint(Canvascanvas, Size size)和 shouldRepaint (CustomPainteroldDelegate) 方法 ,第一个参数canvas就是我们绘制的画布了(跟Android一模一样),paint第二个参数Size就是上面CustomPaint构造方法传入的size, 决定绘制区域的宽高信息

CustomPainter

Flutter 中实现绘制的主要是CustomPainter类、

我们一般继承这个类,来使用它;

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

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

然后放在父控件的child里用CustomPaint包裹

child:  CustomPaint(
    size: Size(200,200),
    painter: MyPainter())

Canvas(画布)

Canvas.drawXXX() 系列方法和 Paint 的基础掌握了,就能够进行简单的绘制需求。

Canvas 类下的所有 draw- 开头的方法,例如 drawCircle() drawArc()

方法 功能
drawLine() 画直线
drawCricle() 画圆
drawOval() 画椭圆
drawRect() 画矩形
drawPoints() 画点
drawArc() 画圆弧

Paint (画笔)

属性名 类型 参考值 功能
color Colors Colors.blueAccent 画笔颜色
strokeCap StrokeCap StrokeCap.round 画笔笔触类型
isAntiAlias bool true 是否启动抗锯齿
blendMode BlendMode BlendMode.exclusion 颜色混合模式【官网可查】
style PaintingStyle PaintingStyle.fill 绘画样式,默认为填充
colorFilter ColorFilter ColorFilter.mode(Colors.blueAccent,BlendMode.exclusion) 颜色渲染模式,一般是矩阵效果来改变的,Flutter只能使用颜色混合模式
maskFilter MaskFilter MaskFilter.blur(BlurStyle.inner,3.0) 模糊遮罩效果,Flutter只有这个
filterQuality FilterQuality FilterQuality.high 颜色渲染模式的质量
stokeWidth double 16.0 画笔的粗细
Paint _paint = Paint()
  ..color = Colors.deepOrange//画笔颜色
  ..strokeCap = StrokeCap.round //画笔笔头类型
  ..isAntiAlias = true //是否开启抗锯齿
  ..blendMode = BlendMode.src//颜色混合模式
  ..style = PaintingStyle.fill //画笔样式,默认为填充
  ..colorFilter = ColorFilter.mode(Colors.blueAccent,
      BlendMode.src) //颜色渲染模式
  ..maskFilter = MaskFilter.blur(BlurStyle.inner, 3.0) //模糊遮罩效果
  ..filterQuality = FilterQuality.high //颜色渲染模式的质量
  ..strokeWidth = 5.0; //画笔的宽度复制代码

好了,基础介绍完了,大家可以直接点击下方,打开Flutter官方文档查看原汁原味的资料哦

1.Canvas

2.Paint

填充颜色 - drawColor(Color color, BlendMode blendMode)

这个方法一般用于画板底色填充及蒙版(使用蒙版时要注意图片混合模式设为BlendMode.srcOver)

canvas.drawColor(Color.fromARGB(80, 255, 0, 0), BlendMode.srcOver);

画点 - drawPoints(PointMode pointMode, List points, Paint paint)

第一个参数是点的模式,分三种

  • PointMode.points
  • PointMode.lines
  • PointMode.polygon

第二个参数是一个点的集合,第三个参数...emmm,好了话不多说,上图

PointMode.points - 点模式

List<Offset> points = new List();
points.add(new Offset(100, 100));
points.add(new Offset(125, 200));
points.add(new Offset(50, 150));
points.add(new Offset(150, 150));
points.add(new Offset(75, 200));
canvas.drawPoints(PointMode.points, points, _paint);

PointMode.lines - 情侣模式

canvas.drawPoints(PointMode.points, points, _paint);

PointMode.polygon - 连线模式

canvas.drawPoints(PointMode.polygon, points, _paint);

画一条直线 - drawLine(Offset p1, Offset p2, Paint paint)

p1线的起点,p2线的终点

canvas.drawLine(Offset(100, 100), Offset(200, 200), _paint);

Flutter并没有提供画多条线的方法,只能多写几次 drawLine() 或者使用 drawPoints() 的情侣模式

画圆弧或扇形 - drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

第一个参数仍然是一块矩形区域,第二个参数 startAngle 与第三个参数 sweepAngle 需要注意是弧度制,要转化一下  乘以个 (pi / 180.0)就行了度(下图中红线是 0 度的位置;顺时针为正角度,逆时针为负角度),第四个参数 useCenter 代表是否与圆心连接,

canvas.drawArc(new Rect.fromLTWH(50, 50, 100, 50), 0.0 * (pi / 180.0), 90 * (pi / 180.0), false, _paint);
canvas.drawArc(new Rect.fromLTWH(50, 50, 100, 50), 200.0* (pi / 180.0), 90 * (pi / 180.0), true, _paint);
_paint.style = PaintingStyle.stroke; // 画线模式
canvas.drawArc(new Rect.fromLTWH(50, 50, 100, 50), 100.0* (pi / 180.0), 90 * (pi / 180.0), false, _paint);

以上就是 Canvas 所有的简单图形的绘制。除了简单图形的绘制, Canvas 还可以使用drawPath(Path path, Paint paint)来绘制自定义图形。

  • 弧度

根据定义,一周的弧度数为2πr/r=2π,360°角=2π弧度,因此,1弧度约为57.3°,即57°17’44.806’’,1°为π/180弧度,近似值为0.01745弧度,周角为2π弧度,平角(即180°角)为π弧度,直角为π/2弧度。

  • 特殊弧度
弧度
0
30° π/6
45° π/4
60° π/3
90° π/2
120° 2π/3
180° π
270° 3π/2
360°

画矩形 - drawRect(Rect rect, Paint paint)

canvas.drawRect(new Rect.fromLTWH(10, 50, 50, 50),_paint);
_paint.style = PaintingStyle.stroke; // 中途将画笔风格设为环形
canvas.drawRect(new Rect.fromLTWH(120, 50, 50, 50),_paint);

这里涉及了Rect的创建的方式:

Rect的创建方式是多样的。包括:

  • fromPoints(Offset a ,offset b): 左上和右下角坐标来确定内切矩形的大小和位置。
  • fromCircle({Offset center,double radius}):圆形的中心点坐标和半径确定外切矩形的大小和位置
  • fromLTRB(left,top,right,bottom): 使用矩形的上下左右的X、Y边界值来确定矩形的大小和位置
  • fromLTWH(left,top,width,height): 使用矩形的左上角的x、y坐标及矩形的宽高来确定矩形的大小和位置

画圆角矩形 - drawRRect(RRect rrect, Paint paint)

canvas.drawRRect(RRect.fromLTRBR(50, 50, 200, 100, Radius.circular(10.0)), _paint);
canvas.drawRRect(RRect.fromLTRBR(50, 150, 200, 250, Radius.elliptical(10.0, 30.0)), _paint);

下面这个矩形看似纵向拉伸过了,其实没有,只是圆角模式是 elliptical (椭圆)

绘制嵌套矩形 - drawDRRect(RRect outer, RRect inner, Paint paint)

和drawRRect类似,使用RRect确定内部、外部矩形大小及弧度,使用paint来完成绘制。 第一个参数的区域必须包括第二个参数的区域,否则无法显示;

canvas.drawDRRect(RRect.fromLTRBR(50, 50, 200, 100, Radius.circular(10.0)),
    RRect.fromLTRBR(60, 60, 190, 90, Radius.circular(10.0)), _paint);

class MyPainter extends CustomPainter {
  
  ///[定义画笔]
  Paint _paint = Paint()
  ..color = Colors.blueAccent //画笔颜色
  ..strokeCap = StrokeCap.round//画笔笔触类型
  ..isAntiAlias = true //是否启动抗锯齿
  ..style = PaintingStyle.stroke //绘画风格,默认为填充
  ..strokeWidth = 5.0; //画笔的宽度

  @override
  void paint(Canvas canvas, Size size) {
      
   //绘制两个矩形
    Rect rect1 = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 60.0);
    Rect rect2 = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 40.0);

    //分别绘制外部圆角矩形和内部的圆角矩形
    RRect outer = RRect.fromRectAndRadius(rect1, Radius.circular(10.0));
    RRect inner = RRect.fromRectAndRadius(rect2, Radius.circular(10.0));
    canvas.drawDRRect(outer, inner, _paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return null;
  }
}

@override
  void paint(Canvas canvas, Size size) {
      
   //绘制两个矩形
    Rect rect1 = Rect.fromCircle(center: Offset(150.0, 40.0), radius: 60.0);
    Rect rect2 = Rect.fromCircle(center: Offset(150.0, 40.0), radius: 40.0);

    //分别绘制外部圆角矩形和内部的圆角矩形
    RRect outer = RRect.fromRectAndRadius(rect1, Radius.circular(30.0));
    RRect inner = RRect.fromRectAndRadius(rect2, Radius.circular(10.0));
    canvas.drawDRRect(outer, inner, _paint);
  }

画圆 - drawCircle(Offset c, double radius, Paint paint)

第一个参数 c 是圆心点的坐标,直接new一个Offset出来填入x,y坐标就完事了,第二个参 radius 是圆的半径,paint不必多说;

canvas.drawCircle(new Offset(200, 200), 100, _paint);

坐标系是以控件左上角开始的,跟初中学的坐标系可不一样;

画椭圆 - drawOval(Rect rect, Paint paint)

这个方法和drawRect()是一毛一样的,只不过一个画的是矩形,一个是椭圆

canvas.drawOval(Rect.fromLTWH(50, 50, 100, 50), _paint);
_paint.style = PaintingStyle.stroke;
canvas.drawOval(Rect.fromLTWH(170, 50, 100, 50), _paint);

画路径 - drawPath(Path path, Paint paint)

这个方法通过描述路径的方式来绘制图形,用法大概是这样:

 Path _path = Path();
  @override
  void paint(Canvas canvas, Size size) {
    _paint.style = PaintingStyle.stroke; // 画线模式
    _path.addArc(new Rect.fromLTWH(50, 50, 50, 50), 135.0 * (pi / 180.0), 225.0 * (pi / 180.0));
    _path.addArc(new Rect.fromLTWH(100, 50, 50, 50), 180.0 * (pi / 180.0), 225.0 * (pi / 180.0));
    _path.lineTo(100, 140);
    _path.lineTo(58, 93);
    canvas.drawPath(_path, _paint);
  }

主要方法 功能
moveTo 将路径起始点移动到指定的位置
lineTo 从当前位置连接到指定点
arcTo 曲线
conicTo 贝济埃曲线
close 关闭路径,连接路径的起始点

Path 主要有方法如下:

直接描述路径的方法还可以细分为两组:添加子图形和画线(直线或曲线)

  • addXXX() - 添加子图形(由于此类方法参数与上面介绍的画简单图形一样,就不多赘述了)
  1. addArc(Rect oval, double startAngle, double sweepAngle) - 添加圆弧
  2. addOval(Rect oval) - 添加圆
  3. addPolygon(List<Offset> points, bool close) - 添加一个由点的集合描述的多边形
  4. addRect(Rect rect) - 添加矩形
  5. addRRect(Rect rect) - 添加圆角矩形
  6. addPath(Path path, Offset offset) - 添加子路径
  • XXXTo() - 画线(直线或曲线)

向目标位置画直线 - lineTo(double x, double y) / relativelineTo(double x, double y)

当前位置向目标位置画一条直线, x 和 y 是目标位置的坐标。这两个方法的区 别是, lineTo(x, y) 的参数是绝对坐标,而 relativeLineTo(x, y) 的参数是相对当前位置的相对坐标

_paint.style = PaintingStyle.stroke; // 画线模式
_path.lineTo(100, 100); // 由当前位置 (0, 0) 向 (100, 100) 画一条直线
_path.relativeLineTo(100, 0); // 由当前位置 (100, 100) 向正右方画100像素的位置
canvas.drawPath(_path, _paint);

画二阶贝塞尔曲线 - quadraticBezierTo(double x1, double y1, double x2, double y2) /  relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) 

x1,y1是控制点的坐标;x2,y2是结束点的坐标;relativeQuadraticBezierTo()同上面相对直线方法

_paint.style = PaintingStyle.stroke; // 画线模式
List<Offset> points = new List();
points.add(new Offset(100, 50)); // 画出控制点位置,方便理解
canvas.drawPoints(PointMode.points, points, _paint);
_path.moveTo(0, 100); // 移动起点到(0,100)
_path.quadraticBezierTo(100, 50, 200, 100);
canvas.drawPath(_path, _paint);

画三阶贝塞尔曲线 - cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) / relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) 

和上面这个 quadraticBezierTo()relativeQuadraticBezierTo() 的二阶贝塞尔曲线同理,就不多说了。

移动到某点 - moveTo(double x, double y) / relativeMoveTo(double dx, double dy)

不论是直线还是贝塞尔曲线,都是以当前位置作为起点,而不能指定起点。但可以通过 moveTo(x, y) 或 relativeMoveTo(x, y) 来改变当前位置,从而间接地设置这些方法的起点。

_paint.style = PaintingStyle.stroke; // 画线模式
_path.moveTo(20, 40); // 移动起点到(20,40)
_path.lineTo(80, 100); // 画条斜线
_path.moveTo(100, 40); // 移动起点到(100,20)
_path.lineTo(100, 100); // 画条直线
canvas.drawPath(_path, _paint);

但凡事都有例外 arcTo() 这个方法并不从当前位置开始绘制

画弧线 - arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)

前三个参数,我们已经很熟悉了,最后一个参数的意思是,画这个弧的时候是拖着笔到起点还是抬下笔到起点

拖着笔

抬着笔

封闭当前路径 - close()

_paint.style = PaintingStyle.stroke; // 画线模式
_path.moveTo(20, 40); // 移动起点到(20,20)
_path.lineTo(80, 100); // 画条斜线
_path.arcTo(new Rect.fromLTWH(60, 60, 100, 100), 0.0 * (pi / 180.0), 90.0 * (pi / 180.0), false);
_path.close(); // 封闭当前路径
canvas.drawPath(_path, _paint);

到这里Canvas图形的绘制就讲的差不多了,图形简单时,使用 drawCircle() drawRect() 等方法来直接绘制;图形复杂时,使用 drawPath() 来绘制自定义图形。 除此之外, Canvas 还可以绘制图片和文字。

画图片 - drawImage(Image image, Offset p, Paint paint) / drawImageRect(Image image, Rect src, Rect dst, Paint paint)

drawImage() 从指定点开始将图片宽高按像素绘制,由于无法控制图片的大小,并不常用;

第一个参数是**ui包下的Image**,并不是 **Image Widget**

Image 可以通过以下代码获取

ui.Image image;

/**
 * 初始化图片
 *
  Future<VoidCallback> initImage() async {
    image = await _loadImage("./assets/images/img.jpg");
    return null;
  }

  /**
   * 通过assets路径,获取资源图片
   */
  Future<Image> _loadImage(String assets) async {
    final ByteData data = await rootBundle.load(assets);
    if (data == null) throw 'Unable to read data';
    Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
    FrameInfo frame = await codec.getNextFrame();
    return frame.image;
  }

然后在initState() 方法中初始化( shouldRepaint() 方法一定要记得返回 true ,否则无法重绘)

void initState() {
  super.initState();
  painter.initImage().then((val) {
    setState(() {
    });
  });
}

最后在 paint() 方法中填入以下代码:

canvas.drawImage(image, new Offset(0, 0), _paint);

drawImageRect() 这个方法经常使用;主要了解第二个参数与第三个参数:

  • Rect src - 原图的区域,一般传图片的宽高
  • Rect dst - 显示的区域, 指图片显示的区域,如果原图区域宽高比与显示区域不一致,原图会被拉伸或压缩
canvas.drawImageRect(image, Offset(0.0, 0.0) & Size(image.width.toDouble(), image.height.toDouble()), Offset(0.0, 0.0) & Size(200, 200), _paint);

正常比例:

拉伸:

画文字 - drawParagraph(Paragraph paragraph, Offset offset)

代码注释的很清楚,这里循环画了5段文字

for (int i = 0; i<5 ;i++){
  // 新建一个段落建造器,然后将文字基本信息填入;
  ParagraphBuilder pb = ParagraphBuilder(ParagraphStyle(
    textAlign: TextAlign.left,
    fontWeight: FontWeight.w300,
    fontStyle: FontStyle.normal,
    fontSize: 15.0+i,
  ));
  pb.pushStyle(ui.TextStyle(color: Colors.black87));
  pb.addText('Flutter一统移动端');
  // 设置文本的宽度约束
  ParagraphConstraints pc = ParagraphConstraints(width: 300);
  // 这里需要先layout,将宽度约束填入,否则无法绘制
  Paragraph paragraph = pb.build()..layout(pc);
  // 文字左上角起始点
  Offset offset = Offset(50, 50+i*40.0);
  canvas.drawParagraph(paragraph, offset);
}

Canvas及paint的使用基础部分大概就是这样了

Clone this wiki locally