当前位置: 首页 > news >正文

Flutter笔记:滑块及其实现分析1

Flutter笔记
滑块分析1

作者李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
邮箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/134900784


本文从设计角度,考虑滑块组件的使用场景,实现一个滑块组件应该包含的功能,介绍 Flutter 中滑块组件的用法,并分析 Slider 的实现源码。


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 不为nullSlider 支持离散的值。
3.颜色定制Slider 的颜色可以通过 activeColorinactiveColorsecondaryActiveColorthumbColoroverlayColor 进行定制。
4.交互方式通过 allowedInteraction 属性,可以定义用户与 Slider 的交互方式。
5.焦点管理通过 autofocusfocusNodeSlider 可以管理焦点。如果autofocustrueSlider 将在初始化时获取焦点。
6.鼠标光标通过 mouseCursor 属性,可以定义鼠标指针在悬停或进入 Slider 时的光标样式。
7.事件回调通过 onChangedonChangeStartonChangeEndSlider 可以在用户拖动滑块选择新值时触发事件。
8.标签显示通过 label 属性,Slider 可以在滑块处于活动状态时显示标签。
9.次要轨道值通过 secondaryTrackValueSlider 可以显示次要轨道。

2. Slider组件

这一小节介绍 Flutter 内置的滑块 Slider 组件的用法。这个组件有两个构造函数,分别是 SliderSlider.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 构造函数基本一致。我们设置了滑块的 值范围离散划分的数量标签 以及各种颜色。同时,我们也定义了onChangedonChangeStartonChangeEnd回调来响应用户的操作。

2.3 离散值功能:divisions属性

divisions 属性用于将 Slider 的整个值范围划分为等间隔的离散值。

例如,如果 min0.0max10.0divisions5,那么Slider可以取的值就是 0.0, 2.0, 4.0, 6.0, 8.0, 10.0。如果 divisionsnull,那么 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,它的值范围从 010,分为 5 个离散的部分。这意味着用户只能选择 0.0, 2.0, 4.0, 6.0, 8.0, 10.0 这些值,不能选择这些值之间的值。当用户拖动滑块改变值时,onChanged 回调会被调用,我们在这个回调中更新 _currentValue 的值并刷新界面。

2.4 颜色定制功能

Slider 组件提供了多个属性来定制滑块的颜色:

  • activeColor:活动部分的颜色,即滑块左侧(或右侧,取决于语言和方向)的轨道颜色。

  • inactiveColor:非活动部分的颜色,即滑块右侧(或左侧,取决于语言和方向)的轨道颜色。

  • secondaryActiveColor:滑块轨道上 thumbSlider.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),
)

在这里插入图片描述

在这个例子中,并通过 activeColorinactiveColorthumbColoroverlayColor 属性来定制滑块的颜色。

当用户拖动滑块时,滑块左侧的轨道颜色为蓝色,滑块右侧的轨道颜色为灰色,滑块 thumb 的颜色为红色,滑块 thumb 被聚焦、悬停或拖动时的高亮颜色为黄色。

2.5 焦点管理:autofocus 和 focusNode 属性

2.4.1 FocusNode 对象

在 Flutter 中,焦点管理是通过 FocusNode 对象来实现的。FocusNode 对象表示用户界面中的一个可以获得键盘输入焦点的元素。

2.4.2 autofocusfocusNode 属性

Slider 组件提供了 autofocusfocusNode 两个属性来管理焦点。

  • 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 ,它的值范围从 0100,分为 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,它的值范围从 0100,分为 5 个离散的部分。

我们使用 label 属性来显示当前滑块的值,这个值是 _currentValue 四舍五入后的整数。

当用户拖动滑块改变值时,onChanged 回调会被调用,我们在这个回调中更新 _currentValue 的值并刷新界面。

这样,用户在拖动滑块时,就可以看到滑块上方显示的当前值。

2.8 次要轨道值:secondaryTrackValue 属性

