Flutter 绘制探索 | 操作坐标系范围

前言

在视频 【Flutter 绘制指南 | 第二集 · 坐标系】 中,实现了画板区域内的单位坐标系。今天来拓展一下,让坐标系支持变换,比如坐标系的平移和缩放,从而让坐标系的功能更加完备。

本文要实现的效果如下,可以通过下方的七个按钮操作坐标系的范围,这样可以查看在当前定义域内的函数曲线,也就是移动坐标轴的功能:本文详细源码见 toly1994328/skeleton/lib/paint/coo


1. 坐标范围的定义

目前坐标的范围是 x:[0~1] , y:[0~1],而且是写死的数据。为了可以让坐标轴的范围值可调整,首先需要对刻度的表现进行优化。比如,使用者可以通过一个配置项指定横纵坐标的最大最小值,我们将这个配置项数据赋值为 AxisRange 类型,定义如下:

其中给出 xSpanySpanget 方法,用于获取横纵坐标轴的跨度。然后让 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 类在提供 movescale 方法,移动就是将坐标轴的范围根据偏移量进行平移,缩放是将坐标轴范围乘以 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();
}

到这里,一个简单的坐标轴范围操作的案例就完成了,还有一些值得优化的部分:比如现在刻度是严格按照份数进行分割的,刻度数值比较乱;另外还可以让用户通过输入框确定坐标范围;对函数图像的绘制方式有待优化;这些会在后续逐步完善。那本文就到这里,谢谢观看 ~

作者:张风捷特烈
链接:https://juejin.cn/post/7185014176501727288

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容