移动端开发不可避免的一个问题就是交互逻辑,不论是(android/ios)原生还是(react-native)混合开发都有一套完整的交互。父组件与子组件之间的嵌套以及事件劫持,从手指在屏幕上按下的那一刻,到在屏幕上移动,再到手指松开,完成了这一系列的触控操作。对于ReactNative而言,我们大部分还是采用官方提供的组件(Text、TouchableXXX),使用的相应的Api去处理。但是,对于一些稍微复杂的交互方式,往往官方提供的不能满足我们的业务需求,那么我就需要定制自己的逻辑组件。
ReactNative中常见的可触控方法
Text
Text 中加入了 onPress={()=>{}} 方法,即点击文字就可以触发函数
Button
Button 中加入了 onPress={()=>{}} 方法,即点击按钮就可以触发函数
Touchable系列组件
TouchableHighlight
TouchableNativeFeedback
TouchableOpacity
TouchableWithoutFeedback
以上组件都支持 onPressIn 、 onPressOut 、 onPress 、 onLongPress 方法,使用方法如同 Text 和 Button 。
手势响应系统(基本)
响应的生命周期
一个View只要通过了俩个触摸事件的申请,就可以成为一个响应者。然后可以获取到触摸点的对应值和所在组件的相关信息。当滑动的时候,就可以得到滑动后的相关信息。当释放的时候,可以把本次滑动的信息更新到状态中或者释放状态更新这个数据。
在ReactNative的触摸事件处理过程中,从触发角度来看,基本上可以分为 可触发状态 和 不可触发状态 。那么在这个触发过程中,就存在俩种生命状态:既由触摸事件申请开始到触摸申请通过和触摸事件申请开始到触摸申请失败。
那么,这个就基本可以分为: 事件响应者 和 非事件响应者 。
在默认情况下,触摸事件是不会直接传递给组件,不能直接触发事件响应,也就是非事件响应者。如果组件在触发时,先进行了申请接收事件处理,并且成为了事件的响应者(相关的方法返回值为true),那就可以做相应的触发事件。
View.props.onStartShouldSetResponder ,这个属性接收一个回调函数,函数原型是 function(evt):bool ,在触摸开始的时候(TouchDown),ReactNative会先调用此函数,询问组件是否需要成为事件的响应者。
View.props.onMoveShouldSetResponder ,这个属性接收一个回调函数,函数原型同样是 function(evt):bool ,在触摸移动的时候(TouchMove),ReactNative会先调用此函数,询问组件是否需要成为事件的响应者。
我们查看通过 react-native 的 PanResponder 源码(节选)来简单分析一下:
/**
* ```
* onPanResponderMove: (event, gestureState) => {}
* ```
*
* A native event is a synthetic touch event with the following form:
*
* - `nativeEvent`
* + `changedTouches` - Array of all touch events that have changed since the last event
*+ 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
*+
* + `identifier` - The ID of the touch
*+ 触摸点的ID
*+
* + `locationX` - The X position of the touch, relative to the element
* + `locationY` - The Y position of the touch, relative to the element
*+ 触摸点相对于父元素的横坐标和纵坐标
*+
* + `pageX` - The X position of the touch, relative to the root element
* + `pageY` - The Y position of the touch, relative to the root element
*+ 触摸点相对于根元素的横坐标和纵坐标
*+
* + `target` - The node id of the element receiving the touch event
*+ 触摸点所在的元素ID
*+
* + `timestamp` - A time identifier for the touch, useful for velocity calculation
*+ 触摸事件的时间戳,可用于移动速度的计算
*+
* + `touches` - Array of all current touches on the screen
*+ 当前屏幕上的所有触摸点的集合
*+
*
* A `gestureState` object has the following:
*
* - `stateID` - ID of the gestureState- persisted as long as there at least
* one touch on screen
* - 滑动手势的 ID,在一次完整的交互中此 ID 保持不变;
*
* - `moveX` - the latest screen coordinates of the recently-moved touch
* - `moveY` - the latest screen coordinates of the recently-moved touch
* - 自上次回调,手势移动距离;
*
* - `x0` - the screen coordinates of the responder grant
* - `y0` - the screen coordinates of the responder grant
* - 滑动手势识别开始的时候的在屏幕中的坐标;
*
* - `dx` - accumulated distance of the gesture since the touch started
* - `dy` - accumulated distance of the gesture since the touch started
* - 从手势开始时,到当前回调是移动距离;
*
* - `vx` - current velocity of the gesture
* - `vy` - current velocity of the gesture
* - 当前手势移动的速度;
*
* - `numberActiveTouches` - Number of touches currently on screen
* - 当期触摸手指数量。
* ....
*/
create: function (config) {
const interactionState = {
handle: (null: ?number),
};
const gestureState = {
// Useful for debugging
stateID: Math.random(),
};
PanResponder._initializeGestureState(gestureState);
const panHandlers = {
onStartShouldSetResponder: function (e) {
return config.onStartShouldSetPanResponder === undefined ?
false :
config.onStartShouldSetPanResponder(e, gestureState);
},
onMoveShouldSetResponder: function (e) {
return config.onMoveShouldSetPanResponder === undefined ?
false :
config.onMoveShouldSetPanResponder(e, gestureState);
},
...
}
}
复制代码
可以看出,在触摸响应开始之前都是在询问是否成为事件的响应者(默认是返回false的)。只有通过了申请,然后才会执行后面的事件响应操作。
响应的流程和逻辑
对于响应的逻辑,其实上节贴出的源码中就很清晰明了了。但是,我觉得还是需要更加直观的表达出来。当然,还有一些嵌套的问题我们也需要考虑。
从出发响应的流程我们看出,触摸的流程其实还是很清晰明了的。那么,我们在处理相应的触发函数的时候,只需要在各个方法中做相应的处理就可以了。
上一段代码看看:
componentWillMount() {
this._panResponder = PanResponder.create({
// 0.触摸开始
// 1.是否相应触摸
onStartShouldSetPanResponder: (evt, gestureState) => {
return true;
},
// 2. 是否相应触摸移动
onMoveShouldSetPanResponder: (evt, gestureState) => {
return true;
},
// 1.1 触摸开始的相应
onPanResponderGrant: (evt, gestureState) => {
// fun:(evt,gestureState)=>{...}
this._highlight();
},
// 2.1 触摸移动中
onPanResponderMove: (evt, gestureState) => {
console.log(`gestureState.dx:${gestureState.dx} `);
console.log(`gestureState.dy:${gestureState.dy} `);
this.setState({
marginTop: this.lastY + gestureState.dy,
marginLeft: this.lastX + gestureState.dx
});
},
// 3. 触摸结束
onPanResponderRelease: (evt, gestureState) => {
// fun:(evt,gestureState)=>{...}
this._unhighlight();
this.lastX = this.state.marginLeft;
this.lastY = this.state.marginTop;
},
// 2.2 触摸被其他响应者打断
onPanResponderTerminate: (evt, gestureState) => {
// fun:(evt,gestureState)=>{...}
}
});
}
render(){
// 将申请的响应者传递进去
return(
{...this._panResponder.panHandlers}
)
}
复制代码
对于触发的生命周期,每一个返回的传值都是一样的。拿到的信息都是触控点和当前布局以及父布局(根节点)的相关信息,可以查看源码的解释。
拿到这些之后,set当前值或者做如何操作都可以按照不同的业务需求做不同的操作。
比如:
改变触发组件的背景颜色:可以在申请响应者方法( onStartShouldSetPanResponder )返回 true 之后,在 onPanResponderGrant:(evt,gestureState)=>{...} 中 setState({...}) 做相应的改变。
拖动过程中做相应的变换:可以在申请响应者移动方法( onMoveShouldSetPanResponder )返回 true 之后,在 onPanResponderMove:(evt,gestureState)=>{...} 中做相应数据的获取,转化、合成等操作,然后 setState({...}) 修改或者更新其值的变化。
拖动结束后释放:可以在释放的相关的方法( onPanResponderRelease:(evt,gestureState)=>{...} )内做释放后的组件值的加成、消减、递增等逻辑处理操作。
当多个组件嵌套的时候,容易出现父组件子组件都注册成响应者的情况。那么我们在触发这种情况下的多组件嵌套的时候,这些组件会怎么响应呢?
其实,在ReactNative中默认使用的冒泡机制,响应最深的组件最先开始响应。但是有些情况下,可能父组件需要处理事件,而禁止子组件响应。此时,就有一套劫持机制。当触摸事件往下传的时候,先询问父组件是否需要劫持,不给子组件传递事件。
View.props.onStartShouldSetResponderCapture ,这个属性接收一个回调函数,返回原型是 function(evt):bool ,在触摸事件开始(TouchDown)的时候,会先询问这个函数是否要劫持事件响应者设置,自己处理,当返回 ture 的时候,表示要劫持。
View.props.onMoveShouldSetResponderCapture ,这个属性接收一个回调函数,返回原型是 function(evt):bool ,在触摸移动事件开始(TouchMove)的时候,会询问这个函数是否要劫持这个事件响应者设置。返回 ture 的时候,表示要劫持。
一些总结:
其实事件触摸事件整体来说是比较简单的,我们这里只是简单的介绍一下各个方法的应用和注意的方面。在每一个稍微复杂的交互设计中,这个几个响应机制的控制和其他组建的配合就显得尤为重要(尤其像Animated.View、Animated.Image)。触发、移动、释放这几个的控制在不同的场景也有着不同的作用。我们这里就例举各种可能存在的场景。需要各位读者自己多做Demo自己去揣测和认真琢磨。
本文是浅析,介绍比较简单。可能存在诸多错误和不足之处,请各位大佬纰漏指出,多多批评。
感谢阅读!:pray: