【Flutter 元件集錄】Switch 是怎樣煉成的| 8月更文挑戰

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

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


一、 Switch 元件使用詳解

可能有人會覺得 Switch 元件非常簡單,有什麼好說的呢?其實 Switch 元件原始碼洋洋灑灑 近千行 ,其中關於主題處理平臺適配事件處理動畫處理繪製處理 都有值得我們學習的地方。那麼廢話不多說,來一起看看 Switch 是怎麼煉成的吧。


1. Switch 最簡使用:valueonChanged

Switch 元件的使用中注意:該元件是 StatelessWidget ,表示本身並不維護 開關狀態。這也就意味著,我把只能通過 重新構建 Switch元件 來切換 開關狀態 。在構建 Switch 時必須傳入 valueonChanged 兩個引數,其中 value 表示 Switch 開關的狀態,onChanged 是狀態變化回撥函式。

如下,在 _SwitchDemoState 中定義狀態 _value 用於表示 Switch 開關的狀態,在 _onChanged 回撥中改變狀態值,並 重新構建 Switch 元件,這樣就能達到點選進行開關的效果。

class SwitchDemo extends StatefulWidget {
  const SwitchDemo({Key? key}) : super(key: key);

  @override
  _SwitchDemoState createState() => _SwitchDemoState();
}

class _SwitchDemoState extends State<SwitchDemo> {
  bool _value = false;

  @override
  Widget build(BuildContext context) {
    return Switch(
      value: _value,
      onChanged: _onChanged,
    );
  }

  void _onChanged(bool value) {
    setState(() {
      _value = value;
    });
  }
}
複製程式碼

其實這裡可能很讓人疑惑 Switch 為什麼不自己維護 開關狀態,要將改狀態交由外界指定呢?既然 SwitchStatelessWidget ,為什麼可以執行滑動的動畫?還有 onChanged 方法又是何時觸發的?帶著這些問題我們來逐漸去認識這個屬性而陌生的 Switch 元件。


2. Switch 的四個主要顏色

Switch 的構造方法中可以看出,其中定義了非常多的顏色相關屬性。

先看前四個顏色屬性:

  • inactiveThumbColor 代表關閉時圓圈的顏色。
  • inactiveTrackColor 代表關閉時滑槽的顏色。

  • activeColor 代表開啟時圓圈的顏色。
  • inactiveTrackColor 代表開啟時滑槽的顏色。

Switch(
  activeColor: Colors.blue,
  activeTrackColor: Colors.green,
  inactiveThumbColor: Colors.orange,
  inactiveTrackColor: Colors.pinkAccent,
  value: _value,
  onChanged: _onChanged,
);
複製程式碼

3. hoverColor 、 mouseCursor 和 splashRadius

前兩個屬性一般只能在桌面或web 端起作用,hoverColor 顧名思義是滑鼠懸浮時,外層的大圈顏色,splashRadius 表示大圈的半徑,如果不想要外圈的懸浮效果,可以將半徑設為 0 。另外, mouseCursor 代表滑鼠的樣式,比如下面的小拳頭是 SystemMouseCursors.grabbing

Switch(
  activeColor: Colors.blue,
  activeTrackColor: Colors.green,
  inactiveThumbColor: Colors.orange,
  inactiveTrackColor: Colors.pinkAccent,
  hoverColor: Colors.blue.withOpacity(0.2),
  mouseCursor: SystemMouseCursors.grabbing,
  value: _value,
  onChanged: _onChanged,
);
複製程式碼

mouseCursor 屬性的型別為 MouseCursor ,其中 SystemMouseCursors 中定義了非常多的滑鼠指標型別以供使用。下面給出幾個效果:

contextMenucopyforbiddentext

5. 指定圖片

通過 activeThumbImageinactiveThumbImage 可以指定小圓中開啟/關閉 時的圖片。另外 onActiveThumbImageErroronInactiveThumbImageError 兩個回撥用於圖片載入錯誤的監聽。

當小圓同時指定 圖片顏色 屬性時,會顯示 圖片

