前端学习踩坑(5)——更高级的手势需求
引言
经过上次与那个小滑块的斗智斗勇后,我对于Animated
和PanResponder
库的理解已经较为深刻了,所以在开发这次需求时,其实并没有花费太多时间和精力。并且现在这个需求还处于一个未完成的状态,目前只简单的整理一下手势检测的部分。
关于上次的小滑块和Animated
和PanResponder
的更多内容(更基础)可以参见下面链接:
前端学习踩坑(3) | LittleBlack
需求
其实这次并不算是设计给我出的难题,这应该算是一个最基本的照片裁剪功能。因为一些项目结构的特殊性,使得我不得不尝试自己从0开始写一个相关的功能。其中需求主要有以下几大难题:
- 中间透明外部渐变的框如何实现
- 图片显示和手势判断不在同一层(图片在底层,但是中间夹了框、按钮、导航栏等)
- 手势的实现(单指移动,双指缩放)
- 图片的裁剪(未实现)
实现
中间透明外部渐变的框如何实现
直接在scss中使用原生css中的渐变代码是没办法实现的,如果一定要用的话,可以使用RN的渐变库react-native-linear-gradient
。
react-native-linear-gradient/react-native-linear-gradient: A component for react-native (github.com)
考虑到学习成本和工期,我选择了直接将设计的图片导出,覆盖在上面。视图层逻辑如下:
按照z轴排序(从高到低)
- 导航栏 / 下方按钮 / 上方提示文字
- 手势检测层(使用绝对定位)
- 渐变展示框
- 图片(使用绝对定位)
图片显示和手势判断不在同一层
有了上文的视图层z轴排序,我们就可以开始着手编写代码了。View的编写和布局问题不再赘述。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| return ( <View style={Styles.photoPickerDiv}> <WCNavigation navigation={navigation} backName={"UserValuesInput"} header={"编辑头像"} style={{zIndex: 20}}/> <View style={{position: 'absolute', zIndex: 10, width: windowWidth}}> <Image source={require("../../assets/images/login/Subtract.png")} style={{height: windowHeight, width: windowWidth, resizeMode: 'cover'}}/> </View> <View style={Styles.photoPickerBlock}> <Text style={{marginTop: 40, zIndex: 20}}> 移动和缩放 </Text> <TouchableOpacity onPress={() => { }}> <Image source={require("../../assets/images/login/photoPickerOk.png")} style={{width: 100, height: 100}}/> </TouchableOpacity> </View> <View style={{flex: 1, justifyContent: 'center', alignItems:'center', width: '100%', height: "100%", position: 'absolute'}}> <Animated.View style={{zIndex: 50, position: 'absolute', width: "100%", height: "100%", transform: [{translateY: 100}]}} {...panResponder.panHandlers}/> <Animated.View style={{transform: [{translateX: pan.x}, {translateY: pan.y}, {scale: currentScale}], zIndex: 0, position: 'absolute'}}> <Image source={{uri: photoUrl}} style={{width: "100%", height: "100%"}}/> </Animated.View> </View>
</View> );
|
其中解决这个问题的核心代码片段如下:
1 2 3 4 5 6 7 8
| <View style={{flex: 1, justifyContent: 'center', alignItems:'center', width: '100%', height: "100%", position: 'absolute'}}> <Animated.View style={{zIndex: 50, position: 'absolute', width: "100%", height: "100%", transform: [{translateY: 100}]}} {...panResponder.panHandlers}/> <Animated.View style={{transform: [{translateX: pan.x}, {translateY: pan.y}, {scale: currentScale}], zIndex: 0, position: 'absolute'}}> <Image source={{uri: photoUrl}} style={{width: "100%", height: "100%"}}/> </Animated.View> </View>
|
通过设置zIndex
,将用于接受panResponder
的View层防止在较高的位置,但是它只负责接受,数据只会作用在下方的图片层。考虑到功能,主要是x和y方向的translate
和sccale
。
其余组件直接设置zIndex
调整即可。
手势的实现
这里贴出最核心的实现基础:
原生事件是指由以下字段组成的合成触摸事件:
nativeEvent
changedTouches
- 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
identifier
- 触摸点的 ID
locationX
- 触摸点相对于父元素的横坐标
locationY
- 触摸点相对于父元素的纵坐标
pageX
- 触摸点相对于根元素的横坐标
pageY
- 触摸点相对于根元素的纵坐标
target
- 触摸点所在的元素 ID
timestamp
- 触摸事件的时间戳,可用于移动速度的计算
touches
- 当前屏幕上的所有触摸点的集合
一个gestureState
对象有如下的字段:
stateID
- 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。
moveX
- 最近一次移动时的屏幕横坐标
moveY
- 最近一次移动时的屏幕纵坐标
x0
- 当响应器产生时的屏幕坐标
y0
- 当响应器产生时的屏幕坐标
dx
- 从触摸操作开始时的累计横向路程
dy
- 从触摸操作开始时的累计纵向路程
vx
- 当前的横向移动速度
vy
- 当前的纵向移动速度
numberActiveTouches
- 当前在屏幕上的有效触摸点的数量
摘自官方文档:PanResponder · React Native 中文网
其中我们要用到的主要有:
用于计算偏移:
gestureState.dx
横向偏移量
gestureState.dy
纵向偏移量
用于计算缩放:
evt.nativeEvent
当前触摸事件
evt.nativeEvent.changedTouches[1]
改变的触摸事件数组,1表示另一个触摸事件,0是自己。
偏移很简单,详见前端学习踩坑(3) | LittleBlack中对于官方案例的讲解,此处基本一致。
缩放就稍微费点功夫,我们要如何通过计算出缩放呢?
这就好比,两个指头在最外侧然后一起往内侧滑动,我们通过手指间当前的距离除以原来的距离,就可以算出缩放的量。
但是要考虑到这只是一个增量,并不是图片真实的scale
,所以我们应该在原先scale的基础上乘以我们算出的这个增量。
核心代码逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| let startLen = useRef(0).current;
let newLen = useRef(0).current;
let currentScale = useRef(new Animated.Value(1)).current;
const panResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: () => true, onPanResponderGrant: () => { pan.setOffset({ // @ts-ignore x: pan.x._value, // @ts-ignore y: pan.y._value, }); }, onPanResponderMove: (evt, gestureState) => { // 使用临时变量保存原先scale的值 // @ts-ignore let temp = currentScale._value; // 当检测到触控点数为1时,进行偏移探测 if (gestureState.numberActiveTouches === 1) { pan.x.setValue(gestureState.dx); pan.y.setValue(gestureState.dy); } // 当触控点数为2时,计算缩放 else if (gestureState.numberActiveTouches === 2) { // 触控事件2 const point1 = evt.nativeEvent.changedTouches[1]; // 触控事件1 const point2 = evt.nativeEvent; // 计算出新的长度 newLen = Math.sqrt((point1.locationX - point2.locationX) ** 2 + (point1.locationY - point2.locationY) ** 2); // 这个判定主要用于处理边界值,因为初始化的startLen为0,直接进行运算会出现问题,所以第一次触摸检测要跳过计 算,只用于给startLen赋值 if (startLen !== 0 && newLen !== 0) { // 更改当前scale currentScale.setValue(temp * newLen / startLen); console.log(currentScale); } // 保存这次计算的新长度用于下次计算(它就是下次的旧长度) startLen = newLen; } }, onPanResponderRelease: () => { pan.flattenOffset(); // 缩放结束后应该把旧长度置为0,否则下次缩放的第一次运算会使用原来的脏数据,造成与我们预期不一致的错误 startLen = 0; }, }) ).current;
|
目前这段代码能够较为正常的实现相关的动能,但是还是有些小小的问题。
由于人的手指抬起时不可能做到永远同时抬起,这就会导致缩放结束后,我们抬手时,会检测到触控点数为1的情况并进行偏移量,但是实际上我们只是在进行缩放,并不需要偏移,所以会有肉眼上可见的图片的小跳动。