《如何制作一个媲美原生体验的选择器组件-卡晨》演讲视频+文字版

简介: 《如何制作一个媲美原生体验的选择器组件-卡晨》演讲视频+文字版
编者按:本文是蚂蚁集团 Ant Design Mobile 工程师卡晨在 SEE Conf 2022 的演讲内容,包括演讲视频及文字内容,欢迎享用。

hi 大家好,我是卡晨,来自蚂蚁集团。在最近的一年里我主要在做 antd-mobile 5.0 版本的开发,这是一个移动端组件库。今天,我们来聊一聊其中的一个组件——选择器 Picker,它的实现思路和技术选型。

这个小分享大概会包括以下几个部分:API 设计、手势和动画、性能、无障碍。


API 设计

让我们从 API 设计开始,先来定义一下 Picker 组件的属性。Picker 是一个数据录入型的组件,所以它需要 value onConfirm 这两个属性来和外界进行数据的交互。然后它还需要一个 columns 属性,用来配置 Picker 中每一列的选项列表。这里我们省略了一些属性,只保留了这几个最关键的。

需要注意的是,columns 属性是一个二维的数组,第一级对应的是选项列,第二级对应的是每一列中的选项列表。

定义好属性之后,我们来拆解一个这个组件的元素构成。首先呢,我们在这里先放一个 Popup 组件,这是 antd-mobile 中的一个基础布局组件,我们可以直接用它来作为弹层的容器。然后我们往里面填充一些内容:我们在头部放两个操作按钮;在主内容区,根据 columns 属性渲染出对应的选项列表,也就是“拨轮”。拨轮的本质就是一个长长的列表,我们会通过 CSS 的 transform: translate 让它进行上下的偏移,并且把溢出的部分隐藏起来,这样看起来就像是一个轮盘在被转动了。最后,我们来增加一个遮罩层,在拨轮的上半部分和下半部分都盖上一层渐变的白色遮罩,这样用户能就看清当前选中的是哪一项了。

由于拨轮这块的逻辑会比较复杂,所以我们可以把它独立拆一个组件出来。这样我们往下的讨论也可以更聚焦一些。

Wheel 只有三个属性:column 选项列表;value 当前选中的值;onSelect 当用户进行选择时会触发的事件。


⼿势 & 动画

接下来呢,我们为它增加一些交互。在用户对 Picker 进行操作时,有两种可能的方式。第一种是直接点击选项,另外一种是通过手势拖拽。为了更好的描述我们如何去实现手势和动画,这里我们把这个问题抽象成一个最简化的版本。这里有一个小方块,我们可以按住鼠标,把它拖拽起来,当我们松开鼠标的时候,小方块会落回原位。

那这里首先呢是需要实现手势的处理,手势的本质就是鼠标或者是手指在 x 轴、y 轴方向上的移动距离。在 React 的生态社区之中,有很多的手势库,我们选择了 use-gesture。我们可以使用它提供的 useDrag hook 来定义一个手势,当触发了拖拽事件时,我们将滑块的 y 轴位移 translateY 设置为当前手势的移动量。接下来,我们需要在拖拽结束的时候,让小球落回原位,这里就涉及到了动画。相信大家提到动画,首先都会想到 CSS transition,因为它实在是太简单直接了。通过这行 CSS 代码,我们可以让滑块的 transform 属性(也就是它的位移)自动的实现动画过渡。但是我们其实只希望它在回落的时候有这个动画效果,而在拖拽中滑块应该是立即跟随鼠标位置的。所以这里我们在手势中增加一个判断,如果用户还正在进行拖拽,那我们给它增加一个 grabbing 的 class name,从而禁用掉 transition。当手势结束时,我们移除掉 grabbing 这个 class name,并且将它的位置设置回原位。这样就回出现一个回落的动画效果。来看一下效果,似乎还是不错的。但是,我们需要再考虑一种边界情况。如果我们在这个滑块正在下落的过程中,再次点击了它,那么我们应该需要实现的是能够在半空中“接住”这个滑块,并且继续拖动它。而事实上现在的效果是,我们一点这个滑块,它就会重新从原点开始被拖拽。这是因为,对于 CSS 动画来说,我们很难获取到它的动画进行到了哪个状态。如果我们希望得到在鼠标按下时滑块当前的位置,那可能就需要通过一些比较特殊的方法来实现了,比如说 getBoundingRect。所以,CSS 动画在某些场景下的确简单好用,但是当我们需要对动画状态做很多的干预和处理的时候,我们很容易会遇到一些它的短板。所以呢,我们需要换一种方案,也就是基于 js 的动画。让我们以 react-spring 为例,它是一个有点像物理引擎的动画库。

