Flutter CustomPaint自定义绘画示例详解
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>正文</li><li>CustomPaint 介绍</li><ul class="second_class_ul"><li>绘制点</li></ul><li>PointMode3种模式</li><ul class="second_class_ul"><li>绘制线 和路径</li><li>绘制五子棋</li></ul><li>总结</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>正文</h2><p>CustomPaint是Flutter中用于自由绘制的一个widget,它与android原生的绘制规则基本一致,以当前Canves(画布)的左上角为原点进行绘制。在有些场景中,我们会需要绘制一些高度定制化的组件,比如 UI 设计师给我们出了个难题 —— 弄一个奇形怪状的边框。这个时候我们就不能直接使用 Flutter 自带的那些组件了,而是需要手动绘制组件,那就会需要用到 <code>CuntomPaint</code> 组件。<code>CustomPaint</code> 组件和前端的 <code>Canvas</code>差不多,允许我们在一个画布上绘制各种元素,包括点、线、矩形、圆弧、文字、图片等等。</p>
<p class="maodian"></p><h2>CustomPaint 介绍</h2>
<p><code>CustomPaint</code>是一个 Widget,其中有三个重要的参数:</p>
<div class="jb51code"><pre class="brush:java;">CustomPaint(
child: childWidget(),
foregroundPainter: foregroundPainter(),
painter: backgroundPainter(),
)
</pre></div>
<p><code>child</code>:<code>CustomPaint</code>的子组件;</p>
<p><code>painter</code>和 <code>foregroundPainter</code>:都是 <code>CustomPainter</code> 类,用于定义 <code>canvas</code> 绘制的内容。区别在于,首先是执行 <code>painter</code> 的绘制指令。然后是在背景上渲染 <code>child</code> 子组件。最后,<code>foregroundPainter</code> 的内容会绘制在 <code>child</code> 上一层。</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919035.jpg" /></p>
<p>案例展示:</p>
<div class="jb51code"><pre class="brush:java;">import 'package:demo202112/utils/common_appbar.dart';
import "package:flutter/material.dart";
class MyPaint extends StatelessWidget {
const MyPaint({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar('CustomPaint'),
body: CustomPaint(
painter: MyPainer(),
child: Container(height: 80,width: 80,child: Text('child测试'),color: Colors.red,),
foregroundPainter: MyForeGroundPainer(),
),
);
}
}
class MyPainer extends CustomPainter{
late Paint _paint;
@override
void paint(Canvas canvas, Size size) {
_paint = Paint();
_paint.color = Colors.blue;
canvas.drawCircle(Offset(100, 100), 100, _paint);
canvas.drawLine(Offset(300, 300), Offset(400, 400), _paint);
// TODO: implement paint
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
class MyForeGroundPainer extends CustomPainter{
late Paint _paint;
@override
void paint(Canvas canvas, Size size) {
_paint = Paint();
_paint.color = Colors.green;
canvas.drawCircle(Offset(100, 100), 70, _paint);
// canvas.drawLine(Offset(300, 300), Offset(400, 400), _paint);
// TODO: implement paint
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
</pre></div>
<p>运行效果:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919036.jpg" /></p>
<p>child: 红色区域,传入一个子widget,这个widget图层会在painter在上,在foregroundPainter之下。</p>
<p>painter:蓝色区域。</p>
<p>foregroundPainter:绿色区域,它与painter都是CustomPainter类型的。通过名字大概也就知道了,它会在painter的上层,也就是说在同样的位置去绘制,foregroundPainter 会覆盖painter。</p>
<p><code>CustomPainter</code>提供了一个paint绘图方法供我们绘制图形,该方法携带<code>canvas</code> 和<code>size</code>两个参数,其中 <code>canvas</code> 是画布,<code>size</code> 是画布大小。<code>canvas</code> 提供了很多绘制图形的方法,比如绘制路径、矩形、圆形和线条等等。</p>
<div class="jb51code"><pre class="brush:java;">//画圆
drawCircle(Offset c, double radius, Paint paint) → void
//画图片
drawImage(Image image, Offset p, Paint paint) → void
//画九宫图
drawImageNine(Image image, Rect center, Rect dst, Paint paint) → void
//画线
drawLine(Offset p1, Offset p2, Paint paint) → void
//画椭圆
drawOval(Rect rect, Paint paint) → void
//画文字
drawParagraph(Paragraph paragraph, Offset offset) → void
//画Rect区域
drawRect(Rect rect, Paint paint) → void
//画阴影
drawShadow(Path path, Color color, double elevation, bool transparentOccluder) → void
</pre></div>
<p class="maodian"></p><h3>绘制点</h3>
<div class="jb51code"><pre class="brush:java;">class MyPoints extends CustomPainter{
Paint _paint = Paint()
..color = Colors.red
..strokeWidth = 15;
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
var points =[
Offset(0, 0),
Offset(size.width/2, size.height/2),
Offset(size.width, size.height),
];
canvas.drawPoints(PointMode.points, points, _paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
</pre></div>
<p>运行效果:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919037.jpg" /></p>
<p class="maodian"></p><h2>PointMode3种模式</h2>
<ul><li>points:点</li><li>lines:将2个点绘制为线段,如果点的个数为奇数,最后一个点将会被忽略</li><li>polygon:将整个点绘制为一条线</li></ul>
<p class="maodian"></p><h3>绘制线 和路径</h3>
<div class="jb51code"><pre class="brush:java;">class MyGraph extends CustomPainter{
final Paint _paint = Paint()
..color = Colors.red
..strokeWidth = 15;
final Paint _paintPath = Paint()
..color = Colors.blue
..strokeWidth = 5
..style = PaintingStyle.fill;
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
//绘制线
canvas.drawLine(Offset(0, 30),Offset(size.width-30, size.height), _paint);
//绘制路径
var _path = Path()
..moveTo(0, 0)
..lineTo(size.width, 0)
..lineTo(size.width, size.height)
..close();
canvas.drawPath(_path, _paintPath);
//这里注意Paint.style,还可以设置为PaintingStyle.fill,
//绘制圆形
canvas.drawCircle(Offset(size.width/2+50, size.height/2+50), 20, _paint);
//绘制椭圆
canvas.drawOval(Rect.fromLTRB(0, 0, size.width, size.height/2), _paint);
//绘制弧
canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height), 0, pi/2, true, _paint);
//绘制圆角矩形
canvas.drawRRect(RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(10)), _paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
</pre></div>
<p>运行效果:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919038.jpg" /></p>
<p class="maodian"></p><h3>绘制五子棋</h3>
<p>首先绘制背景,淡黄色,再绘制棋盘网格线,随后绘制黑白子,具体代码:</p>
<div class="jb51code"><pre class="brush:java;">class CustomPaintRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
size: Size(300, 300), //指定画布大小
painter: MyPainter(),
),
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
double eWidth = size.width / 15;
double eHeight = size.height / 15;
//画棋盘背景
var paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill //填充
..color = Color(0x77cdb175); //背景为纸黄色
canvas.drawRect(Offset.zero & size, paint);
//画棋盘网格
paint
..style = PaintingStyle.stroke //线
..color = Colors.black87
..strokeWidth = 1.0;
for (int i = 0; i <= 15; ++i) {
double dy = eHeight * i;
canvas.drawLine(Offset(0, dy), Offset(size.width, dy), paint);
}
for (int i = 0; i <= 15; ++i) {
double dx = eWidth * i;
canvas.drawLine(Offset(dx, 0), Offset(dx, size.height), paint);
}
//画一个黑子
paint
..style = PaintingStyle.fill
..color = Colors.black;
canvas.drawCircle(
Offset(size.width / 2 - eWidth / 2, size.height / 2 - eHeight / 2),
min(eWidth / 2, eHeight / 2) - 2,
paint,
);
//画一个白子
paint.color = Colors.white;
canvas.drawCircle(
Offset(size.width / 2 + eWidth / 2, size.height / 2 - eHeight / 2),
min(eWidth / 2, eHeight / 2) - 2,
paint,
);
}
//在实际场景中正确利用此回调可以避免重绘开销,本示例我们简单的返回true
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
</pre></div>
<p>运行效果:</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202211/20221109091919039.jpg" /></p>
<p>绘制是比较昂贵的操作,所以我们在实现自绘控件时应该考虑到性能开销,下面是两条关于性能优化的建议:</p>
<ul><li>尽可能的利用好<code>shouldRepaint</code>返回值;在UI树重新build时,控件在绘制前都会先调用该方法以确定是否有必要重绘;假如我们绘制的UI不依赖外部状态,那么就应该始终返回false,因为外部状态改变导致重新build时不会影响我们的UI外观;如果绘制依赖外部状态,那么我们就应该在shouldRepaint中判断依赖的状态是否改变,如果已改变则应返回<code>true</code>来重绘,反之则应返回<code>false</code>不需要重绘。</li><li>绘制尽可能多的分层;在上面五子棋的示例中,我们将棋盘和棋子的绘制放在了一起,这样会有一个问题:由于棋盘始终是不变的,用户每次落子时变的只是棋子,但是如果按照上面的代码来实现,每次绘制棋子时都要重新绘制一次棋盘,这是没必要的。优化的方法就是将棋盘单独抽为一个Widget,并设置其<code>shouldRepaint</code>回调值为false,然后将棋盘Widget作为背景。然后将棋子的绘制放到另一个Widget中,这样落子时只需要绘制棋子。</li></ul>
<p class="maodian"></p><h2>总结</h2>
<p>CustomPaint class提供了让用户自定义widget的能力,它暴露了一个canvas,可以通过这个canvas来绘制widget,CustomPaint会先调用painter绘制背景,然后再绘制child,最后调用foregroundPainter来绘制前景。</p>
<p>canvas--画布,真正的绘制是由canvas跟paint来完成的,画布提供了各种绘制的接口来绘制图形,除此以外画布还提供了平移、缩放、旋转等矩阵变换接口,画布都有固定大小跟形状,还可以使用画布提供的裁剪接口来裁剪画布的大小形状等等</p>
<p>Paint---笔画,是用来设置在画布上面绘制图形时的一些笔画属性,如:颜色、线宽、绘制模式、抗锯齿等等.</p>
<p>自绘控件非常强大,理论上可以实现任何2D图像外观,想更深入的了解,可以找到其对应的RenderObject对象,如Text Widget最终会通过RenderParagraph对象来通过Canvas实现文本绘制逻辑。了解了更底层的绘制逻辑,才能更好的在实际项目中灵活应用。</p>
<p>以上就是Flutter CustomPaint自定义绘画示例详解的详细内容,更多关于Flutter CustomPaint 绘画的资料请关注琼殿技术社区其它相关文章!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>Android Flutter利用CustomPaint绘制基本图形详解</li><li>Flutter绘图组件之CustomPaint使用详解</li><li>用Flutter做桌上弹球(绘图(Canvas&CustomPaint)API)</li><li>Flutter 绘制风车实现示例详解</li><li>Flutter绘制3.4边形及多边形渐变动画实现示例</li><li>Flutter CustomPaint绘制widget使用示例</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]