使用Canvas自绘控件
Canvas就是一块2D画布,内部封装了一些基本绘制的API,开发者可以通过Canvas绘制各种自定义图形。在Flutter中,提供了一个CustomPaint控件,它需要结合一个画笔CustomPainter来实现绘制自定义图形。
Canvas 内的坐标系,其原点在左上角,水平向右为 x 轴正方向,垂直向下为 y 轴正方向

具体步骤
写一个类继承自
CustomPainter,并实现paint和shouldRepaint方法class MyPainter extends CustomPainter { @override void paint(ui.Canvas canvas, ui.Size size) { /// 该方法中实现绘制逻辑 Paint paint = Paint() ..isAntiAlias = true ..color = Colors.blue ..strokeWidth = 10 ..style = PaintingStyle.fill; canvas.drawLine(Offset(10, 10), Offset(250, 250), paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { /// 返回 true 会进行重绘,否则就只会绘制一次 return false; } }使用
CustomPaint包装我们自定义的画笔类,如同普通Widget一样插入控件树中Widget build(BuildContext context) { return Center( child: CustomPaint( size: Size(300, 300), //指定画布大小 painter: MyPainter(), ), ); }
需要注意,绘制是比较耗费性能的操作,所以在实现自绘控件时应该考虑到性能开销,做一些优化。
利用好
shouldRepaint方法的返回值。在控件树重新build时,如果我们绘制的UI不依赖外部状态,那么就应该始终返回false,因为外部状态改变导致重新build时不会影响我们的自绘控件;如果依赖外部状态,则应该在shouldRepaint中判断所依赖的状态是否改变,如果已改变则返回true来重绘,反之则应返回false不重绘。如果
CustomPaint有子控件,为了避免子控件不必要的重绘而提高性能,可以将子控件包裹在RepaintBoundary控件中,这样在绘制时会创建一个新的图层(Layer),其子控件将在新的图层上绘制,而父Widget将在原来图层上绘制。CustomPaint( size: Size(300, 300), painter: MyPainter(), child: RepaintBoundary(child:...)), )
CustomPaint有两个属性用于设置painter
painter: 绘制效果会显示在child的下面foregroundPainter: 绘制效果会显示在child的上面
关于Canvas 的大小
CustomPaint对象创建一块画布,其绘制区域大小与子控件的大小相同。如果未提供child参数(这是可选的),则绘制区域大小由实例化CustomPaint时其设置的size属性来确定。特别注意,默认Canvas是全屏的, 此处大小指的是绘制区域大小,而非Canvas 大小,千万不可混淆!
- 如果
child == null,则绘制区域为size大小 - 如果
child != null,则绘制区域为child大小 - 如果
child != null并且想指定指定绘制区域大小,可使用SizeBox包装CustomPaint
Canvas 的方法
Canvas 的操作主要有两类:
- 针对 Canvas 的变换操作,如平移、旋转、缩放、图层等
- 绘制基础图形的操作,如线段、路径、图片、几何图形等
绘制相关操作
drawArc(Rect rect,double startAngle, double sweepAngle, bool useCenter, Paint paint)
在给定的矩形中画一个弧线。它从椭圆周围的
startAngle弧度开始,直至椭圆周围的startAngle+sweepAngle弧度,零弧度是椭圆右侧与矩形中心相交的水平线相交的点,正角绕椭圆顺时针走。如果useCenter为真,弧线就会闭合回到中心,形成一个圆扇形。否则,弧线不闭合,形成一个圆段。
| 参数 | 类型 | 说明 |
|---|---|---|
| rect | Rect |
圆弧所在椭圆的外接矩形 |
| startAngle | double |
起始位置的弧度。弧度制 |
| sweepAngle | double |
设置圆弧扫过多少弧度。弧度制 |
| useCenter | bool |
表示是否链接到圆弧所在椭圆的中心 |
| paint | Paint |
画笔 |
drawAtlas
(Image atlas, List<RSTransform>transforms, List<Rect>rects, List<Color>colors, BlendMode blendMode, Rect cullRect, Paint paint)在画布上绘制部分图像。当你只想在画布上绘制图像的一部分时(例如使用精灵或缩放时),此方法可以进行优化。 比直接使用剪辑或遮罩更有效。
drawCircle(Offset c, double radius, Paint paint)
绘制一个以第一个参数给出的点为中心的圆,其半径由第二个参数给出,第三个参数给出Paint。圆圈是被填充还是被描边(或者两者都有)由Paint.style控制。
drawDRRect(RRect outer, RRect inner, Paint paint)
用给定的Paint绘制一个由两个圆角矩形的差值组成的形状。这个形状是填充还是描边(或者两者都有)由Paint.style控制。
drawImageNine(Image image, Rect center, Rect dst, Paint paint)
使用给定的 Paint 将给定的 Image 绘制到画布上。使用给定的Paint将给定的Image绘制到画布中。
通过绘制两条水平线和两条垂直线将图像分割成九个部分进行绘制,其中中心参数描述了由这四条线相交的四个点形成的矩形。这样就形成了一个3×3的区域网格,中心区域由中心参数描述)。
角落中的四个区域是在
dst描述的目标矩形的四个角上绘制的,没有缩放。剩下的五个区域是通过拉伸来绘制的,使它们完全覆盖目标矩形,同时保持它们的相对位置。drawImageRect(Image image, Rect src, Rect dst, Paint paint)
在
dst参数给定的轴对齐的矩形中,将src参数描述的给定图像的一部分绘制到画布中。drawLine(Offset p1, Offset p2, Paint paint)
使用给定的
paint在给定的两点之间绘制一条线。 描边该行,此调用将忽略Paint.style的值。drawOval(Rect rect, Paint paint)
绘制一个轴对齐的椭圆形,用给定的Paint填充给定的轴对齐的矩形。椭圆是被填充还是被描边(或两者都有)由Paint.style控制。
-
用给定的Paint填充画布。
drawParagraph(Paragraph paragraph, Offset offset)
将给定Paragraph中的文本在给定Offset处绘制到这个画布中。
drawPath(Path path, Paint paint)
用给定的 Paint 绘制给定的 Path。这个形状是被填充还是被描边(或者两者都有)由 Paint.style 控制。如果路径被填充,那么它的子路径将被隐式关闭(参见 Path.close)。
drawPicture(Picture picture)
在画布上绘制给定的图片。要创建图片,请参阅PictureRecorder。
drawPoints(PointMode pointMode, List
points, Paint paint) 根据给定的PointMode绘制一个点的序列。
drawRawAtlas(Image atlas, Float32List rstTransforms, Float32List rects, Int32List colors, BlendMode blendMode, Rect cullRect, Paint paint)
将图像的一部分
atlas画在画布上。drawRawPoints(PointMode pointMode, Float32List points, Paint paint)
根据给定的PointMode绘制一个点的序列。
drawRect(Rect rect, Paint paint)
用给定的Paint绘制一个矩形。矩形是被填充还是被描边(或两者都有)由Paint.style控制。
drawRRect(RRect rrect, Paint paint)
用给定的Paint绘制一个圆角矩形。矩形是被填充还是被描边(或两者都有)由Paint.style控制。
drawShadow(Path path, Color color, double elevation, bool transparentOccluder)
为Path画出代表给定material标高的阴影。
drawVertices(Vertices vertices, BlendMode blendMode, Paint paint)
将Vertices的集合绘制到画布上。
变换相关操作
scale(double sx, [double sy])
在当前变换中添加一个轴对齐的标尺,在水平方向上按第一个参数缩放,在垂直方向上按第二个参数缩放。
skew(double sx, double sy)
在当前变换中增加一个轴对齐的倾斜度,第一个参数是以原点为单位顺时针上升的水平倾斜度,第二个参数是以原点为单位顺时针上升的垂直倾斜度。
transform(Float64List matrix4)
将当前变换乘以指定的4×4变换矩阵,该矩阵指定为以列为主的值列表。
translate(double dx, double dy)
在当前的变换中添加一个平移,通过第一个参数水平移动坐标空间,通过第二个参数垂直移动坐标空间。
-
在当前的变换中加入旋转。参数的单位是顺时针的弧度。
-
返回保存栈中的条目数,包括初始状态。这意味着如果画布是干净的,则返回1,每次调用
save和saveLayer都会使其递增,每次匹配调用restore都会使其递减。 restore()
弹出当前的保存堆栈(如果有要弹出的内容)。 否则,什么都不做。
save()
在保存堆栈上保存当前变换和剪辑的副本。
saveLayer(Rect bounds, Paint paint)
在保存堆栈上保存当前变换和剪辑的副本,然后创建一个新的组,后续调用将成为该组的一部分。当随后打开保存堆栈时,该组将被扁平化为一个图层,并应用给定的
paint的Paint.colorFilter和Paint.blendMode。
裁剪相关操作
clipPath(Path path, {bool doAntiAlias: true})
将剪辑区域缩小至目前剪辑与给定路径的交点。如果
doAntiAlias为true,则剪辑将被消除锯齿。clipRect(Rect rect, {ClipOp clipOp: ClipOp.intersect, bool doAntiAlias: true})
缩小剪辑区域至目前剪辑与指定矩形的交点
clipRRect(RRect rrect, {bool doAntiAlias: true})
将剪辑区域缩小到当前剪辑和给定圆角矩形的交点。
Paint
在Canvas上绘图时要使用的样式的描述。大多数Canvas上的API都会取一个Paint对象来描述该操作要使用的样式。简单说,该类主要是用来设置真正画笔的一些属性。
| 属性 | 类型 | 简述 |
|---|---|---|
| isAntiAlias | bool |
是否开启抗锯齿,开启抗锯齿能够使边缘平滑 |
| color | Color |
描边或填充形状时使用的颜色。 |
| colorFilter | ColorFilter |
绘制形状或合成图层时应用的颜色滤镜。 |
| filterQuality | FilterQuality |
控制应用滤镜(如 maskFilter)或绘制图像(如 Canvas.drawImageRect 或 Canvas.drawImageNine)时的性能与质量权衡。 |
| invertColors | bool |
绘制时图像的颜色是否反转 |
| maskFilter | MaskFilter |
遮罩滤镜(例如,模糊),在形状被绘制后,但在它被合成到图像之前,应用于它。 |
| imageFilter | ImageFilter |
绘制光栅图像时要使用的ImageFilter。例如,如果要使用Canvas.drawImage模糊图像,应用ImageFilter.blur |
| shader | Shader |
描边或填充形状时要使用的着色器。 |
| strokeCap | StrokeCap |
当样式设置为PaintingStyle.stroke时,要放置在线条末端的边缘风格。如圆角、方形等 |
| strokeJoin | StrokeJoin |
设置两个绘制形状衔接处的风格。如圆角、方形等 |
| strokeWidth | double |
当样式设置为PaintingStyle.stroke时,画笔的宽度。宽度以逻辑像素为单位 |
| style | PaintingStyle |
填充方式。PaintingStyle.fill充满;PaintingStyle.stroke空心 |
| blendMode | BlendMode |
像素混合模式。当画一个shape或者合成图层的时候会生效。 |
关于BlendMode类型,官方做了详细解释,这里给一篇 中文翻译链接
常用绘制方法示例
// 绘制直线
canvas.drawLine(Offset(10, 10), Offset(250, 250), paint);
// 绘制一系列的点,也可连成线段
canvas.drawPoints(
PointMode.points,
[Offset(200, 200), Offset(250, 250), Offset(50, 200), Offset(100, 250)],
paint);
// 绘制路径
var path = Path()
..moveTo(30.0, 100.0)
..lineTo(120.0, 100.0)
..lineTo(90.0, 130.0)
..lineTo(180.0, 130.0)
..close();
canvas.drawPath(path, paint);
// 绘制矩形
Rect rect = Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2), radius: 100);
canvas.drawRect(rect, paint);
// 绘制圆角矩形
Rect rect1 = Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2), radius: 150);
RRect rRect = RRect.fromRectAndRadius(rect1, Radius.circular(20));
canvas.drawRRect(rRect, paint);
// 绘制嵌套圆角矩形
Rect r1 = Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2), radius: 140);
Rect r2 = Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2), radius: 160);
RRect rRect1 = RRect.fromRectAndRadius(r1, Radius.circular(20));
RRect rRect2 = RRect.fromRectAndRadius(r2, Radius.circular(20));
// 第一个参数为外部矩形,第二个参数为内部矩形
canvas.drawDRRect(rRect2, rRect1, paint);
// 绘制圆形
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 100, paint);
// 绘制椭圆
Rect rect2 = Rect.fromLTRB(size.width / 2 - 100, size.height / 2 - 50,
size.width / 2 + 100, size.height / 2 + 50);
canvas.drawOval(rect2, paint);
// 绘制圆弧
Rect rect3 = Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2), radius: 140);
canvas.drawArc(rect3, 0, math.pi / 2, true, paint);
// 绘制阴影
Path path2 = Path()..addRect(rect.translate(20, 0));
canvas.drawShadow(path2, Colors.amberAccent, 20, true);
// 绘制背景色
canvas.drawColor(Colors.pinkAccent, BlendMode.srcIn);
PointMode类型的取值
enum PointMode {
/// 分别绘制每个点。
/// 如果Paint.strokeCap是StrokeCap.round,那么每个点被画成直径为Paint.strokeWidth的圆
/// 否则,每个点被画成一个边长为Paint.strokeWidth的轴对齐的正方形,按照Paint.style的描述进行填充
points,
/// 将每两个点绘制为线段。如果点数为奇数,则忽略最后一个点。
lines,
/// 将整个点的序列画成一条线。线条按Paint.style的描述进行描边
polygon,
}
矩形Rect的几种构造方式
Rect.fromPoints根据两个点(左上角点/右下角点)来绘制Rect.fromLTRB以屏幕左上角为坐标系圆点,分别设置上下左右四个方向距离Rect.fromLTWH根据矩形左上角的点坐标与矩形宽高来绘制Rect.fromCircle根据给定圆形获得一个正方形
圆角矩形RRect的几种构造方式
RRect.fromLTRBXY前四个参数用来绘制矩形位置,剩余两个参数绘制固定 x/y 弧度RRect.fromLTRBR前四个参数用来绘制矩形位置,最后一个参数绘制 Radius 弧度RRect.fromLTRBAndCorners前四个参数用来绘制矩形位置,剩余四个可选参数,根据需求设置四个角 Radius 弧度RRect.fromRectXY第一个参数绘制矩形,剩余两个参数绘制固定 x/y 弧度RRect.fromRectAndRadius第一个参数绘制矩形,最后一个参数绘制 Radius 弧度RRect.fromRectAndCorners第一个参数绘制矩形,剩余四个可选参数,根据需求设置四个角 Radius 弧度
需要注意,当使用drawColor绘制背景色时,其第二个参数BlendMode的类型较为复杂,不一定会达到预期的效果,详细参见BlendMode 文档
除了绘制几何图形,Canvas还能绘制图片,但需要注意,在flutter中有两个名为 Image 的类,它们位于不同的包中。一个是作为Widget的Image ;另一个是ui包中的Image,它是对原始解码图像数据(像素)的不透明句柄。
class MyImage extends CustomPainter{
final ui.Image image;
MyPainter(this.image);
@override
void paint(Canvas canvas, Size size) {
if (_image != null) {
var paint = Paint();
canvas.drawImage(_image, Offset(0, 0), paint);
}
}
@override
bool shouldRepaint(MyPainter oldDelegate){
return oldDelegate.image != this.image;
}
}
加载图片
class _TestDrawImageState extends State<TestDrawImage> {
ui.Image _img;
@override
void initState() {
super.initState();
_loadImage('assets/test.jpg').then((res) {
setState(() {
_img = res;
});
});
}
/// 加载图片
Future<ui.Image> _loadImage(String path) async {
var data = await rootBundle.load(path);
var codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
var info = await codec.getNextFrame();
return info.image;
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: MyPainter(_img),
);
}
}
当我们需要绘制文本时,有两种方式
使用
drawParagraph方法。第一个参数是Paragraph,它来自dart.ui库,是引擎创建的类,不能被继承,需要通过ParagraphBuilder来构造;第二个参数是绘制的位置。需要注意,这里的TextStyle是来自ui库import 'dart:ui' as ui; void paint(Canvas canvas, Size size) { /// 决定文本的大小和样式 final textStyle = ui.TextStyle( color: Colors.black, fontSize: 30, ); /// 决定了ParagraphBuilder用来定位文本段落中的行的配置 final paragraphStyle = ui.ParagraphStyle( textAlign: TextAlign.center, textDirection: TextDirection.ltr, ); final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle) ..pushStyle(textStyle) ..addText('Hello, world.'); // 添加文本和样式 final paragraph = paragraphBuilder.build(); /// 在绘制文本之前必须先进行布局。该任务被传递给Skia引擎 paragraph.layout(ui.ParagraphConstraints(width: 300)); canvas.drawParagraph(paragraph, Offset(50, 100)); }使用
TextPainter绘制。它是一个将TextSpan树绘制到Canvas中的对象,这是Flutter提供的一种简化的封装,无需再导入dart:ui,使用我们熟悉的TextStyle、TextSpan即可。void paint(Canvas canvas, Size size) { final textSpan = TextSpan( text: 'Hello, world.', style: TextStyle( color: Colors.black, fontSize: 30, ), ); /// 创建TextPainter对象 final textPainter = TextPainter( text: textSpan, textDirection: TextDirection.ltr, ); /// 进行布局 textPainter.layout( minWidth: 0, maxWidth: size.width, ); /// 绘制文本 textPainter.paint(canvas, Offset(50, 100)); }
在对Canvas进行变换相关操作时,需要先将画布save(或saveLayer),调用该函数之后的绘制操作和变换操作,会重新记录。变换完成后,再调用restore进行恢复。注意, save() 或者 saveLayer() 必须与 restore() 成对使用,即Canvas 的变换操作需要放到 save() 或 saveLayer() 与restore() 之间进行
canvas.save();
// 平移画布
canvas.translate(100, 100);
canvas.drawImage(background, Offset.zero, paint);
canvas.restore();
saveLayer与save类似,不同的是 saveLayer() 会创建一个新的图层。因此 saveLayer() 到 restore() 之间的操作是在新图层上进行的,最终它们合成到一起。
自绘实例

import 'package:flutter/material.dart';
import 'dart:math' as math;
class CustomArcWidget extends StatelessWidget {
final Color color;
final Color bgColor;
final double radius;
final AlignmentGeometry alignment;
final Widget child;
CustomArcWidget(
{this.color = Colors.white,
this.bgColor = Colors.blue,
@required this.radius,
this.alignment,
this.child});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: ArcPainter(color, bgColor),
child: Container(
alignment: alignment, width: radius, height: radius, child: child),
);
}
}
class ArcPainter extends CustomPainter {
final Color color;
final Color bgColor;
ArcPainter(this.color, this.bgColor);
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 5
..strokeCap = StrokeCap.round
..color = color;
/// 创建一个新的指定大小的图层
canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), paint);
/// 绘制背景色
canvas.drawColor(bgColor, BlendMode.src);
/// 回到旧图层
canvas.restore();
final maxRadius = size.width / 2 - 5;
canvas.drawArc(
Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2), radius: maxRadius),
-240 * (math.pi / 180),
300 * (math.pi / 180),
false,
paint);
paint.strokeWidth = 2;
canvas.drawArc(
Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2),
radius: maxRadius - 5),
-240 * (math.pi / 180),
300 * (math.pi / 180),
false,
paint);
paint.strokeWidth = 1;
canvas.drawArc(
Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2),
radius: maxRadius - 10),
-240 * (math.pi / 180),
300 * (math.pi / 180),
false,
paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
使用
CustomArcWidget(
radius: 100,
alignment: Alignment.center,
child: Text(
"99",
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
实现手绘板
import 'dart:ui';
import 'package:flutter/material.dart';
///
/// 手绘板
///
class HandPaintedBoard extends StatefulWidget {
final Color color;
final double width;
final double height;
final BoardController controller;
HandPaintedBoard(
{this.color = Colors.blue, @required this.width, @required this.height,this.controller});
@override
_HandPaintedBoardState createState() => _HandPaintedBoardState();
}
class _HandPaintedBoardState extends State<HandPaintedBoard> {
GlobalKey _gk = GlobalKey();
final List<DrawStroke> strokes = List<DrawStroke>();
// 总笔画数
int _numOfStrokes = 0;
///
/// 边界检查
///
bool checkBoundary(Offset curPosition) {
Size curSize = _gk.currentContext.size;
RenderBox rBox = _gk.currentContext.findRenderObject();
// 将控件内部的相对位置转换为屏幕上的绝对坐标值
Offset topLeft = rBox.localToGlobal(Offset(0, 0));
Offset bottomRight =
rBox.localToGlobal(Offset(curSize.width, curSize.height));
if (curPosition.dx < topLeft.dx || curPosition.dx > bottomRight.dx)
return false;
if (curPosition.dy < topLeft.dy || curPosition.dy > bottomRight.dy)
return false;
return true;
}
@override
void initState() {
super.initState();
if(widget.controller != null){
widget.controller.reset = reset;
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
key: _gk,
width: widget.width,
height: widget.height,
child: GestureDetector(
onPanDown: (details) {
if (checkBoundary(details.globalPosition)) {
RenderBox rBox = context.findRenderObject();
// 将屏幕上的绝对坐标值转换为控件内部的相对位置
Offset curPoint = rBox.globalToLocal(details.globalPosition);
var stroke = DrawStroke(widget.color);
stroke.points.add(curPoint);
strokes.add(stroke);
setState(() {});
}
},
onPanUpdate: (details) {
RenderBox rBox = context.findRenderObject();
Offset curPoint = rBox.globalToLocal(details.globalPosition);
if (checkBoundary(details.globalPosition)) {
setState(() {
strokes[_numOfStrokes].points.add(curPoint);
});
}
},
onPanEnd: (details) {
_numOfStrokes++;
},
child: CustomPaint(
painter: HandPainter(strokes),
),
));
}
void reset(){
_numOfStrokes = 0;
strokes.clear();
setState(() {});
}
}
typedef ControllerReset = void Function();
class BoardController {
ControllerReset reset;
}
class HandPainter extends CustomPainter {
final List<DrawStroke> strokes;
HandPainter(this.strokes):assert(strokes != null);
@override
void paint(Canvas canvas, Size size) {
var paint = Paint()
..strokeWidth = 5.0
..isAntiAlias = true
..strokeCap = StrokeCap.round;
for (var stroke in strokes) {
paint.color = stroke.color;
// 该模式将点连成线
var _pointMode = PointMode.polygon;
if(stroke.points.length == 1){
// 只有一个点时,pointMode类型为PointMode.points,画点模式
_pointMode = PointMode.points;
}
canvas.drawPoints(_pointMode, stroke.points, paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
///
/// 画的每一笔用一个[DrawStroke]对象表示
///
class DrawStroke {
// 每一笔都由N个点序列组成
final List<Offset> points = List<Offset>();
// 这一笔的颜色
Color color;
DrawStroke(this.color);
}
使用
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Colors.green)
),
child: HandPaintedBoard(
width: 400,height: 400,color: _color,controller: _controller,)),
SizedBox(height: 16,),
Wrap(
children: List.of(Colors.primaries.map<Widget>((color){
return InkWell(
onTap: (){
setState(() {
_color = color;
});
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 4,vertical: 2),
width: 30,
height: 30,
color: color,
),
);
})),
),
SizedBox(height: 16,),
IconButton(
icon: Icon(Icons.delete_forever),
onPressed: (){
_controller.reset();
},
)
],
),
),
公众号“编程之路从0到1”