Android通過交互實現(xiàn)貝塞爾曲線的繪制
前言
之前幾篇我們介紹了貝塞爾曲線的原理、繪制曲線和動效實現(xiàn),這些都是代碼預(yù)設(shè)好的,如果我們要根據(jù)需要自行繪制曲線,就需要使用交互來實現(xiàn)了。本篇我們先來介紹簡單的交互式繪圖,通過獲取觸控位置來設(shè)定貝塞爾曲線的控制點,從而實現(xiàn)交互式繪制曲線。
獲取觸控位置
第一個要解決的問題是如何獲取手指在屏幕的觸控位置。在 Flutter 中,提供了一個 Listener
組件,可以監(jiān)聽各類觸控事件。Listener
的組件構(gòu)造方法定義如下:
const Listener({ Key? key, this.onPointerDown, this.onPointerMove, this.onPointerUp, this.onPointerHover, this.onPointerCancel, this.onPointerSignal, this.behavior = HitTestBehavior.deferToChild, Widget? child, })
其中onPointerXX
都是觸控事件的回調(diào)方法,在回調(diào)里會攜帶觸控的位置信息,具體的參數(shù)如下:
onPointerDown
:觸控點按下去(類似鼠標(biāo)按下未釋放)時的回調(diào)方法,攜帶了一個PointerDownEvent
參數(shù),該參數(shù)會包含點按信息,其中有一個localPosition
參數(shù),即當(dāng)時點按時的位置。onPointerUp
:觸控點按 松開(類似鼠標(biāo)按下后釋放)時的回調(diào)方法,攜帶了一個PointerUpEvent
參數(shù),該參數(shù)會包含點按信息,其中有一個localPosition
參數(shù),即當(dāng)時點按時的位置。onPointerMove
:按下后移動(拖動)時的回調(diào)方法,攜帶了PointerMoveEvent
參數(shù),包含了起始位置和移動的距離等信息。onPointerHover
:這個很好理解,hover
狀態(tài)時的事件,不過在觸控設(shè)備上沒有hover
事件。onPointerCancel
:產(chǎn)生onPointerDown
事件的指針(Pointer)不再指向該接收器(有點費解,目前沒想到具體的應(yīng)用場景,后續(xù)遇到了再研究一下)。onPointerSignal
:當(dāng)指針發(fā)出其他信息時的回調(diào),目前也沒想到具體的場景,可能是桌面應(yīng)用會涉及到。behavior
:這個參數(shù)需要注意,也就是觸控事件的檢測方式,是一個HitTestBehavior
枚舉,默認是deferToChild
,只允許子組件響應(yīng)觸控事件。opaque
自身可以響應(yīng)觸控事件(子組件和自己都可以),但下層的組件無法響應(yīng)觸控事件。translucent
為穿透類型,下層組件也能夠響應(yīng)到觸控事件。我們可以根據(jù)需要來確定觸控事件的響應(yīng)方式。
下面是點按后獲取觸控位置為例代碼,使用的話還是比較簡單的。當(dāng)然,這里要說明一下,如果只是單純地獲取觸控事件,而不需要觸控的細節(jié)(比如位置,觸控力度等),那么官方推薦是使用更高封裝層級的手勢識別組件 GestureDetector
。
Listener( onPointerUp: ((event) { print('position: ${event.localPosition}'); }), behavior: HitTestBehavior.opaque, child: ... ),
交互繪制實現(xiàn)
有了上面的基礎(chǔ),我們獲取到觸控點位置后,刷新界面觸發(fā) Canvas 重繪即可。不過這里有個問題,我們希望是可以連續(xù)繪制,而不是只能繪制一條曲線。比如說我們點按了2個點,那就繪制直線;3個點就繪制2階貝塞爾曲線;4個點繪制3階貝塞爾曲線;4個點以上,把前一條曲線的結(jié)束點當(dāng)做新的曲線的起點,按上述的方式循環(huán)繪制。這樣我們就能夠保證曲線是連續(xù)的了。這里可以通過遞歸方式實現(xiàn),代碼如下:
void drawCurves(Canvas canvas, Paint paint, List<Offset> points) { if (points.length <= 1) { return; } if (points.length == 2) { canvas.drawLine(points[0], points[1], paint); return; } if (points.length == 3) { _draw2OrderBezierCurves(canvas, paint, points); return; } if (points.length == 4) { _draw3OrderBezierCurves(canvas, paint, points); return; } var subPoints = points.sublist(0, 4); drawCurves(canvas, paint, subPoints); drawCurves(canvas, paint, points.sublist(3)); }
此外,考慮可能繪制的不是我們想要的效果,要支持撤銷功能,因此我們加了一個按鈕,點擊按鈕可以刪除最近添加的點,從而實現(xiàn)撤銷功能。我們用 Scaffold
的 floatingActionButton
實現(xiàn),邏輯很簡單,就是如果點數(shù)組不為空的話,就刪除最后一個點,然后刷新界面就好了。
floatingActionButton: IconButton( onPressed: () { if (points.isNotEmpty) { points.removeLast(); setState(() {}); } }, icon: Icon( Icons.backspace, color: Colors.blue, ), ),
繪制代碼
有了上面的基礎(chǔ),繪制代碼就簡單很多了,我們用一個數(shù)組存儲已經(jīng)點擊的點,然后通過這些點遞歸調(diào)用繪制方法就可以 實現(xiàn)交互式繪制了,完整代碼如下所示,這里我們把控制點使用圓圈繪制出來了。
class GestureBezierDemo extends StatefulWidget { GestureBezierDemo({Key? key}) : super(key: key); @override State<GestureBezierDemo> createState() => _GestureBezierDemoState(); } class _GestureBezierDemoState extends State<GestureBezierDemo> { var points = <Offset>[]; @override Widget build(BuildContext context) { return Scaffold( body: Listener( onPointerUp: ((event) { points.add(event.localPosition); setState(() {}); }), behavior: HitTestBehavior.opaque, child: CustomPaint( foregroundPainter: GestureBezierPainter(points: points), child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, color: Color(0xFFF5F5F5), ), ), ), floatingActionButton: IconButton( onPressed: () { if (points.isNotEmpty) { points.removeLast(); setState(() {}); } }, icon: Icon( Icons.backspace, color: Colors.blue, ), ), ); } } class GestureBezierPainter extends CustomPainter { GestureBezierPainter({required this.points}); final List<Offset> points; @override void paint(Canvas canvas, Size size) { print(size); canvas.drawColor(Color(0xFFF1F1F1), BlendMode.color); var paint = Paint()..color = Color(0xFFE53020); paint.strokeWidth = 2.0; paint.style = PaintingStyle.stroke; for (var point in points) { canvas.drawCircle(point, 2.0, paint); } paint.color = Color(0xFF2480F0); drawCurves(canvas, paint, points); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; } void drawCurves(Canvas canvas, Paint paint, List<Offset> points) { if (points.length <= 1) { return; } if (points.length == 2) { canvas.drawLine(points[0], points[1], paint); return; } if (points.length == 3) { _draw2OrderBezierCurves(canvas, paint, points); return; } if (points.length == 4) { _draw3OrderBezierCurves(canvas, paint, points); return; } var subPoints = points.sublist(0, 4); drawCurves(canvas, paint, subPoints); drawCurves(canvas, paint, points.sublist(3)); } _draw3OrderBezierCurves(Canvas canvas, Paint paint, List<Offset> points) { assert(points.length == 4); var yGap = 60.0; var path = Path(); path.moveTo(points[0].dx, points[0].dy); for (var t = 1; t <= 100; t += 1) { var curvePoint = BezierUtil.get3OrderBezierPoint( points[0], points[1], points[2], points[3], t / 100.0); path.lineTo(curvePoint.dx, curvePoint.dy); } canvas.drawPath(path, paint); } _draw2OrderBezierCurves(Canvas canvas, Paint paint, List<Offset> points) { assert(points.length == 3); var path = Path(); path.moveTo(points[0].dx, points[0].dy); for (var t = 1; t <= 100; t += 1) { var curvePoint = BezierUtil.get2OrderBezierPoint( points[0], points[1], points[2], t / 100.0); path.lineTo(curvePoint.dx, curvePoint.dy); } canvas.drawPath(path, paint); } }
運行效果
運行效果如下圖所示。
總結(jié)
本篇其實更多地是介紹獲取屏幕觸控位置的內(nèi)容,要實現(xiàn)交互式繪制就需要掌握用戶的觸控行為,從而繪制對應(yīng)的圖案。接下來我們會繼續(xù)探索更多交互行為,做更多的交互式繪制探索。
到此這篇關(guān)于Android通過交互實現(xiàn)貝塞爾曲線的繪制的文章就介紹到這了,更多相關(guān)Android繪制貝塞爾曲線內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android Studio下的APP目錄結(jié)構(gòu)詳解
這篇文章主要介紹了AndroidStudio下的APP目錄結(jié)構(gòu),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05Android?PickerScrollView滑動選擇控件使用方法詳解
這篇文章主要為大家詳細介紹了Android?PickerScrollView滑動選擇控件,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04Android解決viewpager嵌套滑動沖突并保留側(cè)滑菜單功能
這篇文章主要介紹了 解決viewpager嵌套滑動沖突,并保留側(cè)滑菜單功能,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-06-06Android系統(tǒng)聯(lián)系人全特效實現(xiàn)(上)分組導(dǎo)航和擠壓動畫(附源碼)
本文將為大家講解下Android系統(tǒng)聯(lián)系人全特效實現(xiàn)之分組導(dǎo)航和擠壓動畫,具體實現(xiàn)及源代碼如下,感興趣的朋友可以參考下哈,希望對大家學(xué)習(xí)有所幫助2013-06-06Android 消息分發(fā)使用EventBus的實例詳解
這篇文章主要介紹了Android 消息分發(fā)使用EventBus的實例詳解的相關(guān)資料,在項目中用了許多Handler和broadcast導(dǎo)致代碼冗余,顯得雜亂無章,這里使用EventBus來實現(xiàn)相同的功能,需要的朋友可以參考下2017-07-07