[圖片上傳中...(pic1.png-835f42-1510063522595-0)]
iOS與安卓的原生實(shí)現(xiàn)實(shí)際都已經(jīng)提供了UIButton
與Button
的的按鈕組件唤蔗。但是我們發(fā)現(xiàn)RN并沒有基于這兩個(gè)組件實(shí)現(xiàn)統(tǒng)一的按鈕組件餐弱。那如何在RN中實(shí)現(xiàn)按鈕組件呢?其內(nèi)部實(shí)現(xiàn)又是如何處理的唠帝?本文將從設(shè)計(jì)按鈕,使用按鈕建芙,具體實(shí)現(xiàn)没隘,三部分解析整個(gè)按鈕的實(shí)現(xiàn)邏輯懂扼。
依賴
- RN的版本 0.38
- 系統(tǒng):iOS
按鈕設(shè)計(jì)
MyCustomButton.js
import React, {Component} from 'react';
import {
StyleSheet,
Text,
TouchableHighlight
} from 'react-native';
class MyCustomButton extends React.Component {
props: Props;
constructor(props: Props) {
super(props);
}
render() {
return (
<TouchableHighlight
style={styles.button}
underlayColor="#a5a5a5"
onPress={this.props.onPress}>
<Text style={styles.buttonText}>{this.props.text}</Text>
</TouchableHighlight>
);
}
}
const styles = StyleSheet.create({
button: {
borderWidth: 1,
},
buttonText: {
fontSize: 18,
color: 'red',
backgroundColor:'transparent',
alignSelf:'center'
},
text: {
fontSize: 16,
marginBottom:20
},
});
module.exports = MyCustomButton;
具體使用
import React, { Component } from 'react';
import {
StyleSheet,
View
} from 'react-native';
import MyCustomButton from './MyCustomButton'
class TestScreen extends Component{
_onPress(){
alert('按鈕點(diǎn)擊');
}
render(){
return (<View style={styles.container}>
<View
style={styles.buttonWrap}
>
<MyCustomButton
onPress={this._onPress}
text={"按鈕"}
></MyCustomButton>
</View>
</View>)
}
}
let styles = StyleSheet.create({
container: {
marginTop: 20,
flex: 1
},
buttonWrap:{
height:100,
width:50
}
});
module.exports = TestScreen;
主要想分析按鈕實(shí)現(xiàn)禁荸,因此該Demo例子只是簡(jiǎn)單滿足按鈕的要求。
TouchableHighlight及相關(guān)組件
我們查看上述例子中的TouchableHighlight
的實(shí)現(xiàn)代碼中以下這一段阀湿,由此可以推斷是在TouchableWithoutFeedback
的基礎(chǔ)上做的擴(kuò)展赶熟。
var TouchableHighlight = React.createClass({
propTypes: {
...TouchableWithoutFeedback.propTypes,
}
});
我們看看TouchableWithoutFeedback
以及其他幾種擴(kuò)展的效果:
組件 | 描述 | 效果圖 |
---|---|---|
TouchableWithoutFeedback | 響應(yīng)點(diǎn)擊事件,無任何反饋 | |
TouchableHighlight | 點(diǎn)擊狀態(tài)背景變暗 | |
TouchableOpacity | 點(diǎn)擊狀態(tài)改變背景的透明度 | |
TouchableNativeFeedback | 此組件只支持Android陷嘴,不作分析 | - |
組件API的調(diào)用此處就不作具體介紹映砖,可以查看React Native的官方文檔
Native與jS端按鈕一塊的交互邏輯
下面我們來具體分析RN中按鈕的內(nèi)部實(shí)現(xiàn),從Native切入考慮灾挨。想到iOS端能處理手勢(shì)事件的類--UIGestureRecognizer
邑退。
我們進(jìn)入到node_modules
目錄執(zhí)行grep "UIGestureRecognizer" -rn .
得到如下結(jié)果:
不難看出RN中繼承UIGestureRecognizer
實(shí)現(xiàn)了自己的手勢(shì)處理的派生類RCTTouchHandler
。按鈕的點(diǎn)擊功能的實(shí)現(xiàn)便從RCTTouchHandler
開始分析劳澄。
Native端的處理
RCTTouchHandler入口
全局搜索RCTTouchHandler
我們發(fā)現(xiàn)RN頁(yè)面的承載容器RCTRootView
中的子組件RCTRootContentView
存在RCTTouchHandler
的屬性:
@interface RCTRootContentView : RCTView <RCTInvalidating>
@property (nonatomic, readonly) BOOL contentHasAppeared;
@property (nonatomic, readonly, strong) RCTTouchHandler *touchHandler;
@property (nonatomic, assign) BOOL passThroughTouches;
- (instancetype)initWithFrame:(CGRect)frame
bridge:(RCTBridge *)bridge
reactTag:(NSNumber *)reactTag
sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility NS_DESIGNATED_INITIALIZER;
@end
@implementation RCTRootContentView
{
__weak RCTBridge *_bridge;
UIColor *_backgroundColor;
}
- (instancetype)initWithFrame:(CGRect)frame
bridge:(RCTBridge *)bridge
reactTag:(NSNumber *)reactTag
sizeFlexiblity:(RCTRootViewSizeFlexibility)sizeFlexibility
{
if ((self = [super initWithFrame:frame])) {
_bridge = bridge;
self.reactTag = reactTag;
// 注意此處_touchHandler手勢(shì)實(shí)例初始化完成地技,然后添加到contentView上,這樣contentView便可以處理手勢(shì)事件了
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
[self addGestureRecognizer:_touchHandler];
[_bridge.uiManager registerRootView:self withSizeFlexibility:sizeFlexibility];
self.layer.backgroundColor = NULL;
}
return self;
}
// 省略部分代碼段
@end
RCTTouchHandler初始化邏輯
以下是RCTTouchHandler
的初始化邏輯秒拔,
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertParam(bridge);
// 初始化綁定事件的處理函數(shù)
if ((self = [super initWithTarget:self action:@selector(handleGestureUpdate:)])) {
_eventDispatcher = [bridge moduleForClass:[RCTEventDispatcher class]];
_dispatchedInitialTouches = NO;
_nativeTouches = [NSMutableOrderedSet new];
_reactTouches = [NSMutableArray new];
_touchViews = [NSMutableArray new];
// `cancelsTouchesInView` is needed in order to be used as a top level
// event delegated recognizer. Otherwise, lower-level components not built
// using RCT, will fail to recognize gestures.
self.cancelsTouchesInView = NO;
}
return self;
}
由此得出按鈕的點(diǎn)擊必將觸發(fā)handleGestureUpdate
函數(shù)莫矗。
RCTTouchHandler中手勢(shì)處理
我們都知道手勢(shì)觸發(fā)會(huì)先執(zhí)行如下的函數(shù):
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
因此我們?cè)?code>RCTTouchHandler中的handleGestureUpdate
函數(shù)以及上述函數(shù)中加入斷點(diǎn)調(diào)試分析。根據(jù)代碼執(zhí)行順序來看一下具體實(shí)現(xiàn)邏輯砂缩。
開始觸摸topTouchStart
代碼執(zhí)行流程:
// 1.點(diǎn)擊按鈕時(shí)觸發(fā)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
// 2.記錄點(diǎn)擊view,點(diǎn)擊事件touch,點(diǎn)擊事件對(duì)應(yīng)的reactTouch
- (void)_recordNewTouches:(NSSet<UITouch *> *)touches
// 3.監(jiān)聽手勢(shì)觸發(fā)的函數(shù)
- (void)handleGestureUpdate:(__unused UIGestureRecognizer *)gesture
// 4.更新reactTouch作谚,生成touchEvent,使用_eventDispatcher事件將該觸發(fā)事件發(fā)送給js端
- (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches
eventName:(NSString *)eventName
originatingTime:(__unused CFTimeInterval)originatingTime
// 5.將native統(tǒng)計(jì)的touch信息同步到react的touch中
- (void)_updateReactTouchAtIndex:(NSInteger)touchIndex
// 6.將touch通過發(fā)送事件的方式通知給js
RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
reactTag:self.view.reactTag
reactTouches:reactTouches
changedIndexes:changedIndexes
coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:event];
上述的代碼段6中,self.view.reactTag
,reactTag
是表示reactView
的唯一標(biāo)識(shí)。是由js端的ReactNativeTagHandles.allocateTag()
生成庵芭,有興趣可以自己研究妹懒,此處不作擴(kuò)展分析。
topTouchEnd
的流程與上述的類似不作額外分析双吆。
js端的處理
Touchable手勢(shì)處理類
上面我們看了Native端是如何觸發(fā)按鈕點(diǎn)擊事件的眨唬,如何將native的觸摸事件傳給js端。下面我們將從js繼續(xù)分析按鈕點(diǎn)擊事件的整個(gè)流程:PRESSIN -> PRESSEND伊诵。
我們查看TouchableWithoutFeedback
中如下一段代碼:
// mixins 模式的使用使得TouchableWithoutFeedback擁有Touchable的屬性與方法
const TouchableWithoutFeedback = React.createClass({
mixins: [TimerMixin, Touchable.Mixin],
});
/**
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
* defined on your component.
*/
touchableHandlePress: function(e: Event) {
this.props.onPress && this.props.onPress(e);
},
顯然的按鈕的點(diǎn)擊事件onPress
實(shí)際是觸發(fā)Touchable
中的onPress
函數(shù)的執(zhí)行单绑。下面我們具體看看Touchable
的處理邏輯。
Touchable手勢(shì)處理流程圖
* ======= State Machine =======
*
* +-------------+ <---+ RESPONDER_RELEASE
* |NOT_RESPONDER|
* +-------------+ <---+ RESPONDER_TERMINATED
* +
* | RESPONDER_GRANT (HitRect)
* v
* +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+
* |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN|
* +---------------------------+ +-------------------------+ +------------------------------+
* + ^ + ^ + ^
* |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |LEAVE_ |ENTER_
* |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT
* | | | | | |
* v + v + v +
* +----------------------------+ DELAY +--------------------------+ +-------------------------------+
* |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT|
* +----------------------------+ +--------------------------+ +-------------------------------+
*
* T + DELAY => LONG_PRESS_DELAY_MS + DELAY
*
從中我們能簡(jiǎn)單分析到按鈕點(diǎn)擊事件的幾個(gè)狀態(tài)變化:NOT_RESPONDER
-> [RESPONDER_GRANT]
-> RESPONDER_INACTIVE_PRESS_IN
-> [LEAVE_PRESS_RECT]
-> RESPONDER_ACTIVE_PRESS_OUT
曹宴。我們開啟RN的調(diào)試模式利用Chrome
瀏覽器驗(yàn)證一下搂橙。
Touchable touchableHandleResponderGrant
由上述的流程圖我們將斷點(diǎn)加入到當(dāng)前的函數(shù),點(diǎn)擊按鈕(先不釋放),我們看到如下的調(diào)試結(jié)果:
touchableHandleResponderGrant
中的處理:
touchableHandleResponderGrant: function(e) {
var dispatchID = e.currentTarget;
// Since e is used in a callback invoked on another event loop
// (as in setTimeout etc), we need to call e.persist() on the
// event to make sure it doesn't get reused in the event object pool.
// 1.標(biāo)記為已經(jīng)處理苔巨,避免該event被重復(fù)處理
e.persist();
// 2.清理掉pressOutDelayTimeout
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
this.pressOutDelayTimeout = null;
// 3.初始化當(dāng)前的touchState為States.NOT_RESPONDER;
this.state.touchable.touchState = States.NOT_RESPONDER;
this.state.touchable.responderID = dispatchID;
// 4.接收觸發(fā)開始信號(hào),處理邏輯見下
this._receiveSignal(Signals.RESPONDER_GRANT, e);
// 5.設(shè)置點(diǎn)擊事件有效的時(shí)間間隔废离,執(zhí)行_handleDelay函數(shù)
var delayMS =
this.touchableGetHighlightDelayMS !== undefined ?
Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS;
delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS;
if (delayMS !== 0) {
this.touchableDelayTimeout = setTimeout(
this._handleDelay.bind(this, e),
delayMS
);
} else {
this._handleDelay(e);
}
// 6.設(shè)置長(zhǎng)按事件的觸發(fā)時(shí)間間隔侄泽,執(zhí)行_handleLongDelay函數(shù)
var longDelayMS =
this.touchableGetLongPressDelayMS !== undefined ?
Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS;
longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS;
this.longPressDelayTimeout = setTimeout(
this._handleLongDelay.bind(this, e),
longDelayMS + delayMS
);
},
Touchable _receiveSignal
/**
* Receives a state machine signal, performs side effects of the transition
* and stores the new state. Validates the transition as well.
*
* @param {Signals} signal State machine signal.
* @throws Error if invalid state transition or unrecognized signal.
* @sideeffects
*/
_receiveSignal: function(signal, e) {
var responderID = this.state.touchable.responderID;
var curState = this.state.touchable.touchState;
// 1.Transitions是全局維護(hù)的字典:state ->(singal) -> nextState,
// 具體可以自己查看Transitions定義
var nextState = Transitions[curState] && Transitions[curState][signal];
if (!responderID && signal === Signals.RESPONDER_RELEASE) {
return;
}
if (!nextState) {
throw new Error(
'Unrecognized signal `' + signal + '` or state `' + curState +
'` for Touchable responder `' + responderID + '`'
);
}
if (nextState === States.ERROR) {
throw new Error(
'Touchable cannot transition from `' + curState + '` to `' + signal +
'` for responder `' + responderID + '`'
);
}
if (curState !== nextState) {
// 2.根據(jù)state,nextState蜻韭,singal來判斷當(dāng)前的操作狀態(tài),改變按鈕的狀態(tài)悼尾,執(zhí)行相關(guān)回調(diào)
this._performSideEffectsForTransition(curState, nextState, signal, e);
this.state.touchable.touchState = nextState;
}
},
我們?cè)?code>_performSideEffectsForTransition中看到了如下的代碼段:
if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {
var hasLongPressHandler = !!this.props.onLongPress;
var pressIsLongButStillCallOnPress =
IsLongPressingIn[curState] && ( // We *are* long pressing..
!hasLongPressHandler || // But either has no long handler
!this.touchableLongPressCancelsPress() // or we're told to ignore it.
);
var shouldInvokePress = !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress;
if (shouldInvokePress && this.touchableHandlePress) {
if (!newIsHighlight && !curIsHighlight) {
// we never highlighted because of delay, but we should highlight now
this._startHighlight(e);
this._endHighlight(e);
}
// 此處是真正觸發(fā)onPress函數(shù)的調(diào)用
this.touchableHandlePress(e);
}
}
因此我們大膽的猜測(cè),當(dāng)按鈕點(diǎn)擊完成即觸摸離開時(shí)觸發(fā)Signals.RESPONDER_RELEASE
的行為肖方,完成整個(gè)的按鈕點(diǎn)擊的操作闺魏。搜索全局查看到如下代碼段,加入斷點(diǎn)分析俯画。
/**
* Place as callback for a DOM element's `onResponderRelease` event.
*/
touchableHandleResponderRelease: function(e) {
this._receiveSignal(Signals.RESPONDER_RELEASE, e);
},
當(dāng)我們松開按鈕時(shí)析桥,我們看到如下的調(diào)試結(jié)果:進(jìn)而印證了猜想。
Native中的event到j(luò)s端處理
上述的流程分別分析了Native與js端針對(duì)按鈕點(diǎn)擊事件的處理艰垂,尚且留下一個(gè)疑問就是以下這段代碼,即Native中的event到j(luò)s端具體的處理流程是什么泡仗?
RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
reactTag:self.view.reactTag
reactTouches:reactTouches
changedIndexes:changedIndexes
coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:event];
查看了sendEvent:
函數(shù)的實(shí)現(xiàn),按鈕點(diǎn)擊的整個(gè)js的調(diào)用棧如下圖猜憎,有點(diǎn)嚇到娩怎,RN中的額event
的設(shè)計(jì)也不是幾句話分析清楚的,后續(xù)將寫一篇博文重點(diǎn)介紹這一塊的設(shè)計(jì)拉宗。本文就只需要知道Native的按鈕觸摸的信息是通過事件(event
)的方式傳送給js端的就行了峦树,當(dāng)然你有興趣也可以自己研究。
整個(gè)React Native中按鈕的設(shè)計(jì)到具體實(shí)現(xiàn)基本告一段落旦事,其中部分細(xì)節(jié)未展開分析魁巩,包括按鈕高亮狀態(tài),禁用狀態(tài)姐浮,長(zhǎng)按事件等谷遂,有興趣可以自己分析。后續(xù)將繼續(xù)展開分析event
的實(shí)現(xiàn)卖鲤,歡迎關(guān)注肾扰。文章中有錯(cuò)誤的地方歡迎指正,謝謝蛋逾。