【Flutter 元件集錄】CupertinoActivityIndicator| 8月更文挑戰

張風捷特烈發表於2021-08-06
前言:

這是我參與8月更文挑戰的第 6 天,活動詳情檢視:8月更文挑戰。為應掘金的八月更文挑戰,我準備在本月挑選 31 個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄 的重要素材。希望可以堅持下去,你的支援將是我最大的動力~


一、CupertinoActivityIndicator 的使用

可能看到 CupertinoActivityIndicator 元件,有人會嗤之以鼻:不就是個 iOS 風格的菊花轉 嗎,用起來這麼簡單的物件,有什麼好說的啊,看來你也要水文章了。 在我心目中 CupertinoActivityIndicator 是一個 教科書 級別的元件,它融匯了非常多元件相關的知識要點,比如動畫繪製State 生命週期回撥的使用,是非常值得去學習、分析、品味的。


1. CupertinoActivityIndicator 的屬性

CupertinoActivityIndicator 的使用確實非常簡單,普通構造中只有兩個引數:

屬性名型別預設值用途
animatingbooltrue表示是否進行動畫
radiusdouble10表示指示器半徑


如下是 CupertinoActivityIndicator 兩個屬性使用的小案例,左側半徑 15,且animating 置為 true,所以在不停旋轉,進行 loading 展示。右側半徑 20,且animating 置為 false,則表現為靜態。

class CupertinoActivityIndicatorDemo extends StatelessWidget {
  const CupertinoActivityIndicatorDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 40,
      children:[
        CupertinoActivityIndicator(
          animating: true,
          radius:  15,
        ),
        CupertinoActivityIndicator(
          animating: false,
          radius: 20,
        ),
      ]
    );
  }
}
複製程式碼

2.CupertinoActivityIndicator 的 partiallyRevealed 構造

除了普通構造外,還有一個 partiallyRevealed 構造 ,從下面的定義中可以看出,屬性只有半徑 radius 和進度 progress 。而且 animating 固定為 false,表示這個構造是指定進度的 靜態 效果。

下面是 progress0 ~ 1 間隔 0.1 個效果:

class CupertinoActivityIndicatorDemo extends StatelessWidget {
  const CupertinoActivityIndicatorDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Wrap(
        spacing: 20,
        children: List.generate(
          10,
          (index) => CupertinoActivityIndicator.partiallyRevealed(
            progress: 0.1 * index,
            radius: 15,
          ),
        ).toList());
  }
}
複製程式碼

二、CupertinoActivityIndicator 原始碼知識點

1. CupertinoActivityIndicator 元件原始碼介紹

CupertinoActivityIndicator 繼承自 StatefulWidget ,表示它有內部狀態更新的需求。其中定義了三個成員,用於元件資訊配置,這三個屬性在上面的使用中也介紹了作用。作為一個 StatefulWidget ,其元件構建的邏輯將交由對應的狀態類進行,這裡是 _CupertinoActivityIndicatorState


_CupertinoActivityIndicatorState 的類結構中可以看出,元件的構建依賴於 SizedBoxCustomPaint 。並覆寫了三個 State 生命週期的回撥方法。


2. 動畫的處理

CupertinoActivityIndicator 既然可以進行 loading 旋轉,那必然需要進行動畫處理。如下, _CupertinoActivityIndicatorState 混入 SingleTickerProviderStateMixin,在 initState 中例項化 AnimationController ,這裡可以看出當 widget.animatingtrue ,動畫器控制器會立刻 repeat 重複執行,週期為 1s

dispose 回撥中將 _controller 釋放。

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
複製程式碼

3. didUpdateWidget 回撥

可能很多人不是很清楚這個回撥的作用。當元件重建時,狀態類不會重新初始化的,而是會回撥 didUpdateWidget 來對比新舊兩個 Widget 的配置資訊進行響應邏輯處理。明面上使 元件重建 的方式非常多,比如 setStateValueListenableBuilderFutrueBuilder 等,本質上基本都是 setState

@override
void didUpdateWidget(CupertinoActivityIndicator oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (widget.animating != oldWidget.animating) {
    if (widget.animating)
      _controller.repeat();
    else
      _controller.stop();
  }
}
複製程式碼

