React Native 浅入门 —— 交互篇

权当一个笔记,再写写或许更明白点

习惯了前端世界的交互模式之后(其实就是 DOM 事件),在这个入门的过程中感觉 React Native 的交互处理就是个不适应。

如果要做 React Native 的交互,首先至少要知道这样几个东西:

不过干读这几个文档的话,基本就是一头雾水……

还是一点一点来看罢:

普通行为

TouchableHighlight、TouchableOpacity、TouchableWithoutFeedback 这几个很好弄,官方贴心的直接封装了最基础的 Touch 行为,在任何需要点击的 View 外面直接包上这样的标签就行了。

TouchableHighlight 在点击时表现为高亮
TouchableOpacity 在点击时表现为透明
TouchableWithoutFeedback 在点击时无反馈

这几个效果都是封装好了的,无需开发者操心。

Sample Code

1
2
3
4
5
<TouchableOpacity
onPressIn={this._onPressInCircle.bind(this)}
onPressOut={this._onPressOutCircle.bind(this)}>
<View style={styles.gridItem}></View>
</TouchableOpacity>

然而这些只适用于按钮系……

支持事件:

  • onPress
  • onPressIn
  • onPressOut
  • onLongPress

支持参数:

  • delayLongPress {number} 单位ms
  • delayPressIn {number} 单位ms
  • delayPressOut {number} 单位ms

再特殊点的行为,例如划过,就不用想用这几个货直接实现了。

Gesture Responder System

中文翻译叫:手势应答系统。

主要就是搞手势识别处理的,那其实也就是复杂点的触摸:例如一边摸一遍动啊,摸着还动出花样画个 L 啥的的那种。

事件

决定是否成为处理器

冒泡的:

  • onStartShouldSetResponder touchStart/mouseDown行为发生,是否当前的元素成为处理器
  • onMoveShouldSetResponder touchMove/mouseMove行为发生,是否当前行为成为处理器

不冒泡/未来不冒泡的:

  • onScrollShouldSetResponder 滚动行为发生了,是否当前的元素成为处理器
  • onSelectionChangeShouldSetResponder 选择行为发生了,是否当前元素成为处理器

是否接管成为处理器(因为冒泡是从最深处开始,可以在父级的元素使用此类方法接管):

  • onStartShouldSetResponderCapture touchStart/mouseDown行为发生,是否当前的元素代替最深层的子元素成为处理器
  • onMoveShouldSetResponderCapture touchStart/mouseDown行为发生,是否当前的元素代替最深层的子元素成为处理器

开始处理了

  • onResponderStart 当前处理开始
  • onResponderGrant 现在正在响应触摸事件
  • onResponderMove 用户正移动他们的手指
  • onResponderEnd 当前处理结束
  • onResponderRelease 在触摸最后被引发,即touchUp

跟拦截相关的(当前应答器的身份)

  • onResponderReject 当前视图的应答器不是“我”了,并且还不释放让我来当。
  • onResponderTerminationRequest 其他的东西想成为应答器。应该释放应答吗?返回 true 就是允许释放
  • onResponderTerminate 应答器已经转交给别人担当了。可能在调用onResponderTerminationRequest 之后被其他视图获取,也可能是被操作系统在没有请求的情况下获取了(发生在 iOS 的 control center/notification center)

以上都是在ResponderEventPlugin.js里面实现的,我们直接使用视图 View 配置

行为生命周期

单个元素的行为生命周期

这个图画的我头晕啊……

几个特性

冒泡

之前说到,有两个东西是冒泡的:

  • onScrollShouldSetResponder
  • onSelectionChangeShouldSetResponder

然则默认是触发最深的那个元素,也就是子级元素,如果父级要拦截作为处理器,则需要处理:

  • onStartShouldSetResponderCapture
  • onMoveShouldSetResponderCapture

这两个事件的触发顺序是从父级开始的,所以如果父级设置了返回 true,则会执行父级的处理。

但是如果任一返回了 false,则依然使用子级元素作为处理器。

不过如果父级的 onStartShouldSetResponder 如果返回 false,干脆不会触发父级的验证,onStartShouldSetResponderCapture 返回 true 也没用,Move 也是同理。

拦截

  • onResponderReject 当前视图的应答器不是“我”了,并且还不释放让我来当。
  • onResponderTerminationRequest 其他的东西想成为应答器。应该释放应答吗?返回 true 就是允许释放
  • onResponderTerminate 应答器已经转交给别人担当了。可能在调用onResponderTerminationRequest 之后被其他视图获取,也可能是被操作系统在没有请求的情况下获取了(发生在 iOS 的 control center/notification center)

