Flutter 之 自定義控制元件

移動的小太陽發表於2021-03-22

Flutter 之 自定義控制元件

通過前面的學習,瞭解了常用的佈局方式和常用的子widget,可以完成大部分UI頁面的編寫,但有時候看到的UI控制元件不是這些基礎控制元件就能實現的,這時候怎麼辦呢?

和Android 和iOS 原生開發一樣,Flutter 也提供了兩種方式來實現:組合和自繪。

組合控制元件

有時候,雖然基本控制元件不能完成UI需求,但是可以通過一些基礎widget 組合成一個新的widget,來實現UI需求。

在開發中拿到一個UI頁面,一般按照從上到下、從左到右對UI進行分析,然後使用合適的widget 去編寫頁面。下面就以一個華為應用市場,應用列表的item 為例進行簡單的分析,通過組合的方式組合成一個新的widget。

image.png 以 今日頭條 為例,進行分析: 首先確定item裡面的資料有哪些,定義item 的資料結構

class UpdateItemModel {
  String appIcon; //App圖示
  String appName; //App名稱
  String appType; //App類別
  String appDecs; //App更新日期
  //建構函式語法糖,為屬性賦值
  UpdateItemModel({this.appIcon, this.appName, this.appType, this.appDecs});
}
複製程式碼

image.png

首先:分為左右兩部分,可以使用Row,左邊是一張圖片使用Image,但圖片是圓角的,但普通的 Image 並不支援圓角。這時,我們可以使用 ClipRRect 控制元件來解決這個問題;右邊就比較複雜了,這裡先定義為widget1,

widget1: 可以分上、下兩個部分,可以使用Column,下面是一條分隔線,可以使用 Divider,但看到有邊距,需要再包裹一層Padding;上面部分比較複雜,定義為widget2;

widget2: 又可以分為左右兩部分,可以使用Row,右邊是一個 FlatButton,左邊部分比較複習,定義為widget3;

widget3: 豎直方向上放置的幾個Text,可以使用Column;到這來就分析完成了。下面就看看程式碼和實際的執行效果吧。

image.png 分為 上下兩個部分,下面部分是一個分隔線,上面又可以繼續劃分

image.png 水平排放,可以使用Row完成,左邊是Column裡面放置幾個Text,右邊是一個 FlatButton

下面看看具體程式碼:


class CustomDemo1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("自定義控制元件"),
      ),
      body: UpdateWidget(
        model: UpdateItemModel(
            appName: "今日頭條",
            appIcon:
                "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Foss.huangye88.net%2Flive%2Fuser%2F0%2F1502268284008709600-0.png&refer=http%3A%2F%2Foss.huangye88.net&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1618804524&t=3b2eff4563a5421ed62be433277abe06",
            appType: "新聞",
            appDecs: "海量視訊熱點資訊高效搜尋"),
        onPressed: () {
          print("安裝今日頭條");
        },
      ),
    );
  }
}

class UpdateWidget extends StatelessWidget {
  final UpdateItemModel model; //資料模型
  final VoidCallback onPressed;

  UpdateWidget({Key key, this.model, this.onPressed}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: EdgeInsets.all(10),
          child: ClipRect(
            child: Image.network(
              model.appIcon,
              width: 80,
              height: 80,
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.fromLTRB(0, 10, 10, 0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        model.appName,
                        style: TextStyle(
                            fontWeight: FontWeight.bold,
                            fontSize: 20,
                            color: Colors.black),
                      ),
                      Text(
                        model.appType,
                        style: TextStyle(fontSize: 14, color: Colors.black26),
                      ),
                      Text(
                        model.appDecs,
                        style: TextStyle(fontSize: 14, color: Colors.black26),
                      ),
                    ],
                  ),
                  Container(
                    padding: EdgeInsets.fromLTRB(30, 0, 0, 0),
                    alignment: Alignment.topRight,
                    child: MaterialButton(
                        onPressed: this.onPressed,
                        textColor: Colors.blue,
                        color: Colors.grey,
                        minWidth: 30,
                        height: 25,
                        child: Text(
                          "安裝",
                        ),
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8),
                        )),
                  )
                ],
              ),
            ],
          ),
        )
      ],
    );
  }
}
複製程式碼

image.png

自繪控制元件

在原生 iOS 和 Android 開發中,我們可以繼承 UIView/View,在 drawRect/onDraw 方法裡進行繪製操作。其實,在 Flutter 中也有類似的方案,那就是 CustomPaint。

我們都知道在繪製的過程中有兩個重要的東西--畫布和畫筆,畫筆 Paint,我們可以配置它的各種屬性,比如顏色、樣式、粗細等;Canvas,則提供了各種常見的繪製方法,比如畫線 drawLine、畫矩形 drawRect、畫點 DrawPoint、畫路徑 drawPath、畫圓 drawCircle、畫圓弧 drawArc 等。

一般繪製的流程分為三個部分:

  1. 自定義class 繼承 CustomPainter
  2. 通過 CustomPaint,把自定義的控制元件放到widget中
  3. 像使用正常的widget 一樣使用 新的widget

下面看看一個餅狀圖的例子:

// 1.繼承 CustomPainter,在裡面編寫繪製邏輯
class CustomWidget extends CustomPainter {
  // 生成畫筆
  Paint getPaintByColor(Color color) {
    Paint paint = Paint();
    paint.color = color;
    return paint;
  }

  @override
  void paint(Canvas canvas, Size size) {
    // 這裡面是繪製的邏輯
    double wheelSize = min(size.width, size.height)/2;
    double nbElem = 6; // 分為6份
    // 繪製的圓弧
    double radius = (2 * pi) / nbElem;
    // 建立一個矩形
    Rect boundingRect = Rect.fromCircle(center: Offset(wheelSize, wheelSize),radius: wheelSize);
    // 繪製扇形
    canvas.drawArc(
        boundingRect, 0, radius, true, getPaintByColor(Colors.blueGrey));
    canvas.drawArc(
        boundingRect, radius * 1, radius, true, getPaintByColor(Colors.red));
    canvas.drawArc(
        boundingRect, radius * 2, radius, true, getPaintByColor(Colors.green));
    canvas.drawArc(
        boundingRect, radius * 3, radius, true, getPaintByColor(Colors.blue));
    canvas.drawArc(
        boundingRect, radius * 4, radius, true, getPaintByColor(Colors.brown));
    canvas.drawArc(
        boundingRect, radius * 5, radius, true, getPaintByColor(Colors.amber));
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate != this;
  }
}

//2. 將餅圖包裝成一個新的控制元件,通過 CustomPaint
class Cake extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(200, 200),
      painter: CustomWidget(),
    );
  }
}

// 就可以像使用普通控制元件一樣使用新定義的控制元件
class CustomDemo2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("繪製控制元件"),
      ),
      body: Center(
        child: Cake(),
      ),
    );
  }
}

複製程式碼

image.png

總結

在Flutter 中,自定義控制元件的方式有兩種,組合和自繪。組合的方式是通過一些基本widget 元素的堆積,組合成一個新的控制元件;自繪則是會比較麻煩一點,通過 繼承 CustomPainter 在 paint 方法中完成繪製邏輯;最後把 CustomPainter 放入到 CustomPaint成為一個新控制元件。

還記得剛開始學習 Android 的自定義控制元件,總是很排斥,但發現認真去學習之後,還是很簡單的。Flutter 也是一樣,通過學習是可以快速提高的。一起努力加油吧!

相關文章