單組件觸摸事件處理
在React Native中,響應手勢的基本單位是responder,具體來說,就是最常見的View組件。任何的View組件,都是潛在的responder,如果某個View組件沒有響應手勢操作,那是因爲它還沒有被“開發”。
將一個普通的View組件開發成爲一個能響應手勢操作的responder,非常簡單,只需要按照React Native的gesture responder system的規範,在props上設置幾個方法即可。具體如下:
View.props.onStartShouldSetResponder
View.props.onMoveShouldSetResponder
View.props.onResponderGrant
View.props.onResponderReject
View.props.onResponderMove
View.props.onResponderRelease
View.props.onResponderTerminationRequest
View.props.onResponderTerminate
乍看之下,這幾個方法名字又長有奇怪,但是當了解了React Native對手勢響應的流程之後,記憶這幾個方法也非常容易。
要理解React Native的手勢操作過程,首先要記住一點:
一個React Native應用中只能存在一個responder
正因爲如此,gesture responder system中才存在_reject和_terminate方法。React Native事件響應的基本步驟如下:
1.用戶通過觸摸或者滑動來“激活”某個responder,這個步驟由View.props.onStartShouldSetResponder以及View.props.onMoveShouldSetResponder這兩個方法負負責處理,如果返回值爲true,則表示這個View能夠響應觸摸或者滑動手勢被激活;
2.如果組件被激活,View.props.onResponderGrant方法被調用,一般來說,這個時候需要去改變組建的底色或者透明度,來表示組件已經被激活;
3.接下來,用戶開始滑動手指,此時View.props.onResponderMove方法被調用;
4.當用戶的手指離開屏幕之後,View.props.onResponderRelease方法被調用,此時組件恢復被觸摸之前的樣式,例如底色和透明度恢復之前的樣式,完成一次手勢操作;
綜上所述,一次正常的手勢操作的流程如下所示:
響應touch或者move手勢 -> grant(被激活) -> move -> release(結束事件)
來段簡單的示例代碼:
import React, { Component } from 'react';
import {
Text,
View,
StyleSheet,
ScrollView,
TouchableOpacity,
TouchableHighlight,
TouchableWithoutFeedback
} from 'react-native';
import { Navigation } from 'react-native-navigation';
/**
* 驗證父View與子View事件的分發與攔截(有圖有真相)
*/
class TouchViewScreen extends Component {
static navigatorStyle = {
drawUnderNavBar: false,
tabBarHidden: true
};
constructor(props) {
super(props);
this.state={
bg: 'white',
bg2: 'white'
}
}
componentWillMount(){
this._gestureHandlers = {
onStartShouldSetResponder: () => true,
onMoveShouldSetResponder: ()=> true,
onResponderGrant: ()=>{this.setState({bg: 'red'})},
onResponderMove: ()=>{console.log(123)},
onResponderRelease: ()=>{this.setState({bg: 'white'})},
//----------------------外層View攔截了點擊事件------------------------
// onStartShouldSetResponderCapture: () => true,
// onMoveShouldSetResponderCapture: ()=> true,
};
this._gestureHandlers2 = {
onStartShouldSetResponder: () => true,
onMoveShouldSetResponder: ()=> true,
onResponderGrant: ()=>{this.setState({bg2: 'green'})},
onResponderMove: ()=>{console.log(123)},
onResponderRelease: ()=>{this.setState({bg2: 'white'})}
}
}
render() {
return (
<View style={styles.container}>
<View
{...this._gestureHandlers}
style={[styles.rect,{
backgroundColor: this.state.bg
}]}>
<View
{...this._gestureHandlers2}
style={[styles.rect2,{
backgroundColor: this.state.bg2
}]}
>
</View>
</View>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
rect: {
width: 200,
height: 200,
borderWidth: 1,
borderColor: 'black',
justifyContent: 'center',
alignItems: 'center',
},
rect2: {
width: 100,
height: 100,
borderWidth: 1,
borderColor: 'black'
}
});
export default TouchViewScreen;
嵌套組件事件處理
上一小節介紹的都是針對單個組件來說,事件處理的流程和機制。但是前面也提到了,當組件需要作爲事件處理響應者時,需要通過 onStartShouldSetResponder 或者 onMoveShouldSetResponder 回調返回值爲 true 來申請。假如當多個組件嵌套的時候,這兩個回調都返回了 true 的時候,但是同一個只能有一個事件處理響應者,這種情況怎麼處理呢?爲了便於描述,假設我們的組件佈局如下:
在 RN 中,默認情況下使用冒泡機制,響應最深的組件最先開始響應,所以前面描述的這種情況,如圖中,如果 A、B、C 三個組件的 on*ShouldSetResponder 都返回爲 true,那麼只有 C 組件會得到響應成爲響應者。這種機制才能保證了界面所有的組件才能得到響應。但是有些情況下,可能父組件可能需要處理事件,而禁止子組件響應。RN 提供了一個劫持機制,也就是在觸摸事件往下傳遞的時候,先詢問父組件是否需要劫持,不給子組件傳遞事件,也就是如下兩個回調:
View.props.onStartShouldSetResponderCapture:這個屬性接收一個回調函數,函數原型是 function(evt): bool,在觸摸事件開始(touchDown)的時候,RN 容器組件會回調此函數,詢問組件是否要劫持事件響應者設置,自己接收事件處理,如果返回 true,表示需要劫持;
View.props.onStartShouldSetResponder,這個屬性接收一個回調函數,函數原型是 function(evt): bool,在觸摸事件開始(touchDown)的時候,RN 會回調此函數,詢問組件是否需要成爲事件響應者,接收事件處理,如果返回 true,表示需要成爲響應者;
上如代碼:
我們也是三層嵌套,只不過最外層是一個大View。其實我們在觸摸最裏層的View,三個View都會有感知,但是爲什麼只會響應最裏層View的事件呢?
也就是說,當我們觸摸C組件的時候,先去問一下A組件是否要攔截,如果A不攔截,在去問一下B組件,如果B不攔截,再去問一下C是否攔截,最終三個都不攔截(默認都不攔截)。
/**
* If a parent `View` wants to prevent a child `View` from becoming responder on a touch start,
* it should have this handler which returns `true`.
*
* `View.props.onStartShouldSetResponderCapture: (event) => [true | false]`, where `event` is a
* synthetic touch event as described above.
*/
onStartShouldSetResponderCapture: PropTypes.func,
接下來,順序反過來,去問C是否想成爲事件響應者,恰巧C想成爲事件響應者(默認應該是true)。
/**
* Does this view want to become responder on the start of a touch?
*
* `View.props.onStartShouldSetResponder: (event) => [true | false]`, where `event` is a
* synthetic touch event as described above.
*/
onStartShouldSetResponder: PropTypes.func,
現在,只是C這個組件需要成爲事件響應者,但是不一定能夠成功。只有在調用了onResponderGrant纔是真正成爲響應者,並且會去處理後面的事件。
如果在B裏面使用onStartShouldSetResponderCapture: () => true , 說明B想攔截事件,下面便會執行onResponder***方法。