Switch(
  activeColor: Colors.blue,
  activeThumbImage: AssetImage('assets/images/icon_head.png'),
  inactiveThumbImage: AssetImage('assets/images/icon_8.jpg'),
  activeTrackColor: Colors.green,
  inactiveThumbColor: Colors.orange,
  inactiveTrackColor: Colors.pinkAccent,
  hoverColor: Colors.blue.withOpacity(0.2),
  mouseCursor: SystemMouseCursors.move,
  splashRadius: 15,
  value: _value,
  onChanged: _onChanged,
);
複製程式碼

6.主題相關屬性: thumbColor 和 trackColor

一些具有互動性的 Material 元件會通過有 MaterialState 列舉定義互動行為,有如下 7 個元素。

enum MaterialState {
  hovered,
  focused,
  pressed,
  dragged,
  selected,
  disabled,
  error,
}
複製程式碼

可以看出這兩個成員都是 MaterialStateProperty 型別,那這種型別的物件如何建立,又有什麼特點呢?

---->[Switch 成員宣告]----
final MaterialStateProperty<Color?>? thumbColor;
final MaterialStateProperty<Color?>? trackColor;
複製程式碼

簡單來說通過 MaterialStateProperty.resolveWith 方法,傳入一個函式返回對應泛型資料。如下回撥函式為 getThumbColor ,回撥引數為 Set<MaterialState> 。也僅僅說,會根據 MaterialState 集合,來返回泛型資料。從 thumbColor 屬性原始碼註釋中可以看出,Switch 有如下四種 MaterialState

getThumbColor 中根據 states 的情況,分別對幾種狀態返回不同顏色,這樣 Switch 在不同的狀態下,就會自動使用對應顏色。比如下面的 onChanged: null 代表 Switch 不可用,在 getThumbColor 中當為 disabled ,會返回紅色。

thumbColor 代表小圓顏色,trackColor 代表滑槽顏色,使用方式是一樣的。這裡可能有人會問:有三個屬性可以設定小圓,那它們同時存在,優先順序怎麼樣?結果測試發現,inactiveThumbImage 會優先顯示,優先順序如下:

inactiveThumbImage > thumbColor > inactiveThumbColor > 預設 Switch 主題
複製程式碼

上面提到了 預設 Switch 主題 ,這裡就來說一下 SwitchTheme ,它是一個 InheritedWidget,維護 SwitchThemeData 型別資料,具體內容如下:

我們可以通過在上層巢狀 SwitchTheme 來為子樹中的 Switch 指定預設樣式,由於 MaterialApp 內部繼承了 SwitchTheme 元件,我們可以在 theme 中指定 Switch 的主題樣式。這樣在指定 Switch 的相關顏色屬性,就會使用預設的主題樣式:


7. Switch 的焦點: focusColor 與 autofocus

Switch 元件是擁有焦點的,焦點相關的處理被封裝在元件內部。focusColor 表示聚焦時的顏色,可被聚焦的元件有個特點:在桌面或 web 平臺中可以通過 Tab 鍵,切換焦點。如下是六個 Switch 通過 Tab 鍵切換焦點的效果:

@override
Widget build(BuildContext context) {
  return
    Wrap(
      children: List.generate(6, (index) => Switch(
        value: _value,
        focusColor: Colors.blue.withOpacity(0.1),
        onChanged: _onChanged,
      ))
    );
}
複製程式碼

8. Switch 的尺寸相關: materialTapTargetSize

MaterialTapTargetSize 是一個列舉型別,有兩個元素。該屬性可以影響 Switch 的大小,如下分佈是 paddedshrinkWrap 的效果。通過除錯可知,預設是 padded 。下面在原始碼分析中會詳細介紹該屬性的作用。

enum MaterialTapTargetSize {
  padded,
  shrinkWrap,
}
複製程式碼


二、 挖掘 Switch 原始碼中的一些細節

1. 型別 _SwitchType

Switch 類中有一個 _SwitchType 型別成員,該成員完全被封裝在 Switch 內部,我們是無法直接操作的。 _SwitchType 是隻有兩個元素的列舉類。

enum _SwitchType { material, adaptive }

---->[Switch 成員宣告]----
final _SwitchType _switchType;
複製程式碼

