Flutter中画布与自定义绘制的深入探索
1. 平移与变换
在Flutter里,我们可以借助
Transform
类来对组件进行平移操作。例如,使用
Transform.translate()
构造函数,将
Transform
组件作为要移动组件的父组件:
Transform.translate(
offset: Offset(100, 300),
child: RaisedButton(
child: Text("translated to bottom"),
onPressed: () {},
),
);
也可以使用默认构造函数搭配
Matrix4
来指定平移:
Transform(
transform: Matrix4.translationValues(100, 300, 0),
child: RaisedButton(
child: Text("translated to bottom"),
onPressed: () {},
),
);
当需要对组件应用多个变换时,有两种方式。一种是添加多个
Transform
组件:
Transform.translate(
offset: Offset(70, 200),
child: Transform.rotate(
angle: -45 * (math.pi / 180.0),
child: Transform.scale(
scale: 2.0,
child: RaisedButton(
child: Text("multiple transformations"),
onPressed: () {},
),
),
),
);
这种方式虽易读,但会增加组件树的深度。另一种是使用默认的
Transform
构造函数和
Matrix4
对象:
Transform(
alignment: Alignment.center,
transform: Matrix4.translationValues(70, 200, 0)
..rotateZ(-45 * (math.pi / 180.0))
..scale(2.0, 2.0),
child: RaisedButton(
child: Text("multiple transformations"),
onPressed: () {},
),
);
这种方式避免了嵌套组件,使组件树更简洁。
2. 自定义绘制与画布
Flutter为开发者提供了丰富的工具来构建应用界面。除了使用各种组件,还能借助
CustomPaint
、
CustomPainter
和
Canvas
这三个类来创建具有独特外观和行为的组件。
2.1 Canvas类
Canvas
可看作是我们进行绘图的空间,能绘制线条、圆形和矩形等形状。不过,Flutter的
Canvas
并非真正的画布,它只是一个用于记录图形操作的接口,这些操作将在下一渲染帧绘制。
2.2 Canvas变换与裁剪
在
Canvas
上的所有操作都基于一个坐标系,其原点默认由拥有
Canvas
的
CustomPaint
组件定义。
Canvas
的当前变换会影响所有操作,我们可以随时对其进行变换。此外,
Canvas
有当前裁剪区域,默认裁剪区域是无限的。
2.3 Canvas常用方法
| 方法 | 描述 |
|---|---|
drawArc()
| 用于绘制闭合弧线或圆弧段 |
drawCircle()
| 用于绘制指定半径的圆 |
drawImage()
|
用于在
Canvas
上绘制图像
|
drawLine()
|
用于在
Canvas
上绘制线条
|
drawRect()
|
用于在
Canvas
上绘制矩形
|
rotate()
|
为当前
Canvas
变换添加旋转变换
|
scale()
|
为当前
Canvas
变换添加缩放变换
|
translate()
|
为当前
Canvas
变换添加平移变换
|
更多方法和详细信息可查看文档:https://docs.flutter.io/flutter/dart-ui/Canvas-class.html。
2.4 Paint对象
Paint
对象描述了在
Canvas
上绘制时的样式,可定义颜色和笔触宽度等。所有
Canvas
的绘制方法都将
Paint
对象作为参数,且同一个
Paint
实例可在多次绘制调用中复用。
3. CustomPaint组件
在Flutter中,若要手动绘制图形,需使用
CustomPaint
组件。其主要作用是提供一个
Canvas
对象供我们操作,并委托一个
CustomPainter
对象负责在
Canvas
上绘制。
CustomPaint
组件的构造函数如下:
const CustomPaint({
Key key,
CustomPainter painter,
CustomPainter foregroundPainter,
Size size: Size.zero,
bool isComplex: false,
bool willChange: false,
Widget child
})
各属性说明如下:
-
painter
:在
Canvas
上绘制内容的绘制器实现。
-
foregroundPainter
:在子组件绘制后,在
Canvas
上绘制内容的绘制器实现。
-
size
:若
child
属性不为空,则使用子组件的大小,此值将被忽略;否则,指定绘制所需的大小。
-
isComplex
和
willChange
:为合成器的光栅缓存提供提示,有助于分析渲染成本。
-
child
:组件树中位于下方的子组件。
绘制顺序为:先执行
painter
的操作,然后绘制
child
,最后(若有)
foregroundPainter
在子组件前面绘制。
4. CustomPainter对象
若要创建自己的绘制逻辑,需继承
CustomPainter
类并重写
paint()
和
shouldRepaint()
两个方法。
4.1 paint方法
paint()
方法是
CustomPainter
的核心,每当组件需要重绘时会被调用:
void paint (
Canvas canvas,
Size size
)
该方法接收两个参数:
canvas
用于实际绘制,
size
定义了绘制的边界。绘制操作应在给定区域内进行。
4.2 shouldRepaint方法
shouldRepaint()
方法对Flutter引擎很重要:
bool shouldRepaint (
covariant CustomPainter oldDelegate
)
它接收
oldDelegate
参数,代表上一个负责在
CustomPaint
上绘制的委托。若返回
false
,
paint
调用可能会被优化(但不意味着不会调用)。我们应比较新旧委托,判断与绘制相关的数据是否有变化,有变化则返回
true
。
5. 实际示例 - 饼图组件
下面我们通过一个实际示例,展示如何使用
Canvas
和
CustomPaint
组件创建自定义绘制的组件,这里以饼图和径向图为例。
5.1 定义组件
首先定义
PieChart
组件,它继承自
StatelessWidget
:
class PieChart extends StatelessWidget {
final List<int> values;
final List<Color> colors;
...
}
values
列表表示每个扇形的值,
colors
列表包含用于绘制每个扇形的颜色。其
build()
方法如下:
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: CustomPaint(
painter: PieChartPainter(
values,
colors
),
),
),
],
);
}
由于
CustomPaint
组件需要大小,这里将其放在
Row
组件中,并使用
Expanded
组件填充可用的水平空间。
5.2 定义CustomPainter
定义
PieChartPainter
类,重写
shouldRepaint()
和
paint()
方法。
graph TD;
A[开始] --> B[定义PieChartPainter类];
B --> C[重写shouldRepaint方法];
B --> D[重写paint方法];
C --> E{判断values或colors是否改变};
E -- 是 --> F[返回true];
E -- 否 --> G[返回false];
D --> H[计算圆心和半径];
H --> I[创建Rect实例];
I --> J[计算总数值];
J --> K[调用_paintCircle方法];
K --> L[绘制扇形];
L --> M[结束];
shouldRepaint()
方法:
@override
bool shouldRepaint(PieChartPainter oldDelegate) {
return !ListEquality().equals(oldDelegate.values, values) ||
!ListEquality().equals(oldDelegate.colors, colors);
}
paint()
方法:
@override
void paint(Canvas canvas, Size size) {
var center = Offset(size.width / 2, size.height / 2);
var radius = (size.width * 0.75) / 2;
Rect chartRect = Rect.fromCircle(
center: center,
radius: radius,
);
int total = values.reduce((a, b) => a + b);
_paintCircle(canvas, total, chartRect);
}
_paintCircle()
方法:
void _paintCircle(Canvas canvas, int total, Rect chartRect) {
Paint sectionPaint = Paint()..style = PaintingStyle.fill;
double startAngle = -90;
for (var i = 0; i < values.length; i++) {
final value = values[i];
final color = colors[i];
double sweepAngle = ((value * 360.0) / total);
sectionPaint.color = color;
canvas.drawArc(
chartRect,
startAngle * _toRadians,
sweepAngle * _toRadians,
true,
sectionPaint,
);
startAngle += sweepAngle;
}
}
通过以上步骤,我们就完成了饼图组件的绘制。在绘制过程中,我们首先计算圆心和半径,创建矩形区域,然后计算每个扇形的角度,最后使用
drawArc()
方法绘制扇形。同时,要注意将角度值转换为弧度后再传递给
drawArc()
方法。
Flutter中画布与自定义绘制的深入探索
6. 径向图组件
为了进一步展示
CustomPaint
组件的潜力,我们来创建一个径向图组件。
6.1 定义组件
RadialChart
组件与前面定义的
PieChart
组件非常相似,具有相同的参数和基本目标。其
build()
方法如下:
// part of radial_chart.dart RadialChart widget
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: CustomPaint(
painter: RadialChartPainter(
values,
colors,
Theme.of(context).textTheme.display1,
Directionality.of(context),
),
),
),
],
);
}
与
PieChart
组件的区别在于,
CustomPaint
组件的
painter
属性传递了一个新的
RadialChartPainter
类实例,并且除了
values
和
colors
,还传递了
TextStyle
和
TextDirection
两个额外参数。
6.2 定义CustomPainter
RadialChartPainter
类与
PieChartPainter
类在某些特定部分有所不同。其
paint()
方法如下:
// part of radial_chart.dart RadialChartPainter class
@override
void paint(Canvas canvas, Size size) {
var center = Offset(size.width / 2, size.height / 2);
var radius = size.width * 0.75 / 2;
Rect chartRect = Rect.fromCircle(
center: center,
radius: radius,
);
int total = values.reduce((a, b) => a + b);
_paintTotal(canvas, total, chartRect);
_paintCircle(canvas, total, chartRect);
}
与
PieChartPainter
的
paint()
方法相比,多了一个
_paintTotal(canvas, total, chartRect)
调用。
_paintCircle()
方法:
// part of radial_chart.dart RadialChartPainter class
void _paintCircle(Canvas canvas, int total, Rect chartRect) {
Paint sectionPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 30.0;
double startAngle = -90;
for (var i = 0; i < values.length; i++) {
final value = values[i];
final color = colors[i];
double sweepAngle = ((value * 360.0) / total);
sectionPaint.color = color;
canvas.drawArc(
chartRect,
(startAngle + 2) * _toRadians,
(sweepAngle - 2)* _toRadians,
false,
sectionPaint,
);
startAngle += sweepAngle;
}
}
这里的变化如下:
- 将
sectionPaint
的样式改为
PaintingStyle.stroke
,只绘制形状的轮廓,并设置了
strokeWidth
属性。
- 在将角度值传递给
drawArc
函数之前,对
startAngle
加2°,对
sweepAngle
减2°,使扇形之间留出一点空间,以获得更好的视觉效果。
- 将
useCenter
参数设置为
false
,绘制圆弧段而不是填充的扇形。
_paintTotal()
方法用于在图表中心绘制总数值标签:
void _paintTotal(Canvas canvas, int total, Rect chartRect) {
final totalPainter = TextPainter(
maxLines: 1,
text: TextSpan(
style: textStyle,
text: "$total",
),
textDirection: textDirection,
);
totalPainter.layout(maxWidth: chartRect.width);
totalPainter.paint(
canvas,
chartRect.center.translate(
-totalPainter.width / 2.0,
-totalPainter.height / 2.0,
),
);
}
绘制文本的步骤如下:
1. 实例化一个
TextPainter
对象,定义文本的外观和方向。
2. 调用
layout()
函数,计算文本的视觉位置。
3. 根据文本的大小,将其定位在图表的中心。
7. 总结与拓展
通过以上饼图和径向图的示例,我们可以看到使用
CustomPaint
、
CustomPainter
和
Canvas
类可以创建出具有独特外观和行为的组件。虽然这两个图表看起来很相似,但主要区别在于定义的绘制器。我们可以将这些功能抽象到一个单一的组件中,通过获取所需的图表类型并更改传递给
CustomPaint
组件的绘制器,实现不同类型图表的绘制。
以下是创建自定义绘制组件的一般步骤总结:
1. 定义组件类,继承自
StatelessWidget
或
StatefulWidget
,并确定组件所需的属性。
2. 在组件的
build()
方法中,使用
CustomPaint
组件,并传递自定义的
CustomPainter
实例。
3. 定义
CustomPainter
类,继承自
CustomPainter
,并重写
paint()
和
shouldRepaint()
方法。
4. 在
paint()
方法中,使用
Canvas
的各种绘制方法进行实际绘制。
5. 在
shouldRepaint()
方法中,判断是否需要重绘。
graph LR;
A[定义组件类] --> B[确定属性];
B --> C[编写build方法];
C --> D[使用CustomPaint组件];
D --> E[传递CustomPainter实例];
E --> F[定义CustomPainter类];
F --> G[重写paint方法];
F --> H[重写shouldRepaint方法];
G --> I[使用Canvas绘制];
H --> J{判断是否重绘};
J -- 是 --> K[重绘];
J -- 否 --> L[不重绘];
在实际开发中,我们可以根据需求进一步拓展这些功能,例如添加动画效果、交互功能等,以创建更加丰富和吸引人的用户界面。同时,要注意性能优化,合理使用
shouldRepaint()
方法,避免不必要的重绘。
超级会员免费看
3846

被折叠的 条评论
为什么被折叠?