Slider 组件中,所谓 次要轨道 是指滑块轨道上的一个可选部分,它表示一个次要的值。这个次要的值由 secondaryTrackValue 属性来设置。这个功能可以用于表示一些特殊的场景,例如在一个音频播放器中,value 可以表示当前的播放位置,而 secondaryTrackValue 可以表示缓冲的位置。

secondaryTrackValue 属性的值必须在 minmax 之间。当设置了 secondaryTrackValue,滑块轨道上会显示两个活动部分:

  • 一个是从 minvalue
  • 另一个是从 valuesecondaryTrackValue

这两个活动部分的颜色可以通过 activeColorsecondaryActiveColor 来分别设置。

例如:

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),),);}
}

在这里插入图片描述

在这个例子中,我们通过 activeColorinactiveColorthumbColoroverlayColor 属性来定制滑块的颜色。当用户拖动滑块时,滑块左侧的轨道颜色为蓝色,滑块右侧的轨道颜色为灰色,滑块 thumb 的颜色为红色,滑块 thumb 被聚焦、悬停或拖动时的高亮颜色为绿色。

2.9 关于事件回调

上面小节中实际上已经用到的各个事件回调,这里简单补充说明一下。Slider 组件提供了三个事件回调:

  1. onChanged:当用户通过拖动选择新值时调用。它的参数是新选择的值;
  2. onChangeStart:当用户开始选择新值时调用。它的参数是开始选择时的值;
  3. onChangeEnd:当用户完成选择新值时调用。它的参数是完成选择后的值。

3. Slider组件源码分析

【注】:不考虑 Cupertino 风格的实现(实际上内部通过CupertinoSlider转换),完整分析一个滑块组件的实现,在Flutter源代码中也有两千多行。这部分内容涉及广泛,将在后续逐渐补充。

3.1 Slider类

很显然,一个滑块中是必然存在状态的,因此需要从 StatefulWidget 得到一个 Slider 类,用于实现相关的滑块构造函数,用作外部使用的接口,这也就是前文介绍过的两个构造函数: SliderSlider.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类的静态属性(表)

属性名类型描述
enableAnimationDurationDuration启用/禁用滑块时的动画持续时间
valueIndicatorAnimationDurationDuration显示/隐藏值指示器时的动画持续时间
_traditionalNavShortcutMapMap<ShortcutActivator, Intent>传统导航快捷键映射
_directionalNavShortcutMapMap<ShortcutActivator, Intent>方向导航快捷键映射
非静态属性

_SliderState类的属性(表)

属性名类型描述
overlayControllerAnimationController控制 overlay 显示的动画
valueIndicatorControllerAnimationController控制值指示器显示的动画
enableControllerAnimationController控制滑块启用/禁用的动画
positionControllerAnimationController控制滑块位置的动画
interactionTimerTimer?用于延迟隐藏 overlay 和值指示器的计时器
_renderObjectKeyGlobalKey用于获取滑块的 RenderObject
_actionMapMap<Type, Action>动作映射
_enabledbool滑块是否启用
paintValueIndicatorPaintValueIndicator?用于绘制值指示器的回调
_draggingbool用户是否正在拖动滑块
_focusNodeFocusNode?管理滑块焦点的 FocusNode 对象
_focusedbool滑块是否获得焦点
_hoveringbool鼠标指针是否在滑块上

_SliderState类的方法(表)

方法名参数返回类型描述
initState初始化状态,创建动画控制器和焦点节点
dispose清理资源,取消计时器,销毁动画控制器和焦点节点
_handleChangeddouble value处理滑块值改变的事件
_handleDragStartdouble value处理开始拖动滑块的事件
_handleDragEnddouble value处理结束拖动滑块的事件
_actionHandler_AdjustSliderIntent intent处理滑块的动作
_handleFocusHighlightChangedbool focused处理焦点高亮改变的事件
_handleHoverChangedbool hovering处理鼠标悬停状态改变的事件
_lerpdouble valuedouble将值从 [0, 1] 映射到 [min, max]
_discretizedouble valuedouble将连续的值离散化
_convertdouble valuedouble将值从 [min, max] 映射到 [0, 1],并可能离散化
_unlerpdouble valuedouble将值从 [min, max] 映射到 [0, 1]
buildBuildContext contextWidget构建滑块的 widget
_buildMaterialSliderBuildContext contextWidget构建 Material 风格的滑块
_buildCupertinoSliderBuildContext contextWidget构建 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。

