Android Flutter利用貝塞爾曲線畫一個小海豚
前言
貝塞爾曲線的應(yīng)用填補(bǔ)了計算機(jī)繪制與手繪之前的差距,更能表達(dá)人想畫出的曲線,為了更好的理解萬能的貝塞爾曲線,而海豚是我認(rèn)為在海洋生物中身體曲線最完美的海洋生物,在海洋中游泳速度最高可達(dá)80km/h;比驅(qū)逐艦速度還快,學(xué)習(xí)繪制正好學(xué)到了貝塞爾曲線,那么我們今天就用貝塞爾曲線畫看看能不能畫一只可愛的小海豚呢。
效果圖
先上效果圖:
實現(xiàn)步驟
path
路徑繪制貝塞爾曲線的方法非常簡單,只需要傳入控制點即可,二階就傳1個控制點1個終點,三階就傳2個控制點和1個終點,但是要找到合適控制的點就沒那么容易了,這時候我們?nèi)绻梢杂檬种冈谄聊簧喜粩嗾{(diào)試尋找合適的點豈不是非常方便,接下來我們就先實現(xiàn)下面的功能,通過手指不斷調(diào)試控制點位并將多個貝塞爾曲線進(jìn)行連接。
可以看到一個三階貝塞爾需要1個起點、2個控制點和1個終點組成,首先我們需要通過手勢識別將這些控制點存儲起來然后賦值給繪制組件進(jìn)行更新就可以了,這里我們需要用到狀態(tài)管理ChangeNotifier
類,它繼承Listenable
,因為在繪制組件的構(gòu)造方法里有一個參數(shù)repaint
接受Listenable
類型來控制是否重新繪制,數(shù)據(jù)變化就重新繪制。
const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
因為CustomPainter
的構(gòu)造方法里的repaint
參數(shù)就是負(fù)責(zé)更新繪制的,所以我們先要定義一個類繼承ChangeNotifier
來存儲這些數(shù)據(jù)。
代碼:
class TouchController extends ChangeNotifier { List<Offset> _points = []; //點集合 int _selectIndex = -1;// 選中的點 更新位置用 int get selectIndex => _selectIndex; List<Offset> get points => _points; // 選擇某一個點 保存index set selectIndex(int value) { if (_selectIndex == value) return; _selectIndex = value; notifyListeners();// 通知刷新 } // 選中的點標(biāo)記 Offset? get selectPoint => _selectIndex == -1 ? null : _points[_selectIndex]; // 添加一個點 void addPoint(Offset point) { points.add(point); notifyListeners(); } // 手指移動時更新當(dāng)前點的位置 void updatePoint(int index, Offset point) { points[index] = point; notifyListeners(); } // 刪除最后一個點 相當(dāng)于撤回上一步操作 void removeLast() { points.removeLast(); notifyListeners(); } }
有了存儲數(shù)據(jù)的空間之后,我們就需要通過手勢去獲取這些點,通過手勢在畫布上的操作獲取當(dāng)前的位置進(jìn)行存儲以及更新。
GestureDetector( child: CustomPaint( painter: _DolphinPainter(widget.touchController, widget.image), ), onPanDown: (d) { // 按壓 judgeZone(d.localPosition); }, onPanUpdate: (d) { // 移動 if (widget.touchController.selectIndex != -1) { widget.touchController.updatePoint( widget.touchController.selectIndex, d.localPosition); } }, ) ///判斷出是否在某點的半徑為r圓范圍內(nèi) bool judgeCircleArea(Offset src, Offset dst, double r) => (src - dst).distance <= r; ///手指按下觸發(fā) void judgeZone(Offset src) { /// 循環(huán)所有的點 for (int i = 0; i < widget.touchController.points.length; i++) { // 判斷手指按的位置有沒有按過的點 if (judgeCircleArea(src, widget.touchController.points[i], 20)) { // 有點 不添加更新選中的點 widget.touchController.selectIndex = i; return; } } // 無點 添加新的點 并將選中的點清空 widget.touchController.addPoint(src); widget.touchController.selectIndex = -1; }
到這里我們的手勢按壓和移動就會將數(shù)據(jù)存儲到我們剛才定義的類中,接下來我們需要將這些數(shù)據(jù)賦予真正的繪制組件 CustomPainter
。
class _DolphinPainter extends CustomPainter { final TouchController touchController;// 存儲數(shù)據(jù)類 // final ui.Image image; _DolphinPainter(this.touchController, this.image) // 這個地方傳入需要更新的 Listenable : super(repaint: touchController); List<Offset>? pos; //存儲手勢按壓的點 @override void paint(Canvas canvas, Size size) { // 畫布原點平移到屏幕中央 canvas.translate(size.width / 2, size.height / 2); // ,因為手勢識別的原點是左上角,所以這里將存儲的點相對的原點進(jìn)行偏移到跟畫布一致 負(fù)值向左上角偏移 pos = touchController.points .map((e) => e.translate(-size.width / 2, -size.height / 2)) .toList(); // 定義畫筆 var paint = Paint() ..strokeWidth = 2 ..color = Colors.purple ..style = PaintingStyle.stroke ..isAntiAlias = true; // canvas.drawImage(image, Offset(-image.width / 2, -image.height / 2), paint); // 如果點小于4個 那么就只繪制點 如果>=4個點 那么就繪制貝塞爾曲線 if (pos != null && pos!.length >= 4) { var path = Path(); // 設(shè)置起點 手指第一個按壓的點 path.moveTo(pos![0].dx, (pos![0].dy)); // path添加第一個貝塞爾曲線 path.cubicTo(pos![1].dx,pos![1].dy, pos![2].dx, pos![2].dy, pos![3].dx, pos![3].dy); //繪制輔助線 _drawHelpLine(canvas, size, paint, 0); // 繪制首個貝塞爾曲線 canvas.drawPath(path, paint..color = Colors.purple); // for循環(huán) 繪制第2個以后的曲線 以上個終點為下一個的起點 for (int i = 1; i < (pos!.length - 1) ~/ 3; i++) { //之后貝塞爾曲線的起點都是上一個貝塞爾曲線的終點 // 比如第一個曲線 1,2,3,4.第二個就是4,5,6,7...以此類推,這樣我們才能把線連接起來繪制圖案 // 這里把繪制之前的顏色覆蓋 // canvas.drawPath(path, paint..color = Colors.white); // 繪制輔助線 _drawHelpLine(canvas, size, paint, i); //繪制貝塞爾曲線 path.cubicTo( pos![i * 3 + 1].dx, pos![i * 3 + 1].dy, pos![i * 3 + 2].dx, pos![i * 3 + 2].dy, pos![i * 3 + 3].dx, pos![i * 3 + 3].dy, ); if (i == 8) { path.close(); } canvas.drawPath(path, paint..color = Colors.purple); } // 繪制輔助點 _drawHelpPoint(canvas, paint); // 選中點 _drawHelpSelectPoint(canvas, size, paint); } else { // 繪制輔助點 _drawHelpPoint(canvas, paint); } // 畫眼睛 眼睛位于起點的左側(cè),所以中心點向左偏移 canvas.drawCircle( pos!.first.translate(-50, 5), 10, paint ..color = Colors.black87 ..style = PaintingStyle.stroke ..strokeWidth = 2); canvas.drawCircle( pos!.first.translate(-53, 5), 7, paint ..color = Colors.black87 ..style = PaintingStyle.fill); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } void _drawHelpPoint(Canvas canvas, Paint paint) { canvas.drawPoints( PointMode.points, pos ?? [], paint ..strokeWidth = 10 ..strokeCap = StrokeCap.round ..color = Colors.redAccent); } void _drawHelpSelectPoint(Canvas canvas, Size size, Paint paint) { Offset? selectPos = touchController.selectPoint; selectPos = selectPos?.translate(-size.width / 2, -size.height / 2); if (selectPos == null) return; canvas.drawCircle( selectPos, 10, paint ..color = Colors.green ..strokeWidth = 2); } void _drawHelpLine(Canvas canvas, Size size, Paint paint, int i) { canvas.drawLine( Offset(pos![i * 3].dx, pos![i * 3].dy), Offset(pos![i * 3 + 1].dx, pos![i * 3 + 1].dy), paint ..color = Colors.redAccent ..strokeWidth = 2); canvas.drawLine( Offset(pos![i * 3 + 1].dx, pos![i * 3 + 1].dy), Offset(pos![i * 3 + 2].dx, pos![i * 3 + 2].dy), paint ..color = Colors.redAccent ..strokeWidth = 2); canvas.drawLine( Offset(pos![i * 3 + 2].dx, pos![i * 3 + 2].dy), Offset(pos![i * 3 + 3].dx, pos![i * 3 + 3].dy), paint ..color = Colors.redAccent ..strokeWidth = 2); }
最終在我們的手指的控制以及輔助線的幫助下,圖案就慢慢的繪制出來了。
去掉輔助線和點
然后將畫筆改為填充,那么就得到我們一開始那副可愛的小海豚了。
總結(jié)
通過這個小海豚圖案我們可以更加的理解貝塞爾曲線的繪制機(jī)制,通過你的手勢控制,你也可以畫出任何曲線和任何圖案,可以說貝塞爾曲線就是繪制中的靈魂,掌握了貝塞爾曲線就相當(dāng)于掌握了所有繪制組件,因為理論上來說,所有的二維圖形都可以被貝塞爾曲線畫出來,只要我們能準(zhǔn)確的找到控制的點,就可以繪制無限可能的圖案。
以上就是Android Flutter利用貝塞爾曲線畫一個小海豚的詳細(xì)內(nèi)容,更多關(guān)于Android Flutter海豚的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 使用 Path 實現(xiàn)搜索動態(tài)加載動畫效果
這篇文章主要介紹了Android 使用 Path 實現(xiàn)搜索動態(tài)加載動畫效果,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-08-08Kotlin Extension Function擴(kuò)展函數(shù)詳細(xì)介紹
Kotlin支持使用新功能擴(kuò)展類的能力,而無需通過類實現(xiàn)繼承概念或使用設(shè)計模式,如裝飾器(Decorator)。這是通過稱為擴(kuò)展功能(Extension Function)的特殊方式來完成的。因此,此功能可以有效地使代碼變得更清晰和易于閱讀,并且還可以減少代碼2023-02-02Android viewpager中動態(tài)添加view并實現(xiàn)偽無限循環(huán)的方法
這篇文章主要介紹了Android viewpager中動態(tài)添加view并實現(xiàn)偽無限循環(huán)的方法,涉及Android使用viewpager動態(tài)加載view及view無限循環(huán)顯示的相關(guān)技巧,需要的朋友可以參考下2016-01-01Android環(huán)形進(jìn)度條(安卓默認(rèn)形式)實例代碼
這篇文章主要介紹了Android環(huán)形進(jìn)度條(安卓默認(rèn)形式)實例代碼的相關(guān)資料,需要的朋友可以參考下2016-03-03Android中Activity的四種啟動模式和onNewIntent()
android 中activity的啟動模式分為四種,(standard、singleTop、singTask、singleInstance),本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友參考下吧2018-08-08