前言

在这一篇中,我们讲解下画布的缩放,也就是做一个根据手势缩放进行画布缩放的功能。

我们先来梳理下逻辑:

  • 监听手势,当为一根手指的时候,就延续之前的操作,执行手绘操作,当操作为两根手指的时候,则执行缩放功能。
  • 对画布进行缩放

好了,正文开始!

手势缩放

看了下,GestureDetector 里面有 onScaleStart、onScaleUpdate、onScaleEnd 参数,这…这不是缩放开始、缩放过程中、缩放结束的回调吗?Flutter 真方便,都给封装好了。赶紧试下:

  @overrideWidget build(BuildContext context) {return GestureDetector(onPanStart: (details) {print("onPanStart:准备开始移动");_paintedBoardProvider.onStart(details);},onPanUpdate: (details) {print("onPanUpdate:正在移动");_paintedBoardProvider.onUpdate(details);},onPanEnd: (details) {print("onPanDown:移动结束");widget._invoker.execute(PaintedCommand(_paintedBoardProvider, _paintedBoardProvider.strokes.last));},onScaleStart: (details) {  // <-  新增print("onScaleStart:缩放开始");},onScaleUpdate:  (details) { // <-  新增print("onScaleStart:缩放进行中");},onScaleEnd:  (details) { // <-  新增print("onScaleStart:缩放结束");},child: CustomPaint(painter: MyPainter(_paintedBoardProvider),size: Size.infinite,),);}

运行…

======== Exception caught by widgets library =======================================================
The following assertion was thrown building HandPaintedBoard(dirty, state: _HandPaintedBoardState#7df2e):
Incorrect GestureDetector arguments.Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan.Just use the scale gesture recognizer.

em…咋报错了…

大意就是,缩放手势包含了平移手势,所以,同时赋值缩放手势和平移手势是多余,直接使用缩放手势即可。

具体报错源码就是:

         final bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null;final bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null;if (havePan || haveScale) {if (havePan && haveScale) {throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Incorrect GestureDetector arguments.'),ErrorDescription('Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan.',),ErrorHint('Just use the scale gesture recognizer.'),]);}

这能怎么办?解决!

所以,我们需要把 onPanStart、onPanUpdate、onPanEnd 去掉,只保留 onScaleStart、onScaleUpdate、onScaleEnd,然后在缩放的方法里面进行缩放和平移的区分,所以,我们先定义一个枚举:

enum GestureType {translate, // 平移scale, // 缩放
}

具体区分代码:

class _HandPaintedBoardState extends State<HandPaintedBoard> {PaintedBoardProvider get _paintedBoardProvider =>widget._paintedBoardProvider;// 标识手势GestureType _gestureType = GestureType.translate;  // <- 新增// 记录缩放开始的缩放double _startScale = 1; // <- 新增
class PaintedBoardProvider extends ChangeNotifier {// 缩放比例double scale = 1;  // <- 新增
      onScaleStart: (details) {if (details.pointerCount > 1) {  // 双指_gestureType = GestureType.scale;_startScale = _paintedBoardProvider.scale;} else { // 单指_gestureType = GestureType.translate;_paintedBoardProvider.onStart(details.localFocalPoint);}},onScaleUpdate: (details) {switch (_gestureType) {case GestureType.translate:_paintedBoardProvider.onUpdate(details.localFocalPoint);break;case GestureType.scale:setState(() {_paintedBoardProvider.scale = _startScale + details.scale - 1;});break;}},onScaleEnd: (details) {switch (_gestureType) {case GestureType.translate:widget._invoker.execute(PaintedCommand(_paintedBoardProvider, _paintedBoardProvider.strokes.last));break;case GestureType.scale:print("onScaleEnd:缩放结束");break;}},

主要的思路其实就是:

  • 在 onScaleStart 的时候,判断是单指还是双指,并且进行记录该状态,后续的 onScaleUpdate、onScaleEnd 都是基于这个单指或者双指进行操作的。
  • 在 onScaleStart 中进行数据记录:
    • 单指:创建 stroke 存储当前绘画信息,便于后续手绘。
    • 双指:记录当前的缩放系数。
  • 在 onScaleUpdate 中进行状态更改:
    • 单指:更新 path 数据,进行手绘刷新。
    • 双指:手势过程的缩放系数 details.scale 是基于 1 进行不断增大的,直至缩放过程结束,所以通过 _startScale + details.scale - 1 就能拿到当前 Widget 正确的缩放系数,_startScale 为在 onScaleStart 中存储的当前缩放系数。
  • 在 onScaleEnd 进行事件的收尾处理:
    • 单指:提交命令。
    • 双指:无需操作。

视图缩放

经过以上步骤,我们可以获取得到手势缩放的系数,但是这个系数如何用于放大视图?

目前一般有两种步骤:

  • 对于 canvas 进行缩放,并且对于 canvas 的绘制内容进行全部缩放,例如画笔原有起点为 (1,1),放大后,需要将画笔原有起点进行更改,可能就要变为 (2,2) 了,这种方式需要更改的比较多,所以我就不在这里实践了,有兴趣的同学可以自己试下。
  • 使用 Transform 进行缩放,也就是把整个 Widget 进行放大,所以 canvas 的坐标系是没有改动的,之前绘制的内容不需要重新绘制,目前我采取的是这种方案。这里有个重点,canvas 的坐标系是没有改动的

所以,使用以下代码即可完成缩放功能:

      child: Transform.scale(scale: _paintedBoardProvider.scale,child: CustomPaint(painter: MyPainter(_paintedBoardProvider),size: Size.infinite,),),

所以,这缩放功能就结束了吗?

当然没有这么简单,这后面才是难点。

我们在进行手绘板制作的时候,使用的坐标点是 details.localFocalPoint,它是基于当前视图的坐标点,但是它的 (0,0) 坐标并不是固定为视图的左上角,当视图大于屏幕的时候,它的 (0,0) 是视图与屏幕的交接处,所以,无论使用 Transform 进行如何缩放,对于同一个点击点,其 details.localFocalPoint 的值都是一样的。(这话可能不够严谨,但是对于我当前的 demo 而言,它原有视图就是铺满整个屏幕,无论它使用 Transform 进行缩放多少倍,同个点击点的 details.localFocalPoint 值都是一样的。)

但是,我们特别强调了,在进行缩放后,canvas 的坐标系是没有改动的,只是视图效果放大而已,所以,即使点击的是同一个位置,在 canvas 的坐标系上的位置也是不相同的,所以,我们要对于后续绘画的点进行处理,将 details.localFocalPoint 其转换为基于视图 (0,0) 点的坐标。

图片说明:

  • 蓝框为原图,x、y 为原图的坐标系。
  • Transform 默认是基于中心仅放大的,所以,黄框是实际上放大的效果。
  • 若放大前的 details.localFocalPoint 为 (10,10),那么放大后同个点击处的 details.localFocalPoint 仍然为 (10,10)
  • 由于手绘绘制是基于画布 (0,0) 位置的,也就是黄框的左上角,所以,我们需要把 details.localFocalPoint 加上两条绿边距离,才是真正的手绘坐标点。
  • 那两条绿边怎么计算?我们先算 x 坐标的,假设原图大小为 w1,放大后的大小为 w2,那绿边 x = (w2-w1) / 2,而 w2 其实就是 w1 乘以 scale,所以 x = (scale-1) * w1 /2

实际的代码实操:

首先,我们需要存储原有的画布大小:

class PaintedBoardProvider extends ChangeNotifier {// 画布原有尺寸Size realCanvasSize = Size.zero;

具体的赋值在 MyPainter:

class MyPainter extends CustomPainter {@overridevoid paint(Canvas canvas, Size size) {paintedBoardProvider.realCanvasSize = size;

剩下的就是换算了:

/// 移动开始时void onStart(Offset localPosition) {double startX = localPosition.dx;double startY = localPosition.dy;final newStroke = Stroke(color: isClear ? Colors.transparent : color, width: paintWidth,isClear: isClear, );newStroke.path.moveTo((startX + (scale - 1) * realCanvasSize.width / 2 ) /scale,(startY + (scale - 1) * realCanvasSize.height / 2 ) /scale);_strokes.add(newStroke);}/// 移动void onUpdate(Offset localPosition) {_strokes.last.path.lineTo((localPosition.dx +(scale - 1) * realCanvasSize.width / 2 ) /scale,(localPosition.dy +(scale - 1) * realCanvasSize.height / 2 ) /scale);notifyListeners();}

可能会有人有疑问,为什么换算后的值还要除以 scale,em…这还是因为 canvas 的坐标系没有更改过,我们的换算都是基于真正进行放大后的换算,但是实际上坐标系没有放大,所以还要除以 scale 转换回来。

清除误差点

在具体的实操上,其实人点击屏幕的时候,由于手指接触屏幕面积较大,所以,经常会出现缩放结束后,还会触发绘制的效果,所以,我们在手指抬起之后,对于绘制数据进行初步清理,也就是单点的误差的全部清除,当然,我这种方式还不够严谨,剩下的大家可以根据具体需求进行调整:

      onScaleEnd: (details) {switch (_gestureType) {case GestureType.translate:// 移除由于误操作导致的小点出现final lastBounds = _paintedBoardProvider.strokes.last.path.getBounds();if (lastBounds.width < 0.5 && lastBounds.height < 0.5) {_paintedBoardProvider.strokes.removeLast();_paintedBoardProvider.refreshPaintedBoard();} else {widget._invoker.execute(PaintedCommand(_paintedBoardProvider, _paintedBoardProvider.strokes.last));}break;case GestureType.scale:print("onScaleEnd:缩放结束");break;}},

手绘板的制作——画布缩放(4)相关推荐

  1. 手绘板的制作——画布保存(6)

    「手绘板的制作--手绘(1)」 「手绘板的制作--重置与橡皮擦(2)」 「手绘板的制作--命令模式与撤销.重制(3)」 「手绘板的制作--画布缩放(4)」 「手绘板的制作--画布移动(5)」 前言 经 ...

  2. 手绘板的制作——画布移动(5)

    前言 在上文「手绘板的制作--画布缩放(4)」中,我们学会了画布的缩放,这节我们学习下画布的移动,毕竟放大的画布不能移动的话,那放大还有什么意义.=_= 手势检测 既然要移动,那当然需要检测手势,由于 ...

  3. 手绘板的制作——手绘(1)

    前言 通过上一篇文章「如何优雅地画一张图」我们已经知道如何在画布里面绘画一张图了,这次我准备开一个系列讲解下手绘板的制作,可能包含: 手绘 橡皮擦 撤销 重制 重置 图片导出 命令模式 等功能.具体等 ...

  4. 手绘板的制作——命令模式与撤销、重制(3)

    前言 我们这篇来了解下撤销.重制的功能,其实也就是 undo 和 redo,在这里我们使用命令模式去设计,若对该模式不了解的话,可以考虑看下 「关于命令模式的误区,你知道了吗」. 其实对于命令模式,我 ...

  5. Android View与SurfaceView的手绘板制作

    最近学习了如何使用View与SurfaceView制作简单的手绘板,在此做个小结. 自定义VIew实现手绘板: 首先是使用View来实现手绘板: package com.app.superxlcr.m ...

  6. java画图颜色_手绘板,多种颜色选择。我抄的《疯狂java讲义》的,包我乱导的,但代码能用。...

    [java]代码库import javax.swing.*; import java.awt.image.*; import java.awt.datatransfer.*; import javax ...

  7. 手绘地图如何制作为电子地图实现智慧导览系统之切割瓦片图

    此系列文章主要简述一下如何把手绘地图的静态图片文件制作成为动态的电子地图,进而实现智慧导览. 因为手绘地图是一张静态的图片文件,而且往往很大,因此要转为手绘地图,才更方便用户查看.那么到底如何才能实现 ...

  8. 手绘线条一直画不直_手绘板线条画不直怎么办?板绘画线诀窍分享

    在数位板画画和在纸上画画是有一定区别的,线条很难控制,手绘板线条画不直,那么如何在数位板上画出流畅线条?越是习惯手绘的朋友,那你可能就需要画更多的时间来适应数位板的画图方式,今天微课菌给大家分享一组板 ...

  9. 什么是数位板? 数位板,又名绘图板、绘画板、手绘板等等,是计算机输入设备的一种,通常是由一块板子和一支压感笔组成,它和手写板等作为非常规的输入产品相类似,都针对一定的使用群体。 与手写板所不同的是

    什么是数位板? 数位板,又名绘图板.绘画板.手绘板等等,是计算机输入设备的一种,通常是由一块板子和一支压感笔组成,它和手写板等作为非常规的输入产品相类似,都针对一定的使用群体. 与手写板所不同的是,数 ...

最新文章

  1. ebpf 学习-bpftrace 语法 入门
  2. 重磅!!面试季--最新面试题总结出厂,附题解,后期持续分享!
  3. window.onload与$(document).ready()的区别
  4. 作者:卢祥虎,男,北京金信网银金融信息服务有限公司机器学习算法工程师。...
  5. Bloom是REST API缓存中间件,充当负载平衡器Nginx和REST API服务之间的反向代理
  6. 泰语翻译软件隐私政策
  7. 2023南京理工大学计算机考研信息汇总
  8. php+redis+保存多个值,php向redis list一次性lPush多个值
  9. JS中常见的兼容写法
  10. 深入理解C语言小括号用法
  11. 如何提升抖音播放量?抖音上热门的技巧分享
  12. 【File类、递归】
  13. 业余草网站热门关键字
  14. 【Codecs系列】H.266开源编解码器:vvenc和vvdec介绍
  15. 我的世界java版怎么用结构方块_我的世界结构方块指令(获取结构方块和 使用方法)...
  16. 初露锋芒 MeeGo系统进入智能电视发展领域
  17. 常用正则,姓名匹配,去空格等
  18. 用友开发者社区 - http://udn.yonyou.com
  19. 《深入理解计算机系统》 练习题3.49详解
  20. JAVA动态代理实现

热门文章

  1. 传感器尺寸、像素、DPI分辨率、英寸、毫米的关系
  2. 黄海燕 北京理工大学计算机学院,王文明_北京理工大学计算机学院
  3. Linux 性能优化之 IO 子系统
  4. 极域脱控破解分析+代码实现(杀死和重启,挂起和恢复,解除全屏按钮限制,获取极域安装路径,极域密码破解)
  5. 一鸣心所向:如何做好社团营销?深入简出带你了解。
  6. Nutanix 斩获2020年“云鼎奖”两项殊荣
  7. 亚马逊API接口,Onebound数据
  8. SpingSecurity配置
  9. 等额本息计算 按期计算
  10. 【Week7作业 B】TT的旅行日记【dijkstra】