如果不處理 didUpdateWidget 會有什麼後果?比如通過 Switch 來開關 CupertinoActivityIndicatoranimating 屬性, CupertinoActivityIndicator 重建時,如果沒有 didUpdateWidget 處理,狀態類是無法感知 widget 配置資訊變化的,也就無法完成是否動畫的切換。


4. 繪製的處理

build 方法中,使用 SizedBox 元件進行尺寸的限定,通過 _CupertinoActivityIndicatorPainter 進行繪製。

在很久以前,對於那時還只會 setState 觸發畫板重繪,我一直對這種方式有疑問,因為 setState 更新畫板會讓畫板物件重新建立,這對於繪製動畫來說是很不友好的,因為觸發的頻率非常高。直到我看懂 CupertinoActivityIndicator 的原始碼,才對畫板重繪有了全新的認知。這也為 《Flutter 繪製指南 - 妙筆生花》掃清了最後障礙。

都是看到 CupertinoActivityIndicator 並沒有使用 setState ,卻可以執行動畫來更新內部狀態,這是讓人很興奮的。經過一點點測試發現祕密在於 super(repaint: position) 。畫板可以通過一個 Listenable 物件觸發重繪,而不會觸發任何元件的構建。至於其更深層的實現原理,在 《Flutter 繪製探索》專欄中有詳細的原始碼分析。


具體的繪製邏輯也很簡單,就是遍歷旋轉繪製圓角矩形而已。


4. CupertinoActivityIndicator 的顏色

從原始碼中可以看出 CupertinoActivityIndicator 的顏色是固定的,使用者無法直接設定。但在 暗/亮 模式下,顏色會有差異,如下:

對於 activeColor 會根據 暗/亮 模式進行處理。如下,在暗色模式下,會略顯白色。如果我們想要自己定義的元件支援 暗/亮 模式,也可以效仿一下,進行處理。


三、CupertinoActivityIndicator 的注意點

有一個注意點。比如,我通過 Wrap 包裹 CupertinoActivityIndicator 和另一個 CustomPaint ,通過 BoxPainter 畫一個方塊。

class CupertinoActivityIndicatorDemo extends StatelessWidget {
  const CupertinoActivityIndicatorDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 20,
      children: [
        CupertinoActivityIndicator(
          animating: true,
          radius: 15,
        ),
        CustomPaint(
          size: Size(50, 50),
          painter: BoxPainter(),
        )
      ],
    );
  }
}

class BoxPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    print('-------BoxPainter----------');
    canvas.drawRect(Offset.zero & size, Paint());
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}
複製程式碼

通過日誌可以發現 BoxPainter 會隨著 CupertinoActivityIndicator 的動畫進行重繪。也能有人會非常疑惑,明明 BoxPainter 不需要重繪,為什麼會一直繪製, CupertinoActivityIndicator 太垃圾了。

這也算不上什麼異常,本質就是 RepaintBoundary 機制,通過 debugDumpRenderTree() 方法檢視渲染樹,可以看出:這兩者在同一渲染區域內,如下它們都在 up7。在同一片渲染區域內的一個節點重繪,會連帶這片區域的所有渲染節點重繪。像 WrapColumnRowSingleChildScrollViewStack 這樣可以有多個子元件,對應的渲染物件會在同一層。


我們可以通過 RepaintBoundary,將 CupertinoActivityIndicator 對應的渲染物件隔開,這樣就不會影響其他節點。注意,這並不是 CupertinoActivityIndicator 自身的問題,是 RepaintBoundary 機制使然。

Wrap(
  spacing: 20,
  children: [
    RepaintBoundary( //<----  
      child: CupertinoActivityIndicator(
        animating: false,
        radius: 15,
      ),
    ),
    CustomPaint(
      size: Size(50, 50),
      painter: BoxPainter(),
    ),
  ],
),
複製程式碼

CupertinoActivityIndicator 元件的使用方式到這裡就介紹完畢,雖然是個簡單的小元件,但麻雀雖小五臟俱全。是很值得去研究和學習的。那本文到這裡就結束了,謝謝觀看,明天見~

相關文章