既然是成員變數,必然會在類內部被初始化,一般來說對 成員變數 初始化的地方在 構造方法 中。如下, Switch 的普通構造 中,會將 _switchType 設為 _SwitchType.material


一般來說,列舉物件就是為了分類處理,在 Switch#build 方法中,會根據 _switchType 的值進行不同的構建邏輯,如果是 material ,則所有的平臺都使用Material風格的 Switch 。 如果是 adaptive 會根據平臺的不同,使用不同的風格的 Switch 。在 androidfuchsialinuxwindows 中會使用 Material 風格;在 iOSmacOS 中會使用 Cupertino 風格。

到這裡,可能有人會問, _SwitchType 成員完全被封裝在 Switch 內部,那如何設定 adaptive 型別呢?仔細檢視原始碼可以看出 Switch 還有一個 adaptive 構造,此處會將 _switchType 設為 _SwitchType.adaptive


2. 兩種風格的 Switch 構建

_buildCupertinoSwitch 是當模式為 adaptive 時,用於構建 iOSmacOS 平臺 Switch 元件構建,可以看出其內部是通過 CupertinoSwitch 進行構建,效果如下:


_buildMaterialSwitch 用於構建 Material 風格的 Switch 元件構建,可見其內部通過 _MaterialSwitch 元件進行構建。到這裡我們就可以回答:既然 SwitchStatelessWidget ,為什麼可以執行滑動的動畫?因為 _MaterialSwitch 元件是 StatefulWidget ,它可以在內部改變元件狀態。


3.Switch 尺寸的確定

從上面可以看出,兩種風格的 Switch 都是通過 _getSwitchSize 獲取 Size 尺寸的。如下程式碼中,可以看出,尺寸是通過 MaterialTapTargetSize 物件控制的。如果未指定 materialTapTargetSize 則會通過主題獲取,除錯可以看出,主題中 materialTapTargetSize 預設是 padded

Size _getSwitchSize(ThemeData theme) {
  final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
    ?? theme.switchTheme.materialTapTargetSize
    ?? theme.materialTapTargetSize;
  switch (effectiveMaterialTapTargetSize) {
    case MaterialTapTargetSize.padded:
      return const Size(_kSwitchWidth, _kSwitchHeight);
    case MaterialTapTargetSize.shrinkWrap:
      return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
  }
}
複製程式碼

下面分別是 paddedshrinkWrap 的除錯資訊,可以很清楚地看出尺寸情況。


到這裡 Switch 元件的原始碼就已經面面俱到了,我們可以發現,它作為一個 StatelessWidget 並不能做太多的事,只是定義了很多屬性,並通過別的元件進行構建。也就是說,它本身起到平臺差異的統籌、封裝的作用,目的就是方便使用者使用。


4. onChanged 方法觸發的時機

通過除錯可以發現,onChanged 方法 的觸發是 ToggleableStateMixin#_handleTap 中觸發的。如下是 buildToggleable 的原始碼,可以看出其中通過 GestureDetector 監聽點選事件。

_MaterialSwitchState.build 方法中,可以看到其中通過 GestureDetector 監聽了水平拖拽事件,這也是為什麼 Switch 可以支援拖動的原因,同時 child 屬性是 buildToggleable ,也就是上面的元件,支援點選事件。這是一個很好的多事件監聽的案例。


5.動畫的建立與觸發

仔細看一下滑動的過程,可以看出其中有 位移動畫透明度漸變動畫。 首先來說一下動畫的來源:


這些動畫器都定義在 ToggleableStateMixin 中。而 _MaterialSwitchState 混入了 ToggleableStateMixin


和隱式動畫一樣, _MaterialSwitchState 中的動畫觸發也是通過重構元件,執行 didUpdateWidget 。如果你瞭解隱式動畫,就不難理解 Switch 的動畫觸發機制。

最後,繪製是通過 _SwitchPainter 畫出來的,這個畫板是比較複雜的,這裡就不展開了,有興趣的可以自己研究一下。

Switch 元件的使用方式到這裡就完全介紹完畢,那本文到這裡就結束了,謝謝觀看,明天見~

相關文章