使用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()
之后,会把save
到restore
之间所进行的操作与之前的内容进行合并。 下面看一个例子
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.drawArc
、path.addArc
、path.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
方法用于返回一个之前绘制的路径的一份快照。当我们使用moveTo
、lineTo
、arcTo
、conicTo
等绘制路径时,可以使用此来实现只绘制其中一部分。
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
,就不得不提TextPainter
。TextPainter
是通过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;
}
由于图片加载是异步过程,不能放到CustomPaint
的paint
方法来加载(因为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);
}
//...
}