在上篇文章中,我们介绍了 ReactNative 的基础布局,动画相关内容。除此之外,我们还需要和元素有交互才可以。我们看到在 ReactNative 中,有些组件是自带交互事件的,比如 button
, Text
就有 onPress
事件。但是我们在普通的 View
上面就没有对应的手势事件。且,向前端那些DOM
的手势事件 click
等都不存在的。为此,ReactNative 专门提供了几个直接相应处理事件的组件:TouchableHighlight,TouchableNativeFeedback,TouchableOpacity和TouchableWidthoutFeedback。
Touchable* 组件
ReactNative 为我们提供了几个常用的触控组件:
|
|
他们的功能和使用方法基本类似,只是在Touch的时候反馈的效果不同。一般来说,你可以使用TouchableHighlight来制作按钮或者链接。注意此组件的背景会在用户手指按下时变暗。在 Android 上还可以使用TouchableNativeFeedback,它会在用户手指按下时形成类似墨水涟漪的视觉效果。ouchableOpacity会在用户手指按下时降低按钮的透明度,而不会改变背景的颜色。如果你想在处理点击事件的同时不显示任何视觉反馈,则需要使用TouchableWithoutFeedback。
触摸组件响应事件回调如下:
- onPress:类似前端的 click 事件
- onPressIn:类似前端的 touchstart 事件
- onPressOut:类似前端的 touchend 事件
- onLongPress:长按事件,onPress -> onLongPress
这很前端了,嘿嘿。然而我们并不满足,我就是想在 View
等其他组件上绑定事件呢。
手势相应系统
在不支持事件处理的组件上添加事件,我们要完成对 Responder
的申请和释放(在 ReactNative 持有Responder
的才可以响应事件,且全局只有一个 Responder
)。大致流程如下:
申请成为触摸事件响应者 -> 成为触摸事件响应者 -> 处理触摸事件 -> 释放触摸事件 -> 触摸事件结束
申请
首先我们要成为申请者,RN 提供了两个申请阶段:
|
|
响应
1.手势操作开始,可以理解为 touchstart
:
|
|
其中 event.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 - 当前在屏幕上的有效触摸点的数量
2.手势移动,类似 touchmove
:
|
|
3.手势结束,类似 touchend
:
|
|
4.另一个组件成为手势响应者,当前手势被取消
|
|
基本流程如下:
触摸事件拦截
在前端开发汇总,事件模型分为冒泡和捕获两种。在 RN 中,事件响应类似冒泡,先从子组件开始。向上查找,找到第一个成为响应者的组件为止。如下,如果 A,B,C 都成为了事件响应者,那么只有 C 会触发。
但有的时候,我们就想让组件 A 能响应呢?这就需要一种劫持机制,RN 提供了一个劫持机制,也就是在触摸事件往下传递的时候,先询问父组件是否需要劫持,不给子组件传递事件。
在触摸事件开始(touchDown)的时候,RN 容器组件会回调此函数,询问组件是否要劫持事件响应者设置,自己接收事件处理,如果返回 true,表示需要劫持
|
|
此函数类似,不过是在触摸移动事件(touchMove)询问容器组件是否劫持c
|
|
简单的是示意图如下:
其他的还有:View.props.onResponderTerminationRequest: (evt) => true
- 有其他组件请求接替响应者,当前的View是否“放权”?返回true的话则释放响应者权力。
View.props.onResponderTerminate: (evt) => {}
- 响应者权力已经交出。这可能是由于其他View通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如iOS上的控制中心或是通知中心)。
(注意:这里介绍的都是 PanResponder
,这也是我们常用的,还有原始 Redponder
,本质上就是现在所有方法去掉 Pan
关键字,同时删去 gestureState
回调参数)
pointerEvents 属性
用于控制当前视图是否可以作为触控事件的目标。
- auto:视图可以作为触控事件的目标。
- none:视图不能作为触控事件的目标。
- box-none:视图自身不能作为触控事件的目标,但其子视图可以。类似于你在 CSS 中这样设置:
|
|
- ‘box-only’:视图自身可以作为触控事件的目标,但其子视图不能。类似于你在 CSS 中这样设置:
|
|
简单的应用
有了这些我们就可以建立一个简单的手势封装了,参考另一篇文章 移动端手势交互详解与实现
试着封装一个手势组件实现 tap
,press
, doubletap
(做成高阶组件也行)。
简要代码如下:
|
|