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

news2024/11/28 21:50:41
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, // 如果此小部件将被选为初始焦点,则为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属性来自定义滑块的颜色。

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

销毁组件分析

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


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 方法在其状态类中。在 滑块 组件中,具体说来就是 _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 组件实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1300159.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一个通用游戏后台的设计模式实践总结

搞业务开发的时候,发现有一些代码的开发会让人感觉非常简便舒服,有一些代码的开发却有时候会让人感觉心智负担比较大。 逐步总结的过程中,发现让开发人员写起来感觉舒服的代码,大概率是因为当前模块与其他模块代码耦合度低,开发人员无需花费过多的精力去关注其他模块的实现…

基于java swing 药品销售管理系统

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…

Integer和int相比较

Integer和int相比较 一、 Integer类 在Java中&#xff0c;”万物皆对象“&#xff0c;但是八种基本数据类型是个例外&#xff0c;出于性能等方面的考虑&#xff0c;八种基本数据类型没有类和对象的概念&#xff0c;相应的变量值直接在栈内存中存放。但这带来了一些问题&#…

根据对数器找规律、根据数据量猜题目解法

题目一 小虎去买苹果&#xff0c;商店只提供两种类型的塑料袋&#xff0c;每种类型都有任意数量。1&#xff09;能装下6个苹果的袋子2&#xff09;能装下8个苹果的袋子小虎可以自由使用两种袋子来装苹果&#xff0c;但是小虎有强迫症&#xff0c;他要求自己使用的袋子数量必须…

NAND闪存市场2023年Q3增长2.9%,Q4有望激增20%

TrendForce报告显示&#xff0c;NAND闪存市场在2023年第三季度出现了关键转折&#xff0c;主要由三星的战略性减产决定驱动。最初&#xff0c;市场对终端用户需求的不确定性以及对平淡旺季的担忧导致买家采取保守的方法&#xff0c;库存低、采购慢。然而&#xff0c;随着三星等…

Go压测工具

前言 在做Go的性能分析调研的时候也使用到了一些压测方面的工具&#xff0c;go本身也给我们提供了BenchMark性能测试用例&#xff0c;可以很好的去测试我们的单个程序性能&#xff0c;比如测试某个函数&#xff0c;另外还有第三方包go-wrk也可以帮助我们做http接口的性能压测&…

「Verilog学习笔记」多bit MUX同步器

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 输入数据暂存在data_reg中&#xff0c;使能信号data_en用打两拍的方式跨时钟域传输到时钟域B&#xff0c;最后data_out根据使能信号更新数据。data_en信号在A时钟域用一个D…

【QED】高昂的猫 Ⅰ

目录 题目背景题目描述输入格式输出格式 测试样例样例说明数据范围 思路核心代码 题目背景 这是小橘。因为它总是看起来很高傲&#xff0c;所以人送外号“高昂的猫”。 题目描述 "锕狗"的房间里放着 n n n ( 1 ≤ n ≤ 1 0 9 ) (1 \leq n \leq 10^9) (1≤n≤109)个…

忘记PDF密码了,怎么办?

PDF文件有两种密码&#xff0c;一个打开密码、一个限制编辑密码&#xff0c;因为PDF文件设置了密码&#xff0c;那么打开、编辑PDF文件就会受到限制。忘记了PDF密码该如何解密&#xff1f; PDF和office一样&#xff0c;可以对文件进行加密&#xff0c;但是没有提供恢复密码的功…

C++设计模式——Adapter(适配器)模式

一、Adapter模式是个啥&#xff1f; 适配器模式在软件开发的江湖里&#xff0c;可以说用途是非常广的。下面几个经典的场景来说明适配器模式的使用。 场景一 场景二 假如你正在开发一款股票市场监测程序&#xff0c; 它会从不同来源下载 XML 格式的股票数据&#xff0c; 然后…

USB host是如何区分reset、disconnect以及suspend?

如题。 reset和disconnect 在OTG2.0中&#xff0c;当其配置程host模式&#xff0c;当host发送reset时D/D-线都为低电平&#xff0c;这是USB协议侧的复位&#xff08;与上电复位不同&#xff0c;协议上的复位是为了对连接的dev进行枚举&#xff0c;例如对端点0进行set address…

分类预测 | SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类预测

分类预测 | SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类预测 目录 分类预测 | SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.SSA-HKELM-Adaboost麻雀算法优化混合核极限学习机的数据分类…

完整方案开放下载!详解中国移动《通信网络中量子计算应用研究报告》

8月30日&#xff0c;中国移动在第四届科技周暨战略性新兴产业共创发展大会上重磅发布了《通信网络中量子计算应用研究报告》。 玻色量子作为中国移动在光量子计算领域的唯一一家合作企业兼战投企业&#xff0c;在量子计算应用于通信行业达成了深入合作&#xff0c;并在5G天线多…

【代码随想录】算法训练计划41

dp 1、343. 整数拆分 题目&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 输入: n 10 输出: 36 解释: 10 3 3 4, 3 3 4 36。 思路…

电工--基本放大电路

电压放大倍数、输入电阻和输出电阻是放大电路的三个主要性能指标 共发射极基本交流放大电路 晶体管&#xff1a;电流放大作用。能量较小的输入信号通过晶体管的控制作用&#xff0c;去控制电源所共给的能量&#xff0c;以在输出端获得一个能量较大的信号 集电极电源电压&#…

接口测试 —— Jmeter读取数据库数据作测试参数

1、添加Jdbc Request 2、添加ForEach控制器(右键线程组->逻辑控制器->ForEach控制器) ①输入变量的前缀&#xff1a;mobilephone&#xff1b; 从jdbc request设置的变量得知&#xff0c;我们要取的值为mobilephone_1、mobilephone_2、mobilephone_3......所以这里输入m…

基于单片机智能病床呼叫系统设计

**单片机设计介绍&#xff0c;基于单片机智能病床呼叫系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的智能病床呼叫系统是一种利用单片机技术设计的医疗设备&#xff0c;它能够帮助病人在住院期间快速、方便…

侯捷C++ (二--STL标准库)

CSTL标准库与泛型编程 STL六大部件 容器 Containers分配器 Allocators 一种用来修饰容器或仿函数或迭代器接口的东西算法 Algorithms迭代器 Iterators适配器 Adapters仿函数 Functors 容器 前闭后开 大致分为两种容器&#xff1a;序列容器&#xff0c;关联容器 所谓关联容器…

安装 DevEco Studio 后不能用本地 Node.js 打开

安装 DevEco Studio 后第一次打开时&#xff0c;不能用本地 Node.js 打开 答&#xff1a;因为本地 Node.js 文件夹名字中有空格 Node.js路径只能包含字母、数字、“。”、“_”、“-”、“:”和“V” 解决方法&#xff1a; 1.修改文件夹名称 2.重新下载 注意&#xff1a;找一…

Verilog基础:$random系统函数的使用

相关阅读 Verilog基础​编辑https://blog.csdn.net/weixin_45791458/category_12263729.html $random系统函数语法的BNF范式如下所示&#xff0c;有关BNF范式相关内容&#xff0c;可以浏览以往文章Verilog基础&#xff1a;巴科斯范式(BNF)。 $random系统函数在每次调用时返回一…