前言
「萬物之中,希望至美」,《肖生克的救贖》這句話一直記在心裡,不論生活多麼不易,心有希望,生活一定會越來越好。
「 Hope is a good thing , maybe the best of things , and no good thing ever dies . 」
Flutter 出現已經有一段時間了,搭好環境由於忙其他事就擱置了,恰好這次參與公司 Flutter 專案開發,是時候拾起來了,前兩天瞭解了 Dart 語言,今天剛好看到控制元件這部分,於是簡單寫了個“蛛網”控制元件。
效果圖如下:
初步分析
從效果圖上可以分析得到,“蛛網” 由簡單的線條組成,那需要繪製線條。在 Android 中,可以通過自定義 View ,在 onDraw 方法中呼叫 canvas.drawLine;或者呼叫 canvas.drawPath 繪製線條。那麼在 Flutter 又該怎樣繪製線條?
在 Android 中有 View 提供繪製;同樣在 Flutter 中有 CustomPainter 提供繪製,原始碼中是這麼介紹的:
/// * [Canvas], the class that a custom painter uses to paint.
/// * [CustomPaint], the widget that uses [CustomPainter], and whose sample
/// code shows how to use the above `Sky` class.
/// * [RadialGradient], whose sample code section shows a different take
/// on the sample code above.
abstract class CustomPainter extends Listenable {
複製程式碼
大概意思是:畫家用來自定義繪畫的類,暫且把它理解成 View 。你可以這麼來使用它:
class NetView extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return null;
}
}
複製程式碼
繼承 CustomPainter ,重寫 paint 與 shouldRepaint 方法。paint 方法類似 Android 中的 onDraw 方法,但多了一個 Size 引數,來看下 Size 類的定義:
const Size(double width, double height) : super(width, height);
複製程式碼
構造引數 width ,height 表示繪製區域的寬高,可以理解成畫布大小。Size 需要從外部呼叫的地方傳入,而 Android 需要在 onMeasure 進行測量,Flutter 的方式更加靈活。
shouldRepaint 從方法名就可以知道,用於控制重繪,為了提高效率,一般可以這麼寫:
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return oldDelegate != this;
}
複製程式碼
那麼我們就可以在 paint(Canvas canvas, Size size) 方法中繪製我們想要的形狀。
構思程式碼
蛛網由底部的網狀路徑以及頂部的不規則覆蓋物組成。那可以分成兩部分,第一部分繪製底部網狀;第二部分繪製頂部覆蓋物。
觀察底部網狀路徑由多個正多邊形組成(邊長遞增),那麼可以拆分先繪製一個正多邊形。
在 Flutter 的 canvas 中提供了 drawPath(Path path, Paint paint) 方法繪製路徑,與 Android 的使用方式一樣。通過 Size 可以拿到整個控制元件區域的大小,那麼正多邊形的中點座標就很容易獲取到:
mCenterX = size.width / 2;
mCenterY = size.height / 2;
複製程式碼
為了方便,這裡可以把正多邊形看成圓內切,想到圓,就應該想到圓的半徑,同樣根據控制元件大小獲取半徑:
radius = mCenterX / mEdgeSize
複製程式碼
最後需要獲取到圓內切正多邊形的頂點,簡單的數學公式:
double x = mCenterX + radius * cos(degToRad(angle * j));
double y = mCenterY + radius * sin(degToRad(angle * j));
複製程式碼
比較尷尬的是,Flutter 中並沒有 Math.toRadians 函式,那隻能自己擼一個,就像這樣:
num degToRad(num deg) => deg * (pi / 180.0);
num radToDeg(num rad) => rad * (180.0 / pi);
複製程式碼
繪製多個正多邊形,只需要改變 radius 的值:
// mEdgeSize 表示多邊形的邊數 i + 1
radius = mCenterX / mEdgeSize * (i + 1);
複製程式碼
到此,底部的網狀路徑繪製差不多,頂部的覆蓋物類似,隨機改變 radius 的長度:
double value = (random.nextInt(10) + 1) / 10;
double x = mCenterX + radiusMaxLimit * cos(degToRad(angle * i)) * value;
double y = mCenterY + radiusMaxLimit * sin(degToRad(angle * i)) * value;
複製程式碼
接著看看程式碼如何寫。
起名字
起一個接地氣的名字,能夠讓你眼前一亮,就叫 SpiderView
編寫程式碼
SpiderView類
先來看看 SpiderView 類的成員變數:
Paint mPaint;
// 覆蓋物畫筆
Paint mCoverPaint;
// 文字畫筆
Paint mTextPaint;
Path mPath;
// 繪製邊數預設為6
int mEdgeSize = 6;
final double CIRCLE_ANGLE = 360;
// 整個繪製區域的中點座標
double mCenterX = 0;
double mCenterY = 0;
複製程式碼
畫筆,路勁初始化:
SpiderView(this.mEdgeSize) {
// 初始化畫筆
mPaint = new Paint();
mPaint.color = randomRGB();
// 設定抗鋸齒
mPaint.isAntiAlias = true;
// 樣式為描邊
mPaint.style = PaintingStyle.stroke;
mPath = new Path();
mCoverPaint = new Paint();
mCoverPaint.isAntiAlias = true;
mCoverPaint.style = PaintingStyle.fill;
mCoverPaint.color = randomARGB();
mTextPaint = new Paint();
mTextPaint.isAntiAlias = true;
mTextPaint.style = PaintingStyle.fill;
mTextPaint.color = Colors.blue;
}
複製程式碼
paint 方法:
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
mCenterX = size.width / 2;
mCenterY = size.height / 2;
// 圖層 防止重新整理屬性結構
canvas.save();
drawSpiderEdge(canvas);
drawCover(canvas);
drawText(canvas);
canvas.restore();
}
複製程式碼
繪製底部網狀路勁,程式碼相對簡單,有疑問請留言:
/**
* 繪製邊線
*/
void drawSpiderEdge(Canvas canvas) {
double angle = CIRCLE_ANGLE / mEdgeSize;
double radius = 0;
double radiusMaxLimit = mCenterX > mCenterY ? mCenterX : mCenterY;
for (int i = 0; i < mEdgeSize; i++) {
mPath.reset();
radius = radiusMaxLimit / mEdgeSize * (i + 1);
for (int j = 0; j < mEdgeSize + 1; j++) {
// 移動
if (j == 0) {
mPath.moveTo(mCenterX + radius, mCenterY);
} else {
double x = mCenterX + radius * cos(degToRad(angle * j));
double y = mCenterY + radius * sin(degToRad(angle * j));
mPath.lineTo(x, y);
}
}
mPath.close();
canvas.drawPath(mPath, mPaint);
}
drawSpiderAxis(canvas, radiusMaxLimit, angle);
}
/**
* 繪製軸線
*/
void drawSpiderAxis(Canvas canvas, double radius, double angle) {
for (int i = 0; i < mEdgeSize; i++) {
mPath.reset();
mPath.moveTo(mCenterX, mCenterX);
double x = mCenterX + radius * cos(degToRad(angle * i));
double y = mCenterY + radius * sin(degToRad(angle * i));
mPath.lineTo(x, y);
canvas.drawPath(mPath, mPaint);
}
}
複製程式碼
繪製頂部覆蓋物:
/**
* 繪製覆蓋區域
*/
void drawCover(Canvas canvas) {
mPath.reset();
Random random = new Random();
double angle = CIRCLE_ANGLE / mEdgeSize;
double radiusMaxLimit = min(mCenterY, mCenterY);
for (int i = 0; i < mEdgeSize; i++) {
double value = (random.nextInt(10) + 1) / 10;
double x = mCenterX + radiusMaxLimit * cos(degToRad(angle * i)) * value;
double y = mCenterY + radiusMaxLimit * sin(degToRad(angle * i)) * value;
if (i == 0) {
mPath.moveTo(x, mCenterY);
} else {
mPath.lineTo(x, y);
}
}
mPath.close();
canvas.drawPath(mPath, mCoverPaint);
}
複製程式碼
總結
Flutter 初學,希望對大家有所幫助,如果你有更好的方案,請留言喲。
想了解更多 Flutter 自定義控制元件,請關注「控制元件人生」公眾號。每日有乾貨推送,還有現金紅包發放,原創不易,戳戳手指掃碼關注。