Flutter 的自定义 UI 系列(三)--画布 Canvas
canvas 的绘制能力
- Flutter 系统提供的 Canvas 可以绘制常规的几何图形和图形的变换操作
常规的几何图形
点 drawPoints
void drawPoints(PointMode pointMode, List<Offset> points, Paint paint)
pointMode
: 绘制方式- PointMode.
points
点,单独绘制每个点 - PointMode.
lines
线段,每两个点连成一条线段 - PointMode.
polygon
线,所有的点连成一条线
- PointMode.
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);
}
- 示例效果
线 drawLine
void drawLine(Offset p1, Offset p2, Paint paint)
- p1:起始点
- p2:终止点
设置
paint
的strokeWidth
可以改变线宽
- 示例代码
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);
}
- 示例效果
矩形
/// 矩形
void drawRect(Rect rect, Paint paint)
/// 圆角矩形
void drawRRect(RRect rrect, Paint paint)
/// 两个矩形裁剪后的圆环
void drawDRRect(RRect outer, RRect inner, Paint paint)
设置
paint
的style
属性,绘制边框还是填充模式
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);
}
- 示例效果
圆和弧
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);
}
- 示例效果
图片 drawImage
void drawImage(Image image, Offset offset, Paint paint)
image
: 图片源,此 image 并不是Wdiget里Image,而是 dart.ui 下的 Imageoffset
: 图片左上角的位置
获取 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);
}
}
- 示例效果(设置目标区域的大小,可以实现图片缩放绘制)
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);
}
}
- 示例效果
颜色与阴影
void drawColor(Color color, BlendMode blendMode)
颜色背景, blendMode
:颜色混合模式
void drawShadow(Path path, Color color, double elevation, bool transparentOccluder)
path
:阴影路径color
:阴影颜色elevation
:阴影的高度transparentOccluder
:官方文档上翻译过来意思是遮挡对象是否透明的
。个人理解,因为阴影是一个光源在画布上方被遮挡时产生的,transparentOccluder
为false
时,这个遮挡物是不透明的。
- 示例代码
@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);
}
- 示例效果
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);
}
贝塞尔曲线
// 二阶曲线
void conicTo(double x1, double y1, double x2, double y2, double weight)
绘制贝塞尔曲线
x1
和y1
: 控制点x2
和y2
: 曲线目标定点weight
:权重,大于1,则曲线为双曲线; 如果权重等于 1,则为抛物线; 如果小于 1,则为椭圆
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
三阶贝塞尔曲线
x1
和y1
: 控制点1x2
和y2
: 控制点2x3
和y3
: 曲线目标定点
- 示例代码
@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);
}
- 示例效果
相对坐标
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);
}
- 示例效果
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);
}
- 示例效果
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,
);
}
- 示例效果
Canvas 的变换
Canvas 提供了4种变换操作,位移、旋转、缩放、斜切,其效果类似于系统提供的 Transform 组件,在熟悉这四种变换操作前,要先了解 Flutter 中的坐标系。
坐标系
Flutter 中坐标系是当前画布的左上角为原点,向左为 x 轴正向,向下为 y 轴正向。角度是弧度制,以距离原点水平位置右侧为 0 度,顺顺时针方向。即 pi 为原点左侧水平方向。
位移 translate
void translate(double dx, double dy)
移动画布原点到指定的位置,dx
和dy
可以为负数
缩放 scale
void scale(double sx, [double? sy])
缩放操作,sx
和sy
分别是x轴和y轴方向上的缩放比例 ,如果未指定sy ,则sx将用于两个方向的比例。sx
和sy
可以为负数,为负数时会产生对向旋转的效果。
如图所示,黑色为原坐标系,红色为变换后的坐标系。
旋转 rotate
void rotate(double radians)
以原点为中心旋转画布,radians
:旋转角度,弧度制,即 pi=180º
如图所示,黑色为原坐标系,红色为变换后的坐标系。
斜切 skew
void skew(double sx, double sy)
延x或y轴方向做斜切的变换,sx
和sy
为x轴和y轴方向切度值,值为三角函数中的 tan
值,即 45
度时 tan 值为 1
。
如图所示,黑色为原坐标系,红色为变换后的坐标系。
save 和 restore
原文档:Saves a copy of the current transform and clip on the save stack. 即有个保存画布状态的栈,当调用
save
时,会把当前画布保存到栈中。调用restore
时,会把从栈中取出画布状态。因为用栈保存,所以 Canvas 的save
和restore
,有如下特性
- 成对出现的,即有一个
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);
}
- 示例效果
总结
Canvas 的功能非常强大,其中还有些api是我比较模糊的,待以后慢慢补充。下一篇文章将会用做一些我们开发中常见的 UI 需求,用实际的代码来展示 Canvas 强大的功能。
转载自:https://juejin.cn/post/7022914592779010055