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

举报
jcLee95 发表于 2023/12/09 23:38:32 2023/12/09
【摘要】 本文从设计角度,考虑滑块组件的使用场景,实现一个滑块组件应该包含的功能,介绍 Flutter 中滑块组件的用法,并分析 Slider 的实现源码。
Flutter笔记
滑块分析


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




Flutter 中,Slider 组件是一个非常灵活和强大的工具,它可以在各种不同的应用场景中帮助用户选择和调整值。

在实际的应用开发中,Slider 组件有着广泛的应用场景。例如,在音频或视频播放的应用中,Slider 组件常常被用于调整音量或者播放进度。用户可以通过拖动滑块来增加或减少音量,或者跳转到视频的特定位置。在这种情况下,Slider 的最小值通常设置为0(表示无声或视频开始),最大值设置为100(表示最大音量或视频结束)。

另外,在一些图形设计或绘图应用中,Slider组件可以用于选择颜色的不同阴影。用户可以通过拖动滑块来选择从纯黑到纯白的不同灰度。在这种情况下,Slider的最小值和最大值可以表示颜色的最深和最浅阴影。

再如,在一些日历或时间管理应用中,Slider组件可以用于选择时间或日期。例如,用户可以通过拖动滑块来选择一天中的特定时间或一年中的特定日期。在这种情况下,Slider 的 最小值(min) 和 最大值(max) 可以表示一天的开始和结束,或一年的开始和结束。


这一小节是写给像自己定义滑块组件的读者的——假设自己设计一个Slider组件,它可以具备什么样的功能呢?归纳起来,可以考虑实现下面这些方面的功能:

序号 功能 描述
1. 值选择 Slider 组件允许用户通过拖动滑块在 最小值(min)和 最大值(max)之间选择一个值。当前选中的值由 value 属性表示。
2. 离散和连续值 通过 divisions 属性,Slider 可以支持连续和离散的值。如果divisions为null,Slider 支持连续的值;如果 divisions 不为nullSlider 支持离散的值。
3. 颜色定制 Slider 的颜色可以通过 activeColorinactiveColorsecondaryActiveColorthumbColor  overlayColor 进行定制。
4. 交互方式 通过 allowedInteraction 属性,可以定义用户与 Slider 的交互方式。
5. 焦点管理 通过 autofocus  focusNodeSlider 可以管理焦点。如果autofocus  trueSlider 将在初始化时获取焦点。
6. 鼠标光标 通过 mouseCursor 属性,可以定义鼠标指针在悬停或进入 Slider 时的光标样式。
7. 事件回调 通过 onChangedonChangeStart  onChangeEndSlider 可以在用户拖动滑块选择新值时触发事件。
8. 标签显示 通过 label 属性,Slider 可以在滑块处于活动状态时显示标签。
9. 次要轨道值 通过 secondaryTrackValueSlider 可以显示次要轨道。

这一小节介绍 Flutter 内置的滑块 Slider 组件的用法。这个组件有两个构造函数,分别是 Slider  Slider.adaptive,它们都用来创建滑块,但是在行为上有一些不同:

  • Slider 构造函数创建的是一个 Material Design 滑块,它在所有平台上的外观和行为都是一致的;
  • Slider.adaptive 构造函数创建的滑块会根据当前平台自适应其外观和行为。例如,当运行在 iOS 平台时,它会尽可能地模仿 iOS 风格的滑块。这使得你的应用可以更好地融入到用户的设备和操作系统中,提供更自然的用户体验。

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, // 如果此小部件将被选为初始焦点,则为True
  SliderInteraction? 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属性来自定义滑块的颜色。


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, // 如果此小部件将被选为初始焦点,则为True
  SliderInteraction? 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 构造函数基本一致。我们设置了滑块的 值范围离散划分的数量标签 以及各种颜色。同时,我们也定义了onChangedonChangeStart  onChangeEnd回调来响应用户的操作。


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

例如,如果 min  0.0max  10.0divisions  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;

  @override
  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 的值并刷新界面。


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

在这里插入图片描述

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

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


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 失去焦点。


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 的值并刷新界面。这样,用户在拖动滑块时,就可以看到滑块上方显示的当前值。


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 的值并刷新界面。

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


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

  @override
  State<SliderDemo> createState() => _SliderDemoState();
}

class _SliderDemoState extends State<SliderDemo> {
  // 定义一个状态变量,表示滑块的当前值
  double _currentValue = 50;

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

在这里插入图片描述

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


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

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

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


很显然,一个滑块中是必然存在状态的,因此需要从 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 类中实现的:


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方法。这部分源代码如下:

@override
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.0
  enableController.value = widget.onChanged != null ? 1.0 : 0.0;
  // 将滑块的值转换为 [0, 1] 范围内的值,并设置给 positionController
  positionController.value = _convert(widget.value);
  // 创建动作映射
  _actionMap = <Type, Action<Intent>>{
    _AdjustSliderIntent: CallbackAction<_AdjustSliderIntent>(
      onInvoke: _actionHandler,
    ),
  };
  // 如果 widget 没有提供 focusNode,则创建一个新的 focusNode
  if (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 方法。其源代码为:

@Override
void dispose() {
  // 取消交互计时器
  interactionTimer?.cancel();
  // 销毁 overlay 的动画控制器
  overlayController.dispose();
  // 销毁值指示器的动画控制器
  valueIndicatorController.dispose();
  // 销毁启用/禁用滑块的动画控制器
  enableController.dispose();
  // 销毁滑块位置的动画控制器
  positionController.dispose();
  // 移除 overlayEntry
  overlayEntry?.remove();
  // 销毁 overlayEntry
  overlayEntry?.dispose();
  // 将 overlayEntry 设置为 null
  overlayEntry = null;
  // 销毁焦点节点
  _focusNode?.dispose();
  // 调用父类的 dispose 方法
  super.dispose();
}

没有什么太多可以说的,这段代码主要完成了资源的清理工作。包括取消交互计时器,销毁动画控制器,移除和销毁 overlayEntry,以及销毁焦点节点。这些操作都是为了避免内存泄漏,当滑块不再需要时,应该调用这个方法来释放资源。

build 方法分析

不论是有状态组件还是无状态组件都是通过 build 组件实现其 UI,对于一个有状态组件,build 方法在其状态类中。在 滑块 组件中,具体说来就是 _SliderState  build 方法。其代码如下:

@Override
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,则在 AndroidFuchsiaLinux  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 组件实现。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。