手绘板的制作——画布缩放(4)
前言
在這一篇中,我們講解下畫布的縮放,也就是做一個根據(jù)手勢縮放進行畫布縮放的功能。
我們先來梳理下邏輯:
- 監(jiān)聽手勢,當為一根手指的時候,就延續(xù)之前的操作,執(zhí)行手繪操作,當操作為兩根手指的時候,則執(zhí)行縮放功能。
- 對畫布進行縮放
好了,正文開始!
手勢縮放
看了下,GestureDetector 里面有 onScaleStart、onScaleUpdate、onScaleEnd 參數(shù),這…這不是縮放開始、縮放過程中、縮放結(jié)束的回調(diào)嗎?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:移動結(jié)束");widget._invoker.execute(PaintedCommand(_paintedBoardProvider, _paintedBoardProvider.strokes.last));},onScaleStart: (details) { // <- 新增print("onScaleStart:縮放開始");},onScaleUpdate: (details) { // <- 新增print("onScaleStart:縮放進行中");},onScaleEnd: (details) { // <- 新增print("onScaleStart:縮放結(jié)束");},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,然后在縮放的方法里面進行縮放和平移的區(qū)分,所以,我們先定義一個枚舉:
enum GestureType {translate, // 平移scale, // 縮放 }具體區(qū)分代碼:
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:縮放結(jié)束");break;}},主要的思路其實就是:
- 在 onScaleStart 的時候,判斷是單指還是雙指,并且進行記錄該狀態(tài),后續(xù)的 onScaleUpdate、onScaleEnd 都是基于這個單指或者雙指進行操作的。
- 在 onScaleStart 中進行數(shù)據(jù)記錄:
- 單指:創(chuàng)建 stroke 存儲當前繪畫信息,便于后續(xù)手繪。
- 雙指:記錄當前的縮放系數(shù)。
- 在 onScaleUpdate 中進行狀態(tài)更改:
- 單指:更新 path 數(shù)據(jù),進行手繪刷新。
- 雙指:手勢過程的縮放系數(shù) details.scale 是基于 1 進行不斷增大的,直至縮放過程結(jié)束,所以通過 _startScale + details.scale - 1 就能拿到當前 Widget 正確的縮放系數(shù),_startScale 為在 onScaleStart 中存儲的當前縮放系數(shù)。
- 在 onScaleEnd 進行事件的收尾處理:
- 單指:提交命令。
- 雙指:無需操作。
視圖縮放
經(jīng)過以上步驟,我們可以獲取得到手勢縮放的系數(shù),但是這個系數(shù)如何用于放大視圖?
目前一般有兩種步驟:
- 對于 canvas 進行縮放,并且對于 canvas 的繪制內(nèi)容進行全部縮放,例如畫筆原有起點為 (1,1),放大后,需要將畫筆原有起點進行更改,可能就要變?yōu)?(2,2) 了,這種方式需要更改的比較多,所以我就不在這里實踐了,有興趣的同學可以自己試下。
- 使用 Transform 進行縮放,也就是把整個 Widget 進行放大,所以 canvas 的坐標系是沒有改動的,之前繪制的內(nèi)容不需要重新繪制,目前我采取的是這種方案。這里有個重點,canvas 的坐標系是沒有改動的。
所以,使用以下代碼即可完成縮放功能:
child: Transform.scale(scale: _paintedBoardProvider.scale,child: CustomPaint(painter: MyPainter(_paintedBoardProvider),size: Size.infinite,),),所以,這縮放功能就結(jié)束了嗎?
當然沒有這么簡單,這后面才是難點。
我們在進行手繪板制作的時候,使用的坐標點是 details.localFocalPoint,它是基于當前視圖的坐標點,但是它的 (0,0) 坐標并不是固定為視圖的左上角,當視圖大于屏幕的時候,它的 (0,0) 是視圖與屏幕的交接處,所以,無論使用 Transform 進行如何縮放,對于同一個點擊點,其 details.localFocalPoint 的值都是一樣的。(這話可能不夠嚴謹,但是對于我當前的 demo 而言,它原有視圖就是鋪滿整個屏幕,無論它使用 Transform 進行縮放多少倍,同個點擊點的 details.localFocalPoint 值都是一樣的。)
但是,我們特別強調(diào)了,在進行縮放后,canvas 的坐標系是沒有改動的,只是視圖效果放大而已,所以,即使點擊的是同一個位置,在 canvas 的坐標系上的位置也是不相同的,所以,我們要對于后續(xù)繪畫的點進行處理,將 details.localFocalPoint 其轉(zhuǎn)換為基于視圖 (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 轉(zhuǎn)換回來。
清除誤差點
在具體的實操上,其實人點擊屏幕的時候,由于手指接觸屏幕面積較大,所以,經(jīng)常會出現(xiàn)縮放結(jié)束后,還會觸發(fā)繪制的效果,所以,我們在手指抬起之后,對于繪制數(shù)據(jù)進行初步清理,也就是單點的誤差的全部清除,當然,我這種方式還不夠嚴謹,剩下的大家可以根據(jù)具體需求進行調(diào)整:
onScaleEnd: (details) {switch (_gestureType) {case GestureType.translate:// 移除由于誤操作導致的小點出現(xiàn)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:縮放結(jié)束");break;}},總結(jié)
以上是生活随笔為你收集整理的手绘板的制作——画布缩放(4)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: zabix监控linux进程名称,Zab
- 下一篇: 原码一位除法