來源:Songlcy 發(fā)布時(shí)間:2018-11-01 14:37:42 閱讀量:2125
源碼已開源到Github,詳細(xì)代碼可以查看:《React Native 觸摸事件代碼實(shí)踐》。
在基礎(chǔ)篇,對(duì)RN中的觸摸事件做了詳細(xì)的介紹。相信大家對(duì)于觸摸事件流程機(jī)制有了更為清晰的認(rèn)識(shí)。沒有瀏覽的可以先看看基礎(chǔ)篇:《 React Native 手勢(shì)觸摸事件機(jī)制詳解(基礎(chǔ)篇)》
本篇博客中,同樣延續(xù)基礎(chǔ)篇中結(jié)尾的內(nèi)容,對(duì)觸摸事件的執(zhí)行流程從代碼層執(zhí)行流程進(jìn)行更深的說明,并使用RN系統(tǒng)提供的高級(jí)API作為實(shí)戰(zhàn),完成高仿微信通訊錄字母索引導(dǎo)航欄的效果。Gif圖加載較慢,來看下靜態(tài)圖效果:
一、RN系統(tǒng)觸摸組件觸發(fā)機(jī)制
在基礎(chǔ)篇中,我們對(duì)RN系統(tǒng)提供的觸摸組件(Touchable*)作了一些介紹,并通過代碼來說明如何實(shí)現(xiàn)點(diǎn)擊相關(guān)的事件,如下:
<TouchableOpacity
style={ styles.btn }
onPressIn={(evt)=>this.onPressIn(evt)}
onPressOut={(evt)=>this.onPressOut(evt)}
onLongPress={(evt)=>this.onLongPress(evt)}
onPress={(evt)=>this.onPress(evt)}>
<Text style={ styles.btnText }>點(diǎn)擊按鈕</Text>
</TouchableOpacity>
事件的實(shí)現(xiàn)方式很簡單,使用 console.log 打印當(dāng)前的事件名稱。
測試用例
(1)手指按下,不彈起
(2)手指按下,彈起
(3)手指按下,不彈起,并滑動(dòng)出View范圍
測試結(jié)果
(1)手指按下,不彈起
未綁定長按事件
onPressIn
綁定長按事件
onPressIn
onLongPress
(2)手指按下,彈起
非長按(單擊)
onPressIn
onPressOut
onPress
長按
onPressIn
onLongPress
onPressOut
(3)手指按下,不彈起,并滑動(dòng)出View范圍
非長按
onPressIn
onPressOut
長按
onPressIn
onLongPress
onPressOut
結(jié)論
從上述三種觸發(fā)方式中打印的log,我們可以對(duì) onPress,onPressIn,onPressOut,onLongPress 事件的觸發(fā)條件和觸發(fā)順序有一個(gè)比較清晰的了解:
(1)未綁定長按事件,手指按下,不彈起只會(huì)觸發(fā) onPressIn,反之,則會(huì)觸發(fā) onPressIn -> onLongPress
(2)未綁定長按事件,手指按下,彈起,即單擊,會(huì)觸發(fā) onPressIn -> onPressOut -> onPress,反之,如果長按后彈起,會(huì)觸發(fā)onPressIn -> onLongPress -> onPressOut
(3)未綁定長按事件,手指按下,不彈起,并滑動(dòng)出View范圍,組件會(huì)自動(dòng)失去焦點(diǎn),并會(huì)觸發(fā) onPressIn -> onPressOut,反之,則會(huì)觸發(fā) onPressIn -> onLongPress -> onPressOut
可以看到,系統(tǒng)為我們提供了觸摸組件相對(duì)來說較為簡單,基本可以完全覆蓋點(diǎn)擊場景。如果涉及較為復(fù)雜的觸摸交互,還是需要自定義觸摸事件上場,接下來我們繼續(xù)來看自定義觸摸事件的觸發(fā)機(jī)制。
二、RN系統(tǒng)觸摸組件觸發(fā)機(jī)制
單組件
在RN中任何View組件都可以實(shí)現(xiàn)自定義觸摸事件,方式很簡單,只需要將觸摸事件行為方式作為props屬性傳遞給View組件即可。測試代碼如下:
componentWillMount() {
this.gestureHandlers = {
/**
* 在手指觸摸開始時(shí)申請(qǐng)成為響應(yīng)者
*/
onStartShouldSetResponder: (evt) => {
console.log('onStartShouldSetResponder');
return true;
},
/**
* 在手指在屏幕移動(dòng)時(shí)申請(qǐng)成為響應(yīng)者
*/
onMoveShouldSetResponder: (evt) => {
console.log('onMoveShouldSetResponder');
return true;
},
/**
* 申請(qǐng)成功,組件成為了事件處理響應(yīng)者,這時(shí)組件就開始接收后序的觸摸事件輸入。
* 一般情況下,這時(shí)開始,組件進(jìn)入了激活狀態(tài),并進(jìn)行一些事件處理或者手勢(shì)識(shí)別的初始化
*/
onResponderGrant: (evt) => {
console.log('onResponderGrant');
},
/**
* 表示申請(qǐng)失敗了,這意味者其他組件正在進(jìn)行事件處理,
* 并且它不想放棄事件處理,所以你的申請(qǐng)被拒絕了,后續(xù)輸入事件不會(huì)傳遞給本組件進(jìn)行處理。
*/
onResponderReject: (evt) => {
console.log('onResponderReject');
},
/**
* 表示手指按下時(shí),成功申請(qǐng)為事件響應(yīng)者的回調(diào)
*/
onResponderStart: (evt) => {
console.log('onResponderStart');
},
/**
* 表示觸摸手指移動(dòng)的事件,這個(gè)回調(diào)可能非常頻繁,所以這個(gè)回調(diào)函數(shù)的內(nèi)容需要盡量簡單
*/
onResponderMove: (evt) => {
console.log('onResponderMove');
},
/**
* 表示觸摸完成(touchUp)的時(shí)候的回調(diào),表示用戶完成了本次的觸摸交互,這里應(yīng)該完成手勢(shì)識(shí)別的處理,
* 這以后,組件不再是事件響應(yīng)者,組件取消激活
*/
onResponderRelease: (evt) => {
console.log('onResponderRelease');
},
/**
* 組件結(jié)束事件響應(yīng)的回調(diào)
*/
onResponderEnd: (evt) => {
console.log('onResponderEnd');
},
/**
* 當(dāng)其他組件申請(qǐng)成為響應(yīng)者時(shí),詢問你是否可以釋放響應(yīng)者角色讓給其他組件
*/
onResponderTerminationRequest: (evt) => {
console.log('onResponderTerminationRequest');
return true;
},
/**
* 如果 onResponderTerminationRequest 回調(diào)函數(shù)返回為 true,
* 則表示同意釋放響應(yīng)者角色,同時(shí)會(huì)回調(diào)如下函數(shù),通知組件事件響應(yīng)處理被終止
* 這可能是由于其他View通過onResponderTerminationRequest請(qǐng)求的,也可能是由操作系統(tǒng)強(qiáng)制奪權(quán)(比如iOS上的控制中心或是通知中心)。
*/
onResponderTerminate: (evt) => {
console.log('onResponderTerminate');
}
}
}
render() {
return (
<View { ...this.gestureHandlers } style={ styles.container } />
)
}
上述代碼中我們?cè)?ComponentWillMount 生命周期函數(shù)中定義了 gestureHandlers,并定義了觸摸事件行為函數(shù),通過 log 打印的方式來看自定義觸摸事件執(zhí)行機(jī)制。View組件的 style 樣式為一個(gè)藍(lán)色填充的圓形,附上效果圖
效果圖
測試用例
(1)onStartShouldSetResponder 方法返回 false,onMoveShouldSetResponder 返回 false
(2)onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 false
(3)onStartShouldSetResponder 方法返回 false,onMoveShouldSetResponder 返回 true
(4)onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 true
測試觸摸方式分兩種:
(1)單擊事件
(2)滑動(dòng)事件
測試結(jié)果
(1)onStartShouldSetResponder 方法返回 false,onMoveShouldSetResponder 返回 false
單擊
onStartShouldSetResponder
滑動(dòng)
onStartShouldSetResponder
onMoveShouldSetResponder
(2)onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 false
單擊
按下
onStartShouldSetResponder
onResponderGrant
onResponderStart
彈起
onResponderEnd
onResponderRelease
滑動(dòng)
按下
onStartShouldSetResponder
onResponderGrant
onResponderStart
移動(dòng)
onResponderMove(多次執(zhí)行)
彈起
onResponderEnd
onResponderRelease
(3)onStartShouldSetResponder 方法返回 false,onMoveShouldSetResponder 返回 true
單擊
onStartShouldSetResponder
滑動(dòng)
按下
onStartShouldSetResponder
移動(dòng)
onMoveShouldSetResponder
onResponderGrant
onResponderMove(多次執(zhí)行)
彈起
onResponderEnd
onResponderRelease
(4)onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 true
與 onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 false 執(zhí)行結(jié)果完全一致。
測試結(jié)論
(1)當(dāng) onStartShouldSetResponder 、onMoveShouldSetResponder 方法都返回 false,當(dāng)手指按下或者進(jìn)行移動(dòng)時(shí),RN系統(tǒng)詢問當(dāng)前組件并發(fā)現(xiàn)在點(diǎn)擊和滑動(dòng)時(shí)都不申請(qǐng)成為事件響應(yīng)者,只會(huì)執(zhí)行 onStartShouldSetResponder 、onMoveShouldSetResponder 方法,并立刻結(jié)束,后續(xù)觸摸事件不會(huì)被觸發(fā)。
(2)onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 false,當(dāng)手指按下時(shí),RN系統(tǒng)詢問當(dāng)前組件,發(fā)現(xiàn) onStartShouldSetResponder 方法返回 true,此時(shí)申請(qǐng)成為響應(yīng)者(不存在多組件觸摸事件互斥場景),在申請(qǐng)成功后,會(huì)依次執(zhí)行 onResponderGrant 、onResponderStart 方法。由于當(dāng)前組件已經(jīng)成為了響應(yīng)者,此時(shí)手指如果繼續(xù)移動(dòng),系統(tǒng)并不會(huì)觸發(fā) onMoveShouldSetResponder 方法,并立刻執(zhí)行 onResponderMove 方法。當(dāng)手指離開屏幕,此時(shí)會(huì)觸發(fā)onResponderEnd、onResponderRelease 方法,一次完整的觸摸事件結(jié)束。
(3)onStartShouldSetResponder 方法返回 false,onMoveShouldSetResponder 返回 true,當(dāng)手指按下時(shí),RN系統(tǒng)詢問當(dāng)前組件,發(fā)現(xiàn) onStartShouldSetResponder 方法返回 false,此時(shí)不申請(qǐng)成為響應(yīng)者,執(zhí)行 onStartShouldSetResponder方法完畢后立刻結(jié)束。由于當(dāng)前組件不是響應(yīng)者,此時(shí)手指按下后如果繼續(xù)移動(dòng),系統(tǒng)會(huì)觸發(fā) onMoveShouldSetResponder 方法,再次詢問當(dāng)前組件是否要申請(qǐng)成為響應(yīng)者,此方法返回 true ,即申請(qǐng)成為響應(yīng)者,在申請(qǐng)成功后,會(huì)依次執(zhí)行 onResponderGrant 、onResponderMove 方法。當(dāng)手指離開屏幕,此時(shí)會(huì)觸發(fā)onResponderEnd、onResponderRelease 方法,一次完整的觸摸事件結(jié)束。
??與(2)測試對(duì)比可以發(fā)現(xiàn):當(dāng) onStartShouldSetResponder 方法返回 true,法返手指按下申請(qǐng)響應(yīng)者授權(quán)成功后,會(huì)依次執(zhí)行 onResponderGrant 、onResponderStart 方法。而當(dāng) onStartShouldSetResponder 方法返回 false,onMoveShouldSetResponder 方法返回 true 時(shí),此時(shí)在滑動(dòng)申請(qǐng)成為響應(yīng)者授權(quán)成功后,會(huì)依次執(zhí)行 onResponderGrant 、onResponderMove 方法,而不會(huì)觸發(fā)onResponderStart 方法。
(4)onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 true,結(jié)論是與onStartShouldSetResponder 方法返回 true,onMoveShouldSetResponder 返回 false,執(zhí)行機(jī)制相同。其實(shí)此時(shí)結(jié)合上述的分析,我們很容易明白原因。在onStartShouldSetResponder 方法返回 true,并申請(qǐng)成為響應(yīng)者,此時(shí)在移動(dòng)過程中系統(tǒng)不會(huì)再去詢問 onMoveShouldSetResponder,即會(huì)忽略該方法。并繼續(xù)執(zhí)行后續(xù)的觸摸事件。最終形成一次完整的觸摸事件操作。
多組件
在實(shí)際開發(fā)過程中,肯定不僅僅處理單組件觸摸事件行為,可能會(huì)涉及到多個(gè)組件間的交互,或者多層次的嵌套組件交互。在RN中,我們知道在同一時(shí)刻,只會(huì)存在一個(gè)響應(yīng)者。當(dāng)使用一個(gè)手指激活一個(gè)組件成為響應(yīng)者后,在當(dāng)前手指不釋放的情況下,又去觸摸激活另一個(gè)組件會(huì)發(fā)生什么呢?這就會(huì)涉及到多個(gè)組件間共同申請(qǐng)響應(yīng)者互斥的場景。還記得我們?cè)诨A(chǔ)篇中介紹的onResponderTerminationRequest、onResponderTerminate 方法嗎?
onResponderTerminationRequest 方法需要返回一個(gè)bool類型值,來決定當(dāng)有其他組件同時(shí)申請(qǐng)成響應(yīng)者時(shí),當(dāng)前組件是否放棄響應(yīng)者權(quán)限。此時(shí)會(huì)分為兩種情況:
(1)onResponderTerminationRequest 方法返回 true
此時(shí)系統(tǒng)發(fā)現(xiàn)當(dāng)有新當(dāng)組件申請(qǐng)成為響應(yīng)者時(shí),當(dāng)前組件會(huì)釋放掉響應(yīng)者身份,并將權(quán)限交與新組件。新組件onResponderGrant 方法被調(diào)用 ,當(dāng)前組件的 onResponderTerminate 方法被調(diào)用,并將響應(yīng)者身份權(quán)限狀態(tài)釋放。
(2)onResponderTerminationRequest 方法返回 false
此時(shí)系統(tǒng)發(fā)現(xiàn)當(dāng)有新當(dāng)組件申請(qǐng)成為響應(yīng)者時(shí),當(dāng)前組件不會(huì)釋放掉響應(yīng)者身份。新組件申請(qǐng)響應(yīng)者權(quán)限失敗,這也意味其他組件正在進(jìn)行事件處理,并且它不想放棄事件處理,所以你的申請(qǐng)被拒絕了,后續(xù)輸入事件不會(huì)傳遞給本組件進(jìn)行處理。新組件的 onResponderReject 方法會(huì)被調(diào)用。
由于在模擬器上不好模擬多指觸控請(qǐng)求,當(dāng)家可以嘗試編寫demo,并運(yùn)行真機(jī)查看效果。
在基礎(chǔ)篇中,我們同樣介紹了事件攔截機(jī)制。通過 onStartShouldSetResponderCapture、onMoveShouldSetResponderCapture 方法來決定當(dāng)前組件是否需要攔截觸摸事件。當(dāng)組件的 onStartShouldSetResponderCapture 或者 onMoveShouldSetResponderCapture 方法 返回 true,表示會(huì)攔截掉當(dāng)前觸摸事件,交給自己處理。此時(shí)系統(tǒng)會(huì)忽略不執(zhí)行 onStartShouldSetResponder、onMoveShouldSetResponder 方法直接執(zhí)行onResponderGrant 標(biāo)示當(dāng)前組件已成為事件響應(yīng)者,進(jìn)而處理后續(xù)觸摸事件。當(dāng)前組件如果有子組件,那么子組件不會(huì)收到觸摸事件。
三、觸摸手勢(shì)高級(jí)API - PanResponder
通過上述對(duì)于自定義View的觸摸事件機(jī)制來看,對(duì)于開發(fā)者來說還是較為復(fù)雜,不夠友好。使用起來也不是特別方便。官方同樣考慮到這個(gè)問題,為我們提供了更高抽象到API:PanResponder。
源碼中對(duì)于create的描述:
/**
* @param config Enhanced versions of all of the responder callbacks
* that provide not only the typical `ResponderSyntheticEvent`, but also the
* `PanResponder` gesture state. Simply replace the word `Responder` with
* `PanResponder` in each of the typical `onResponder*` callbacks. For
* example, the `config` object would look like:
*
* - `onMoveShouldSetPanResponder: (e, gestureState) => {...}`
* - `onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}`
* - `onStartShouldSetPanResponder: (e, gestureState) => {...}`
* - `onStartShouldSetPanResponderCapture: (e, gestureState) => {...}`
* - `onPanResponderReject: (e, gestureState) => {...}`
* - `onPanResponderGrant: (e, gestureState) => {...}`
* - `onPanResponderStart: (e, gestureState) => {...}`
* - `onPanResponderEnd: (e, gestureState) => {...}`
* - `onPanResponderRelease: (e, gestureState) => {...}`
* - `onPanResponderMove: (e, gestureState) => {...}`
* - `onPanResponderTerminate: (e, gestureState) => {...}`
* - `onPanResponderTerminationRequest: (e, gestureState) => {...}`
* - `onShouldBlockNativeResponder: (e, gestureState) => {...}`
*
* In general, for events that have capture equivalents, we update the
* gestureState once in the capture phase and can use it in the bubble phase
* as well.
*
* Be careful with onStartShould* callbacks. They only reflect updated
* `gestureState` for start/end events that bubble/capture to the Node.
* Once the node is the responder, you can rely on every start/end event
* being processed by the gesture and `gestureState` being updated
* accordingly. (numberActiveTouches) may not be totally accurate unless you
* are the responder.
*/
create(config: PanResponderCallbacks): PanResponderInstance;
PanResponder 的使用方式非常簡單,通過調(diào)用 PanResponder 的 create 靜態(tài)方法創(chuàng)建手勢(shì)對(duì)象,傳入 config 參數(shù)實(shí)現(xiàn)對(duì)應(yīng)的觸摸回調(diào)即可。手勢(shì)的邏輯和流程與自定義觸摸事件保持一致。最后調(diào)用 panResponer 的 panHandlers 作為 props 傳給View組件:
<View {...this.panResponder.panHandlers } style={ styles.container } />
官方對(duì)于 config 參數(shù)的描述:
@param config 所有響應(yīng)者回調(diào)的增強(qiáng)版本,不僅提供典型的 ResponderSyntheticEvent,而且還提供 PanResponder 手勢(shì)狀態(tài)。只需將 onResponder * 一詞改為 PanResponder 。例如,config 對(duì)象看起來像:
onMoveShouldSetPanResponder :( e,gestureState)=> {...}
onMoveShouldSetPanResponderCapture:(e,gestureState)=> {...}
onStartShouldSetPanResponder :( e,gestureState)=> {...}
onStartShouldSetPanResponderCapture:(e,gestureState)=> {...}
onPanResponderReject :( e,gestureState)=> {...}
onPanResponderGrant :( e,gestureState)=> {...}
onPanResponderStart :( e,gestureState)=> {...}
onPanResponderEnd :( e,gestureState)=> {...}
onPanResponderRelease :( e,gestureState)=> {...}
onPanResponderMove :( e,gestureState)=> {...}
onPanResponderTerminate:(e,gestureState)=> {...}
onPanResponderTerminationRequest:(e,gestureState)=> {...}
onShouldBlockNativeResponder :( e,gestureState)=> {...}
通常來說,對(duì)那些有對(duì)應(yīng)捕獲事件的事件來說,我們?cè)诓东@階段更新 gestureState 一次,然后在冒泡階段直接使用即可。
注意 onStartShould* 回調(diào)。他們只會(huì)在此節(jié)點(diǎn)冒泡/捕獲的開始/結(jié)束事件中提供已經(jīng)更新過的 gestureState。一旦這個(gè)節(jié)點(diǎn)成為了事件的響應(yīng)者,則所有的開始/結(jié)束事件都會(huì)被手勢(shì)正確處理,并且 gestureState 也會(huì)被正確更新。(numberActiveTouches)有可能沒有包含所有的觸摸點(diǎn),除非組件本身就是觸摸事件的響應(yīng)者。
可以發(fā)現(xiàn)PanResponder包含一個(gè)onShouldBlockNativeResponder 方法,該方法需要返回一個(gè) bool 值,決定當(dāng)前組件是否應(yīng)該阻止原生組件成為JS響應(yīng)者。 默認(rèn)返回true。目前只支持android平臺(tái)設(shè)備。
了解完P(guān)anResponder具有的觸摸方式,接下來我們看下gestureState參數(shù)包含的一些觸摸事件屬性:
stateID - 觸摸狀態(tài)的 ID。在屏幕上有至少一個(gè)觸摸點(diǎn)的情況下,這個(gè) ID 會(huì)一直有效。
moveX - 最近一次移動(dòng)時(shí)的屏幕橫坐標(biāo)
moveY - 最近一次移動(dòng)時(shí)的屏幕縱坐標(biāo)
x0 - 當(dāng)響應(yīng)器產(chǎn)生時(shí)的屏幕橫坐標(biāo)
y0 - 當(dāng)響應(yīng)器產(chǎn)生時(shí)的屏幕縱坐標(biāo)
dx - 從觸摸操作開始時(shí)的累計(jì)橫向路程
dy - 從觸摸操作開始時(shí)的累計(jì)縱向路程
vx - 當(dāng)前的橫向移動(dòng)速度
vy - 當(dāng)前的縱向移動(dòng)速度
numberActiveTouches - 當(dāng)前在屏幕上的有效觸摸點(diǎn)的數(shù)量
從屬性的行為上來看,相對(duì)自定義的onResponder*觸摸事件,提供了更高級(jí)的描述。例如:移動(dòng)速度、累計(jì)滑動(dòng)的坐標(biāo)點(diǎn)等等。更方便開發(fā)者來處理一些復(fù)雜的觸摸交互。
四、觸摸手勢(shì)高級(jí)屬性 - pointerEvents
系統(tǒng)在View中提供了一個(gè)觸摸事件的高級(jí)屬性:pointerEvents。官方對(duì)該屬性的描述為:控制View是否可以成為觸摸事件的目標(biāo)。
官方為 pointerEvents 屬性提供了4個(gè)可供選擇的值:
none: View永遠(yuǎn)不是觸摸事件的目標(biāo),即發(fā)生在本組件與本組件的子組件上的觸摸事件都會(huì)交給本組件的父組件處理.
box-none: 發(fā)生在本組件顯示范圍內(nèi),但不是子組件顯示范圍內(nèi)的事件交給本組件,在子組件顯示范圍內(nèi)交給子組件處理,即視圖永遠(yuǎn)不是觸摸事件的目標(biāo),但它的子視圖可以是。
box-only: 發(fā)生在本組件顯示范圍內(nèi)的觸摸事件將全部由本組件處理,即使觸摸事件發(fā)生在本組件的子組件顯示范圍內(nèi)
auto: 視圖可以成為觸摸事件的目標(biāo),但視組件的不同而定,并不是所有的子組件都支持box-none和box-only兩個(gè)值
由于 pointerEvents 不會(huì)影響布局/外觀,因此我們選擇不在樣式上包含 pointerEvents。 是否將 pointerEvents 作為屬性來設(shè)置,視平臺(tái)而定。例如,在RN中,pointerEvents 作為View的屬性來設(shè)置。但在 CSS 中,可能以className來設(shè)置。
該屬性可以方便我們處理類似“穿透”事件傳遞的處理。例如,絕對(duì)定位導(dǎo)致多層次view間的遮蓋后,事件如何傳遞處理等。我們拿高德地圖為例:
在地圖組件上覆蓋了一層組件用來進(jìn)行其他操作,又不想讓這個(gè)圖像組件影響用戶手指拖動(dòng)地圖的操作,這時(shí)就需 pointerEvents 屬性來解決這個(gè)問題.
看上面的描述可能不太直觀,為了模仿上述效果,我們?cè)诖a中定義一個(gè)可滑動(dòng)列表,并且在列表上層定義一個(gè)View組件。
以上就是關(guān)于手勢(shì)觸摸的全部內(nèi)容了,我們花了大量的篇幅通過代碼執(zhí)行的日志結(jié)果分析了觸摸事件的行為方式,相信大家對(duì)于RN觸摸事件有了更加深刻的認(rèn)知。學(xué)而不思則罔,思而不學(xué)則殆。接下來我們通過實(shí)現(xiàn)高仿微信通訊錄快速查找,來加深觸摸手勢(shì)在實(shí)際開發(fā)過程中的實(shí)現(xiàn)流程。
五、高仿微信通訊錄字母索引導(dǎo)航欄
實(shí)現(xiàn)微信通訊錄右側(cè)字母索引導(dǎo)航,需要我們處理手勢(shì)相關(guān)的操作行為。使用自定義觸摸事件及 PanResponder,都可以相對(duì)輕松的完成。本例中,我們將介紹使用 PanResponder 如何實(shí)現(xiàn)。
從整體交互分析來看,要實(shí)現(xiàn)該功能有以下幾點(diǎn):
(1)手指按下或單擊時(shí),字母導(dǎo)航欄背景色改變,得到當(dāng)前觸摸區(qū)域?qū)?yīng)的字母,并在屏幕中心以 Dialog 方式顯示,根據(jù)index 將 SectionLst 滑動(dòng)到對(duì)應(yīng)位置
(2)手指觸摸字母導(dǎo)航欄區(qū)域,并滑動(dòng),則同樣得到當(dāng)前觸摸區(qū)域?qū)?yīng)的字母,并在屏幕中心以 Dialog 方式顯示,根據(jù)index 將 SectionLst 滑動(dòng)到對(duì)應(yīng)位置
(3)手指離開屏幕,字母導(dǎo)航欄背景色還原,Dialog 隱藏
在實(shí)現(xiàn)之前,我們先定義好需要使用到的常量:
const { height } = Dimensions.get('window');
// 標(biāo)題欄高度
const HEADER_HEIGHT = 64;
// 字母搜索容器高度
const WORD_SEARCH_CONTAINER_HEIGHT = height - HEADER_HEIGHT;
// 字母搜索容器縱向 padding
const WORD_SEARCH_CONTAINER_V_PADDING = 8;
// 字母觸摸區(qū)域之外的高度(標(biāo)題欄,padding,margin等)
const WORD_TOUCH_OUTSIDE_HEIGHT = HEADER_HEIGHT + WORD_SEARCH_CONTAINER_V_PADDING * 2
// 單個(gè)字母高度
const WORD_HEIGHT = Math.floor((WORD_SEARCH_CONTAINER_HEIGHT - WORD_SEARCH_CONTAINER_V_PADDING * 2) / 28);
// 搜索字母,長度: 28
const WORDS = ['↑', '?' ,'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W', 'X', 'Y', 'Z', '#'];
要實(shí)現(xiàn)第一步,首先需要使當(dāng)前索引導(dǎo)航欄成為事件響應(yīng)者。為了方便,我們將其封裝成單獨(dú)組件,也符合了RN的組件化開發(fā)思想。在手指按下時(shí),需要使當(dāng)前組件申請(qǐng)成為響應(yīng)者,所以我們需要實(shí)現(xiàn) onStartSetShouldPanResponder 方法,并 return true。在成為響應(yīng)者,授權(quán)成功后,改變導(dǎo)航欄背景色,即在 onPanResponderGrant 方法中,更新當(dāng)前組件背景色。同時(shí)獲取當(dāng)前觸摸的y坐標(biāo)點(diǎn),因?yàn)槲覀兏鶕?jù)屏幕高度固定了每個(gè)字母占據(jù)的空間高度,所以可以根據(jù)當(dāng)前觸摸屏幕的y坐標(biāo)點(diǎn)結(jié)合字母空間高度計(jì)算出當(dāng)前的字母 index。根據(jù) index 即可獲取到第幾個(gè)字母。并作dialog展示。
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
return true;
},
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
},
onStartShouldSetPanResponder: (evt, gestureState) => {
return true;
},
onPanResponderGrant: (evt, gestureState) => {
const { index, word } = this.getWord(gestureState.y0);
this.props.showWordDialog && this.props.showWordDialog(index, word);
this.changeBgColor(this.refs.wordContainer, 'rgba(0,0,0,0.3)');
}
第二步:監(jiān)聽手勢(shì)滑動(dòng),我們需要實(shí)現(xiàn) onPanResponderMove 方法,在該方法中,獲取當(dāng)前滑動(dòng)所在屏幕的 y 坐標(biāo)點(diǎn),重復(fù)第二步的計(jì)算,得出當(dāng)前觸摸區(qū)域?qū)?yīng)的字母。
onPanResponderMove:(evt, gestureState) => {
// 獲取當(dāng)前滑動(dòng)到的位置,根據(jù)當(dāng)前位置,取出index 對(duì)應(yīng) word
const { index, word } = this.getWord(gestureState.moveY);
this.props.showWordDialog && this.props.showWordDialog(index, word);
}
第三步:在手指離開屏幕后,當(dāng)前觸摸事件結(jié)束,可以在 onPanResponderRelease 方法中以回調(diào)方式隱藏 dialog。
onPanResponderRelease:(evt, gestureState) => {
// 隱藏dialog
this.props.hideWordDialog && this.props.hideWordDialog();
this.changeBgColor(this.refs.wordContainer, 'transparent');
}
通過以上三步,我們就能很輕松的實(shí)現(xiàn)字母索引導(dǎo)航欄的效果了。為了高度可定制化,將dialog的實(shí)現(xiàn)方式剝離,以回調(diào)的方式在索引組件中實(shí)現(xiàn),降低組件間依賴。
/**
* 顯示字母Dialog提示
*/
showWordDialog(index, word) {
this.setState({
word
});
this.scrollSectionList(index)
}
/**
* 隱藏字母Dialog提示
*/
hideWordDialog() {
this.setState({ word: null });
}
/**
* 滾動(dòng)SectionList
*/
scrollSectionList(index) {
this.refs.sectionList.scrollToLocation({animated: false, itemIndex: 0, sectionIndex: index, viewOffset: 26});
}
根據(jù)索引滑動(dòng)列表到功能,我們可以結(jié)合SectionList來實(shí)現(xiàn)。具體可以看源碼即可。
關(guān)于React Native觸摸手勢(shì)事件的內(nèi)容就全部結(jié)束了。通過基礎(chǔ)篇、進(jìn)階篇兩篇內(nèi)容從基礎(chǔ)的了解到功能的實(shí)踐,相信大家對(duì)RN的觸摸事件系統(tǒng)有了非常深刻的理解。大家有任何問題,可以加入我們的技術(shù)討論組交流溝通。我的微信號(hào):mir_song
---------------------
作者:Songlcy
來源:CSDN
原文:https://blog.csdn.net/u013718120/article/details/83213261
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請(qǐng)附上博文鏈接!
在線
客服
客服
熱線
7*24小時(shí)客服服務(wù)熱線
關(guān)注
微信
關(guān)注官方微信