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

简介: 《如何制作一个媲美原生体验的选择器组件-卡晨》演讲视频+文字版
编者按:本文是蚂蚁集团 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 上翻阅我们的代码仓库。谢谢大家。


相关文章
|
编解码 算法 前端开发
《移动端的真实感渲染-慎思》演讲视频 + 文字版
《移动端的真实感渲染-慎思》演讲视频 + 文字版
185 0
|
机器学习/深度学习 存储 人工智能
免费下载 | 极致酷黑!优酷“暗黑模式”的技术实现 | 开发者必读(131期)
《优酷 APP 全量支持“暗黑模式” ——设计与技术完整总结》现已发布!设计理念+技术架构+改造实践,全方位解读暗黑模式的技术实现。
1933 0
|
5月前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
533 0
|
编解码 移动开发 测试技术
|
6月前
|
开发者 图形学 API
从零起步,深度揭秘:运用Unity引擎及网络编程技术,一步步搭建属于你的实时多人在线对战游戏平台——详尽指南与实战代码解析,带你轻松掌握网络化游戏开发的核心要领与最佳实践路径
【8月更文挑战第31天】构建实时多人对战平台是技术与创意的结合。本文使用成熟的Unity游戏开发引擎,从零开始指导读者搭建简单的实时对战平台。内容涵盖网络架构设计、Unity网络API应用及客户端与服务器通信。首先,创建新项目并选择适合多人游戏的模板,使用推荐的网络传输层。接着,定义基本玩法,如2D多人射击游戏,创建角色预制件并添加Rigidbody2D组件。然后,引入网络身份组件以同步对象状态。通过示例代码展示玩家控制逻辑,包括移动和发射子弹功能。最后,设置服务器端逻辑,处理客户端连接和断开。本文帮助读者掌握构建Unity多人对战平台的核心知识,为进一步开发打下基础。
226 0
|
JSON 小程序 IDE
打造小程序版本公号和自托管的公号(3):为miniblog接入markdown和增强的一物一码
本文关键字:微信一物一码,一文一码,小程序码转为普通二维码
612 0
打造小程序版本公号和自托管的公号(3):为miniblog接入markdown和增强的一物一码
|
Web App开发 编解码 前端开发
墙裂科普:移动端适配的基础概念
近几年移动端设备数量增长迅速,导致各种设备屏幕尺寸不一,分辨率也是不尽相同。 作为一个前端开发者,就不得不面对移动端页面适配的问题,而适配的终极目标,就是让页面能在不同尺寸,不同分辨率的移动设备上,尽可能的实现合理(我对“合理”的认识是:在保证良好的用户浏览体验的前提下,尽可能的保留页面的完整性)展示。
3073 0
|
5月前
|
图形学 iOS开发 Android开发
从Unity开发到移动平台制胜攻略:全面解析iOS与Android应用发布流程,助你轻松掌握跨平台发布技巧,打造爆款手游不是梦——性能优化、广告集成与内购设置全包含
【8月更文挑战第31天】本书详细介绍了如何在Unity中设置项目以适应移动设备,涵盖性能优化、集成广告及内购功能等关键步骤。通过具体示例和代码片段,指导读者完成iOS和Android应用的打包与发布,确保应用顺利上线并获得成功。无论是性能调整还是平台特定的操作,本书均提供了全面的解决方案。
201 0

热门文章

最新文章