Android Flutter利用貝塞爾曲線畫一個(gè)小海豚
前言
貝塞爾曲線的應(yīng)用填補(bǔ)了計(jì)算機(jī)繪制與手繪之前的差距,更能表達(dá)人想畫出的曲線,為了更好的理解萬(wàn)能的貝塞爾曲線,而海豚是我認(rèn)為在海洋生物中身體曲線最完美的海洋生物,在海洋中游泳速度最高可達(dá)80km/h;比驅(qū)逐艦速度還快,學(xué)習(xí)繪制正好學(xué)到了貝塞爾曲線,那么我們今天就用貝塞爾曲線畫看看能不能畫一只可愛的小海豚呢。
效果圖
先上效果圖:

實(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)就沒那么容易了,這時(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類,它繼承Listenable,因?yàn)樵诶L制組件的構(gòu)造方法里有一個(gè)參數(shù)repaint接受Listenable類型來(lái)控制是否重新繪制,數(shù)據(jù)變化就重新繪制。
const CustomPainter({ Listenable? repaint }) : _repaint = repaint;因?yàn)?code>CustomPainter的構(gòu)造方法里的repaint參數(shù)就是負(fù)責(zé)更新繪制的,所以我們先要定義一個(gè)類繼承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ì)在畫布上的操作獲取當(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++) {
// 判斷手指按的位置有沒有按過(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)我們需要將這些數(shù)據(jù)賦予真正的繪制組件 CustomPainter。
class _DolphinPainter extends CustomPainter {
final TouchController touchController;// 存儲(chǔ)數(shù)據(jù)類
// 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) {
// 畫布原點(diǎn)平移到屏幕中央
canvas.translate(size.width / 2, size.height / 2);
// ,因?yàn)槭謩?shì)識(shí)別的原點(diǎn)是左上角,所以這里將存儲(chǔ)的點(diǎn)相對(duì)的原點(diǎn)進(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);
// 如果點(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)繪制圖案
// 這里把繪制之前的顏色覆蓋
// 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);
}
// 畫眼睛 眼睛位于起點(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)

然后將畫筆改為填充,那么就得到我們一開始那副可愛的小海豚了。
總結(jié)
通過(guò)這個(gè)小海豚圖案我們可以更加的理解貝塞爾曲線的繪制機(jī)制,通過(guò)你的手勢(shì)控制,你也可以畫出任何曲線和任何圖案,可以說(shuō)貝塞爾曲線就是繪制中的靈魂,掌握了貝塞爾曲線就相當(dāng)于掌握了所有繪制組件,因?yàn)槔碚撋蟻?lái)說(shuō),所有的二維圖形都可以被貝塞爾曲線畫出來(lái),只要我們能準(zhǔn)確的找到控制的點(diǎn),就可以繪制無(wú)限可能的圖案。
以上就是Android Flutter利用貝塞爾曲線畫一個(gè)小海豚的詳細(xì)內(nèi)容,更多關(guān)于Android Flutter海豚的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決Android解析圖片的OOM問(wèn)題的方法!!!
我們?cè)诰帉慉ndroid程序的時(shí)候經(jīng)常要用到許多圖片,不同圖片總是會(huì)有不同的形狀、不同的大小,程序占用了過(guò)高的內(nèi)存就容易出現(xiàn)OOM(OutOfMemory)異常。本篇文章主要講訴了解決Android解析圖片的OOM問(wèn)題,有興趣的可以了解一下。2016-11-11
Android Spinner與適配器模式詳解及實(shí)例代碼
這篇文章主要介紹了Android Spinner與適配器模式詳解相關(guān)資料,并附代碼實(shí)例,需要的朋友可以參考下2016-10-10
Android仿網(wǎng)易新聞圖片詳情下滑隱藏效果示例代碼
這篇文章主要給大家介紹了關(guān)于利用Android如何仿網(wǎng)易新聞圖片詳情下滑隱藏效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
Android開發(fā)中Activity的生命周期及加載模式詳解
這篇文章主要介紹了Android開發(fā)中Activity的生命周期及加載模式詳解的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-05-05
Android 實(shí)現(xiàn)釘釘自動(dòng)打卡功能
這篇文章主要介紹了Android 實(shí)現(xiàn)釘釘自動(dòng)打卡功能的步驟,幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-03-03
Android開發(fā)之CheckBox的簡(jiǎn)單使用與監(jiān)聽功能示例
這篇文章主要介紹了Android開發(fā)之CheckBox的簡(jiǎn)單使用與監(jiān)聽功能,結(jié)合簡(jiǎn)單實(shí)例形式分析了Android使用CheckBox控件的布局與功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-07-07
Android編程讀取Assets所有文件(遍歷每一個(gè)文件夾)并存入sdcard的方法
這篇文章主要介紹了Android編程讀取Assets所有文件(遍歷每一個(gè)文件夾)并存入sdcard的方法,涉及Android針對(duì)文件與目錄的遍歷及I/O操作相關(guān)技巧,需要的朋友可以參考下2016-02-02

