前言:
这是我参与8月更文挑战的第 3 天,活动详情查看:8月更文挑战。为应掘金的八月更文挑战
,我准备在本月挑选 31
个以前没有介绍过的组件,进行全面分析和属性介绍。这些文章将来会作为 Flutter 组件集录
的重要素材。希望可以坚持下去,你的支持将是我最大的动力~
| 本系列 | 组件文章 | 列表 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | 1.NotificationListener | 2.Dismissible | 3.Switch | | 4.Scrollbar | 5.ClipPath | 6.CupertinoActivityIndicator | | 7.Opacity | 8.FadeTransition | 9. AnimatedOpacity | | 10. FadeInImage | 11. Offstage | 12. TickerMode | | 13. Visibility | 14. Padding | 15. AnimatedContainer | | 16.CircleAvatar | 17.PhysicalShape | 18.Divider | | 19.Flexible、Expanded 和 Spacer | 20.Card | |
一、 Switch 组件使用详解
可能有人会觉得 Switch
组件非常简单,有什么好说的呢?其实 Switch
组件源码洋洋洒洒 近千行
,其中关于主题处理
、平台适配
、事件处理
、动画处理
、绘制处理
都有值得我们学习的地方。那么废话不多说,来一起看看 Switch
是怎么炼成的吧。
1. Switch 最简使用:value
与 onChanged
Switch
组件的使用中注意:该组件是 StatelessWidget
,表示本身并不维护 开关状态
。这也就意味着,我把只能通过 重新构建 Switch组件
来切换 开关状态
。在构建 Switch
时必须传入 value
和 onChanged
两个参数,其中 value
表示 Switch
开关的状态,onChanged
是状态变化回调函数。
如下,在 _SwitchDemoState
中定义状态 _value
用于表示 Switch
开关的状态,在 _onChanged
回调中改变状态值,并 重新构建 Switch
组件,这样就能达到点击进行开关
的效果。
```dart class SwitchDemo extends StatefulWidget { const SwitchDemo({Key? key}) : super(key: key);
@override _SwitchDemoState createState() => _SwitchDemoState(); }
class _SwitchDemoState extends State { bool _value = false;
@override Widget build(BuildContext context) { return Switch( value: _value, onChanged: _onChanged, ); }
void _onChanged(bool value) { setState(() { _value = value; }); } } ```
其实这里可能很让人疑惑 Switch
为什么不自己维护 开关状态
,要将改状态交由外界指定
呢?既然 Switch
是 StatelessWidget
,为什么可以执行滑动的动画?还有 onChanged
方法又是何时触发的?带着这些问题我们来逐渐去认识这个属性而陌生的 Switch
组件。
2. Switch 的四个主要颜色
从 Switch
的构造方法中可以看出,其中定义了非常多的颜色相关属性。
先看前四个颜色属性:
inactiveThumbColor
代表关闭时圆圈
的颜色。inactiveTrackColor
代表关闭时滑槽
的颜色。
activeColor
代表打开时圆圈
的颜色。inactiveTrackColor
代表打开时滑槽
的颜色。
dart Switch( activeColor: Colors.blue, activeTrackColor: Colors.green, inactiveThumbColor: Colors.orange, inactiveTrackColor: Colors.pinkAccent, value: _value, onChanged: _onChanged, );
3. hoverColor 、 mouseCursor 和 splashRadius
前两个属性一般只能在桌面或web 端起作用,hoverColor
顾名思义是鼠标悬浮时,外层的大圈颜色,splashRadius
表示大圈的半径,如果不想要外圈的悬浮效果,可以将半径设为 0 。另外, mouseCursor
代表鼠标的样式,比如下面的小拳头是 SystemMouseCursors.grabbing
。
dart Switch( activeColor: Colors.blue, activeTrackColor: Colors.green, inactiveThumbColor: Colors.orange, inactiveTrackColor: Colors.pinkAccent, hoverColor: Colors.blue.withOpacity(0.2), mouseCursor: SystemMouseCursors.grabbing, value: _value, onChanged: _onChanged, );
mouseCursor
属性的类型为 MouseCursor
,其中 SystemMouseCursors
中定义了非常多的鼠标指针类型以供使用。下面给出几个效果:
| contextMenu | copy | forbidden | text | | ----------- | ---- | --------- | ---- | | |
|
|
|
5. 指定图片
通过 activeThumbImage
和 inactiveThumbImage
可以指定小圆中开启/关闭
时的图片。另外 onActiveThumbImageError
和 onInactiveThumbImageError
两个回调用于图片加载错误的监听。
当小圆同时指定 图片
和 颜色
属性时,会显示 图片
。
dart Switch( activeColor: Colors.blue, activeThumbImage: AssetImage('assets/images/icon_head.png'), inactiveThumbImage: AssetImage('assets/images/icon_8.jpg'), activeTrackColor: Colors.green, inactiveThumbColor: Colors.orange, inactiveTrackColor: Colors.pinkAccent, hoverColor: Colors.blue.withOpacity(0.2), mouseCursor: SystemMouseCursors.move, splashRadius: 15, value: _value, onChanged: _onChanged, );
6.主题相关属性: thumbColor 和 trackColor
一些具有交互性的 Material
组件会通过有 MaterialState
枚举定义交互行为,有如下 7
个元素。
dart enum MaterialState { hovered, focused, pressed, dragged, selected, disabled, error, }
可以看出这两个成员都是 MaterialStateProperty
类型,那这种类型的对象如何创建,又有什么特点呢?
dart ---->[Switch 成员声明]---- final MaterialStateProperty<Color?>? thumbColor; final MaterialStateProperty<Color?>? trackColor;
简单来说通过 MaterialStateProperty.resolveWith
方法,传入一个函数返回对应泛型数据。如下回调函数为 getThumbColor
,回调参数为 Set<MaterialState>
。也仅仅说,会根据 MaterialState
集合,来返回泛型数据。从 thumbColor
属性源码注释中可以看出,Switch
有如下四种 MaterialState
。
在 getThumbColor
中根据 states
的情况,分别对几种状态返回不同颜色,这样 Switch
在不同的状态下,就会自动使用对应颜色。比如下面的 onChanged: null
代表 Switch
不可用,在 getThumbColor
中当为 disabled
,会返回红色。
thumbColor
代表小圆颜色,trackColor
代表滑槽
颜色,使用方式是一样的。这里可能有人会问:有三个属性可以设置小圆,那它们同时存在,优先级怎么样?结果测试发现,inactiveThumbImage
会优先显示,优先级如下:
dart inactiveThumbImage > thumbColor > inactiveThumbColor > 默认 Switch 主题
上面提到了 默认 Switch 主题
,这里就来说一下 SwitchTheme
,它是一个 InheritedWidget
,维护 SwitchThemeData
类型数据,具体内容如下:
我们可以通过在上层嵌套 SwitchTheme
来为子树中的 Switch
指定默认样式,由于 MaterialApp
内部继承了 SwitchTheme
组件,我们可以在 theme
中指定 Switch
的主题样式。这样在指定 Switch
的相关颜色属性,就会使用默认的主题样式:
7. Switch 的焦点: focusColor 与 autofocus
Switch
组件是拥有焦点的,焦点相关的处理被封装在组件内部。focusColor
表示聚焦时的颜色,可被聚焦的组件有个特点:在桌面或 web 平台中可以通过 Tab
键,切换焦点。如下是六个 Switch
通过 Tab
键切换焦点的效果:
dart @override Widget build(BuildContext context) { return Wrap( children: List.generate(6, (index) => Switch( value: _value, focusColor: Colors.blue.withOpacity(0.1), onChanged: _onChanged, )) ); }
8. Switch 的尺寸相关: materialTapTargetSize
MaterialTapTargetSize
是一个枚举类型,有两个元素。该属性可以影响 Switch
的大小,如下分布是 padded
和 shrinkWrap
的效果。通过调试可知,默认是 padded
。下面在源码分析中会详细介绍该属性的作用。
dart enum MaterialTapTargetSize { padded, shrinkWrap, }
二、 挖掘 Switch 源码中的一些细节
1. 类型 _SwitchType
Switch
类中有一个 _SwitchType
类型成员,该成员完全被封装在 Switch
内部,我们是无法直接操作的。 _SwitchType
是只有两个元素
的枚举类。
```dart enum _SwitchType { material, adaptive }
---->[Switch 成员声明]---- final _SwitchType _switchType; ```
既然是成员变量,必然会在类内部被初始化,一般来说对 成员变量
初始化的地方在 构造方法
中。如下,Switch 的普通构造
中,会将 _switchType
设为 _SwitchType.material
。
一般来说,枚举对象就是为了分类处理,在 Switch#build
方法中,会根据 _switchType
的值进行不同的构建逻辑
,如果是 material
,则所有的平台都使用Material
风格的 Switch
。 如果是 adaptive
会根据平台的不同,使用不同的风格的 Switch
。在 android
、 fuchsia
、 linux
、 windows
中会使用 Material
风格;在 iOS
和 macOS
中会使用 Cupertino
风格。
到这里,可能有人会问, _SwitchType
成员完全被封装在 Switch
内部,那如何设置 adaptive
类型呢?仔细查看源码可以看出 Switch
还有一个 adaptive
构造,此处会将 _switchType
设为 _SwitchType.adaptive
。
2. 两种风格的 Switch 构建
_buildCupertinoSwitch
是当模式为 adaptive
时,用于构建 iOS
和 macOS
平台 Switch
组件构建,可以看出其内部是通过 CupertinoSwitch
进行构建,效果如下:
_buildMaterialSwitch
用于构建 Material
风格的 Switch
组件构建,可见其内部通过 _MaterialSwitch
组件进行构建。到这里我们就可以回答:既然 Switch
是 StatelessWidget
,为什么可以执行滑动的动画?因为 _MaterialSwitch
组件是 StatefulWidget
,它可以在内部改变组件状态。
3.Switch 尺寸的确定
从上面可以看出,两种风格的 Switch
都是通过 _getSwitchSize
获取 Size
尺寸的。如下代码中,可以看出,尺寸是通过 MaterialTapTargetSize
对象控制的。如果未指定 materialTapTargetSize
则会通过主题获取,调试可以看出,主题中 materialTapTargetSize
默认是 padded
。
dart Size _getSwitchSize(ThemeData theme) { final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize ?? theme.switchTheme.materialTapTargetSize ?? theme.materialTapTargetSize; switch (effectiveMaterialTapTargetSize) { case MaterialTapTargetSize.padded: return const Size(_kSwitchWidth, _kSwitchHeight); case MaterialTapTargetSize.shrinkWrap: return const Size(_kSwitchWidth, _kSwitchHeightCollapsed); } }
下面分别是 padded
和 shrinkWrap
的调试信息,可以很清楚地看出尺寸情况。
到这里 Switch
组件的源码就已经面面俱到了,我们可以发现,它作为一个 StatelessWidget
并不能做太多的事,只是定义了很多属性,并通过别的组件进行构建。也就是说,它本身起到平台差异的统筹、封装的作用,目的就是方便用户使用。
4. onChanged 方法触发的时机
通过调试可以发现,onChanged 方法
的触发是 ToggleableStateMixin#_handleTap
中触发的。如下是 buildToggleable
的源码,可以看出其中通过 GestureDetector
监听点击事件。
在 _MaterialSwitchState.build
方法中,可以看到其中通过 GestureDetector
监听了水平拖拽事件,这也是为什么 Switch
可以支持拖动的原因,同时 child
属性是 buildToggleable
,也就是上面的组件,支持点击事件。这是一个很好的多事件监听的案例。
5.动画的创建与触发
仔细看一下滑动的过程,可以看出其中有 位移动画
和 透明度渐变动画
。 首先来说一下动画的来源:
这些动画器都定义在 ToggleableStateMixin
中。而 _MaterialSwitchState
混入了 ToggleableStateMixin
。
和隐式动画一样, _MaterialSwitchState
中的动画触发也是通过重构组件,执行 didUpdateWidget
。如果你了解隐式动画,就不难理解 Switch
的动画触发机制。
最后,绘制是通过 _SwitchPainter
画出来的,这个画板是比较复杂的,这里就不展开了,有兴趣的可以自己研究一下。
Switch 组件
的使用方式到这里就完全介绍完毕,那本文到这里就结束了,谢谢观看,明天见~