Flutter 绘图部分详解
分享是每个优秀的程序员所必备的品质
Flutter绘图主要是CustomerPaint和Paint组合使用绘制出我们需要的图形。下面笔者会按基础逐渐深入,结合代码和注释,通俗易懂。
看完这篇,相信关于Flutter的绘图部分应该不是问题了!
CustomPainter
:具体绘制的类,需要绘制的图形和自定义的画笔,一般会自定义一个类来继承。需要重写一下两个方法:
// 实现具体绘制的方法
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
}
// 控制自定义绘制是否需要重绘的,返回false代表在构建完成后不需要重绘。如果存在依赖于一个变量并且该变量发生了变化,那么你在这里返回true,就会重新绘制。
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return null;
}
CustomPaint
:在绘制阶段提供一个 Canvas 画布
主要的参数:
child:子widget
painter : 绘制的对象,传入CustomPainter对象,且绘制是在child之前。若设置了child,则绘制的内容会被覆盖,即chind内容在绘制内容之上。
foregroundPainter: 绘制的对象,传入CustomPainter对象,它的绘制是在child之后,即chind内容在绘制内容之下。
size:绘制区域的大小,取决于child
若child != null ,则绘制的区域为child的大小
若child == null ,则绘制的区域为size设置的大小
若child != null & size != null,若child == null ,可以用SizeBox包裹一下CustomPaint;
Paint
:画笔,设置颜色,线宽,填充等多种样式
Paint _paint = Paint()
..color = Colors.blueAccent //画笔颜色
..strokeWidth = 15.0 //画笔的宽度
..strokeCap = StrokeCap.round //画笔笔触类型(butt、round、square)
..isAntiAlias = true //是否启动抗锯齿
..blendMode = BlendMode.exclusion //绘制形状或合成层时应用的混合模式(如颜色混合模式,或在截取Image后合并Image用,枚举实在是太多了,笔者就不一一列举了,有需要的同学可以自己看下源码)
..style = PaintingStyle.fill //绘画风格,默认为填充
..colorFilter = ColorFilter.mode(Colors.blueAccent,BlendMode.exclusion) //颜色渲染模式,一般是矩阵效果来改变的,但是flutter中只能使用颜色混合模式
..maskFilter = MaskFilter.blur(BlurStyle.inner, 3.0) //模糊遮罩效果,flutter中只有这个
..filterQuality = FilterQuality.high //颜色渲染模式的质量
接下来就根据几个示例来应用
1 :绘制线:void drawLine(Offset p1, Offset p2, Paint paint)
class PaintDemo1 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
canvas.drawLine(Offset(50.0,50.0), Offset(150.0,50.0), _paint);
canvas.drawLine(Offset(50.0, 100.0), Offset(250.0, 100.0), _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果图:

2 :绘制点:void drawPoints(PointMode pointMode, List points, Paint paint)
class PaintDemo2 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
canvas.drawPoints(
///PointMode的枚举类型有三个,points(点),lines(线,隔点连接),polygon(线,相邻连接)
PointMode.points,
[
Offset(200.0, 50.0),
Offset(300.0, 120.0),
Offset(300.0, 250.0),
Offset(200.0, 320.0),
Offset(100.0, 250.0),
Offset(100.0, 120.0),
Offset(200.0, 50.0),
],
_paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
points
类型运行效果:

lines
类型运行效果:
polygon
类型运行效果:
3、绘制圆rawCircle void drawOval(Rect rect, Paint paint)
// 圆形是否填充或描边(或两者)由Paint.style控制(fill填充)。
class PaintDemo3 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
//绘制圆 参数(圆心,半径,画笔)
canvas.drawCircle(
Offset(200.0, 120.0),
100.0,
_paint..color = Colors.redAccent..style = PaintingStyle.stroke //绘画风格改为stroke
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:

4、绘制椭圆rawCircle void drawCircle(Offset c, double radius, Paint paint)
class PaintDemo4 extends CustomPainter{
// 使用左上角点和右下角点坐标来确定矩形的大小和位置,椭圆是在这个矩形之中内切的,正方形中就是个圆了
@override
void paint(Canvas canvas, Size size) {
Rect rect1 = Rect.fromPoints(Offset(100.0, 100.0), Offset(300.0, 200.0));
canvas.drawOval(rect1, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:

5、绘制圆弧void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
class PaintDemo5 extends CustomPainter{
@override
// Rect来确认圆弧的位置,还需要开始的弧度、结束的弧度、是否使用中心点绘制(圆弧两段是否连接圆心)、以及paint弧度
void paint(Canvas canvas, Size size) {
Rect rect2 = Rect.fromCircle(center: Offset(200.0, 50.0), radius: 80.0);
canvas.drawArc(rect2, 0.0, pi, false, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:
useCenter = false

useCenter = true

6、绘制矩形、圆角矩形
class PaintDemo6 extends CustomPainter{
@override
// Rect来确认圆弧的位置,还需要开始的弧度、结束的弧度、是否使用中心点绘制、以及paint弧度
void paint(Canvas canvas, Size size) {
// 1、用Rect构建一个边长50,中心点坐标为100,100的矩形
Rect rect = Rect.fromCircle(center: Offset(60.0, 100.0), radius: 50.0);
canvas.drawRect(rect, _paint);
// 2、圆角矩形
Rect rect0 = Rect.fromCircle(center: Offset(180.0, 100.0), radius: 50.0);
RRect rrect = RRect.fromRectAndRadius(rect0, Radius.circular(10.0));
canvas.drawRRect(rrect, _paint);
// 3、分别绘制外部圆角矩形和内部的圆角矩形
Rect rect1 = Rect.fromCircle(center: Offset(320.0, 100.0), radius: 60.0);
Rect rect2 = Rect.fromCircle(center: Offset(320.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 false;
}
}
运行效果:

7、绘制路径drawPathvoid drawPath(Path path, Paint paint)
class PaintDemo7 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
/* Path的常用方法:
* moveTo :将路径起始点移动到指定的位置
* relativeMoveTo : 相对于当前位置移动到
* lineTo :从当前位置连接指定点
* relativeLineTo : 相对当前位置连接到
* arcTo : 曲线
* conicTo : 贝塞尔曲线
* add** : 添加其他图形,如addArc,在路径是添加圆弧
* contains : 路径上是否包括某点
* transfor : 给路径做matrix4变换
* combine : 结合两个路径
* close : 关闭路径,连接路径的起始点
* reset : 重置路径,恢复到默认状态
*/
Path path = Path();
path.moveTo(50.0, 50.0);
path.lineTo(100, 100.0);
path.lineTo(50.0, 150.0);
path.lineTo(100.0, 200.0);
canvas.drawPath(path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:

8、绘制阴影
// 路径、阴影的颜色、阴影扩散的范围、
void drawShadow(path, color, elevation, transparentOccluder)
class PaintDemo8 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
canvas.drawShadow(Path()
..moveTo(50.0, 50.0)
..lineTo(150.0, 50.0)
..lineTo(150.0, 150.0)
..lineTo(50.0, 150.0)
..close(), Colors.red, 3, false);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:

9、二阶贝塞尔曲线绘制弧线void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)
我们用贝塞尔曲线来绘制一个大写字母C和字母G。
方法注解:
/*
* rect 矩形,startAngle是开始的弧度,sweepAngle是结束的弧度
* 如果“forceMoveTo”参数为false,则添加一条直线段和一条弧段。
* 如果“forceMoveTo”参数为true,则启动一个新的子路径,其中包含一个弧段。
*/
引申一下,对于Rect,有多种构建方式
// Rect构建方式
fromPoints(Offset a, Offset b)
使用左上和右下角坐标来确定矩形的大小和位置
fromCircle({ Offset center, double radius })
使用圆的圆心点坐标和半径和确定外切矩形的大小和位置
fromLTRB(double left, double top, double right, double bottom)
使用矩形左边的X坐标、矩形顶部的Y坐标、矩形右边的X坐标、矩形底部的Y坐标来确定矩形的大小和位置
fromLTWH(double left, double top, double width, double height)
使用矩形左边的X坐标、矩形顶部的Y坐标矩形的宽高来确定矩形的大小和位置
代码:
class PaintDemo8 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
Path path = Path()..moveTo(100.0, 100.0);
Rect rect = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 60.0);
path.arcTo(rect, pi*0.2, pi*1.5, true); // 字母C
//path.arcTo(rect, pi*0.0, pi*1.6, false); // 字母G
canvas.drawPath(path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}[图片上传失败...(image-ecbd30-1595388326757)]
运行结果:
C

G

9.1使用二阶贝塞尔曲线直接画一个圆
class PaintDemo9 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
Path path = new Path()..moveTo(100.0, 100.0);
Rect rect = Rect.fromCircle(center: Offset(200.0, 200.0), radius: 60.0);
path.arcTo(rect, 0.0, 3.14*2, true);
canvas.drawPath(path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
10、三阶贝塞尔曲线绘制void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
相比于二阶的,三阶贝塞尔曲线就是说两个点之间的线 有两个控制点

如上
要画出如图的曲线,A、B、C是要经过的路径点,其中
AB段的控制点为点p1、p2
BC段的控制点为点p3、p4
控制点需自行设置,不同的控制点画出来的圆弧是不一样的!
则代码:
class PaintDemo10 extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
_paint..style = PaintingStyle.stroke;
Path path = Path();
// A点
path.moveTo(0, 50);
// B点
path.cubicTo(0, 25, 40, 0, 80, 0);
// C点
path.cubicTo(120, 0, 160, 25, 160, 50);
canvas.drawPath(path, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:

下面笔者用cubicTo
绘制一个心形送给最爱的慧同学!
class PaintDemo11 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
double width = 200;
double height = 300;
_paint..style = PaintingStyle.fill..color = Colors.redAccent;
// 右边一半
Path path = Path();
path.moveTo(width / 2, height / 4);
path.cubicTo((width * 6) / 7, height / 9, width, (height * 2) / 5, width / 2, (height * 7) / 12);
// 左边一半
path.moveTo(width / 2, height / 4);
path.cubicTo(width / 7, height / 9, width / 21, (height * 2) / 5, width / 2, (height * 7) / 12);
canvas.drawPath(path,_paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:

11、绘制文字
文字(以及图片)的绘制,需要Dart中的相关方法,
记得要导入import 'dart:ui' as ui;
class PaintDemo12 extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
ParagraphBuilder pb = ParagraphBuilder(
ParagraphStyle(
textAlign: TextAlign.center,
fontSize: 20,
fontWeight: FontWeight.w600,
fontStyle: FontStyle.normal, // 正常 or 斜体
maxLines: 1
)
);
pb.pushStyle(
ui.TextStyle(color: Colors.redAccent)
);
pb.addText("RC LOVE TMH");
// 绘制的宽度
ParagraphConstraints pc = ParagraphConstraints(width: 350.0);
Paragraph paragraph = pb.build()..layout(pc);
canvas.drawParagraph(paragraph, Offset(30, 300));
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
运行效果:

12、绘制图片
drawImage
drawImageRect
drawImageNine
注意:图片一定是已经加载完成的
可以通过canvas.clipRect(Rect)来裁剪画布
这玩意有点绕,需要ui/painting文件的Image,这个Image不是Widget而是一个Uint8List 格式的图片流,还要通过instantiateImageCodec获取图片编解码器,它是返回一个Future的方法,而且传入一个Uint8List。
图片转换成Uint8List的方法:
// 本地图片转换成Uint8List的方法
Future<ui.Image> getImage(String asset) async {
ByteData data = await rootBundle.load(asset);
Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
FrameInfo frameInfo = await codec.getNextFrame();
return frameInfo.image;
}
// 网络图片和本地图片换成Uint8List的方法
Future <ui.Image> loadImage(var path, bool networkImage) async {
ImageStream stream;
if (networkImage) {
stream = NetworkImage(path).resolve(ImageConfiguration.empty);
} else {
stream = AssetImage(path, bundle: rootBundle)
.resolve(ImageConfiguration.empty);
}
Completer <ui.Image> completer = Completer<ui.Image>();
void listener(ImageInfo frame, bool synchronousCall) {
final ui.Image image = frame.image;
completer.complete(image);
stream.removeListener(ImageStreamListener(listener));
}
stream.addListener(ImageStreamListener(listener));
return completer.future;
}
示例:点击按钮生成图片并且预览
class DrawDemo extends StatefulWidget {
@override
_DrawDemoState createState() => _DrawDemoState();
}
class _DrawDemoState extends State<DrawDemo> {
Uint8List imageMemory = null;
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
SizedBox(height: 30,),
imageMemory == null ? Container():SingleChildScrollView(child: Image.memory(imageMemory,width: 300,fit: BoxFit.fitWidth,),),
imageMemory == null ?
InkWell(
child: Container(width: 160,height: 80,color: Colors.redAccent,),
onTap: ()async {
var pictureRecorder = ui.PictureRecorder(); // 图片记录仪
var canvas = Canvas(pictureRecorder); //canvas接受一个图片记录仪
var images = await getImage('assets/rt.JPG'); // 使用方法获取Unit8List格式的图片
// 绘制图片
canvas.drawImage(images, Offset(0, 0), Paint());
// 绘制文字
ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: TextAlign.center,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.normal,
fontSize: 50.0));
pb.pushStyle(ui.TextStyle(color: Colors.black));
pb.addText("贤合庄真的是太辣了🌶🌶🌶🌶🌶");
ParagraphConstraints pc = ui.ParagraphConstraints(width: images.width.toDouble());
ui.Paragraph paragraph = pb.build()..layout(pc);
canvas.drawParagraph(paragraph, Offset(0, 200));
//生成图片
var picture = await pictureRecorder.endRecording().toImage(750, 1334);//设置生成图片的宽和高
var pngImageBytes = await picture.toByteData(format: ui.ImageByteFormat.png);
// var imgBytes = Uint8List.view(pngImageBytes.buffer);
Uint8List pngBytes = pngImageBytes.buffer.asUint8List();
setState(() {
imageMemory = pngBytes;
});
},
):Container()
],
),
);
}
// 本地图片转换成Uint8List的方法
Future<ui.Image> getImage(String asset) async {
ByteData data = await rootBundle.load(asset);
Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
FrameInfo frameInfo = await codec.getNextFrame();
return frameInfo.image;
}
}
点击按钮后,运行效果(存在延时):

13、雷达图
边长、位置、颜色等可高度自定义
class RadarDemo extends CustomPainter {
int sideNumber = 6; // 多边形边数
int layerNumber = 4; // 维度分几层
double c_X;// view 的中心点
double c_Y;
double maxRadius; // 半径,最大的半径
Paint linePaint; // 划线的画笔
Path path; // 路径
Paint maskPaint; // 遮罩的画笔
RadarDemo(int sideNumber) {
this.sideNumber = sideNumber;
linePaint = Paint()
..color = randomRGB()
..isAntiAlias = true
..style = PaintingStyle.stroke;
path = Path();
maskPaint = Paint()
..color = randomARGB()
..isAntiAlias = true
..style = PaintingStyle.fill;
}
@override
void paint(Canvas canvas, Size size) {
c_X = size.width / 2;
c_Y = size.height / 2;
if (c_X > c_Y) {
maxRadius = c_Y;
} else {
maxRadius = c_X;
}
canvas.save();
drawPolygon(canvas);
drawMaskLayer(canvas);
canvas.restore();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return oldDelegate != this;
}
double eachRadius;
double eachAngle;
// 绘制多边形边框
void drawPolygon(Canvas canvas) {
///每个角的度数
eachAngle = 360 / sideNumber;
///找好所有的顶点,连接起来即可
for (int i = 0; i < layerNumber; i++) {
path.reset();
eachRadius = maxRadius / layerNumber * (i + 1);
for (int j = 0; j < sideNumber + 1; j++) {
if (j == 0) {
path.moveTo(c_X + eachRadius, c_Y);
} else {
double x = c_X + eachRadius * cos(degToRad(eachAngle * j));
double y = c_Y + eachRadius * sin(degToRad(eachAngle * j));
path.lineTo(x, y);
}
}
path.close();
canvas.drawPath(path, linePaint);
}
drawLineLinkPoint(canvas, eachAngle, eachRadius);
}
// 连接多边形顶点和中心点
void drawLineLinkPoint(Canvas canvas, double eachAngle, double eachRadius) {
path.reset();
for (int i = 0; i < sideNumber; i++) {
path.moveTo(c_X, c_Y);
double x = c_X + eachRadius * cos(degToRad(eachAngle * i));
double y = c_Y + eachRadius * sin(degToRad(eachAngle * i));
path.lineTo(x, y);
path.close();
canvas.drawPath(path, linePaint);
}
}
// 绘制遮罩
void drawMaskLayer(Canvas canvas) {
path.reset();
for (int i = 0; i < sideNumber; i++) {
double mRandomInt = randomInt();
double x =
c_X + maxRadius * cos(degToRad(eachAngle * i)) * mRandomInt;
double y =
c_Y + maxRadius * sin(degToRad(eachAngle * i)) * mRandomInt;
if (i == 0) {
path.moveTo(x, c_Y);
} else {
path.lineTo(x, y);
}
}
path.close();
canvas.drawPath(path, maskPaint);
}
num degToRad(num deg) => deg * (pi / 180.0);
num radToDeg(num rad) => rad * (180.0 / pi);
Color randomRGB() {
Random random = new Random();
return Color.fromARGB(
255, random.nextInt(255), random.nextInt(255), random.nextInt(255));
}
Color randomARGB() {
Random random = Random();
return Color.fromARGB(random.nextInt(90), random.nextInt(255),
random.nextInt(255), random.nextInt(255));
}
double randomInt() {
Random random = new Random();
return (random.nextInt(10) + 1) / 10;
}
}
运行效果图:

简单demo : RCFlutterDrawDemo