Flutter自定義控制元件第一式,炫酷“蛛網”控制元件

文淑發表於2019-04-18

前言

「萬物之中,希望至美」,《肖生克的救贖》這句話一直記在心裡,不論生活多麼不易,心有希望,生活一定會越來越好。

「 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 自定義控制元件,請關注「控制元件人生」公眾號。每日有乾貨推送,還有現金紅包發放,原創不易,戳戳手指掃碼關注。

Flutter自定義控制元件第一式,炫酷“蛛網”控制元件

相關文章