首先,我们使用它提供的 useSpring 创建出一个弹簧动画,动画的初始位置是 y 等于 0。然后我们通过 animated.div 这样一个特殊的元素,把刚刚得到的动画量 y 和 slider 的位移量绑定在一起。接下来我们对手势的处理逻辑进行一下调整,当手势结束时,我们把 y 重置为 0。而如果手势还在拖拽中,我们把 y 立即更新为当前的手势偏移量。然后这里需要特别注意的是,我们在手势的配置中增加了一个 from 参数,它表示的是每一次手势开始的时候,初始的偏移量是多少。而大家可以看到,这里我们给它设置的初始偏移量是 y 这个动画变量的当前值。也就是说,我们会让手势从动画当前进行到位置继续,而不是强制它从原点开始。现在,我们就可以拥有完美的手势体验了。基于同样的思路,我们可以给 Picker 也增加上手势和动画效果,当然会更复杂一些,但是思路是相通的。说完手势和动画,我们再来考虑另一个问题。当我们在使用选择器时,很多个拨轮之间可能是有联动关系的。比如省市区的选择。但是就目前的数据结构而言,Picker 组件的多个拨轮之间是彼此独立的。所以我们需要能够在第一列变化时动态调整第二列的选项列表。OK,那我们来改一下 columns 属性,让它允许用户传入函数动态生成 columns。然后我们基于 Picker 的这个能力,封装出一个 CascadePicker。

所以我们现在有了两个组件:一个通用的 Picker 和一个专用于级联场景的 CascadePicker。他们的属性几乎是完全一样的,但是 Picker 的选项是通过 columns 配置的,columns 是一个二维的数组。而 CascadePicker 的选项是通过 options 配置的。和 columns 不同的是,它是一个树形的结构,更准确的说,是多个树形节点组成的数组。

大家可以看到,这两个组件,有 16 个完全相同的属性,却只有一个不同的属性那为什么不把这两个组件合二为一呢?就像这样:

我们可以让 columns 属性同时支持两种类型的数据格式,然后在 Picker 组件中去做判断,看用户当前的数据是不是级联模式,然后用对应的逻辑去进行处理。听起来也不难实现。但是这种方案也存在着一些弊端:首先,左右两种数据结构看起来只是增加了一个可选的 children 属性,但是数据类型已经不一致了,左边是一个两级的数组,而右边是一个一级的数组。如果不特别注意,非常容易搞混和写错。第二点就是 columns 属性已经不再名副其实了,如果说 columns 是一个数组,代表的是很多列选项,那这个数组其中的每一个元素应该是一列选项才对,就像左边这样,第一列是省,第二列是市。而对于右边这种级联的数据结构,数组中的第一个元素是浙江节点,第二个元素是江苏节点,我们能说浙江是一列、江苏是另一列么?显然是不可以的。如果我们给这种结构的数据起个名字,那也应该叫做 tree、treeNodes 或者是 options。第三点,是这样做会导致很多逻辑被塞在 Picker 组件中,而过度的逻辑耦合可能会对组件的后续改动造成一些限制。假如说我们想给 Picker 组件增加一个新属性,那就要同时考虑级联和非级联的两种情况。而如果我们希望做一些内部实现上的优化,复杂的参数结构也可能会带来额外的成本。我们的原则是,可以选择重复,但不应该去选择错误的抽象。优秀的代码结构和 API 设计不应该以组件数量、属性数量来进行评判。如果我们不去谨慎地处理抽象,仅仅是依靠外在的“形似”来做聚合,那反而可能会导致代码越来越难以维护。基于类似的思路,我们还可以制作日期选择器组件。它最核心的部分也是这个 columns 的生成函数。


性能

我们来试一下:感觉还是很流畅的。不过,如果我们把 CPU 的性能做一些限制,让它变慢 6 倍,就会发现有一点点的卡顿,在 Chrome 的性能面板中,我们可以很明显的观察到有几帧丢失了。

那我们来排查一下,这里我们用到了 React dev tools 的 profiler 工具,我们对刚刚这个操作进行一下录制和分析,会发现 Wheel 组件被频繁的渲染了。如果我们把被渲染的组件高亮一下,就会发现,当滑动第三个拨轮的时候,可以看到五个拨轮全部都被重新渲染了,这显然是不符合预期的。解决方案自然是给 Wheel 加个 memo,但是这里直接套一层 memo 是不会有效果的,我们还需要做一些特殊的处理,包括调整 Wheel 的属性、自定义 props 的对比逻辑。因为今天时间有限,这些我们就不具体展开了,感兴趣的朋友可以在 GitHub 上阅读 antd-mobile 的源代码。OK,经过这波优化之后,我们再来测试一下效果,可以看到转动第三个拨轮,只会触发它自己的重新渲染。React Profiler 中也可以看到组件的渲染有明显的减少。丢帧的情况也不存在了。


⽆障碍

