作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
郵箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/134900784
本文從設計角度,考慮滑塊組件的使用場景,實現一個滑塊組件應該包含的功能,介紹 Flutter 中滑塊組件的用法,并分析 Slider 的實現源碼。
目 錄
- 1. 概述
- 2. Slider組件
- 2.1 通過Slider構造函數創建滑塊
- 2.2 通過Slider.adaptive構造函數創建滑塊
- 2.3 離散值功能:divisions屬性
- 2.4 顏色定制功能
- 2.5 焦點管理:autofocus 和 focusNode 屬性
- 2.4.1 FocusNode 對象
- 2.4.2 `autofocus` 和 `focusNode` 屬性
- 2.6 交互方式:allowedInteraction 屬性
- 2.7 標簽功能:label 屬性
- 2.8 次要軌道值:secondaryTrackValue 屬性
- 2.9 關于事件回調
- 3. Slider組件源碼分析
- 3.1 Slider類
- 3.1 _SliderState類
- TickerProviderStateMixin
- _SliderState類的靜態屬性(表)
- _SliderState類的屬性(表)
- _SliderState類的方法(表)
- 狀態初始化分析
- 銷毀組件分析
- build 方法分析
- 風格構建分析
- _buildMaterialSlider方法
- _buildCupertinoSlider方法
1. 概述
1.1 滑塊組件的使用場景
Flutter 中,Slider 組件是一個非常靈活和強大的工具,它可以在各種不同的應用場景中幫助用戶選擇和調整值。
在實際的應用開發中,Slider 組件有著廣泛的應用場景。例如,在音頻或視頻播放的應用中,Slider 組件常常被用于調整音量或者播放進度。用戶可以通過拖動滑塊來增加或減少音量,或者跳轉到視頻的特定位置。在這種情況下,Slider 的最小值通常設置為0(表示無聲或視頻開始),最大值設置為100(表示最大音量或視頻結束)。
另外,在一些圖形設計或繪圖應用中,Slider組件可以用于選擇顏色的不同陰影。用戶可以通過拖動滑塊來選擇從純黑到純白的不同灰度。在這種情況下,Slider的最小值和最大值可以表示顏色的最深和最淺陰影。
再如,在一些日歷或時間管理應用中,Slider組件可以用于選擇時間或日期。例如,用戶可以通過拖動滑塊來選擇一天中的特定時間或一年中的特定日期。在這種情況下,Slider 的 最小值(min) 和 最大值(max) 可以表示一天的開始和結束,或一年的開始和結束。
1.2 一個滑塊組件應該具備的功能
這一小節是寫給像自己定義滑塊組件的讀者的——假設自己設計一個Slider組件,它可以具備什么樣的功能呢?歸納起來,可以考慮實現下面這些方面的功能:
序號 | 功能 | 描述 |
---|---|---|
1. | 值選擇 | Slider 組件允許用戶通過拖動滑塊在 最小值(min)和 最大值(max)之間選擇一個值。當前選中的值由 value 屬性表示。 |
2. | 離散和連續值 | 通過 divisions 屬性,Slider 可以支持連續和離散的值。如果divisions為null,Slider 支持連續的值;如果 divisions 不為null ,Slider 支持離散的值。 |
3. | 顏色定制 | Slider 的顏色可以通過 activeColor 、inactiveColor 、secondaryActiveColor 、thumbColor 和 overlayColor 進行定制。 |
4. | 交互方式 | 通過 allowedInteraction 屬性,可以定義用戶與 Slider 的交互方式。 |
5. | 焦點管理 | 通過 autofocus 和 focusNode ,Slider 可以管理焦點。如果autofocus 為 true ,Slider 將在初始化時獲取焦點。 |
6. | 鼠標光標 | 通過 mouseCursor 屬性,可以定義鼠標指針在懸停或進入 Slider 時的光標樣式。 |
7. | 事件回調 | 通過 onChanged 、onChangeStart 和 onChangeEnd ,Slider 可以在用戶拖動滑塊選擇新值時觸發事件。 |
8. | 標簽顯示 | 通過 label 屬性,Slider 可以在滑塊處于活動狀態時顯示標簽。 |
9. | 次要軌道值 | 通過 secondaryTrackValue ,Slider 可以顯示次要軌道。 |
2. Slider組件
這一小節介紹 Flutter 內置的滑塊 Slider 組件的用法。這個組件有兩個構造函數,分別是 Slider 和 Slider.adaptive,它們都用來創建滑塊,但是在行為上有一些不同:
- Slider 構造函數創建的是一個 Material Design 滑塊,它在所有平臺上的外觀和行為都是一致的;
- Slider.adaptive 構造函數創建的滑塊會根據當前平臺自適應其外觀和行為。例如,當運行在 iOS 平臺時,它會盡可能地模仿 iOS 風格的滑塊。這使得你的應用可以更好地融入到用戶的設備和操作系統中,提供更自然的用戶體驗。
2.1 通過Slider構造函數創建滑塊
const Slider(Key? key,required double value, // 當前選中的值double? secondaryTrackValue, // 次要軌道值required ValueChanged<double>? onChanged, // 當用戶通過拖動選擇新值時調用ValueChanged<double>? onChangeStart, // 當用戶開始選擇新值時調用ValueChanged<double>? onChangeEnd, // 當用戶完成選擇新值時調用double min = 0.0, // 用戶可以選擇的最小值double max = 1.0, // 用戶可以選擇的最大值int? divisions, // 離散劃分的數量String? label, // 當滑塊處于活動狀態并滿足SliderThemeData.showValueIndicator時,顯示在滑塊上方的標簽Color? activeColor, // 活動部分的顏色Color? inactiveColor, // 非活動部分的顏色Color? secondaryActiveColor, // 滑塊軌道上thumb和Slider.secondaryTrackValue之間部分的顏色Color? thumbColor, // thumb的顏色MaterialStateProperty<Color?>? overlayColor, // 通常用于指示滑塊thumb被聚焦、懸停或拖動的高亮顏色MouseCursor? mouseCursor, // 鼠標指針進入或懸停在小部件上時的光標SemanticFormatterCallback? semanticFormatterCallback, // 用于從滑塊值創建語義值的回調FocusNode? focusNode, // 用作此小部件的焦點節點的可選焦點節點bool autofocus = false, // 如果此小部件將被選為初始焦點,則為TrueSliderInteraction? allowedInteraction // 用戶與滑塊交互的允許方式
)
例如:
Slider(value: _currentValue,min: 0,max: 100,divisions: 5,label: '$_currentValue',onChanged: (double value) {setState(() {_currentValue = value;});},onChangeStart: (double startValue) {print('Started change at $startValue');},onChangeEnd: (double endValue) {print('Ended change at $endValue');},activeColor: Colors.blue,inactiveColor: Colors.grey,thumbColor: Colors.red,overlayColor: MaterialStateProperty.all(Colors.green),
)
在這個例子中,我們創建了一個Slider,它的值范圍從0到100,分為5個離散的部分。當用戶拖動滑塊改變值時,onChanged回調會被調用,我們在這個回調中更新_currentValue的值并刷新界面。同時,我們也定義了onChangeStart和onChangeEnd回調來在開始和結束拖動時打印消息。最后,我們通過activeColor、inactiveColor、thumbColor和overlayColor屬性來自定義滑塊的顏色。
2.2 通過Slider.adaptive構造函數創建滑塊
const Slider.adaptive(Key? key,required double value, // 當前選中的值double? secondaryTrackValue, // 次要軌道值required ValueChanged<double>? onChanged, // 當用戶通過拖動選擇新值時調用ValueChanged<double>? onChangeStart, // 當用戶開始選擇新值時調用ValueChanged<double>? onChangeEnd, // 當用戶完成選擇新值時調用double min = 0.0, // 用戶可以選擇的最小值double max = 1.0, // 用戶可以選擇的最大值int? divisions, // 離散劃分的數量String? label, // 當滑塊處于活動狀態并滿足SliderThemeData.showValueIndicator時,顯示在滑塊上方的標簽MouseCursor? mouseCursor, // 鼠標指針進入或懸停在小部件上時的光標Color? activeColor, // 活動部分的顏色Color? inactiveColor, // 非活動部分的顏色Color? secondaryActiveColor, // 滑塊軌道上thumb和Slider.secondaryTrackValue之間部分的顏色Color? thumbColor, // thumb的顏色MaterialStateProperty<Color?>? overlayColor, // 通常用于指示滑塊thumb被聚焦、懸停或拖動的高亮顏色SemanticFormatterCallback? semanticFormatterCallback, // 用于從滑塊值創建語義值的回調FocusNode? focusNode, // 用作此小部件的焦點節點的可選焦點節點bool autofocus = false, // 如果此小部件將被選為初始焦點,則為TrueSliderInteraction? allowedInteraction // 用戶與滑塊交互的允許方式
)
以下是一個使用Slider.adaptive構造函數創建滑塊的例子:
Slider.adaptive(value: _currentValue,min: 0,max: 100,divisions: 5,label: '$_currentValue',onChanged: (double value) {setState(() {_currentValue = value;});},onChangeStart: (double startValue) {print('Started change at $startValue');},onChangeEnd: (double endValue) {print('Ended change at $endValue');},activeColor: Colors.blue,inactiveColor: Colors.grey,thumbColor: Colors.red,overlayColor: MaterialStateProperty.all(Colors.green),
)
在這個例子中,我們使用了 Slider.adaptive 構造函數,它會根據當前平臺創建一個自適應的滑塊。其他的用法和 Slider 構造函數基本一致。我們設置了滑塊的 值范圍、離散劃分的數量、標簽 以及各種顏色。同時,我們也定義了onChanged
、onChangeStart
和 onChangeEnd
回調來響應用戶的操作。
2.3 離散值功能:divisions屬性
divisions
屬性用于將 Slider 的整個值范圍劃分為等間隔的離散值。
例如,如果 min
為 0.0
,max
為 10.0
,divisions
為 5
,那么Slider可以取的值就是 0.0
, 2.0
, 4.0
, 6.0
, 8.0
, 10.0
。如果 divisions
為 null
,那么 Slider 可以取任意值。
例如:
class _MyHomePageState extends State<MyHomePage> {double _currentValue = 0;Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('divisions屬性例子'),),body: Center(child: Slider(value: _currentValue,min: 0,max: 10,divisions: 5,onChanged: (double value) {setState(() {_currentValue = value;});},),),);}
}
在這個例子中,我們創建了一個 Slider,它的值范圍從 0
到 10
,分為 5
個離散的部分。這意味著用戶只能選擇 0.0
, 2.0
, 4.0
, 6.0
, 8.0
, 10.0
這些值,不能選擇這些值之間的值。當用戶拖動滑塊改變值時,onChanged
回調會被調用,我們在這個回調中更新 _currentValue
的值并刷新界面。
2.4 顏色定制功能
Slider 組件提供了多個屬性來定制滑塊的顏色:
-
activeColor
:活動部分的顏色,即滑塊左側(或右側,取決于語言和方向)的軌道顏色。 -
inactiveColor
:非活動部分的顏色,即滑塊右側(或左側,取決于語言和方向)的軌道顏色。 -
secondaryActiveColor
:滑塊軌道上thumb
和 Slider.secondaryTrackValue
之間部分的顏色。 -
thumbColor
:滑塊thumb
的顏色。 -
overlayColor
:通常用于指示滑塊thumb
被 聚焦、懸停 或 拖動 的高亮顏色,它是一個 MaterialStateProperty<Color?> 類型的屬性。
例如:
Slider(value: _currentValue,min: 0,max: 100,divisions: 5,label: _currentValue.round().toString(),onChanged: (double value) {setState(() {_currentValue = value;});},activeColor: Colors.blue,inactiveColor: Colors.grey,thumbColor: Colors.red,overlayColor: MaterialStateProperty.all(Colors.yellow),
)
在這個例子中,并通過 activeColor
、inactiveColor
、thumbColor
和 overlayColor
屬性來定制滑塊的顏色。
當用戶拖動滑塊時,滑塊左側的軌道顏色為藍色,滑塊右側的軌道顏色為灰色,滑塊 thumb
的顏色為紅色,滑塊 thumb
被聚焦、懸停或拖動時的高亮顏色為黃色。
2.5 焦點管理:autofocus 和 focusNode 屬性
2.4.1 FocusNode 對象
在 Flutter 中,焦點管理是通過 FocusNode 對象來實現的。FocusNode 對象表示用戶界面中的一個可以獲得鍵盤輸入焦點的元素。
2.4.2 autofocus
和 focusNode
屬性
而Slider 組件提供了 autofocus
和 focusNode
兩個屬性來管理焦點。
-
autofocus 屬性是一個布爾值,如果為 true,則 Slider 將在初始化時自動獲取焦點。
-
focusNode 屬性是一個 FocusNode 對象,你可以通過它來控制 Slider 的焦點狀態。例如,你可以調用 FocusNode 的 requestFocus 和 unfocus 方法來手動讓 Slider 獲得或失去焦點。
例如:
FocusNode _focusNode = FocusNode();Slider(value: _currentValue,min: 0,max: 100,divisions: 5,label: _currentValue.round().toString(),onChanged: (double value) {setState(() {_currentValue = value;});},autofocus: true,focusNode: _focusNode,
)
在這個例子中,我們通過 autofocus
屬性讓它在初始化時自動獲取焦點。我們還創建了一個 FocusNode 對象,并通過 focusNode
屬性將它關聯到 Slider。這樣,我們就可以通過 _focusNode
來手動控制 Slider 的焦點狀態。例如,我們可以在某個按鈕的點擊事件中調用 _focusNode.requestFocus
來讓 Slider 獲得焦點,或調用 _focusNode.unfocus
來讓 Slider 失去焦點。
2.6 交互方式:allowedInteraction 屬性
allowedInteraction屬性用于定義用戶與滑塊的交互方式。這個屬性的類型是SliderInteraction,它是一個枚舉類型,包含以下幾個值:
- SliderInteraction.
all
:允許所有交互,包括 拖動、點擊 和 使用鍵盤。 - SliderInteraction.
drag
:只允許拖動 滑塊。 - SliderInteraction.
none
:不 允許任何交互。
例如:
Slider(value: _currentValue,min: 0,max: 100,divisions: 5,label: _currentValue.round().toString(),onChanged: (double value) {setState(() {_currentValue = value;});},allowedInteraction: SliderInteraction.drag,
)
其中,我們創建了一個 Slider ,它的值范圍從 0
到 100
,分為 5
個離散的部分。
我們使用 allowedInteraction
屬性來限制用戶只能通過拖動滑塊來改變值,不能通過點擊或使用鍵盤。當用戶拖動滑塊改變值時,onChanged
回調會被調用,我們在這個回調中更新 _currentValue
的值并刷新界面。這樣,用戶在拖動滑塊時,就可以看到滑塊上方顯示的當前值。
2.7 標簽功能:label 屬性
label
屬性用于在滑塊處于 活動狀態 并滿足SliderThemeData.showValueIndicator 時,顯示在滑塊上方的標簽。這個標簽通常用于顯示當前滑塊的值,幫助用戶更準確地選擇值。
例如:
Slider(value: _currentValue,min: 0,max: 100,divisions: 5,label: _currentValue.round().toString(),onChanged: (double value) {setState(() {_currentValue = value;});},
)
在這個例子中,我們創建了一個Slider,它的值范圍從 0
到 100
,分為 5
個離散的部分。
我們使用 label
屬性來顯示當前滑塊的值,這個值是 _currentValue
四舍五入后的整數。
當用戶拖動滑塊改變值時,onChanged
回調會被調用,我們在這個回調中更新 _currentValue
的值并刷新界面。
這樣,用戶在拖動滑塊時,就可以看到滑塊上方顯示的當前值。
2.8 次要軌道值:secondaryTrackValue 屬性
在 Slider 組件中,所謂 次要軌道 是指滑塊軌道上的一個可選部分,它表示一個次要的值。這個次要的值由 secondaryTrackValue
屬性來設置。這個功能可以用于表示一些特殊的場景,例如在一個音頻播放器中,value 可以表示當前的播放位置,而 secondaryTrackValue 可以表示緩沖的位置。
secondaryTrackValue
屬性的值必須在 min
和 max
之間。當設置了 secondaryTrackValue
,滑塊軌道上會顯示兩個活動部分:
- 一個是從
min
到value
; - 另一個是從
value
到secondaryTrackValue
。
這兩個活動部分的顏色可以通過 activeColor
和 secondaryActiveColor
來分別設置。
例如:
import 'package:flutter/material.dart';class SliderDemo extends StatefulWidget {const SliderDemo({Key? key}) : super(key: key);State<SliderDemo> createState() => _SliderDemoState();
}class _SliderDemoState extends State<SliderDemo> {// 定義一個狀態變量,表示滑塊的當前值double _currentValue = 50;Widget build(BuildContext context) {return Center(// 創建 Slider 組件child: Slider(// 設置滑塊的當前值value: _currentValue,// 設置滑塊的最小值和最大值min: 0,max: 100,// 設置滑塊的離散劃分的數量divisions: 5,// 設置滑塊的標簽,顯示當前值label: _currentValue.round().toString(),// 當用戶拖動滑塊選擇新值時,更新 _currentValue 的值并刷新界面onChanged: (double value) {setState(() {_currentValue = value;});},// 設置滑塊的顏色activeColor: Colors.blue,inactiveColor: Colors.grey,thumbColor: Colors.red,overlayColor: MaterialStateProperty.all(Colors.green),),);}
}
在這個例子中,我們通過 activeColor
、inactiveColor
、thumbColor
和 overlayColor
屬性來定制滑塊的顏色。當用戶拖動滑塊時,滑塊左側的軌道顏色為藍色,滑塊右側的軌道顏色為灰色,滑塊 thumb
的顏色為紅色,滑塊 thumb
被聚焦、懸停或拖動時的高亮顏色為綠色。
2.9 關于事件回調
上面小節中實際上已經用到的各個事件回調,這里簡單補充說明一下。Slider 組件提供了三個事件回調:
onChanged
:當用戶通過拖動選擇新值時調用。它的參數是新選擇的值;onChangeStart
:當用戶開始選擇新值時調用。它的參數是開始選擇時的值;onChangeEnd
:當用戶完成選擇新值時調用。它的參數是完成選擇后的值。
3. Slider組件源碼分析
【注】:不考慮 Cupertino 風格的實現(實際上內部通過CupertinoSlider轉換),完整分析一個滑塊組件的實現,在Flutter源代碼中也有兩千多行。這部分內容涉及廣泛,將在后續逐漸補充。
3.1 Slider類
很顯然,一個滑塊中是必然存在狀態的,因此需要從 StatefulWidget 得到一個 Slider 類,用于實現相關的滑塊構造函數,用作外部使用的接口,這也就是前文介紹過的兩個構造函數: Slider 和 Slider.adaptive,即:
const Slider({super.key,required this.value,this.secondaryTrackValue,required this.onChanged,this.onChangeStart,this.onChangeEnd,this.min = 0.0,this.max = 1.0,this.divisions,this.label,this.activeColor,this.inactiveColor,this.secondaryActiveColor,this.thumbColor,this.overlayColor,this.mouseCursor,this.semanticFormatterCallback,this.focusNode,this.autofocus = false,this.allowedInteraction,
}) : _sliderType = _SliderType.material,assert(min <= max),assert(value >= min && value <= max,'Value $value is not between minimum $min and maximum $max'),assert(secondaryTrackValue == null || (secondaryTrackValue >= min && secondaryTrackValue <= max),'SecondaryValue $secondaryTrackValue is not between $min and $max'),assert(divisions == null || divisions > 0);
const Slider.adaptive({super.key,required this.value,this.secondaryTrackValue,required this.onChanged,this.onChangeStart,this.onChangeEnd,this.min = 0.0,this.max = 1.0,this.divisions,this.label,this.mouseCursor,this.activeColor,this.inactiveColor,this.secondaryActiveColor,this.thumbColor,this.overlayColor,this.semanticFormatterCallback,this.focusNode,this.autofocus = false,this.allowedInteraction,
}) : _sliderType = _SliderType.adaptive,assert(min <= max),assert(value >= min && value <= max,'Value $value is not between minimum $min and maximum $max'),assert(secondaryTrackValue == null || (secondaryTrackValue >= min && secondaryTrackValue <= max),'SecondaryValue $secondaryTrackValue is not between $min and $max'),assert(divisions == null || divisions > 0);
具體的功能當然通過 createState 放在對應的 _SliderState 類中實現的:
3.1 _SliderState類
TickerProviderStateMixin
TickerProviderStateMixin 是一個 Flutter 混入(Mixin),它可以為其混入的類提供 Ticker 對象。Ticker 是一個計時器,它可以每秒觸發多次回調,用于驅動基于時間的動畫。
【注:小知識】在 Flutter 中,許多動畫相關的類,如 AnimationController,需要一個 TickerProvider 來創建自己的 Ticker。
當你將 TickerProviderStateMixin 混入到一個 State 對象中,這個 State 對象就可以作為 TickerProvider,用于創建 Ticker。推薦參考我的另一篇博客《Flutter筆記:Ticker及其應用》
class _SliderState extends State<Slider> with TickerProviderStateMixin
可以看到,_SliderState 混入了 TickerProviderStateMixin,這意味著 _SliderState 可以提供 Ticker 對象。這對于 Slider 組件來說非常重要,因為 Slider 組件可能需要驅動一些基于時間的動畫,例如當用戶拖動滑塊時,滑塊的位置需要根據時間平滑地變化。
_SliderState類的靜態屬性(表)
屬性名 | 類型 | 描述 |
---|---|---|
enableAnimationDuration | Duration | 啟用/禁用滑塊時的動畫持續時間 |
valueIndicatorAnimationDuration | Duration | 顯示/隱藏值指示器時的動畫持續時間 |
_traditionalNavShortcutMap | Map<ShortcutActivator, Intent> | 傳統導航快捷鍵映射 |
_directionalNavShortcutMap | Map<ShortcutActivator, Intent> | 方向導航快捷鍵映射 |
非靜態屬性 |
_SliderState類的屬性(表)
屬性名 | 類型 | 描述 |
---|---|---|
overlayController | AnimationController | 控制 overlay 顯示的動畫 |
valueIndicatorController | AnimationController | 控制值指示器顯示的動畫 |
enableController | AnimationController | 控制滑塊啟用/禁用的動畫 |
positionController | AnimationController | 控制滑塊位置的動畫 |
interactionTimer | Timer? | 用于延遲隱藏 overlay 和值指示器的計時器 |
_renderObjectKey | GlobalKey | 用于獲取滑塊的 RenderObject |
_actionMap | Map<Type, Action> | 動作映射 |
_enabled | bool | 滑塊是否啟用 |
paintValueIndicator | PaintValueIndicator? | 用于繪制值指示器的回調 |
_dragging | bool | 用戶是否正在拖動滑塊 |
_focusNode | FocusNode? | 管理滑塊焦點的 FocusNode 對象 |
_focused | bool | 滑塊是否獲得焦點 |
_hovering | bool | 鼠標指針是否在滑塊上 |
_SliderState類的方法(表)
方法名 | 參數 | 返回類型 | 描述 |
---|---|---|---|
initState | 無 | 無 | 初始化狀態,創建動畫控制器和焦點節點 |
dispose | 無 | 無 | 清理資源,取消計時器,銷毀動畫控制器和焦點節點 |
_handleChanged | double value | 無 | 處理滑塊值改變的事件 |
_handleDragStart | double value | 無 | 處理開始拖動滑塊的事件 |
_handleDragEnd | double value | 無 | 處理結束拖動滑塊的事件 |
_actionHandler | _AdjustSliderIntent intent | 無 | 處理滑塊的動作 |
_handleFocusHighlightChanged | bool focused | 無 | 處理焦點高亮改變的事件 |
_handleHoverChanged | bool hovering | 無 | 處理鼠標懸停狀態改變的事件 |
_lerp | double value | double | 將值從 [0, 1] 映射到 [min, max] |
_discretize | double value | double | 將連續的值離散化 |
_convert | double value | double | 將值從 [min, max] 映射到 [0, 1],并可能離散化 |
_unlerp | double value | double | 將值從 [min, max] 映射到 [0, 1] |
build | BuildContext context | Widget | 構建滑塊的 widget |
_buildMaterialSlider | BuildContext context | Widget | 構建 Material 風格的滑塊 |
_buildCupertinoSlider | BuildContext context | Widget | 構建 Cupertino 風格的滑塊 |
showValueIndicator | 無 | 無 | 顯示值指示器 |
狀態初始化分析
既然Slider是有狀態組件,有狀態組件的狀態是通過State類的 initState
實現的,因此可以分析_SliderState的initState方法。這部分源代碼如下:
void initState() {super.initState();// 創建一個控制 overlay 顯示的動畫控制器overlayController = AnimationController(duration: kRadialReactionDuration,vsync: this,);// 創建一個控制值指示器顯示的動畫控制器valueIndicatorController = AnimationController(duration: valueIndicatorAnimationDuration,vsync: this,);// 創建一個控制滑塊啟用/禁用的動畫控制器enableController = AnimationController(duration: enableAnimationDuration,vsync: this,);// 創建一個控制滑塊位置的動畫控制器positionController = AnimationController(duration: Duration.zero,vsync: this,);// 如果滑塊啟用,則將 enableController 的值設置為 1.0,否則設置為 0.0enableController.value = widget.onChanged != null ? 1.0 : 0.0;// 將滑塊的值轉換為 [0, 1] 范圍內的值,并設置給 positionControllerpositionController.value = _convert(widget.value);// 創建動作映射_actionMap = <Type, Action<Intent>>{_AdjustSliderIntent: CallbackAction<_AdjustSliderIntent>(onInvoke: _actionHandler,),};// 如果 widget 沒有提供 focusNode,則創建一個新的 focusNodeif (widget.focusNode == null) {_focusNode ??= FocusNode();}
}
在這個方法中,首先調用 super.initState() 來確保父類的初始化邏輯被執行(慣例,沒啥說的)。
-
然后,創建四個 AnimationController 對象,它們分別用于控制 overlay 的顯示、值指示器的顯示、滑塊的啟用/禁用以及滑塊位置的動畫。每個 AnimationController 都需要一個持續時間和一個 vsync 參數,vsync 參數通常設置為 this,表示當前的 _SliderState 對象將作為 TickerProvider。
-
接著,設置 enableController 和 positionController 的初始值。如果 widget.onChanged 不為 null,則 enableController 的值設置為 1.0,表示滑塊是啟用的;否則,設置為 0.0,表示滑塊是禁用的。positionController 的值設置為 widget.value 對應的位置。
-
然后,初始化 _actionMap,它是一個映射,將 _AdjustSliderIntent 映射到一個 CallbackAction,這個 CallbackAction 的回調函數是 _actionHandler。
-
最后,如果 widget.focusNode 為 null,則創建一個新的 FocusNode 對象。這個 FocusNode 對象用于管理滑塊的焦點。如果 widget.focusNode 不為 null,則使用 widget.focusNode。
銷毀組件分析
接著 initState
看 dispose
也算是思維常態,畢竟創建了相關的資源就需要銷毀以防止內存泄漏。對應的是有狀態組件狀態類的 dispose
方法。這里具體說來就是 _SliderState 的 dispose
方法。其源代碼為:
void dispose() {// 取消交互計時器interactionTimer?.cancel();// 銷毀 overlay 的動畫控制器overlayController.dispose();// 銷毀值指示器的動畫控制器valueIndicatorController.dispose();// 銷毀啟用/禁用滑塊的動畫控制器enableController.dispose();// 銷毀滑塊位置的動畫控制器positionController.dispose();// 移除 overlayEntryoverlayEntry?.remove();// 銷毀 overlayEntryoverlayEntry?.dispose();// 將 overlayEntry 設置為 nulloverlayEntry = null;// 銷毀焦點節點_focusNode?.dispose();// 調用父類的 dispose 方法super.dispose();
}
沒有什么太多可以說的,這段代碼主要完成了資源的清理工作。包括取消交互計時器,銷毀動畫控制器,移除和銷毀 overlayEntry
,以及銷毀焦點節點。這些操作都是為了避免內存泄漏,當滑塊不再需要時,應該調用這個方法來釋放資源。
build 方法分析
不論是有狀態組件還是無狀態組件都是通過 build
組件實現其 UI,對于一個有狀態組件,build
方法在其狀態類中。在 滑塊 組件中,具體說來就是 _SliderState 的 build
方法。其代碼如下:
Widget build(BuildContext context) {// 確保當前的 context 中有 Material 組件assert(debugCheckHasMaterial(context));// 確保當前的 context 中有 MediaQuery 組件assert(debugCheckHasMediaQuery(context));// 根據滑塊的類型來構建不同的滑塊switch (widget._sliderType) {// 如果滑塊的類型是 Material,則構建 Material 風格的滑塊case _SliderType.material:return _buildMaterialSlider(context);// 如果滑塊的類型是 Adaptive,則根據平臺來構建不同風格的滑塊case _SliderType.adaptive: {final ThemeData theme = Theme.of(context);switch (theme.platform) {// 如果平臺是 Android、Fuchsia、Linux 或 Windows,則構建 Material 風格的滑塊case TargetPlatform.android:case TargetPlatform.fuchsia:case TargetPlatform.linux:case TargetPlatform.windows:return _buildMaterialSlider(context);// 如果平臺是 iOS 或 macOS,則構建 Cupertino 風格的滑塊case TargetPlatform.iOS:case TargetPlatform.macOS:return _buildCupertinoSlider(context);}}}
}
前面兩個 assert
基本就是寫組件的模板套路,不是我們討論的主要功能。我們的關注焦點在下面的 switch
塊中。
很清楚,可以看到:在 _SliderState 類的 build
方法中,根據滑塊的類型和當前的平臺來構建不同風格的滑塊。如果滑塊的類型是 Material,則無論在什么平臺上都構建 Material 風格的滑塊;如果滑塊的類型是 Adaptive,則在 Android、Fuchsia、Linux 和 Windows 平臺上構建 Material 風格的滑塊,在 iOS 和 macOS 平臺上構建 Cupertino 風格的滑塊。
風格構建分析
在分析 build
方法時,我們注意到依據平臺不同,分別具體交給 _buildMaterialSlider
方法 和 _buildCupertinoSlider
方法實現具體的 build。
_buildMaterialSlider方法
該方法用于,其源代碼為:
Widget _buildMaterialSlider(BuildContext context) {// 獲取當前主題final ThemeData theme = Theme.of(context);// 獲取當前滑塊主題SliderThemeData sliderTheme = SliderTheme.of(context);// 獲取默認的滑塊主題final SliderThemeData defaults = theme.useMaterial3 ? _SliderDefaultsM3(context) : _SliderDefaultsM2(context);// 如果 widget 有 active 或 inactive 顏色指定,我們盡可能地將它們插入到滑塊主題中。// 如果開發者需要更多的控制,那么他們需要使用 SliderTheme。默認的顏色來自 ThemeData.colorScheme。// 這些顏色,以及默認的形狀和文本樣式都符合 Material 指南。// 定義默認的軌道形狀、刻度標記形狀、覆蓋形狀、拇指形狀、值指示器形狀、顯示值指示器和允許的交互const SliderTrackShape defaultTrackShape = RoundedRectSliderTrackShape();const SliderTickMarkShape defaultTickMarkShape = RoundSliderTickMarkShape();const SliderComponentShape defaultOverlayShape = RoundSliderOverlayShape();const SliderComponentShape defaultThumbShape = RoundSliderThumbShape();final SliderComponentShape defaultValueIndicatorShape = defaults.valueIndicatorShape!;const ShowValueIndicator defaultShowValueIndicator = ShowValueIndicator.onlyForDiscrete;const SliderInteraction defaultAllowedInteraction = SliderInteraction.tapAndSlide;// 定義滑塊的狀態final Set<MaterialState> states = <MaterialState>{if (!_enabled) MaterialState.disabled,if (_hovering) MaterialState.hovered,if (_focused) MaterialState.focused,if (_dragging) MaterialState.dragged,};// 值指示器的顏色與拇指和活動軌道(可以由 activeColor 定義)不同,// 如果使用 RectangularSliderValueIndicatorShape。在所有其他情況下,值指示器被認為與活動顏色相同。final SliderComponentShape valueIndicatorShape = sliderTheme.valueIndicatorShape ?? defaultValueIndicatorShape;final Color valueIndicatorColor;if (valueIndicatorShape is RectangularSliderValueIndicatorShape) {valueIndicatorColor = sliderTheme.valueIndicatorColor ?? Color.alphaBlend(theme.colorScheme.onSurface.withOpacity(0.60), theme.colorScheme.surface.withOpacity(0.90));} else {valueIndicatorColor = widget.activeColor ?? sliderTheme.valueIndicatorColor ?? theme.colorScheme.primary;}// 定義有效的覆蓋顏色Color? effectiveOverlayColor() {return widget.overlayColor?.resolve(states)?? widget.activeColor?.withOpacity(0.12)?? MaterialStateProperty.resolveAs<Color?>(sliderTheme.overlayColor, states)?? MaterialStateProperty.resolveAs<Color?>(defaults.overlayColor, states);}// 使用 widget 的屬性和默認值來更新滑塊主題sliderTheme = sliderTheme.copyWith(trackHeight: sliderTheme.trackHeight ?? defaults.trackHeight,activeTrackColor: widget.activeColor ?? sliderTheme.activeTrackColor ?? defaults.activeTrackColor,inactiveTrackColor: widget.inactiveColor ?? sliderTheme.inactiveTrackColor ?? defaults.inactiveTrackColor,secondaryActiveTrackColor: widget.secondaryActiveColor ?? sliderTheme.secondaryActiveTrackColor ?? defaults.secondaryActiveTrackColor,disabledActiveTrackColor: sliderTheme.disabledActiveTrackColor ?? defaults.disabledActiveTrackColor,disabledInactiveTrackColor: sliderTheme.disabledInactiveTrackColor ?? defaults.disabledInactiveTrackColor,disabledSecondaryActiveTrackColor: sliderTheme.disabledSecondaryActiveTrackColor ?? defaults.disabledSecondaryActiveTrackColor,activeTickMarkColor: widget.inactiveColor ?? sliderTheme.activeTickMarkColor ?? defaults.activeTickMarkColor,inactiveTickMarkColor: widget.activeColor ?? sliderTheme.inactiveTickMarkColor ?? defaults.inactiveTickMarkColor,disabledActiveTickMarkColor: sliderTheme.disabledActiveTickMarkColor ?? defaults.disabledActiveTickMarkColor,disabledInactiveTickMarkColor: sliderTheme.disabledInactiveTickMarkColor ?? defaults.disabledInactiveTickMarkColor,thumbColor: widget.thumbColor ?? widget.activeColor ?? sliderTheme.thumbColor ?? defaults.thumbColor,disabledThumbColor: sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor,overlayColor: effectiveOverlayColor(),valueIndicatorColor: valueIndicatorColor,trackShape: sliderTheme.trackShape ?? defaultTrackShape,tickMarkShape: sliderTheme.tickMarkShape ?? defaultTickMarkShape,thumbShape: sliderTheme.thumbShape ?? defaultThumbShape,overlayShape: sliderTheme.overlayShape ?? defaultOverlayShape,valueIndicatorShape: valueIndicatorShape,showValueIndicator: sliderTheme.showValueIndicator ?? defaultShowValueIndicator,valueIndicatorTextStyle: sliderTheme.valueIndicatorTextStyle ?? defaults.valueIndicatorTextStyle,);// 解析有效的鼠標光標final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)?? sliderTheme.mouseCursor?.resolve(states)?? MaterialStateMouseCursor.clickable.resolve(states);// 解析有效的允許交互final SliderInteraction effectiveAllowedInteraction = widget.allowedInteraction?? sliderTheme.allowedInteraction?? defaultAllowedInteraction;// 這個大小用作值指示器的繪制的最大邊界// 必須與 range_slider.dart 中同名函數保持同步。Size screenSize() => MediaQuery.sizeOf(context);// 定義獲取輔助功能焦點的回調VoidCallback? handleDidGainAccessibilityFocus;switch (theme.platform) {case TargetPlatform.android:case TargetPlatform.fuchsia:case TargetPlatform.iOS:case TargetPlatform.linux:case TargetPlatform.macOS:break;case TargetPlatform.windows:handleDidGainAccessibilityFocus = () {// 當滑塊獲得輔助功能焦點時,自動激活滑塊。if (!focusNode.hasFocus && focusNode.canRequestFocus) {focusNode.requestFocus();}};}// 定義快捷鍵映射final Map<ShortcutActivator, Intent> shortcutMap;switch (MediaQuery.navigationModeOf(context)) {case NavigationMode.directional:shortcutMap = _directionalNavShortcutMap;case NavigationMode.traditional:shortcutMap = _traditionalNavShortcutMap;}// 獲取文本縮放因子final double textScaleFactor = theme.useMaterial3? MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.3).textScaleFactor: MediaQuery.textScalerOf(context).textScaleFactor;// 返回語義組件,包含焦點行為檢測器和滑塊渲染對象組件return Semantics(container: true,slider: true,onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,child: FocusableActionDetector(actions: _actionMap,shortcuts: shortcutMap,focusNode: focusNode,autofocus: widget.autofocus,enabled: _enabled,onShowFocusHighlight: _handleFocusHighlightChanged,onShowHoverHighlight: _handleHoverChanged,mouseCursor: effectiveMouseCursor,child: CompositedTransformTarget(link: _layerLink,child: _SliderRenderObjectWidget(key: _renderObjectKey,value: _convert(widget.value),secondaryTrackValue: (widget.secondaryTrackValue != null) ? _convert(widget.secondaryTrackValue!) : null,divisions: widget.divisions,label: widget.label,sliderTheme: sliderTheme,textScaleFactor: textScaleFactor,screenSize: screenSize(),onChanged: (widget.onChanged != null) && (widget.max > widget.min) ? _handleChanged : null,onChangeStart: _handleDragStart,onChangeEnd: _handleDragEnd,state: this,semanticFormatterCallback: widget.semanticFormatterCallback,hasFocus: _focused,hovering: _hovering,allowedInteraction: effectiveAllowedInteraction,),),),);
}
可以看到 _buildMaterialSlider 方法中首先獲取了當前的主題和滑塊主題。
-
然后根據 widget 的屬性和默認值來更新滑塊主題。
-
然后,它解析了有效的鼠標光標和允許的交互。
-
最后,它返回了一個包含焦點行為檢測器和滑塊渲染對象組件的語義組件。
這個組件包含了滑塊的所有交互邏輯和渲染邏輯,包括焦點處理、鼠標懸停處理、滑塊值改變的處理等。
_buildCupertinoSlider方法
_buildCupertinoSlider 方法中,主要完成了 Cupertino 風格滑塊的構建,正如前文所述,將用于 iOS 和 macOS 平臺。
Widget _buildCupertinoSlider(BuildContext context) {// 滑塊的渲染框有固定的高度,但會占用可用的寬度。// 以這種方式包裝 [CupertinoSlider] 將有助于保持相同的大小。return SizedBox(width: double.infinity,child: CupertinoSlider(// 設置滑塊的值value: widget.value,// 設置滑塊值改變時的回調onChanged: widget.onChanged,// 設置開始拖動滑塊時的回調onChangeStart: widget.onChangeStart,// 設置結束拖動滑塊時的回調onChangeEnd: widget.onChangeEnd,// 設置滑塊的最小值min: widget.min,// 設置滑塊的最大值max: widget.max,// 設置滑塊的分段數divisions: widget.divisions,// 設置滑塊的活動顏色activeColor: widget.activeColor,// 設置滑塊的拇指顏色,如果沒有指定,則使用白色thumbColor: widget.thumbColor ?? CupertinoColors.white,),);
}
首先,它創建了一個 SizedBox,并設置其寬度為 double.infinity,這樣可以使滑塊占用可用的全部寬度。
然后,它在 SizedBox 中創建了一個 CupertinoSlider 組件,并設置了滑塊的各種屬性,包括值、最小值、最大值、分段數、活動顏色和拇指顏色等。
可見,Slider 組件被用于 iOS 和 macOS 平臺時,實際上內部將自動使用 CupertinoSlider 組件實現。