前言
在视频 【Flutter 绘制指南 | 第二集 · 坐标系】 中,实现了画板区域内的单位坐标系。今天来拓展一下,让坐标系支持变换,比如坐标系的平移和缩放,从而让坐标系的功能更加完备。
本文要实现的效果如下,可以通过下方的七个按钮操作坐标系的范围,这样可以查看在当前定义域内的函数曲线,也就是移动坐标轴的功能:本文详细源码见 toly1994328/skeleton/lib/paint/coo
1. 坐标范围的定义
目前坐标的范围是 x:[0~1]
, y:[0~1]
,而且是写死的数据。为了可以让坐标轴的范围值可调整,首先需要对刻度的表现进行优化。比如,使用者可以通过一个配置项指定横纵坐标的最大最小值,我们将这个配置项数据赋值为 AxisRange
类型,定义如下:
其中给出 xSpan
和 ySpan
的 get
方法,用于获取横纵坐标轴的跨度。然后让 Coordinate
类持有坐标轴范围对象:
class AxisRange {
final double maxX;
final double minX;
final double maxY;
final double minY;
const AxisRange({
this.maxX = 1.0,
this.minX = -1.0,
this.maxY = 1.0,
this.minY = -1.0,
});
double get xSpan => maxX - minX;
double get ySpan => maxY - minY;
}
2. 刻度值的绘制优化
接下来,就要根据坐标轴的范围来绘制刻度和网格。拿坐标系横轴来说,刻度个数 xScaleCount
由用户指定,这样很容易计算出每个刻度间的步长 step
。横轴最左侧是坐标轴横轴范围的最小值,最右侧是范围最大值。然后遍历 xScaleCount
次即可:
如下是 x 轴刻度遍历绘制的逻辑刻度和竖向网格线的逻辑:其中刻度的偏移量,由当前刻度值处占总长的分率乘以区域宽度计算得出。
double stepX = (range.xSpan / xScaleCount);
for(int i =0;i<=xScaleCount;i++){
double value = range.minX+stepX*i;
double offsetX = (value - range.minX) / range.xSpan * size.width;
textPainter.text = TextSpan(
text: value.toStringAsFixed(2),
style: const TextStyle(fontSize: 12, color: Colors.black),
);
textPainter.layout();
// 绘制文字
textPainter.paint(
canvas,
Offset(offsetX - textPainter.size.width / 2, 5),
);
// 绘制网格
canvas.drawLine(
Offset(offsetX, 0),
Offset(offsetX, -size.height),
gridAxisPaint,
);
}
纵轴的刻度的绘制同理,这样只要指定坐标轴的范围,然后更新画板,坐标轴的对应刻度就会发生改变:
3. 坐标轴上的点
接下来就是最关键的一步,如何在坐标轴上描点。由于展示的坐标系上的点和实际的画板中绘制的逻辑像素并不相同,所以需要对坐标系上的点进行一下转换,使其称为画板中的绝对坐标。转换起来也并不是很困难,根据当前坐标占横纵坐标的分率,再结合画板尺寸就很容易算出当前点在画板中的绝对坐标:
void _drawPoint(Canvas canvas, Size size){
Offset p = const Offset(0.6, 0.6);
double x = (p.dx - coordinate.range.minX)/coordinate.range.xSpan*size.width;
double y = (p.dy - coordinate.range.minY)/coordinate.range.ySpan*size.height;
canvas.drawCircle(Offset(x, y), 10, _mainPainter);
}
这样无论坐标轴的范围如何变化,都不会影响坐标点在坐标系中的位置,如下横坐标范围在 0.10~2.10
之间:
4. 绘制函数
有了坐标轴,其实函数图像的绘制还是比较简单的,无非就是在当前坐标系下收集点,然后根据点画线罢了。比如在坐标系中绘制一个 sin 函数曲线,定义域在 [-1~1] 之间:
绘制逻辑如下,给定 pointCount
表示曲线中点的个数,个数越多曲线越精细,相对来说绘制也就越耗时。思路是遍历 pointCount
次,每次遍历时使用函数关系取点,将点连接就可以得到折线,只不过取的点很多就呈现了曲线效果。后面也可以少取一下点,通过贝塞尔曲线拟合点。
void _drawFn(Canvas canvas, Size size,){
List<Offset> points = [];
int pointCount = 500;
double step = coordinate.range.xSpan/pointCount;
for(int i =0;i<pointCount;i++){
double dx = coordinate.range.minX+step*i;
double dy = 0.6*sin(dx*5);
double x = (dx - coordinate.range.minX)/coordinate.range.xSpan*size.width;
double y = (dy - coordinate.range.minY)/coordinate.range.ySpan*size.height;
points.add(Offset(x, y));
}
canvas.drawPoints(PointMode.polygon, points, _mainPainter..color=Colors.blue);
}
这样多收集几条点集,就可以绘制多条曲线,比如下面红色是 y=x
函数,紫色是 y = x*x*x/20
函数:
5. 操作坐标系范围
现在一切准备就绪,接下来只要通过点击按钮,操作坐标系范围,即可实现如下效果:
在 Coordinate
类在提供 move
和 scale
方法,移动就是将坐标轴的范围根据偏移量进行平移,缩放是将坐标轴范围乘以 rate
倍率:
---->[coordinate.dart]----
void move(Offset offset) {
range = AxisRange(
minY: range.minY + offset.dy,
maxY: range.maxY + offset.dy,
minX: range.minX + offset.dx,
maxX: range.maxX + offset.dx,
);
}
void scale(double rate) {
range = AxisRange(
minY: range.minY * rate,
maxY: range.maxY * rate,
minX: range.minX * rate,
maxX: range.maxX * rate,
);
}
在点击按钮时触发 _onTapCtrl
方法,其中根据 CtrlType
执行对应方法即可,比如点击左上右下时让坐标轴进行移动:
void _onTapCtrl(CtrlType type) {
if(type == CtrlType.right){
coordinate.move(const Offset(0.1, 0));
}
if(type == CtrlType.left){
coordinate.move(const Offset(-0.1, 0));
}
if(type == CtrlType.up){
coordinate.move(const Offset(0, 0.1));
}
if(type == CtrlType.down){
coordinate.move(const Offset(0, -0.1));
}
pointValues.repaint();
}
到这里,一个简单的坐标轴范围操作的案例就完成了,还有一些值得优化的部分:比如现在刻度是严格按照份数进行分割的,刻度数值比较乱;另外还可以让用户通过输入框确定坐标范围;对函数图像的绘制方式有待优化;这些会在后续逐步完善。那本文就到这里,谢谢观看 ~
作者:张风捷特烈