小工具是非常棒的!而且Flutter为我们提供了大量的小工具。而且Flutter为我们提供了很多开箱即用的小工具。
然而,有些时候,我们希望对我们在屏幕上绘制的内容有更多的控制。
为此,我们可以用CustomPainter
,直接在画布上绘制。
所以在这篇文章中,我们将看到如何在屏幕上(画)出一张快乐的脸。
由于这个教程不需要任何第三方软件包,我们可以用Dartpad来构建这个教程。
用CustomPainter在屏幕上画快乐的脸
初始设置
我们可以从一个MaterialApp
开始,包含一个Scaffold
,一个外部的白色Container
,和一个内部的黄色Container
。
import 'package:flutter/material.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
home: Scaffold(
// Outer white container with padding
body: Container(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 80),
color: Colors.white,
// Inner yellow container
child: Container(
color: Colors.yellow,
),
),
),
);
}
}
如果我们在Dartpad上运行这段代码,我们会得到这样的结果。
带有两个容器的Dartpad预览
接下来,我们可以添加一个 CustomPaint
widget作为内部容器的一个子节点。
Container(
color: Colors.yellow,
child: CustomPaint(painter: FaceOutlinePainter()),
),
CustomPaint
需要一个 类型的参数painter
CustomPainter
的参数,是Flutter APIs中的一个抽象类。
为了绘制东西,我们需要子类CustomPainter
。
class FaceOutlinePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: draw something with canvas
}
@override
bool shouldRepaint(FaceOutlinePainter oldDelegate) => false;
}
我们稍后会看一下这个问题--但首先--我们有一个问题要解决。
如果我们再次运行这个应用程序,有些事情就不对了,因为我们的黄色Container
已经不可见了。
Dartpad预览中缺少黄色的容器
发生这种情况是因为CustomPainter
并不总是能与试图限制其大小的父级小部件友好相处。
为了解决这个问题,我们可以指定父级Container
的宽度和高度。
child: Container(
width: 300,
height: 300,
color: Colors.yellow,
child: CustomPaint(painter: FaceOutlinePainter()),
),
如果我们再次运行代码,我们会得到。
带有固定尺寸黄色容器的Dartpad预览
这就好些了,但仍然不理想。我们希望容器占据UI窗口的全部可用空间。
如果有一个小部件可以告诉我们父窗口的大小,我们就可以用它来调整width
和height
。
这个小部件是存在的,它被称为LayoutBuilder
。让我们来使用它。
LayoutBuilder(
// Inner yellow container
builder: (_, constraints) => Container(
width: constraints.widthConstraints().maxWidth,
height: constraints.heightConstraints().maxHeight,
color: Colors.yellow,
child: CustomPaint(painter: FaceOutlinePainter()),
),
),
在这里,我们把LayoutBuilder
'的最大宽度和高度约束传递给Container
中的相应参数。
如果我们再次运行该代码,我们可以看到容器现在占据了所有的可用空间。
带有可调整大小的黄色容器的Dartpad预览
当我们在Dartpad中调整UI窗口的大小时,这甚至也能发挥作用。
2020年1月28日更新。有人指出,
LayoutBuilder
,在这种情况下是没有必要的。相反,把width: double.infinity, height: double.infinity
作为参数传给黄色的Container
就可以了。
我们都很好,我们可以回到我们的FaceOutlinePainter
。
class FaceOutlinePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: draw something with canvas
}
@override
bool shouldRepaint(FaceOutlinePainter oldDelegate) => false;
}
你好,我是一个画家 👨🎨
我们可以在paint()
方法中用Canvas
对象来画东西,我们可以用shouldRepaint()
来指定我们的画师何时应该重画。由于我们的绘图器没有任何可变的状态,在这个例子中,shouldRepaint()
可以返回false
。
Canvas
是一个大类,有很多方法用于绘制各种图形。
这些方法都有一个共同的参数:一个Paint
对象。
想要一个带有靛蓝笔触的油漆对象,4pt厚?那就来吧。
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4.0
..color = Colors.indigo;
要一个红色的填充物吗?很简单。
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.red;
一旦我们有了一个Paint
对象,我们就可以用它来画东西。
canvas.drawRect(
Rect.fromLTWH(20, 40, 100, 100),
paint,
);
在paint()
,我们可以随意调用画布上的绘画方法。
尊重边界
边界在Flutter中很重要,就像在现实生活中一样😉。
paint()
方法也给了我们一个Size
参数。
我们的CustomPaint
的边界是一个大小的函数:(0, 0, size.width, size.height)
。
让我们重新审视一下我们的设置代码。
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
// Outer white container with padding
body: Container(
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 80),
child: LayoutBuilder(
// Inner yellow container
builder: (_, constraints) => Container(
width: constraints.widthConstraints().maxWidth,
height: constraints.heightConstraints().maxHeight,
color: Colors.yellow,
child: CustomPaint(painter: FaceOutlinePainter()),
),
),
),
),
);
}
}
FaceOutlinePainter
的大小与黄色Container
的边界相匹配,而黄色 又受到其父Container
的padding的影响。
在这种设置下,paint()
方法的参数size
将相当于屏幕尺寸,减去padding。
因此,如果我们想保持在边界内,我们的绘制坐标应该是正数,并且不超过 size.width
和size.height
。
让我们把东西放在一起,画一个快乐的脸。
class FaceOutlinePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Define a paint object
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4.0
..color = Colors.indigo;
// Left eye
canvas.drawRRect(
RRect.fromRectAndRadius(Rect.fromLTWH(20, 40, 100, 100), Radius.circular(20)),
paint,
);
// Right eye
canvas.drawOval(
Rect.fromLTWH(size.width - 120, 40, 100, 100),
paint,
);
// Mouth
final mouth = Path();
mouth.moveTo(size.width * 0.8, size.height * 0.6);
mouth.arcToPoint(
Offset(size.width * 0.2, size.height * 0.6),
radius: Radius.circular(150),
);
mouth.arcToPoint(
Offset(size.width * 0.8, size.height * 0.6),
radius: Radius.circular(200),
clockwise: false,
);
canvas.drawPath(mouth, paint);
}
@override
bool shouldRepaint(FaceOutlinePainter oldDelegate) => false;
}
上面的大部分代码设置了一些坐标,相对于画布的大小。绘画是通过调用各种画布绘画方法来完成的。
注意:嘴巴的坐标是
size.width
和size.height
的一个函数。这确保了嘴的大小与父部件成正比。
这就是最终的结果,在Dartpad中更新。
使用CustomPainter在屏幕上的快乐表情
这里是Dartpad上的完整例子。你可以玩一玩,看看当你调整UI窗口的大小时,绘画器是如何更新的。
总结
让我们对我们所学到的东西做一个总结。
- 我们可以使用一个
CustomPaint
widget来做自定义绘画。 - 这需要一个类型为
CustomPainter
的画师对象。 - 我们可以编写我们自己的
CustomPainter
子类,并重写paint()
和shouldRepaint()
方法。 - 我们可以使用
Canvas
对象来绘制不同的形状。 - 我们可以使用具有各种填充和描边属性的
Paint
对象,来配置我们的形状的外观。
最后,我们必须记住,当我们使用一个CustomPainter
,我们的部件层次结构需要一些额外的注意。换句话说,我们需要指定父width
和height
的Container
。如果我们想让它根据窗口动态地调整大小,我们可以使用来自LayoutBuilder
。
值得注意的是,如果我们不使用
Scaffold
,就没有必要使用LayoutBuilder
。在这种情况下,如果我们在MaterialApp
的home
参数中传递一个Container
,那么一切都可以正常工作。
来吧,我的朋友,画一幅艺术作品吧。🎨
总结
我们已经学会了如何在Flutter中绘画,通过在CustomPainter
子类中绘制自定义形状。
当现有的widget API不够用时,您可以用它来定义自定义形状。
你知道还有什么是很酷的吗?
你可以使用Firebase ML Vision和摄像头API来做实时的特征检测**(条形码**、人脸、标签和文本)。并使用画师来实时绘制覆盖图。
买嘿,也许这是一个未来教程的主题。😎
绘画愉快!🙂