销毁组件分析

接着 initStatedispose 也算是思维常态,毕竟创建了相关的资源就需要销毁以防止内存泄漏。对应的是有状态组件状态类的 dispose 方法。这里具体说来就是 _SliderStatedispose 方法。其源代码为:


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 方法在其状态类中。在 滑块 组件中,具体说来就是 _SliderStatebuild 方法。其代码如下:


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,则在 AndroidFuchsiaLinuxWindows 平台上构建 Material 风格的滑块,在 iOSmacOS 平台上构建 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 风格滑块的构建,正如前文所述,将用于 iOSmacOS 平台。

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 组件被用于 iOSmacOS 平台时,实际上内部将自动使用 CupertinoSlider 组件实现。

相关文章:

Flutter笔记:滑块及其实现分析1

Flutter笔记 滑块分析1 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/134900784 本文从设计角度&#…...

【React Hooks】useReducer()

useReducer 的三个参数是可选的&#xff0c;默认就是initialState&#xff0c;如果在调用的时候传递第三个参数那么他就会改变为你传递的参数&#xff0c;实际开发不建议这样写。会增加代码的不可读性。 使用方法&#xff1a; 必须将 useReducer 的第一个参数&#xff08;函数…...

如何把kubernetes pod中的文件拷贝到宿主机上或者把宿主机上文件拷贝到kubernetes pod中

1. 创建一个 Kubernetes Pod 首先&#xff0c;下面是一个示例Pod的定义文件&#xff08;pod.yaml&#xff09;&#xff1a; cat > nginx.yaml << EOF apiVersion: v1 kind: Pod metadata:name: my-nginx spec:containers:- name: nginximage: nginx EOF kubectl app…...

Android 13 - Media框架(20)- ACodec(二)

这一节开始我们就来学习 ACodec 的实现 1、创建 ACodec ACodec 是在 MediaCodec 中创建的&#xff0c;这里先贴出创建部分的代码&#xff1a; mCodec mGetCodecBase(name, owner);if (mCodec NULL) {ALOGE("Getting codec base with name %s (owner%s) failed", n…...

TCP单聊和UDP群聊

TCP协议单聊 服务端&#xff1a; import java.awt.BorderLayout; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.V…...

智能优化算法应用:基于鲸鱼算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于鲸鱼算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于鲸鱼算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.鲸鱼算法4.实验参数设定5.算法结果6.参考文献7.MA…...

TortoiseGit 小乌龟svn客户端软件查看仓库地址

进入代码路径...

uniapp微信小程序分包,小程序分包

前言&#xff0c;都知道我是一个后端开发、所以今天来写一下uniapp。 起因是美工给我的切图太大&#xff0c;微信小程序不让了&#xff0c;在网上找了一大堆分包的文章&#xff0c;我心思我照着写的啊&#xff0c;怎么就一直报错呢&#xff1f; 错误原因 tabBar的页面被我放在分…...

『Linux升级路』进度条小程序

一、预备知识 在编写『Linux升级路』进度条小程序之前&#xff0c;我们需要了解一些预备知识。本文将详细介绍缓冲区和回车换行的概念。 1.1 缓冲区 缓冲区是计算机内存中的一块区域&#xff0c;用于临时存储数据。在编程中&#xff0c;我们经常使用缓冲区来临时保存数据&am…...

使用rust slint开发桌面应用

安装QT5&#xff0c;过程省略 安装rust&#xff0c;过程省略 创建工程 cargo new slint_demo 在cargo.toml添加依赖 [dependencies] slint "1.1.1" [build-dependencies] slint-build "1.1.1" 创建build.rs fn main() {slint_build::compile(&quo…...

