「萬物之中,但願至美」,《肖生克的救贖》這句話一直記在內心,不論生活多麼不易,心有但願,生活必定會愈來愈好。java
「 Hope is a good thing , maybe the best of things , and no good thing ever dies . 」git
Flutter 出現已經有一段時間了,搭好環境因爲忙其餘事就擱置了,剛好此次參與公司 Flutter 項目開發,是時候拾起來了,前兩天瞭解了 Dart 語言,今天恰好看到控件這部分,因而簡單寫了個「蛛網」控件。github
效果圖以下: web
從效果圖上能夠分析獲得,「蛛網」 由簡單的線條組成,那須要繪製線條。在 Android 中,能夠經過自定義 View ,在 onDraw 方法中調用 canvas.drawLine;或者調用 canvas.drawPath 繪製線條。那麼在 Flutter 又該怎樣繪製線條?canvas
在 Android 中有 View 提供繪製;一樣在 Flutter 中有 CustomPainter 提供繪製,源碼中是這麼介紹的:app
/// * [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 。你能夠這麼來使用它:dom
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 類的定義:ide
const Size(double width, double height) : super(width, height);
複製代碼
構造參數 width ,height 表示繪製區域的寬高,能夠理解成畫布大小。Size 須要從外部調用的地方傳入,而 Android 須要在 onMeasure 進行測量,Flutter 的方式更加靈活。函數
shouldRepaint 從方法名就能夠知道,用於控制重繪,爲了提升效率,通常能夠這麼寫:this
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return oldDelegate != this;
}
複製代碼
那麼咱們就能夠在 paint(Canvas canvas, Size size) 方法中繪製咱們想要的形狀。
蛛網由底部的網狀路徑以及頂部的不規則覆蓋物組成。那能夠分紅兩部分,第一部分繪製底部網狀;第二部分繪製頂部覆蓋物。
觀察底部網狀路徑由多個正多邊形組成(邊長遞增),那麼能夠拆分先繪製一個正多邊形。
經過 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 類的成員變量:
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 自定義控件,請關注「控件人生」公衆號。每日有乾貨推送,還有現金紅包發放,原創不易,戳戳手指掃碼關注。