当前位置: 首页 > 科技观察

ReactNative触摸事件处理详解

时间:2023-03-12 03:52:58 科技观察

触摸是移动设备的核心功能,是移动应用交互的基础。Android和iOS各自都有完善的触摸事件处理机制。ReactNative(以下简称RN)提供了统一的处理方式,可以轻松处理界面中组件的触摸事件和用户手势。本文试图介绍RN中的触摸事件处理。1、RN基础触摸组件除了Text,RN的其他组件默认不支持点击事件,也不能响应基础触摸事件。所以RN提供了几个直接处理响应事件的组件,基本可以满足大部分的点击。处理要求TouchableHighlight、TouchableNativeFeedback、TouchableOpacity和TouchableWithoutFeedback。因为这些组件的功能和使用方法基本相似,只是Touch的反馈效果不同,所以我们一般使用Touchable**来代替。Touchable**有以下回调方法:onPressIn:点击开始;onPressOut:点击结束或离开;onPress:点击事件回调;onLongPress:长按事件回调。它们的基本使用方法如下,这里以TouchableHighlight为例:{()=>console.log("onPress")}onLongPress={()=>console.log("onLongPress")}>RN中提供的触摸组件使用起来非常简单,可以参考官方文档,这里就不详细介绍了。下面主要介绍用户触摸事件的处理。2.单组件触摸事件处理我们知道RN组件默认是不处理触摸事件的。一个组件要处理触摸事件,首先要“申请”成为触摸事件的响应者(Responder)。事件处理完成后,响应者的角色就会被释放。一个触摸事件处理周期是从用户用手指按下屏幕到用户抬起手指。这是一个完整的用户触摸操作。单个组件的单个操作交互处理的生命周期如下:我们来详细分析下事件处理的生命周期。在整个事件处理过程中,组件可能处于两种身份之一,并且可以相互切换:非事件响应者和事件响应者。非事件响应者默认情况下,触摸事件输入不会直接传递给组件,无法进行事件响应处理,即非事件响应者。如果组件要处理触摸事件,它必须首先申请成为事件响应者。组件有如下两个属性做这样的应用:View.props.onStartShouldSetResponder,这个属性接收一个回调函数,函数原型为function(evt):bool,当触摸事件开始(touchDown)时,RN会调用back这个函数,询问组件是否需要成为事件响应者,接收事件处理,如果返回true,说明需要成为响应者;view.props.onMoveShouldSetResponder,它和上一个的属性差不多,不过这个是触摸进行中(touchMove),RN询问组件是否要成为响应者,返回true表示是。如果组件通过上述方法返回true,则表示它发出了成为事件响应者的请求,想要接收后续的事件输入。因为同一时间只能有一个事件处理响应者,RN还需要协调所有组件的事件处理请求,所以并不是每个组件申请都能成功。RN通过以下两个回调将自己的申请结果通知组件:View.props.onResponderGrant:(evt)=>{}:表示申请成功,组件成为事件处理的响应者。此时组件开始接收后续的触摸事件输入。一般情况下,此时组件进入活动状态,进行一些事件处理或手势识别的初始化。view.props.onResponderReject:(evt)=>{}:表示应用失败,说明其他组件正在处理事件,它不想放弃事件处理,所以你的应用被拒绝,后续输入事件不会交付处理此组件。如果事件响应者通过以上步骤,组件申请成为事件响应者,后续的事件输入会通过回调函数通知给组件,如下:View.props.onResponderStart:(evt)=>{}:表示当手指按下时,成功申请了事件响应者的回调;View.props.onResponderMove:(evt)=>{}:表示触摸手指移动事件,这个回调可能会很频繁,所以这个回调函数的内容尽量简单;view.props.onResponderRelease:(evt)=>{}:表示触摸完成(touchUp)时的回调,说明用户已经完成了本次触摸交互,手势识别过程应该在这里完成。之后,该组件不再是事件响应者,该组件被停用。View.props.onResponderEnd:(evt)=>{}:表示组件结束事件响应的回调。从上图也可以看出,在一个组件成为事件响应者期间,其他组件也可能会申请触摸事件处理。这时候RN会通过回调询问你是否可以将responder角色释放给其他组件。回调如下:View.props.onResponderTerminationRequest:(evt)=>bool如果回调函数返回true,表示释放响应者角色,会回调下面的函数通知组件事件响应processinghasbeenterminated:View.props.onResponderTerminate:(evt)=>{}当系统直接终止组件的事件处理时也会出现这个回调,比如用户在触摸操作中突然调用。事件数据结构上面我们已经看到,触摸事件处理的回调有一个evt参数,其中包含一个触摸事件数据nativeEvent。nativeEvent的详细说明如下:identifier:触摸的ID,一般对应一个手指,多点触摸时用于区分是哪个手指触摸了该事件;locationX和locationY:触摸点相对于组件的位置;pageX和pageY:触摸点相对于屏幕的位置;timestamp:当前触摸事件的时间戳,可用于滑动计算;target:接收当前触摸事件的组件ID;changedTouches:evt数组,上次回调上报的触摸事件,升级之间所有事件的这个Array。因为在用户的触摸过程中会产生大量的事件,有时可能上报不及时,系统会采用这种方式分批上报;touches:evt数组,多点触摸时,包含当前所有触摸点的事件。在这些数据中,最常用的是locationX和locationY数据。需要注意的是,因为这是Native数据,所以它们的单位是实际像素。如果要转换为RN中的逻辑单元,可以使用如下方法:varpX=evt.nativeEvent.locationX/PixelRatio.get();3.嵌套组件事件处理上一节是针对单个组件的。事件处理的过程和机制。但是前面提到,当一个组件需要作为事件处理响应者时,需要通过onStartShouldSetResponder或者onMoveShouldSetResponder回调的返回值true来申请。如果嵌套多个组件时,这两个回调都返回true,但同一个只能有一个eventhandlerresponder,这种情况怎么处理?为了便于描述,假设我们的组件布局如下:在RN中,默认使用冒泡机制,响应最深的组件***开始响应,所以在上面描述的情况下,如图如图,如果A、B、C三个组件的on*ShouldSetResponder都返回Iftrue,那么只有C组件会得到响应,成为响应者。这种机制可以保证接口的所有组件都能得到响应。但在某些情况下,父组件可能需要处理事件,而子组件被禁止响应。RN提供了一种劫持机制,即touch事件向下传递时,先询问父组件是否需要劫持,不将事件传递给子组件,即下面两个回调:View.props.onStartShouldSetResponderCapture:该属性接收一个Callback函数,函数原型为function(evt):bool。当触摸事件开始(touchDown)时,RN容器组件会回调这个函数,询问组件是否劫持事件响应者设置,自己接收事件处理。如果返回true,表示需要劫持;View.props.onMoveShouldSetResponderCapture:这个功能类似,但是在触摸移动事件(touchMove)中询问容器组件是否劫持。这种劫持机制可以看作是一种下沉机制,对应于上面的冒泡机制,我们可以将RN事件处理流程总结如下图所示:注意,图中的*表示可以是Start也可以是Move,比如如on*ShouldSetResponderCapture表示onStartShouldSetResponderCapture或onMoveShouldSetResponderCapture,其他类似。当touch事件开始的时候,先调用组件A的onStartShouldSetResponderCapture,如果回调返回false,会按照图传给组件B,然后再调用组件B的onStartShouldSetResponderCapture,如果返回true,则事件不再被触发传给C组件,直接调用该组件的onResponderStart,B组件就成为事件响应者,后续事件直接传给它。其他分析类似。注意图中还有onTouchStart/onTouchStop回调。此回调不受响应者的影响。范围内的所有组件都会回调该函数,调用顺序是从最深的组件到最顶层的组件。4.手势识别上一节我们只介绍了简单的触摸事件处理机制及其使用方法。其实连续的触摸事件可以形成一些更高级的手势,比如我们最常见的滑屏内容,双指缩放(Pinch)或者旋转图片。这一切都是通过手势识别完成的。因为有些手势是非常常用的,所以RN还提供了内置的手势识别库PanResponder,封装了上面的事件回调函数,处理触摸事件数据,完成滑动手势识别,为我们提供了更高级更有意义的接口。如下:onMoveShouldSetPanResponder:(e,gestureState)=>boolonMoveShouldSetPanResponderCapture:(e,gestureState)=>boolonStartShouldSetPanResponder:(e,gestureState)=>boolonStartShouldSetPanResponderCapture:(e,gestureState)=>boolonPanResponderReject:(e>{...}onPanResponderGrant:(e,gestureState)=>{...}onPanResponderStart:(e,gestureState)=>{...}onPanResponderEnd:(e,gestureState)=>{...}onPanResponderRelease:(e,gestureState)=>{...}onPanResponderMove:(e,gestureState)=>{…}onPanResponderTerminate:(e,gestureState)=>{…}onPanResponderTerminationRequest:(e,gestureState)=>{…}onShouldBlockNativeResponder:(e,gestureState)=>bool可以看成你可以看到,这些接口基本对应了前面收到的基本回调,功能也差不多,这里不再赘述。有一个专门的回调onShouldBlockNativeResponder表示是否使用Native平台的事件处理,默认是关闭的,都是使用JS中的事件处理。需要注意的是,该功能目前只能在安卓平台上使用。不过这里的回调函数多了一个新的参数gestureState,就是滑动相关的数据,也就是对基础触摸数据的分析处理。其内容如下:stateID:滑动手势的ID,在一次完整的交互中保持不变;moveX和moveY:自上次回调以来,手势移动的距离;x0和y0:滑动手势识别开始时在屏幕上的坐标;dx和dy:手势开始到当前回调的移动距离;vx和vy:当前手势移动的速度;numberActiveTouches:当前时间段内手指触摸的次数。下面介绍一个简单的例子。在本例中,您可以用手指在界面上拖动圆形控件。使用示例如下:importReactfrom'react';import{AppRegistry,PanResponder,StyleSheet,View,processColor,}from'react-native';varCIRCLE_SIZE=80;varCIRCLE_COLOR='blue';varCIRCLE_HIGHLIGHT_COLOR='green';varPanResponderExample=React.createClass({statics:{title:'PanResponderSample',description:'ShowstheuseofPanRespondertoprovidebasicgesturehandling.',},_panResponder,0previous:{_previousTop:_previousTop:_circleStyles:{},circle:(null:?{setNativeProps(props:Object):void}),componentWillMount:function(){this._panResponder=PanResponder.create({onStartShouldSetPanResponder:(evt,gestureState)=>true,onMoveShouldSetPanResponder:(evt,gestureState)=>true,onPanResponderGrant:this._handlePanResponderGrant,onPanResponderMove:this._handlePanResponderMove,onPanResponderRelease:this._handlePanResponderEnd,onPanResponderTerminate:this._handlePanResponderEnd_ft;Tthis._handlePanResponderEnd_previous,})=84;this._circleStyles={style:{左:this._previousLeft,top:this._previousTop}};},componentDidMount:function(){this._updatePosition();},render:function(){return({this.circle=circle;}}style={styles.circle}{...this._panResponder.panHandlers}/>);},_highlight:function(){constcircle=this.circle;circle&&circle.setNativeProps({style:{backgroundColor:processColor(CIRCLE_HIGHLIGHT_COLOR)}});},_unHighlight:function(){constcircle=this.circle;circle&&circle.setNativeProps({style:{backgroundColor:processColor(CIRCLE_COLOR)}});},_updatePosition:function(){this.circle&&this.circle.setNativeProps(this._circleStyles);},_handlePanResponderGrant:function(e:Object,gestureState:Object){this._highlight();},_handlePanResponderMove:function(e:对象,手势状态:对象){this._circleStyles.style.left=this._previousLeft+gestureState.dx;this._circleStyles.style.top=this._previousTop+gestureState.dy;this._updatePosition();},_handlePanResponderEnd:function(e:Object,gestureState:Object){this._unHighlight();this._previousLeft+=gestureState.dx;this._previousTop+=gestureState.dy;},});varstyles=StyleSheet.create({circle:{宽度:CIRCLE_SIZE,高度:CIRCLE_SIZE,borderRadius:CIRCLE_SIZE/2,backgroundColor:CIRCLE_COLOR,位置:'absolute',left:0,top:0,},container:{flex:1,paddingTop:64,},});可以看到在componentWillMount中创建了一个PanResponder实例,并设置了相关属性,然后将这个对象设置到View属性中,如下:剩下的代码比较简单5.总结通过上面的介绍,我们可以看出RN提供了类似于Native平台的事件处理机制,因此也可以实现各种触摸事件处理,甚至是复杂的手势识别。在嵌套组件的事件处理上,RN提供了“冒泡”和“下沉”两个方向的事件处理,有点类似于AndroidNative上不久前支持的NestedScrolling,提供了更强大的事件处理机制。另外需要注意的是,由于RN的异步通信和执行机制,所以上面介绍的所有回调函数都是在JS线程中,而不是NativeUI线程中,Native平台的Touch事件都是在UI中线。因此,在JS中通过触摸或手势实现动画可能会造成延迟。