Flutter桌面应用程序定义系统托盘Tray

文章目录 概念实现方案1. tray_manager依赖库支持平台实现步骤 2. system_tray依赖库支持平台实现步骤 3. 两种方案对比4. 注意事项5. 话题拓展 概念 系统托盘&#xff1a;系统托盘是一种用户界面元素&#xff0c;通常出现在操作系统的任务栏或桌面顶部。它是一个水平的狭长区…...

docker:安装mysql以及最佳实践

文章目录 1、拉取镜像2、运行容器3、进入容器方式一方式二方式三容器进入后连接mysql和在宿主机连接mysql的区别 持久化数据持久化数据最佳实践 1、拉取镜像 docker pull mysql2、运行容器 docker run -d -p 3307:3306 --name mysql-container -e MYSQL_ROOT_PASSWORD123456 …...

uniapp实战 —— 自定义顶部导航栏

效果预览 下图中的红框区域 范例代码 src\pages.json 配置隐藏默认顶部导航栏 "navigationStyle": "custom", // 隐藏默认顶部导航src\pages\index\components\CustomNavbar.vue 封装自定义顶部导航栏的组件&#xff08;要点在于&#xff1a;获取屏幕边界…...

中国移动频段划分

1、900MHz&#xff08;Band8&#xff09;上行&#xff1a;889-904MHz&#xff0c;下行&#xff1a;934-949MHz&#xff0c;带宽共计15MHz&#xff0c;目前部署&#xff1a;2G/NB-IoT/4G 2、1800MHz&#xff08;Band3&#xff09;上行&#xff1a;1710-1735MHz&#xff0c;下行…...

《PySpark大数据分析实战》-01.关于数据

&#x1f4cb; 博主简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是wux_labs。&#x1f61c; 热衷于各种主流技术&#xff0c;热爱数据科学、机器学习、云计算、人工智能。 通过了TiDB数据库专员&#xff08;PCTA&#xff09;、TiDB数据库专家&#xff08;PCTP…...

Qt/C++视频监控拉流显示/各种rtsp/rtmp/http视频流/摄像头采集/视频监控回放/录像存储

一、前言 本视频播放组件陆陆续续写了6年多&#xff0c;一直在持续更新迭代&#xff0c;视频监控行业客户端软件开发首要需求就是拉流显示&#xff0c;比如给定一个rtsp视频流地址&#xff0c;你需要在软件上显示实时画面&#xff0c;其次就是录像保存&#xff0c;再次就是一些…...

Vue.js - 界面设计工具和UI组件库

ViewDesign ViewDesign是一款开源的在线设计工具&#xff0c;它主要提供了一种可视化的界面设计方法&#xff0c;可以帮助设计师和开发人员更高效地完成界面设计和开发工作。 ViewDesign的特点是支持在线协作&#xff0c;可以多人同时进行设计&#xff0c;提高了设计效率&…...

【贪心算法】 Opponents

这道题写伪代码就好了&#xff01; Description Arya has n opponents in the school. Each day he will fight with all opponents who are present this day. His opponents have some fighting plan that guarantees they will win, but implementing this plan requires pr…...

【git 相关操作】

git status - 查看当前状态 git add - 将文件添加到暂存区 git commit -m "msg" - 提交暂存区文件到本地仓库 git push origin master - 本地仓库文件推送到远程仓库 git merge - 合并分支 git clone - 从指定地址克隆项目 git log - 查看commit日志 git stash push …...

流媒体音视频/安防视频云平台/可视化监控平台EasyCVR无法启动且打印panic报错,是什么原因?

国标GB视频监控管理平台/视频集中存储/云存储EasyCVR能在复杂的网络环境中&#xff0c;将分散的各类视频资源进行统一汇聚、整合、集中管理&#xff0c;实现视频资源的鉴权管理、按需调阅、全网分发、智能分析等。AI智能大数据视频分析EasyCVR平台已经广泛应用在工地、工厂、园…...

