網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
貝塞爾曲線的應(yīng)用填補(bǔ)了計(jì)算機(jī)繪制與手繪之前的差距,更能表達(dá)人想畫(huà)出的曲線,為了更好的理解萬(wàn)能的貝塞爾曲線,而海豚是我認(rèn)為在海洋生物中身體曲線最完美的海洋生物,在海洋中游泳速度最高可達(dá)80km/h;比驅(qū)逐艦速度還快,學(xué)習(xí)繪制正好學(xué)到了貝塞爾曲線,那么我們今天就用貝塞爾曲線畫(huà)看看能不能畫(huà)一只可愛(ài)的小海豚呢。
效果圖
先上效果圖:
實(shí)現(xiàn)步驟
path
路徑繪制貝塞爾曲線的方法非常簡(jiǎn)單,只需要傳入控制點(diǎn)即可,二階就傳1個(gè)控制點(diǎn)1個(gè)終點(diǎn),三階就傳2個(gè)控制點(diǎn)和1個(gè)終點(diǎn),但是要找到合適控制的點(diǎn)就沒(méi)那么容易了,這時(shí)候我們?nèi)绻梢杂檬种冈谄聊簧喜粩嗾{(diào)試尋找合適的點(diǎn)豈不是非常方便,接下來(lái)我們就先實(shí)現(xiàn)下面的功能,通過(guò)手指不斷調(diào)試控制點(diǎn)位并將多個(gè)貝塞爾曲線進(jìn)行連接。
可以看到一個(gè)三階貝塞爾需要1個(gè)起點(diǎn)、2個(gè)控制點(diǎn)和1個(gè)終點(diǎn)組成,首先我們需要通過(guò)手勢(shì)識(shí)別將這些控制點(diǎn)存儲(chǔ)起來(lái)然后賦值給繪制組件進(jìn)行更新就可以了,這里我們需要用到狀態(tài)管理ChangeNotifier
類(lèi),它繼承Listenable
,因?yàn)樵诶L制組件的構(gòu)造方法里有一個(gè)參數(shù)repaint
接受Listenable
類(lèi)型來(lái)控制是否重新繪制,數(shù)據(jù)變化就重新繪制。
const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
因?yàn)?code>CustomPainter的構(gòu)造方法里的repaint
參數(shù)就是負(fù)責(zé)更新繪制的,所以我們先要定義一個(gè)類(lèi)繼承ChangeNotifier
來(lái)存儲(chǔ)這些數(shù)據(jù)。
代碼:
class TouchController extends ChangeNotifier { List<Offset> _points = []; //點(diǎn)集合 int _selectIndex = -1;// 選中的點(diǎn) 更新位置用 int get selectIndex => _selectIndex; List<Offset> get points => _points; // 選擇某一個(gè)點(diǎn) 保存index set selectIndex(int value) { if (_selectIndex == value) return; _selectIndex = value; notifyListeners();// 通知刷新 } // 選中的點(diǎn)標(biāo)記 Offset? get selectPoint => _selectIndex == -1 ? null : _points[_selectIndex]; // 添加一個(gè)點(diǎn) void addPoint(Offset point) { points.add(point); notifyListeners(); } // 手指移動(dòng)時(shí)更新當(dāng)前點(diǎn)的位置 void updatePoint(int index, Offset point) { points[index] = point; notifyListeners(); } // 刪除最后一個(gè)點(diǎn) 相當(dāng)于撤回上一步操作 void removeLast() { points.removeLast(); notifyListeners(); } }
有了存儲(chǔ)數(shù)據(jù)的空間之后,我們就需要通過(guò)手勢(shì)去獲取這些點(diǎn),通過(guò)手勢(shì)在畫(huà)布上的操作獲取當(dāng)前的位置進(jìn)行存儲(chǔ)以及更新。
GestureDetector( child: CustomPaint( painter: _DolphinPainter(widget.touchController, widget.image), ), onPanDown: (d) { // 按壓 judgeZone(d.localPosition); }, onPanUpdate: (d) { // 移動(dòng) if (widget.touchController.selectIndex != -1) { widget.touchController.updatePoint( widget.touchController.selectIndex, d.localPosition); } }, ) ///判斷出是否在某點(diǎn)的半徑為r圓范圍內(nèi) bool judgeCircleArea(Offset src, Offset dst, double r) => (src - dst).distance <= r; ///手指按下觸發(fā) void judgeZone(Offset src) { /// 循環(huán)所有的點(diǎn) for (int i = 0; i < widget.touchController.points.length; i++) { // 判斷手指按的位置有沒(méi)有按過(guò)的點(diǎn) if (judgeCircleArea(src, widget.touchController.points[i], 20)) { // 有點(diǎn) 不添加更新選中的點(diǎn) widget.touchController.selectIndex = i; return; } } // 無(wú)點(diǎn) 添加新的點(diǎn) 并將選中的點(diǎn)清空 widget.touchController.addPoint(src); widget.touchController.selectIndex = -1; }
到這里我們的手勢(shì)按壓和移動(dòng)就會(huì)將數(shù)據(jù)存儲(chǔ)到我們剛才定義的類(lèi)中,接下來(lái)我們需要將這些數(shù)據(jù)賦予真正的繪制組件 CustomPainter
。
class _DolphinPainter extends CustomPainter { final TouchController touchController;// 存儲(chǔ)數(shù)據(jù)類(lèi) // final ui.Image image; _DolphinPainter(this.touchController, this.image) // 這個(gè)地方傳入需要更新的 Listenable : super(repaint: touchController); List<Offset>? pos; //存儲(chǔ)手勢(shì)按壓的點(diǎn) @override void paint(Canvas canvas, Size size) { // 畫(huà)布原點(diǎn)平移到屏幕中央 canvas.translate(size.width / 2, size.height / 2); // ,因?yàn)槭謩?shì)識(shí)別的原點(diǎn)是左上角,所以這里將存儲(chǔ)的點(diǎn)相對(duì)的原點(diǎn)進(jìn)行偏移到跟畫(huà)布一致 負(fù)值向左上角偏移 pos = touchController.points .map((e) => e.translate(-size.width / 2, -size.height / 2)) .toList(); // 定義畫(huà)筆 var paint = Paint() ..strokeWidth = 2 ..color = Colors.purple ..style = PaintingStyle.stroke ..isAntiAlias = true; // canvas.drawImage(image, Offset(-image.width / 2, -image.height / 2), paint); // 如果點(diǎn)小于4個(gè) 那么就只繪制點(diǎn) 如果>=4個(gè)點(diǎn) 那么就繪制貝塞爾曲線 if (pos != null && pos!.length >= 4) { var path = Path(); // 設(shè)置起點(diǎn) 手指第一個(gè)按壓的點(diǎn) path.moveTo(pos![0].dx, (pos![0].dy)); // path添加第一個(gè)貝塞爾曲線 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); // 繪制首個(gè)貝塞爾曲線 canvas.drawPath(path, paint..color = Colors.purple); // for循環(huán) 繪制第2個(gè)以后的曲線 以上個(gè)終點(diǎn)為下一個(gè)的起點(diǎn) for (int i = 1; i < (pos!.length - 1) ~/ 3; i++) { //之后貝塞爾曲線的起點(diǎn)都是上一個(gè)貝塞爾曲線的終點(diǎn) // 比如第一個(gè)曲線 1,2,3,4.第二個(gè)就是4,5,6,7...以此類(lèi)推,這樣我們才能把線連接起來(lái)繪制圖案 // 這里把繪制之前的顏色覆蓋 // 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); } // 繪制輔助點(diǎn) _drawHelpPoint(canvas, paint); // 選中點(diǎn) _drawHelpSelectPoint(canvas, size, paint); } else { // 繪制輔助點(diǎn) _drawHelpPoint(canvas, paint); } // 畫(huà)眼睛 眼睛位于起點(diǎn)的左側(cè),所以中心點(diǎn)向左偏移 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); }
最終在我們的手指的控制以及輔助線的幫助下,圖案就慢慢的繪制出來(lái)了。
去掉輔助線和點(diǎn)
然后將畫(huà)筆改為填充,那么就得到我們一開(kāi)始那副可愛(ài)的小海豚了。
總結(jié)
通過(guò)這個(gè)小海豚圖案我們可以更加的理解貝塞爾曲線的繪制機(jī)制,通過(guò)你的手勢(shì)控制,你也可以畫(huà)出任何曲線和任何圖案,可以說(shuō)貝塞爾曲線就是繪制中的靈魂,掌握了貝塞爾曲線就相當(dāng)于掌握了所有繪制組件,因?yàn)槔碚撋蟻?lái)說(shuō),所有的二維圖形都可以被貝塞爾曲線畫(huà)出來(lái),只要我們能準(zhǔn)確的找到控制的點(diǎn),就可以繪制無(wú)限可能的圖案。
原文鏈接:https://juejin.cn/post/7085739301970903047
相關(guān)推薦
- 2022-06-12 C語(yǔ)言sizeof和strlen的指針和數(shù)組面試題詳解_C 語(yǔ)言
- 2022-03-26 C++實(shí)現(xiàn)關(guān)機(jī)功能詳細(xì)代碼_C 語(yǔ)言
- 2022-12-29 Python?Setuptools的?setup.py實(shí)例詳解_python
- 2022-07-09 python?監(jiān)控某個(gè)進(jìn)程內(nèi)存的情況問(wèn)題_python
- 2022-07-21 React生命周期
- 2022-07-29 Android錄音功能的實(shí)現(xiàn)以及踩坑實(shí)戰(zhàn)記錄_Android
- 2022-02-26 Assert.assertEquals()方法參數(shù)詳解_Android
- 2023-03-15 Pandas中字符串和時(shí)間轉(zhuǎn)換與格式化的實(shí)現(xiàn)_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支