话说这个还没搞明白怎么用……

简单用法

直接写属性,作为 prop:

1
<View onResponderStart={(evt) => true} />

也可以使用…运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GridItem extends Component {
get touchProps() {
return {
onStartShouldSetResponder: (evt) => true,
onResponderGrant: (evt) => {
console.log('child');
console.log(evt);
},
onResponderTerminationRequest: (evt) => true
};
}
render() {
return (
<View {...this.touchProps}></View>
)
}
}

也可以使用 PanResponder(这个会在实际处理的事件前加个 Pan,输出时又会去掉,而且会增加一个参数 gestureState):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GridItem extends Component {
componentWillMount() {
this._panGesture = PanResponder.create({
onStartShouldSetResponder: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
console.log('child');
console.log(evt);
console.log(gestureState);
},
onPanResponderTerminationRequest: (evt, gestureState) => true
});
}
render() {
return (
<View {...this._panResponder.panHandlers}></View>
)
}
}

特殊行为

划走切换的效果

可以参考:http://www.terlici.com/2015/04/06/simle-slide-menu-react-native.html
就是使用 PanResponder + Animation做的。

这个回头我自己再搞个出来。

手势解锁

因为不想用 WebView 做,所以这里都是从纯 React Native 的角度去考虑的。

因为生命周期中,TouchIn 是起点,所以如果在外面按住了划过元素,元素是不会有反应的……

单纯的子级接管作为处理器然后释放也是没用的,如果同时设置 Capture,父级的优先级大……

那么是否可以这样呢?父级判断碰撞,然后释放处理权?但是拦截的判断只在最开始触发的时候能搞,所以似乎还是行不通的。而且都碰撞到了,如果能直接处理子元素不是更简便么?

没那么简单,需要看看这三个方法:

子元素的获取

使用 refs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<View style={styles.gridView} {...this._panResponder.panHandlers} >
<View style={styles.gridLine}>
<GridItem ref="item1" />
<GridItem ref="item2" />
<GridItem ref="item3" />
</View>
<View style={styles.gridLine}>
<GridItem ref="item4" />
<GridItem ref="item5" />
<GridItem ref="item6" />
</View>
<View style={styles.gridLine}>
<GridItem ref="item7" />
<GridItem ref="item8" />
<GridItem ref="item9" />
</View>
</View>

这样就可以直接通过 this.refs[name] 获取到子元素了。

获取子元素的位置

这是从 React Native 的 Issue 1374 拿到的方法:

1
2
3
4
5
6
7
8
var RCTUIManager = require('NativeModules').UIManager;
var view = this.refs[name];
var handle = React.findNodeHandle(view);
RCTUIManager.measure(handle, (x, y, width, height, pageX, pageY) => {
// x,y 似乎是当前container的坐标
// width, height 是宽高
// pageX, pageY 是在屏幕中的坐标(起始坐标)
})

所以,元素在屏幕中的范围是:pageX ~ pageX + width, pageY ~ pageY + height

至少是个简单的正方形,如果是其他形状例如圆形,可能还需要计算圆心和半径的大小。

获取当前 touch 的坐标

之前说过 PanResponder 会给事件方法增加一个参数 gestureState

一个 gestureState 对象有以下属性:

  • stateID:gestureState 的ID-在屏幕上保持至少一个触发动作的时间
  • moveX:最近动态触发的最新的屏幕坐标
  • x0:应答器横向的屏幕坐标
  • y0:应答器纵向的屏幕坐标
  • dx:触发开始后累积的横向动作距离
  • dy:触发开始后累积的纵向动作距离
  • vx:当前手势的横向速度
  • vy:当前手势的纵向速度
  • numberActiveTouch:屏幕上当前触发的数量

那么 touch 位置的坐标可以这么获得:[x0 + dx, y0 + dy]

当然也可以使用都有的 evt 参数:

changedTouches - Array of all touch events that have changed since the last event
identifier - The ID of the touch
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 screen
pageY - The Y position of the touch, relative to the screen
target - The node id of the element receiving the touch event
timestamp - A time identifier for the touch, useful for velocity calculation
touches - Array of all current touches on the screen

直接用[pageX, pageY] 就行了。

这样就可以进行简单的碰撞计算了,计算位置是否在某个子元素的范围内就行了。

实际上至此手势解锁的几个关键问题已经解决,正在写一个手势解锁的组件:k-react-native-swipe-unlock 玩耍。

To Be Continued.