H264之NALU结构详解

摘要&#xff1a;本文详细描述了AVC的NALU的码流结构&#xff0c;以及各个层面上NALU详细的构成。   关键字&#xff1a;AVC&#xff0c;NALU 1 NALU简介 NAL层即网络抽象层&#xff08;Network Abstraction Layer&#xff09;&#xff0c;是为了方便在网络上传输的一种抽象…...

快速整合EasyExcel实现Excel的上传下载

1.EasyExcel 2.Excel的上传&#xff08;读Excel&#xff09; 3.Excel的下载&#xff08;写Excel&#xff09; 4.结语 1.EasyExcel 首先&#xff0c;这里给出EasyExcel的官方文档&#xff1a;https://easyexcel.opensource.alibaba.com/ alibaba.com不用我多说了吧&#xff0c;大…...

MongoDB的条件操作符

本文主要介绍MongoDB的条件操作符。 目录 MongoDB条件操作符1.比较操作符2.逻辑操作符3.元素操作符4.数组操作符5.文本搜索操作符 MongoDB条件操作符 MongoDB的条件操作符主要分为比较操作符、逻辑操作符、元素操作符、数组操作符、文本搜索操作符等几种类型。 以下是这些操作…...

【Linux】探索Linux进程状态 | 僵尸进程 | 孤儿进程

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 目录 一、进程状态1.1运行状态1.2阻塞状态1.3挂起状态 二、具体L…...

大数据股票简单分析

目录标题 内容说明解题量化金融的含义量化交易策略 点击直接资料领取 内容 1解释量化金融的含义&#xff0c;调研并给出至少 5种量化交易的策略或方法 2.完成Tushare Pro 的安装、注册&#xff0c;获取自己的 Token&#xff0c;查阅网站内的接口讲解和示例; 3通过Python 编程完…...

从零开始搭建链上dex自动化价差套利程序(11)

风险控制 需要将仓位杠杆控制到3倍以内&#xff0c;由于dydx与apex没有获取仓位杠杆的接口&#xff0c;但是每次发送交易的数额可以决定&#xff0c;故而可以设置每次发送总仓位1.5倍杠杆的数额&#xff0c;然后设置一个变量保证每个方向上的交易不超过2次&#xff0c;即可保证…...

2023.12面试题汇总小结

文章目录 Java字节码都包括哪些内容Java双亲委派机制如何打破Java Memory Model是什么synchronized的锁优化是什么CountDownLatch、CyclicBarrier、Semaphore有啥区别&#xff0c;什么场景下使用MySQL MVCC原理MySQL RR隔离级别&#xff0c;会出现幻读吗MySQL的RR隔离级别下&am…...

Linux权限命令详解

Linux权限命令详解 文章目录 Linux权限命令详解一、什么是权限&#xff1f;二、权限的本质三、Linux中的用户四、linux中文件的权限4.1 文件访问者的分类&#xff08;人&#xff09;4.2 文件类型和访问权限&#xff08;事物属性&#xff09; 五、快速掌握修改权限的做法【第一种…...

【Android】Glide的简单使用(下)

文章目录 缓存设置内存缓存硬盘缓存自定义磁盘缓存行为图片请求优先级缩略图旋转图片Glide的回调:TargetsBaseTargetTarget注意事项设置具体尺寸的Target 调试及Debug获取异常信息 配置第三方网络库自定义缓存 缓存设置 GlideApp .with(context).load(gifUrl).asGif().error(…...

TCP对数据的拆分

应用程序的数据一般都比较大&#xff0c;因此TCP会按照网络包的大小对数据进行拆分。 当发送缓冲区中的数据超过MSS的长度&#xff0c;数据会被以MSS长度为单位进行拆分&#xff0c;拆分出来的数据块被放进单独的网路包中。 根据发送缓冲区中的数据拆分情况&#xff0c;当判断…...