大家可能会觉得,现在这个 Picker 组件总该算是做的差不多了吧?很遗憾,并不是。在我们的身边,有很多的失明人士,根据 2018 年的统计,在中国,每 80 个人中就有一位视力残疾患者。他们没有办法清楚地看到屏幕上显示的文字和按钮,那对于他们来说,想要操作手机,就需要借助于屏幕阅读器。大家可以想象出来,一位盲人,该如何使用这个 Picker 组件吗?我们来直接试验一下:相信这个体验不用多说,正常人都没办法忍受,那,又该怎么优化呢?作为开发者,我们大多数人应该都对无障碍这件事情挺头大的,特别是对于 Picker 这种看起来交互就很复杂的组件。无障碍能力本身并不复杂,通过屏幕阅读器,我们能做的操作就是非常简单的上一项、下一项、点击一下当前项,这本身再简单不过了。但是我们的网页是在一个二维平面上呈现的,而屏幕阅读器却只能把它识别为一个一维的线性的信息结构,这之间的差异,才是导致无障碍能力实现困难的根源。我们尝试了很多办法,最终,选择了去为屏幕阅读器单独实现一套交互。

首先,我们让下面的这层拨轮被屏幕阅读器忽略掉,然后我们再往上面叠加一层内容,包括一段文字,表示当前这一列选择了哪个选项,以及两个操作按钮,用来选择上一项和选择下一项。最后,我们把上面这一层内容设置为完全透明,让我们来看一下实际效果。说了这么多,我们已经差不多要结束了,但在最后,我想替整个 antd-mobile 的社区,以及所有 antd-mobile v5 的使用者感谢参与过 v5 版本建设的同学,从 21 年 7 月推送到 GitHub 开始,到年底为止,在这短短半年的时间里,我们收到了来自 50 位社区同学的 325 个 PR , 460 个 commit 和 416 个 issue。不论是参与代码的编写,还是反馈问题或者是参与讨论,这些都对 antd-mobile 的建设都有着非常非常重要的意义,感谢大家对 antd-mobile 的支持。人们常说,“一个人可以走的更快,但一群人可以走的更远”,我想或许这也是对开源精神的最佳诠释吧。如果大家对 antd-mobile 中的 Picker 组件感兴趣的话,欢迎在 GitHub 上翻阅我们的代码仓库。谢谢大家。


相关文章
|
3月前
|
开发者 图形学 iOS开发
掌握Unity的跨平台部署与发布秘籍,让你的游戏作品在多个平台上大放异彩——从基础设置到高级优化,深入解析一站式游戏开发解决方案的每一个细节,带你领略高效发布流程的魅力所在
【8月更文挑战第31天】跨平台游戏开发是当今游戏产业的热点,尤其在移动设备普及的背景下更为重要。作为领先的游戏开发引擎,Unity以其卓越的跨平台支持能力脱颖而出,能够将游戏轻松部署至iOS、Android、PC、Mac、Web及游戏主机等多个平台。本文通过杂文形式探讨Unity在各平台的部署与发布策略,并提供具体实例,涵盖项目设置、性能优化、打包流程及发布前准备等关键环节,助力开发者充分利用Unity的强大功能,实现多平台游戏开发。
97 0
|
6月前
|
XML 前端开发 数据可视化
《CSS 简易速速上手小册》第4章:视觉美学(2024 最新版)
《CSS 简易速速上手小册》第4章:视觉美学(2024 最新版)
54 1
|
6月前
|
开发框架 编译器 定位技术
探索游戏开源世界:引擎与框架的宝库
本文介绍了多个开源游戏引擎和框架,如 Bevy(用 Rust 构建)、Mach、Axmol、Cocos、Open 3D Engine、KorGE、Tiled、OpenRA 和 Godot。这些工具降低了游戏开发的门槛,支持跨平台开发,并推动了游戏行业的发展。
|
前端开发 UED
「前端游戏开发体验」我用react实现网页游戏的全过程(包括规则设计)
用技术实现梦想,用梦想打开创意之门。游戏开发体验挺新奇的,我用react实现网页游戏的全过程(包括规则设计)。
765 1
|
编解码 算法 前端开发
《移动端的真实感渲染-慎思》演讲视频 + 文字版
《移动端的真实感渲染-慎思》演讲视频 + 文字版
153 0
|
移动开发 JavaScript
原生js H5适配它来了 保姆级教学
原生js H5适配它来了 保姆级教学
|
存储 前端开发 算法
1024程序节|完全开源、编程原生体验、轻量化,不来试试吗
1024程序节|完全开源、编程原生体验、轻量化,不来试试吗
186 0
1024程序节|完全开源、编程原生体验、轻量化,不来试试吗
|
Web App开发 移动开发 安全
开发移动端常见的问题集合
开发移动端常见的问题集合
1208 0
|
编解码 移动开发 测试技术
|
传感器 开发框架 数据可视化
有前端开发经验,3D可视化项目快速入门! 三维建模 js 全景 虚拟园区 ThingJS
3D可视化是新兴行业,可以考虑物联网3D可视化或者3D影视游戏,前景都很看好。
有前端开发经验,3D可视化项目快速入门!  三维建模 js 全景 虚拟园区 ThingJS