[图片上传中...(pic1.png-835f42-1510063522595-0)]
iOS与安卓的原生实现实际都已经提供了UIButton
与Button
的的按钮组件。但是我们发现RN并没有基于这两个组件实现统一的按钮组件。那如何在RN中实现按钮组件呢?其内部实现又是如何处理的?本文将从设计按钮,使用按钮,具体实现,三部分解析整个按钮的实现逻辑。
依赖
- RN的版本 0.38
- 系统:iOS
按钮设计
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('按钮点击');
}
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;
主要想分析按钮实现,因此该Demo例子只是简单满足按钮的要求。
TouchableHighlight及相关组件
我们查看上述例子中的TouchableHighlight
的实现代码中以下这一段,由此可以推断是在TouchableWithoutFeedback
的基础上做的扩展。
var TouchableHighlight = React.createClass({
propTypes: {
...TouchableWithoutFeedback.propTypes,
}
});
我们看看TouchableWithoutFeedback
以及其他几种扩展的效果:
组件 | 描述 | 效果图 |
---|---|---|
TouchableWithoutFeedback | 响应点击事件,无任何反馈 | |
TouchableHighlight | 点击状态背景变暗 | |
TouchableOpacity | 点击状态改变背景的透明度 | |
TouchableNativeFeedback | 此组件只支持Android,不作分析 | - |
组件API的调用此处就不作具体介绍,可以查看React Native的官方文档
Native与jS端按钮一块的交互逻辑
下面我们来具体分析RN中按钮的内部实现,从Native切入考虑。想到iOS端能处理手势事件的类--UIGestureRecognizer
。
我们进入到node_modules
目录执行grep "UIGestureRecognizer" -rn .
得到如下结果:
不难看出RN中继承UIGestureRecognizer
实现了自己的手势处理的派生类RCTTouchHandler
。按钮的点击功能的实现便从RCTTouchHandler
开始分析。
Native端的处理
RCTTouchHandler入口
全局搜索RCTTouchHandler
我们发现RN页面的承载容器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手势实例初始化完成,然后添加到contentView上,这样contentView便可以处理手势事件了
_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);
// 初始化绑定事件的处理函数
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;
}
由此得出按钮的点击必将触发handleGestureUpdate
函数。
RCTTouchHandler中手势处理
我们都知道手势触发会先执行如下的函数:
- (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;
因此我们在RCTTouchHandler
中的handleGestureUpdate
函数以及上述函数中加入断点调试分析。根据代码执行顺序来看一下具体实现逻辑。
开始触摸topTouchStart
代码执行流程:
// 1.点击按钮时触发
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
// 2.记录点击view,点击事件touch,点击事件对应的reactTouch
- (void)_recordNewTouches:(NSSet<UITouch *> *)touches
// 3.监听手势触发的函数
- (void)handleGestureUpdate:(__unused UIGestureRecognizer *)gesture
// 4.更新reactTouch,生成touchEvent,使用_eventDispatcher事件将该触发事件发送给js端
- (void)_updateAndDispatchTouches:(NSSet<UITouch *> *)touches
eventName:(NSString *)eventName
originatingTime:(__unused CFTimeInterval)originatingTime
// 5.将native统计的touch信息同步到react的touch中
- (void)_updateReactTouchAtIndex:(NSInteger)touchIndex
// 6.将touch通过发送事件的方式通知给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
的唯一标识。是由js端的ReactNativeTagHandles.allocateTag()
生成,有兴趣可以自己研究,此处不作扩展分析。
topTouchEnd
的流程与上述的类似不作额外分析。
js端的处理
Touchable手势处理类
上面我们看了Native端是如何触发按钮点击事件的,如何将native的触摸事件传给js端。下面我们将从js继续分析按钮点击事件的整个流程: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);
},
显然的按钮的点击事件onPress
实际是触发Touchable
中的onPress
函数的执行。下面我们具体看看Touchable
的处理逻辑。
Touchable手势处理流程图
* ======= 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
*
从中我们能简单分析到按钮点击事件的几个状态变化:NOT_RESPONDER
-> [RESPONDER_GRANT]
-> RESPONDER_INACTIVE_PRESS_IN
-> [LEAVE_PRESS_RECT]
-> RESPONDER_ACTIVE_PRESS_OUT
。我们开启RN的调试模式利用Chrome
浏览器验证一下。
Touchable touchableHandleResponderGrant
由上述的流程图我们将断点加入到当前的函数,点击按钮(先不释放),我们看到如下的调试结果:
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.标记为已经处理,避免该event被重复处理
e.persist();
// 2.清理掉pressOutDelayTimeout
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
this.pressOutDelayTimeout = null;
// 3.初始化当前的touchState为States.NOT_RESPONDER;
this.state.touchable.touchState = States.NOT_RESPONDER;
this.state.touchable.responderID = dispatchID;
// 4.接收触发开始信号,处理逻辑见下
this._receiveSignal(Signals.RESPONDER_GRANT, e);
// 5.设置点击事件有效的时间间隔,执行_handleDelay函数
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.设置长按事件的触发时间间隔,执行_handleLongDelay函数
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是全局维护的字典: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.根据state,nextState,singal来判断当前的操作状态,改变按钮的状态,执行相关回调
this._performSideEffectsForTransition(curState, nextState, signal, e);
this.state.touchable.touchState = nextState;
}
},
我们在_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);
}
// 此处是真正触发onPress函数的调用
this.touchableHandlePress(e);
}
}
因此我们大胆的猜测,当按钮点击完成即触摸离开时触发Signals.RESPONDER_RELEASE
的行为,完成整个的按钮点击的操作。搜索全局查看到如下代码段,加入断点分析。
/**
* Place as callback for a DOM element's `onResponderRelease` event.
*/
touchableHandleResponderRelease: function(e) {
this._receiveSignal(Signals.RESPONDER_RELEASE, e);
},
当我们松开按钮时,我们看到如下的调试结果:进而印证了猜想。
Native中的event到js端处理
上述的流程分别分析了Native与js端针对按钮点击事件的处理,尚且留下一个疑问就是以下这段代码,即Native中的event到js端具体的处理流程是什么?
RCTTouchEvent *event = [[RCTTouchEvent alloc] initWithEventName:eventName
reactTag:self.view.reactTag
reactTouches:reactTouches
changedIndexes:changedIndexes
coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:event];
查看了sendEvent:
函数的实现,按钮点击的整个js的调用栈如下图,有点吓到,RN中的额event
的设计也不是几句话分析清楚的,后续将写一篇博文重点介绍这一块的设计。本文就只需要知道Native的按钮触摸的信息是通过事件(event
)的方式传送给js端的就行了,当然你有兴趣也可以自己研究。
整个React Native中按钮的设计到具体实现基本告一段落,其中部分细节未展开分析,包括按钮高亮状态,禁用状态,长按事件等,有兴趣可以自己分析。后续将继续展开分析event
的实现,欢迎关注。文章中有错误的地方欢迎指正,谢谢。