前端学习踩坑(3)
前端学习踩坑(3)——一个小滑块的开发过程
需求
在React-Native Expo项目中,需要一个可以拖动的滑块。但是:
- 滑块的可拖动范围需要被约束
- 滑块的拖动按钮为图片,且图片需要随着滑块所在点位的不同发生变化
- 滑块的拖动过程中,有一定数量的吸附点位,当松手后滑块应该自动吸附到最近的点位上
初探(4hours)
看起来这么一个好看炫酷的组件,当我看到它的第一眼自然也是很喜欢的。
但是!!问题就在于开发是由我来负责的,这么一个小小的功能所要实现要涉及到的知识可不少啊。
早在设计评审之前,我就已经和设计的同学沟通了关于这个滑块的相关事宜。
看在他这么关心我的份上,开学当面感谢他的时候就少砍两刀吧
首先,我秉持着白嫖优先的原则,搜索了相关的RN开源组件。我发现了这么一个组件:
我兴奋的安装并上手测试了一下它,发现只需要一行import和一行<Slider>
就可以创建出一个优雅的滑动条,我十分开心,一度觉得稳了。但是就在我查阅它的README时,我发现事情似乎没那么简单。
react-native-slider
虽然支持对于滑动按钮图片的设置,但是关于如何更改图片的样式,文档中却只字未提。
我原以为是我理解的问题,似乎只是一个Image.propsType的理解问题,我又查阅了RN官方关于Image的文档,尝试在Image的source对象中添加对于图片大小的约束,发现并没有效果。
于是,我被迫使用了一种很不优雅的手段,我直接在Figma导出图片时,手动调整了图片的大小,就在我以为虽然很不优雅但是至少能解决问题的时候,我惊讶的发现:图片的大小没有发生变化。
我一脸懵逼啊,只能求助万能的Github,然后我看到了这个:
How to adjust the size of the thumb button · Issue #97 · callstack/react-native-slider (github.com)
原来早在3年前,就有人和我提出了一样的问题,看来至少不是我的水平问题(其实就是)。
看来这个组件的开发者自己都还没有解决这个问题啊!!!!
然后我看到了一个热心网友给出的解决方案:
我像是抓到了救命稻草一般,立刻去了react-native-vector-icons的仓库地址,照着README一顿安装 + 配置,然后我得到了以下问题:
寄!所以这条白嫖之路行不通了啊,谈判失败,开始攻坚。
上手开发(13hours)
在CSDN的帮助下,我了解到了开发这个功能需要的几个库,其中主要包括View, Image, Animated, PanResponder,都是RN的原生库,不用太担心多平台的兼容问题。
核心功能仍有以下三点:
- 滑块的可拖动范围需要被约束
- 滑块的拖动按钮为图片,且图片需要随着滑块所在点位的不同发生变化
- 滑块的拖动过程中,有一定数量的吸附点位,当松手后滑块应该自动吸附到最近的点位上
万幸,在RN官方的PanResponder文档中,就给出了和需求差不多的官方案例,下面只需要理解其中的原理就可以开始面向文档写代码了!
以下为官方案例:
1 | import React, { useRef } from "react"; |
看起来很简单的一个案例,但是对于我这种Animated和PanResponse无任何了解的菜鸡来说,还是花了相当多的时间的(3hours吧)。
1 | const pan = useRef(new Animated.ValueXY()).current; |
通过useRef Hook来新建一个用于存储二维动画value的变量pan。使用Ref而不使用state主要是为了设置state造成的re-render和闭包造成的变量无法正常修改。
然后我们来看creat方法中的三个属性:
1 | onPanResponderGrant: () => { |
这里我还是很好奇,虽然Animated.Value自带一个_value属性,但是ts检查总是报错,现在也还没弄懂为啥。
从这里开始引出对于Animated.Value的理解问题。Animated.Value中有两个很重要的属性,一个是offset,一个是value。而上面的代码正是对offset的设置。
关于这部分,我也和mentor讨论了很久,最后我借助坐标系和向量的概念来理解了这两个属性。
对于value的真实值,我们可以理解为在offset基础上 + pan的偏移量计算出的位置。
value的值对应的坐标系就是一个静止的恒定的基础坐标系。而offset的值则是上一次滑动后计算出来的value位置的坐标系。
因为只有在上一次偏移所在的坐标系上来和这次的手势做计算,计算出相对于初始坐标系的值才是正确的,否则就相当于每次滑动都是把滑块从初始坐标系的原点开始滑动。
1 | onPanResponderMove: Animated.event( |
中间的这段代码看起来使用的一个Animated.event方法,但是其实很好理解,可以理解为一种对于pan的解构赋值。这段代码等价于:
1 | onPanResponderMove: (evt, gestureState) => { |
而其中的gestureState.dx,gestureState.dy则表示在不同方向上的手势的偏移量。
最后,使用这段代码,使用偏移量和手势偏移量计算出相对于原始坐标系的值,保存在pan中,并清空偏移量,在开头将上次计算出的结果置为新的偏移量。
1 | onPanResponderRelease: () => { |
以上便是这个例子中最核心的逻辑,而我们也不难想到,如果要进行可滑动范围的约束,我们应该从onPanResponderMove方法下手,而控制最终的吸附,我们应该从onPanResponderRelease方法下手。
想法可行,实践开始!
在实际开发过程中,由于我的需求只需要一个方向的值,所以我直接使用了而不是Animated.ValueXY。
但是Animated.Value的计算操作很复杂,需要调用它给出的方法,并且没办法直接使用关系运算符比较大小,这对于范围的约束功能开发来说是很难接受的一点。
所以,我想到使用一个number类型的变量来保存pan的值,由它代替pan来进行运算和判断操作,并将计算好的值赋值给pan,将该变量记为point。
point会在每次release时加等于新的偏移量,确实和上文的pan的原理相同,但是由于数字的方便性,操作简单的多。
1 | point += gestureState.dx; |
如何在移动过程中判断手指是否超过了界限约束呢?在手指移动中,我们能获得到的相关数据都可以从gestureState获取。point为我们保存了上一次滑动结束的坐标,同样,这也是这次滑动开始的初始坐标,我们新建一个temp,让它继承point的值并加上随着滑动不断变化的gestureState.x。一旦temp大于了约束界限,不就说明滑块划出了既定的界限么?
1 | onPanResponderMove: (evt, gestureState) => { |
这下就简单了,我们只需要在判断到temp超过界限时,手动设置pan的值让滑块停止滑动就好了嘛~
但是事实并不是这样子,这段代码中有一个很重要的问题,当时的我花费了相当多的时间和精力才找出问题。
这个offset一旦设置,你随后使用setValue方法时,也会在offset的基础上进行计算,也就是说你不能直接更改最后计算出的相对于原始坐标的值。所以,我们应该在setValue的基础上,减去offset来抵消setOffset对我们想设置原始值的影响,最后代码如下:
1 | onPanResponderMove: (evt, gestureState) => { |
到此为止,约束滑块滑动范围的任务就完成了。
让我们进入onPanResponderRelease来看看如何实现松手后滑块的吸附功能。
首先,为了实现上文提到的用point来保存pan的值,我们需要在point的基础上加等于gestureState.dx。但是还不仅仅是这样,由于move中的约束,并不是所有的dx都可以被加上,最终的结果只能在约束范围之内,所以完整代码如下:
1 | onPanResponderRelease: (evt, gestureState) => { |
然后,通过一个简单的数学计算,算出什么位置该吸附在哪个点位,并使用Animated中的spring弹性动画来控制吸附,让它变得更加丝滑,完整代码如下:
1 | onPanResponderRelease: (evt, gestureState) => { |
如果说能够很好的解决Animated.Value中的_value报错问题,似乎可以直接使用_value来代替point(错误不影响编译和运行,只存在于IDE中。