在前兩篇文章裏面分別談了Weex如何在Native端初始化的和Weex是如何高效的渲染Native的原生UI的。Native這邊還缺一塊,那就是Native產生的一些事件,是怎麼傳回給JS的。這篇文章就詳細分析這一部分。javascript
在Weex中,目前最新版本中事件總共分爲4種類型,通用事件,Appear 事件,Disappear 事件,Page 事件。css
在Weex的組件裏面只包含前三種事件,即通用事件,Appear 事件,Disappear 事件。java
當WXComponent添加事件的時候,會調用如下函數:數組
- (void)_addEventOnMainThread:(NSString *)addEventName
{
WX_ADD_EVENT(appear, addAppearEvent)
WX_ADD_EVENT(disappear, addDisappearEvent)
WX_ADD_EVENT(click, addClickEvent)
WX_ADD_EVENT(swipe, addSwipeEvent)
WX_ADD_EVENT(longpress, addLongPressEvent)
WX_ADD_EVENT(panstart, addPanStartEvent)
WX_ADD_EVENT(panmove, addPanMoveEvent)
WX_ADD_EVENT(panend, addPanEndEvent)
WX_ADD_EVENT(horizontalpan, addHorizontalPanEvent)
WX_ADD_EVENT(verticalpan, addVerticalPanEvent)
WX_ADD_EVENT(touchstart, addTouchStartEvent)
WX_ADD_EVENT(touchmove, addTouchMoveEvent)
WX_ADD_EVENT(touchend, addTouchEndEvent)
WX_ADD_EVENT(touchcancel, addTouchCancelEvent)
[self addEvent:addEventName];
}複製代碼
WX_ADD_EVENT是一個宏:weex
#define WX_ADD_EVENT(eventName, addSelector) \
if ([addEventName isEqualToString:@#eventName]) {\
[self addSelector];\
}複製代碼
便是判斷待添加的事件addEventName的名字和默認支持的事件名字eventName是否一致,若是一致,就執行addSelector方法。閉包
最後會執行一個addEvent:方法,每一個組件裏面會能夠重寫這個方法。在這個方法裏面作的就是對組件的狀態的標識。app
好比WXWebComponent組件裏面的addEvent:方法:dom
- (void)addEvent:(NSString *)eventName
{
if ([eventName isEqualToString:@"pagestart"]) {
_startLoadEvent = YES;
}
else if ([eventName isEqualToString:@"pagefinish"]) {
_finishLoadEvent = YES;
}
else if ([eventName isEqualToString:@"error"]) {
_failLoadEvent = YES;
}
}複製代碼
在這個方法裏面即對Web組件裏面的狀態進行了標識。ide
接下來就看看這幾個組件是怎麼識別事件的觸發的。函數
在WXComponent的定義裏,定義了以下和事件相關的變量:
@interface WXComponent ()
{
@package
BOOL _appearEvent;
BOOL _disappearEvent;
UITapGestureRecognizer *_tapGesture;
NSMutableArray *_swipeGestures;
UILongPressGestureRecognizer *_longPressGesture;
UIPanGestureRecognizer *_panGesture;
BOOL _listenPanStart;
BOOL _listenPanMove;
BOOL _listenPanEnd;
BOOL _listenHorizontalPan;
BOOL _listenVerticalPan;
WXTouchGestureRecognizer* _touchGesture;
}複製代碼
上述變量裏面就包含有4個手勢識別器和1個自定義手勢識別器。因此Weex的通用事件裏面就包含這5種,點擊事件,輕掃事件,長按事件,拖動事件,通用觸摸事件。
首先看點擊事件:
WX_ADD_EVENT(click, addClickEvent)複製代碼
點擊事件是經過上面這個宏加到指定視圖上的。這個宏上面提到過了。這裏直接把宏展開
#define WX_ADD_EVENT(click, addClickEvent) \
if ([addEventName isEqualToString:@「click」]) {\
[self addClickEvent];\
}複製代碼
若是addEventName傳進來event的是@「click」,那麼就是執行addClickEvent方法。
- (void)addClickEvent
{
if (!_tapGesture) {
_tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onClick:)];
_tapGesture.delegate = self;
[self.view addGestureRecognizer:_tapGesture];
}
}複製代碼
給當前的視圖增長一個點擊手勢,觸發的方法是onClick:方法。
- (void)onClick:(__unused UITapGestureRecognizer *)recognizer
{
NSMutableDictionary *position = [[NSMutableDictionary alloc] initWithCapacity:4];
CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
if (!CGRectEqualToRect(self.calculatedFrame, CGRectZero)) {
CGRect frame = [self.view.superview convertRect:self.calculatedFrame toView:self.view.window];
position[@"x"] = @(frame.origin.x/scaleFactor);
position[@"y"] = @(frame.origin.y/scaleFactor);
position[@"width"] = @(frame.size.width/scaleFactor);
position[@"height"] = @(frame.size.height/scaleFactor);
}
[self fireEvent:@"click" params:@{@"position":position}];
}複製代碼
一旦用戶點擊屏幕,就會觸發點擊手勢,點擊手勢就會執行上述的onClick:方法。在這個方法中,Weex會計算點擊出點擊到的視圖的座標以及寬高尺寸。
說到這裏就須要提到Weex的座標計算方法了。
在平常iOS開發中,開發者使用的計算單位是pt。
iPhone5分辨率320pt x 568pt
iPhone6分辨率375pt x 667pt
iPhone6 Plus分辨率414pt x 736pt
因爲每一個屏幕的ppi不一樣(ppi:Pixels Per Inch,即每英寸所擁有的像素數目,屏幕像素密度。),最終會致使分辨率的不一樣。
這也就是咱們平常說的@1x,@2x,@3x,目前iPhone手機也就3種ppi
@1x,163ppi(iPhone3gs)
@2x,326ppi(iPhone四、4s、五、5s、6,6s,7)
@3x,401ppi(iPhone6+、6s+、7+)
px即pixels像素,1px表明屏幕上一個物理的像素點。
iPhone5像素640px x 1136px
iPhone6像素750px x 1334px
iPhone6 Plus像素1242px x 2208px
而Weex的開發中,目前都是用的px,並且Weex 對於長度值目前只支持像素px值,還不支持相對單位(em、rem)。
那麼就須要pt和px的換算了。
在Weex的世界裏,定義了一個默認屏幕尺寸,用來適配iOS,Android各類不一樣大小的屏幕。
// The default screen width which helps us to calculate the real size or scale in different devices.
static const CGFloat WXDefaultScreenWidth = 750.0;複製代碼
在Weex中定義的默認的屏幕寬度是750,注意是寬度。
+ (CGFloat)defaultPixelScaleFactor
{
static CGFloat defaultScaleFactor;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultScaleFactor = [self portraitScreenSize].width / WXDefaultScreenWidth;
});
return defaultScaleFactor;
}複製代碼
這裏計算了一個默認的縮放比例因子,portraitScreenSize裏面計算出了屏幕在portrait方向下的大小,即若是方向是landscape,那麼縮放比例因子應該等於WXScreenSize().height / WXDefaultScreenWidth,反之應該等於WXScreenSize().width / WXDefaultScreenWidth。
這裏計算的是pt。
iPhone 四、4s、五、5s、5c、SE的比例因子是0.42666667
iPhone 六、6s、7比例因子是0.5
iPhone 6+、6s+、7+比例因子是0.552
計算視圖的縮放尺寸主要在這個方法裏面被計算。
- (void)_calculateFrameWithSuperAbsolutePosition:(CGPoint)superAbsolutePosition
gatherDirtyComponents:(NSMutableSet<WXComponent *> *)dirtyComponents
{
if (!_cssNode->layout.should_update) {
return;
}
_cssNode->layout.should_update = false;
_isLayoutDirty = NO;
// 計算視圖的Frame
CGRect newFrame = CGRectMake(WXRoundPixelValue(_cssNode->layout.position[CSS_LEFT]),
WXRoundPixelValue(_cssNode->layout.position[CSS_TOP]),
WXRoundPixelValue(_cssNode->layout.dimensions[CSS_WIDTH]),
WXRoundPixelValue(_cssNode->layout.dimensions[CSS_HEIGHT]));
BOOL isFrameChanged = NO;
// 比較newFrame和_calculatedFrame,第一次_calculatedFrame爲CGRectZero
if (!CGRectEqualToRect(newFrame, _calculatedFrame)) {
isFrameChanged = YES;
_calculatedFrame = newFrame;
[dirtyComponents addObject:self];
}
CGPoint newAbsolutePosition = [self computeNewAbsolutePosition:superAbsolutePosition];
_cssNode->layout.dimensions[CSS_WIDTH] = CSS_UNDEFINED;
_cssNode->layout.dimensions[CSS_HEIGHT] = CSS_UNDEFINED;
_cssNode->layout.position[CSS_LEFT] = 0;
_cssNode->layout.position[CSS_TOP] = 0;
[self _frameDidCalculated:isFrameChanged];
for (WXComponent *subcomponent in _subcomponents) {
[subcomponent _calculateFrameWithSuperAbsolutePosition:newAbsolutePosition gatherDirtyComponents:dirtyComponents];
}
}複製代碼
newFrame就是計算出來的縮放過的Frame。
若是嘗試本身手動計算Vue.js上設置的px與實際的視圖座標值相比,你會發現永遠都差一點,雖然誤差很少,可是總有偏差,緣由在哪裏呢?就在WXRoundPixelValue這個函數裏面。
CGFloat WXRoundPixelValue(CGFloat value)
{
CGFloat scale = WXScreenScale();
return round(value * scale) / scale;
}複製代碼
WXRoundPixelValue這個函數裏面進行了一次四捨五入的計算,這裏會對精度有所損失,因此就會致使最終Native的組件的座標會誤差一點。
舉個例子:
<style>
.pic{
width: 200px;
height: 200px;
margin-top: 100px;
left: 200px;
background-color: #a88859;
}
</style>複製代碼
這裏是一個imageComponent,座標是距離上邊距100px,距離左邊距200px,寬200px,高200px。
假設咱們是在iPhone 7+的屏幕上,ppi對應的應該是scale = 3(即@3x)。
按照Weex的上述的計算方法算,那麼對應縮放的px爲:
x = 200 * ( 414.0 / 750.0 ) = 110.400000
y = 100 * ( 414.0 / 750.0 ) = 55.200000
width = 200 * ( 414.0 / 750.0 ) = 110.400000
height = 200 * ( 414.0 / 750.0 ) = 110.400000複製代碼
再轉換成pt:
x = round ( 110.400000 * 3 ) / 3 = 110.333333
y = round ( 55.200000 * 3 ) / 3 = 55.333333
width = round ( 110.400000 * 3 ) / 3 = 110.333333
height = round ( 110.400000 * 3 ) / 3 = 110.333333複製代碼
若是隻是單純的認爲是針對750的成比縮放,那麼這裏110.333333 / ( 414.0 / 750.0 ) = 199.87922101,你會發現這個數字距離200仍是差了零點幾。精度就是損失在了round函數上了
那麼當前的imageComponent在父視圖裏面的Frame = (110.333333,55.333333,110.333333,110.333333)。
回到onClick:方法裏面。
- (void)onClick:(__unused UITapGestureRecognizer *)recognizer
{
NSMutableDictionary *position = [[NSMutableDictionary alloc] initWithCapacity:4];
CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
if (!CGRectEqualToRect(self.calculatedFrame, CGRectZero)) {
CGRect frame = [self.view.superview convertRect:self.calculatedFrame toView:self.view.window];
position[@"x"] = @(frame.origin.x/scaleFactor);
position[@"y"] = @(frame.origin.y/scaleFactor);
position[@"width"] = @(frame.size.width/scaleFactor);
position[@"height"] = @(frame.size.height/scaleFactor);
}
[self fireEvent:@"click" params:@{@"position":position}];
}複製代碼
若是點擊到視圖,就會觸發點擊手勢的處理方法,就會進入到上述方法裏。
這裏會計算出點擊到的視圖相對於window的絕對座標。
CGRect frame = [self.view.superview convertRect:self.calculatedFrame toView:self.view.window];複製代碼
上面這句話會進行一個座標轉換。座標系轉換到全局的window的左邊。
仍是按照上面舉的例子,若是imageComponent通過轉換之後,frame = (110.33333333333333, 119.33333333333334, 110.33333333333333, 110.33333333333331),這裏就是y軸的距離發生了變化,由於就加上了navigation + statusBar 的64的高度。
計算出了這個window絕對座標以後,還要還原成相對於750.0寬度的「尺寸」。這裏之因此打引號,就是由於這裏有精度損失,在round函數那裏丟了一些精度。
x = 110.33333333333333 / ( 414.0 / 750.0 ) = 199.8792270531401
y = 119.33333333333334 / ( 414.0 / 750.0 ) = 216.1835748792271
width = 110.33333333333333 / ( 414.0 / 750.0 ) = 199.8792270531401
height = 110.33333333333333 / ( 414.0 / 750.0 ) = 199.8792270531401複製代碼
上述就是點擊之後通過轉換最終獲得的座標,這個座標會傳遞給JS。
接着是輕掃事件。
WX_ADD_EVENT(swipe, addSwipeEvent)複製代碼
這個宏和上面點擊事件的展開原理同樣,這裏再也不贅述。
若是addEventName傳進來event的是@「swipe」,那麼就是執行addSwipeEvent方法。
- (void)addSwipeEvent
{
if (_swipeGestures) {
return;
}
_swipeGestures = [NSMutableArray arrayWithCapacity:4];
// 下面的代碼寫的比較「奇怪」,緣由在於UISwipeGestureRecognizer的direction屬性,是一個可選的位掩碼,可是每一個手勢識別器又只能處理一個方向的手勢,因此就致使了下面須要生成四個UISwipeGestureRecognizer的手勢識別器。
SEL selector = @selector(onSwipe:);
// 新建一個upSwipeRecognizer
UISwipeGestureRecognizer *upSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:selector];
upSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionUp;
upSwipeRecognizer.delegate = self;
[_swipeGestures addObject:upSwipeRecognizer];
[self.view addGestureRecognizer:upSwipeRecognizer];
// 新建一個downSwipeRecognizer
UISwipeGestureRecognizer *downSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:selector];
downSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
downSwipeRecognizer.delegate = self;
[_swipeGestures addObject:downSwipeRecognizer];
[self.view addGestureRecognizer:downSwipeRecognizer];
// 新建一個rightSwipeRecognizer
UISwipeGestureRecognizer *rightSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:selector];
rightSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
rightSwipeRecognizer.delegate = self;
[_swipeGestures addObject:rightSwipeRecognizer];
[self.view addGestureRecognizer:rightSwipeRecognizer];
// 新建一個leftSwipeRecognizer
UISwipeGestureRecognizer *leftSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:selector];
leftSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
leftSwipeRecognizer.delegate = self;
[_swipeGestures addObject:leftSwipeRecognizer];
[self.view addGestureRecognizer:leftSwipeRecognizer];
}複製代碼
上面會新建4個方向上的手勢識別器。由於每一個手勢識別器又只能處理一個方向的手勢,因此就致使了須要生成四個UISwipeGestureRecognizer的手勢識別器。
給當前的視圖增長一個輕掃手勢,觸發的方法是onSwipe:方法。
- (void)onSwipe:(UISwipeGestureRecognizer *)gesture
{
UISwipeGestureRecognizerDirection direction = gesture.direction;
NSString *directionString;
switch(direction) {
case UISwipeGestureRecognizerDirectionLeft:
directionString = @"left";
break;
case UISwipeGestureRecognizerDirectionRight:
directionString = @"right";
break;
case UISwipeGestureRecognizerDirectionUp:
directionString = @"up";
break;
case UISwipeGestureRecognizerDirectionDown:
directionString = @"down";
break;
default:
directionString = @"unknown";
}
CGPoint screenLocation = [gesture locationInView:self.view.window];
CGPoint pageLoacation = [gesture locationInView:self.weexInstance.rootView];
NSDictionary *resultTouch = [self touchResultWithScreenLocation:screenLocation pageLocation:pageLoacation identifier:gesture.wx_identifier];
[self fireEvent:@"swipe" params:@{@"direction":directionString, @"changedTouches":resultTouch ? @[resultTouch] : @[]}];
}複製代碼
當用戶輕掃之後,會觸發輕掃手勢,因而會在window上和rootView上會獲取到2個座標。
- (NSDictionary *)touchResultWithScreenLocation:(CGPoint)screenLocation pageLocation:(CGPoint)pageLocation identifier:(NSNumber *)identifier
{
NSMutableDictionary *resultTouch = [[NSMutableDictionary alloc] initWithCapacity:5];
CGFloat scaleFactor = self.weexInstance.pixelScaleFactor;
resultTouch[@"screenX"] = @(screenLocation.x/scaleFactor);
resultTouch[@"screenY"] = @(screenLocation.y/scaleFactor);
resultTouch[@"pageX"] = @(pageLocation.x/scaleFactor);
resultTouch[@"pageY"] = @(pageLocation.y/scaleFactor);
resultTouch[@"identifier"] = identifier;
return resultTouch;
}複製代碼
screenLocation和pageLocation兩個座標點,仍是會根據縮放比例還原成相對於750寬度的頁面的座標。screenLocation的X值和Y值、pageLocation的X值和Y值分別封裝到resultTouch字典裏。
@implementation UIGestureRecognizer (WXGesture)
- (NSNumber *)wx_identifier
{
NSNumber *identifier = objc_getAssociatedObject(self, _cmd);
if (!identifier) {
static NSUInteger _gestureIdentifier;
identifier = @(_gestureIdentifier++);
self.wx_identifier = identifier;
}
return identifier;
}
- (void)setWx_identifier:(NSNumber *)wx_identifier
{
objc_setAssociatedObject(self, @selector(wx_identifier), wx_identifier, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end複製代碼
最後resultTouch裏面還包含一個identifier的參數,這個identifier是一個全局惟一的NSUInteger。wx_identifier被關聯到了各個手勢識別器上了。
接着是輕掃事件。
WX_ADD_EVENT(longpress, addLongPressEvent)複製代碼
這個宏和上面點擊事件的展開原理同樣,這裏再也不贅述。
若是addEventName傳進來event的是@「longpress」,那麼就是執行addLongPressEvent方法。
- (void)addLongPressEvent
{
if (!_longPressGesture) {
_longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onLongPress:)];
_longPressGesture.delegate = self;
[self.view addGestureRecognizer:_longPressGesture];
}
}複製代碼
給當前的視圖增長一個長按手勢,觸發的方法是onLongPress:方法。
- (void)onLongPress:(UILongPressGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan) {
CGPoint screenLocation = [gesture locationInView:self.view.window];
CGPoint pageLoacation = [gesture locationInView:self.weexInstance.rootView];
NSDictionary *resultTouch = [self touchResultWithScreenLocation:screenLocation pageLocation:pageLoacation identifier:gesture.wx_identifier];
[self fireEvent:@"longpress" params:@{@"changedTouches":resultTouch ? @[resultTouch] : @[]}];
} else if (gesture.state == UIGestureRecognizerStateEnded) {
gesture.wx_identifier = nil;
}
}複製代碼
長按手勢傳給JS的參數和輕掃的參數changedTouches幾乎一致。在長按手勢開始的時候就傳遞給JS兩個Point,screenLocation和pageLoacation,以及手勢的wx_identifier。這部分和輕掃手勢基本同樣,很少贅述。
拖動事件在Weex裏面包含5個事件。分別對應着拖動的5種狀態:拖動開始,拖動中,拖動結束,水平拖動,豎直拖動。
WX_ADD_EVENT(panstart, addPanStartEvent)
WX_ADD_EVENT(panmove, addPanMoveEvent)
WX_ADD_EVENT(panend, addPanEndEvent)
WX_ADD_EVENT(horizontalpan, addHorizontalPanEvent)
WX_ADD_EVENT(verticalpan, addVerticalPanEvent)複製代碼
爲了區分上面5種狀態,Weex還對每一個狀態增長了一個BOOL變量來判斷當前的狀態。分別以下:
BOOL _listenPanStart;
BOOL _listenPanMove;
BOOL _listenPanEnd;
BOOL _listenHorizontalPan;
BOOL _listenVerticalPan;複製代碼
經過宏增長的5個事件,實質都是執行了addPanGesture方法,只不過每一個狀態的事件都會跟對應的BOOL變量。
- (void)addPanStartEvent
{
// 拖動開始
_listenPanStart = YES;
[self addPanGesture];
}
- (void)addPanMoveEvent
{
// 拖動中
_listenPanMove = YES;
[self addPanGesture];
}
- (void)addPanEndEvent
{
// 拖動結束
_listenPanEnd = YES;
[self addPanGesture];
}
- (void)addHorizontalPanEvent
{
// 水平拖動
_listenHorizontalPan = YES;
[self addPanGesture];
}
- (void)addVerticalPanEvent
{
// 豎直拖動
_listenVerticalPan = YES;
[self addPanGesture];
}複製代碼
最終都是調用addPanGesture方法:
- (void)addPanGesture
{
if (!_panGesture) {
_panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPan:)];
_panGesture.delegate = self;
[self.view addGestureRecognizer:_panGesture];
}
}複製代碼
給當前的視圖增長一個拖動手勢,觸發的方法是onPan:方法。
- (void)onPan:(UIPanGestureRecognizer *)gesture
{
CGPoint screenLocation = [gesture locationInView:self.view.window];
CGPoint pageLoacation = [gesture locationInView:self.weexInstance.rootView];
NSString *eventName;
NSString *state = @"";
NSDictionary *resultTouch = [self touchResultWithScreenLocation:screenLocation pageLocation:pageLoacation identifier:gesture.wx_identifier];
if (gesture.state == UIGestureRecognizerStateBegan) {
if (_listenPanStart) {
eventName = @"panstart";
}
state = @"start";
} else if (gesture.state == UIGestureRecognizerStateEnded) {
if (_listenPanEnd) {
eventName = @"panend";
}
state = @"end";
gesture.wx_identifier = nil;
} else if (gesture.state == UIGestureRecognizerStateChanged) {
if (_listenPanMove) {
eventName = @"panmove";
}
state = @"move";
}
CGPoint translation = [_panGesture translationInView:self.view];
if (_listenHorizontalPan && fabs(translation.y) <= fabs(translation.x))="" {="" [self="" fireevent:@"horizontalpan"="" params:@{@"state":state,="" @"changedtouches":resulttouch="" ?="" @[resulttouch]="" :="" @[]}];="" }="" if="" (_listenverticalpan="" &&="" fabs(translation.y)=""> fabs(translation.x)) {
[self fireEvent:@"verticalpan" params:@{@"state":state, @"changedTouches":resultTouch ? @[resultTouch] : @[]}];
}
if (eventName) {
[self fireEvent:eventName params:@{@"changedTouches":resultTouch ? @[resultTouch] : @[]}];
}
}
複製代碼
拖動事件最終傳給JS的resultTouch字典和前兩個手勢的原理同樣,也是須要傳入兩個Point,screenLocation和pageLoacation,這裏再也不贅述。
根據_listenPanStart,_listenPanEnd,_listenPanMove判斷當前的狀態,並生成與之對應的eventName和state字符串。
根據_panGesture在當前視圖上拖動造成的有方向的向量,進行判斷當前拖動的方向。
最後就是通用的觸摸事件。
Weex裏面對每一個Component都新建了一個手勢識別器。
@interface WXTouchGestureRecognizer : UIGestureRecognizer
@property (nonatomic, assign) BOOL listenTouchStart;
@property (nonatomic, assign) BOOL listenTouchMove;
@property (nonatomic, assign) BOOL listenTouchEnd;
@property (nonatomic, assign) BOOL listenTouchCancel;
@property (nonatomic, assign) BOOL listenPseudoTouch;
{
__weak WXComponent *_component;
NSUInteger _touchIdentifier;
}
- (instancetype)initWithComponent:(WXComponent *)component NS_DESIGNATED_INITIALIZER;
@end複製代碼
WXTouchGestureRecognizer是繼承自UIGestureRecognizer。裏面就5個BOOL。分別表示5種狀態。
WXTouchGestureRecognizer會弱引用當前的WXComponent,而且也依舊有touchIdentifier。
Weex經過如下4個宏註冊觸摸事件方法。
WX_ADD_EVENT(touchstart, addTouchStartEvent)
WX_ADD_EVENT(touchmove, addTouchMoveEvent)
WX_ADD_EVENT(touchend, addTouchEndEvent)
WX_ADD_EVENT(touchcancel, addTouchCancelEvent)複製代碼
經過上述宏增長的4個事件,實質都是改變每一個狀態的事件都會跟對應的BOOL變量。
- (void)addTouchStartEvent
{
self.touchGesture.listenTouchStart = YES;
}
- (void)addTouchMoveEvent
{
self.touchGesture.listenTouchMove = YES;
}
- (void)addTouchEndEvent
{
self.touchGesture.listenTouchEnd = YES;
}
- (void)addTouchCancelEvent
{
self.touchGesture.listenTouchCancel = YES;
}複製代碼
當用戶開始觸摸屏幕,在屏幕上移動,手指從屏幕上結束觸摸,取消觸摸,分別都會觸發touchesBegan:,touchesMoved:,touchesEnded:,touchesCancelled:方法。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
if (_listenTouchStart) {
[self fireTouchEvent:@"touchstart" withTouches:touches];
}
if(_listenPseudoTouch) {
NSMutableDictionary *styles = [_component getPseudoClassStyles:@"active"];
[_component updatePseudoClassStyles:styles];
}
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
if (_listenTouchMove) {
[self fireTouchEvent:@"touchmove" withTouches:touches];
}
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
if (_listenTouchEnd) {
[self fireTouchEvent:@"touchend" withTouches:touches];
}
if(_listenPseudoTouch) {
[self recoveryPseudoStyles:_component.styles];
}
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesCancelled:touches withEvent:event];
if (_listenTouchCancel) {
[self fireTouchEvent:@"touchcancel" withTouches:touches];
}
if(_listenPseudoTouch) {
[self recoveryPseudoStyles:_component.styles];
}
}複製代碼
上述的4個事件裏面實質都是在調用fireTouchEvent:withTouches:方法:
- (void)fireTouchEvent:(NSString *)eventName withTouches:(NSSet<UITouch *> *)touches
{
NSMutableArray *resultTouches = [NSMutableArray new];
for (UITouch *touch in touches) {
CGPoint screenLocation = [touch locationInView:touch.window];
CGPoint pageLocation = [touch locationInView:_component.weexInstance.rootView];
if (!touch.wx_identifier) {
touch.wx_identifier = @(_touchIdentifier++);
}
NSDictionary *resultTouch = [_component touchResultWithScreenLocation:screenLocation pageLocation:pageLocation identifier:touch.wx_identifier];
[resultTouches addObject:resultTouch];
}
[_component fireEvent:eventName params:@{@"changedTouches":resultTouches ?: @[]}];
}複製代碼
最終這個方法和前3個手勢同樣,都須要給resultTouches傳入2個Point和1個wx_identifier。原理一致。
至於座標如何傳遞給JS見第二章。
若是一個位於某個可滾動區域內的組件被綁定了 appear 事件,那麼當這個組件的狀態變爲在屏幕上可見時,該事件將被觸發。
因此綁定了Appear 事件的都是能夠滾動的視圖。
WX_ADD_EVENT(appear, addAppearEvent)複製代碼
經過上述的宏給能夠滾動的視圖增長Appear 事件。也就是當前視圖執行addAppearEvent方法。
- (void)addAppearEvent
{
_appearEvent = YES;
[self.ancestorScroller addScrollToListener:self];
}複製代碼
在Weex的每一個組件裏面都有2個BOOL記錄着當前_appearEvent和_disappearEvent的狀態。
BOOL _appearEvent;
BOOL _disappearEvent;複製代碼
當增長對應的事件的時候,就會把對應的BOOL變成YES。
- (id<WXScrollerProtocol>)ancestorScroller
{
if(!_ancestorScroller) {
WXComponent *supercomponent = self.supercomponent;
while (supercomponent) {
if([supercomponent conformsToProtocol:@protocol(WXScrollerProtocol)]) {
_ancestorScroller = (id<WXScrollerProtocol>)supercomponent;
break;
}
supercomponent = supercomponent.supercomponent;
}
}
return _ancestorScroller;
}複製代碼
因爲Appear 事件和 Disappear 事件都必需要求是滾動視圖,因此這裏會遍歷當前視圖的supercomponent,直到找到一個遵循WXScrollerProtocol的supercomponent。
- (void)addScrollToListener:(WXComponent *)target
{
BOOL has = NO;
for (WXScrollToTarget *targetData in self.listenerArray) {
if (targetData.target == target) {
has = YES;
break;
}
}
if (!has) {
WXScrollToTarget *scrollTarget = [[WXScrollToTarget alloc] init];
scrollTarget.target = target;
scrollTarget.hasAppear = NO;
[self.listenerArray addObject:scrollTarget];
}
}複製代碼
在滾動視圖裏麪包含有一個listenerArray,數組裏面裝的都是被監聽的對象。添加進這個數組會先判斷當前是否有相同的WXScrollToTarget,避免重複添加,若是沒有重複的就新建一個WXScrollToTarget,再添加進listenerArray中。
@interface WXScrollToTarget : NSObject
@property (nonatomic, weak) WXComponent *target;
@property (nonatomic, assign) BOOL hasAppear;
@end複製代碼
WXScrollToTarget是一個普通的對象,裏面弱引用了當前須要監聽的WXComponent,以及一個BOOL變量記錄當前是否Appear了。
當滾動視圖滾動的時候,就會觸發scrollViewDidScroll:方法。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//apply block which are registered
WXSDKInstance *instance = self.weexInstance;
if ([self.ref isEqualToString:WX_SDK_ROOT_REF] &&
[self isKindOfClass:[WXScrollerComponent class]]) {
if (instance.onScroll) {
instance.onScroll(scrollView.contentOffset);
}
}
if (_lastContentOffset.x > scrollView.contentOffset.x) {
_direction = @"right";
} else if (_lastContentOffset.x < scrollView.contentOffset.x) {
_direction = @"left";
} else if(_lastContentOffset.y > scrollView.contentOffset.y) {
_direction = @"down";
} else if(_lastContentOffset.y < scrollView.contentOffset.y) {
_direction = @"up";
}
_lastContentOffset = scrollView.contentOffset;
// check sticky
[self adjustSticky];
[self handleLoadMore];
[self handleAppear];
if (self.onScroll) {
self.onScroll(scrollView);
}
}複製代碼
在上面的方法中[self handleAppear]就是觸發了判斷是否Appear了。
- (void)handleAppear
{
if (![self isViewLoaded]) {
return;
}
UIScrollView *scrollView = (UIScrollView *)self.view;
CGFloat vx = scrollView.contentInset.left + scrollView.contentOffset.x;
CGFloat vy = scrollView.contentInset.top + scrollView.contentOffset.y;
CGFloat vw = scrollView.frame.size.width - scrollView.contentInset.left - scrollView.contentInset.right;
CGFloat vh = scrollView.frame.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom;
CGRect scrollRect = CGRectMake(vx, vy, vw, vh);;
// notify action for appear
for(WXScrollToTarget *target in self.listenerArray){
[self scrollToTarget:target scrollRect:scrollRect];
}
}複製代碼
上面這個方法會把listenerArray數組裏面的每一個WXScrollToTarget對象都調用scrollToTarget:scrollRect:方法。根據當前滾動的狀況傳入一個CGRect,這個CGRect就是當前滾動到那個矩形區域的座標信息以及寬和高。
- (void)scrollToTarget:(WXScrollToTarget *)target scrollRect:(CGRect)rect
{
WXComponent *component = target.target;
if (![component isViewLoaded]) {
return;
}
// 計算出當前的可見區域的頂部座標
CGFloat ctop;
if (component && component->_view && component->_view.superview) {
ctop = [component->_view.superview convertPoint:component->_view.frame.origin toView:_view].y;
} else {
ctop = 0.0;
}
// 計算出當前的可見區域的底部座標
CGFloat cbottom = ctop + CGRectGetHeight(component.calculatedFrame);
// 計算出當前的可見區域的左邊界座標
CGFloat cleft;
if (component && component->_view && component->_view.superview) {
cleft = [component->_view.superview convertPoint:component->_view.frame.origin toView:_view].x;
} else {
cleft = 0.0;
}
// 計算出當前的可見區域的右邊界座標
CGFloat cright = cleft + CGRectGetWidth(component.calculatedFrame);
// 獲取傳入的滾動的區域
CGFloat vtop = CGRectGetMinY(rect), vbottom = CGRectGetMaxY(rect), vleft = CGRectGetMinX(rect), vright = CGRectGetMaxX(rect);
// 判斷當前可見區域是否包含在傳入的滾動區域內,若是在,而且監聽了appear事件,就觸發appear事件,不然若是監聽了disappear事件就觸發disappear事件
if(cbottom > vtop && ctop <= vbottom && cleft <= vright && cright > vleft){
if(!target.hasAppear && component){
target.hasAppear = YES;
// 若是當前監聽了appear,就觸發appear事件
if (component->_appearEvent) {
[component fireEvent:@"appear" params:_direction ? @{@"direction":_direction} : nil];
}
}
} else {
if(target.hasAppear && component){
target.hasAppear = NO;
// 若是當前監聽了disappear,就觸發disappear事件
if(component->_disappearEvent){
[component fireEvent:@"disappear" params:_direction ? @{@"direction":_direction} : nil];
}
}
}
}複製代碼
scrollToTarget:scrollRect:方法的核心就是拿當前可視區域和傳入的滾動區域進行對比,若是在該區域內,且監聽了appear事件,就會觸發appear事件,若是不在該區域內,且監聽了disappear事件,就會觸發disappear事件。
若是一個位於某個可滾動區域內的組件被綁定了 disappear 事件,那麼當這個組件被滑出屏幕變爲不可見狀態時,該事件將被觸發。
同理,綁定了Disappear 事件的都是能夠滾動的視圖。
WX_ADD_EVENT(disappear, addDisappearEvent)複製代碼
經過上述的宏給能夠滾動的視圖增長Disappear 事件。也就是當前視圖執行addDisappearEvent方法。
- (void)addDisappearEvent
{
_disappearEvent = YES;
[self.ancestorScroller addScrollToListener:self];
}複製代碼
接下去的和Appear 事件的原理就如出一轍了。
暫時Weex只支持 iOS 和 Android,H5 暫不支持。
Weex 經過 viewappear 和 viewdisappear 事件提供了簡單的頁面狀態管理能力。
viewappear 事件會在頁面就要顯示或配置的任何頁面動畫被執行前觸發,例如,當調用 navigator 模塊的 push 方法時,該事件將會在打開新頁面時被觸發。viewdisappear 事件會在頁面就要關閉時被觸發。
與組件Component的 appear 和 disappear 事件不一樣的是,viewappear 和 viewdisappear 事件關注的是整個頁面的狀態,因此它們必須綁定到頁面的根元素上。
特殊狀況下,這兩個事件也能被綁定到非根元素的body組件上,例如wxc-navpage組件。
舉個例子:
- (void)_updateInstanceState:(WXState)state
{
if (_instance && _instance.state != state) {
_instance.state = state;
if (state == WeexInstanceAppear) {
[[WXSDKManager bridgeMgr] fireEvent:_instance.instanceId ref:WX_SDK_ROOT_REF type:@"viewappear" params:nil domChanges:nil];
} else if (state == WeexInstanceDisappear) {
[[WXSDKManager bridgeMgr] fireEvent:_instance.instanceId ref:WX_SDK_ROOT_REF type:@"viewdisappear" params:nil domChanges:nil];
}
}
}複製代碼
好比在WXBaseViewController裏面,有這樣一個更新當前Instance狀態的方法,這個方法裏面就會觸發 viewappear 和 viewdisappear 事件。
其中WX_SDK_ROOT_REF就是_root
#define WX_SDK_ROOT_REF @"_root"複製代碼
上述更新狀態的方法一樣出如今WXEmbedComponent組件中。
- (void)_updateState:(WXState)state
{
if (_renderFinished && _embedInstance && _embedInstance.state != state) {
_embedInstance.state = state;
if (state == WeexInstanceAppear) {
[self setNavigationWithStyles:self.embedInstance.naviBarStyles];
[[WXSDKManager bridgeMgr] fireEvent:self.embedInstance.instanceId ref:WX_SDK_ROOT_REF type:@"viewappear" params:nil domChanges:nil];
}
else if (state == WeexInstanceDisappear) {
[[WXSDKManager bridgeMgr] fireEvent:self.embedInstance.instanceId ref:WX_SDK_ROOT_REF type:@"viewdisappear" params:nil domChanges:nil];
}
}
}複製代碼
在Weex中,iOS Native把事件傳遞給JS目前只有2種方式,一是Module模塊的callback,二是經過Component組件自定義的通知事件。
在WXModuleProtocol中定義了2種能夠callback給JS的閉包。
/** * @abstract the module callback , result can be string or dictionary. * @discussion callback data to js, the id of callback function will be removed to save memory. */
typedef void (^WXModuleCallback)(id result);
/** * @abstract the module callback , result can be string or dictionary. * @discussion callback data to js, you can specify the keepAlive parameter to keep callback function id keepalive or not. If the keepAlive is true, it won't be removed until instance destroyed, so you can call it repetitious. */
typedef void (^WXModuleKeepAliveCallback)(id result, BOOL keepAlive);複製代碼
兩個閉包均可以callback把data傳遞迴給JS,data能夠是字符串或者字典。
這兩個閉包的區別在於:
在Weex中使用WXModuleCallback回調,不少狀況是把狀態回調給JS,好比成功或者失敗的狀態,還有一些出錯的信息回調給JS。
好比在WXStorageModule中
- (void)setItem:(NSString *)key value:(NSString *)value callback:(WXModuleCallback)callback
{
if ([self checkInput:key]) {
callback(@{@"result":@"failed",@"data":@"key must a string or number!"});
return;
}
if ([self checkInput:value]) {
callback(@{@"result":@"failed",@"data":@"value must a string or number!"});
return;
}
if ([key isKindOfClass:[NSNumber class]]) {
key = [((NSNumber *)key) stringValue];
}
if ([value isKindOfClass:[NSNumber class]]) {
value = [((NSNumber *)value) stringValue];
}
if ([WXUtility isBlankString:key]) {
callback(@{@"result":@"failed",@"data":@"invalid_param"});
return ;
}
[self setObject:value forKey:key persistent:NO callback:callback];
}複製代碼
在調用setItem:value:callback:方法裏面,若是setKey-value的時候失敗了,會把錯誤信息經過WXModuleCallback回調給JS。
固然,若是調用存儲模塊WXStorageModule的某些查詢信息的方法:
- (void)length:(WXModuleCallback)callback
{
callback(@{@"result":@"success",@"data":@([[WXStorageModule memory] count])});
}
- (void)getAllKeys:(WXModuleCallback)callback
{
callback(@{@"result":@"success",@"data":[WXStorageModule memory].allKeys});
}複製代碼
length:和getAllKeys:方法調用成功,會把成功的狀態和數據經過WXModuleCallback回調給JS。
在Weex中使用了WXModuleKeepAliveCallback的模塊總共只有如下4個:
WXDomModule,WXStreamModule,WXWebSocketModule,WXGlobalEventModule
在WXDomModule模塊中,JS調用獲取Component組件的位置信息和寬高信息的時候,須要把這些座標和尺寸信息回調給JS,不過這裏雖然用到了WXModuleKeepAliveCallback,可是keepAlive是false,並無用到屢次回調的功能。
在WXStreamModule模塊中,因爲這是一個傳輸流的模塊,因此確定須要用到WXModuleKeepAliveCallback,須要持續不斷的監聽數據的變化,並把進度回調給JS,這裏用到了keepAlive。WXStreamModule模塊中也會用到WXModuleCallback,WXModuleCallback會即時把各個狀態回調給JS。
在WXWebSocketModule模塊中
@interface WXWebSocketModule()
@property(nonatomic,copy)WXModuleKeepAliveCallback errorCallBack;
@property(nonatomic,copy)WXModuleKeepAliveCallback messageCallBack;
@property(nonatomic,copy)WXModuleKeepAliveCallback openCallBack;
@property(nonatomic,copy)WXModuleKeepAliveCallback closeCallBack;
@end複製代碼
用到了4個WXModuleKeepAliveCallback回調,這4個callback分別是把error錯誤信息,message收到的數據,open打開連接的狀態,close關閉連接的狀態,持續的回調給JS。
在WXGlobalEventModule模塊中,有一個fireGlobalEvent:方法。
- (void)fireGlobalEvent:(NSNotification *)notification
{
NSDictionary * userInfo = notification.userInfo;
NSString * userWeexInstanceId = userInfo[@"weexInstance"];
WXSDKInstance * userWeexInstance = [WXSDKManager instanceForID:userWeexInstanceId];
// 防止userInstanceId存在,可是instance實際已經被銷燬了
if (!userWeexInstanceId || userWeexInstance == weexInstance) {
for (WXModuleKeepAliveCallback callback in _eventCallback[notification.name]) {
callback(userInfo[@"param"], true);
}
}
}複製代碼
開發者能夠經過WXGlobalEventModule進行全局的通知,在userInfo裏面能夠夾帶weexInstance的參數。native是不須要關心userWeexInstanceId,這個參數是給JS用的。
Native開發者只須要在用到了WXGlobalEventModule的模塊里加上事件的監聽者,而後發送全局通知便可。userInfo[@"param"]會被回調給JS。
在開頭咱們介紹的Weex事件的4種類型,通用事件,Appear 事件,Disappear 事件,Page 事件,所有都是經過fireEvent:params:domChanges:這種方式,Native觸發事件以後,Native把參數傳遞給JS的。
在WXComponent裏面定義了2個能夠給JS發送消息的方法:
/** * @abstract Fire an event to the component in Javascript. * * @param eventName The name of the event to fire * @param params The parameters to fire with **/
- (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params;
/** * @abstract Fire an event to the component and tell Javascript which value has been changed. * Used for two-way data binding. * * @param eventName The name of the event to fire * @param params The parameters to fire with * @param domChanges The values has been changed, used for two-way data binding. **/
- (void)fireEvent:(NSString *)eventName params:(nullable NSDictionary *)params domChanges:(nullable NSDictionary *)domChanges;複製代碼
這兩個方法的區別就在於最後一個domChanges的參數,有這個參數的方法主要多用於Weex的Native和JS的雙向數據綁定。
- (void)fireEvent:(NSString *)eventName params:(NSDictionary *)params
{
[self fireEvent:eventName params:params domChanges:nil];
}
- (void)fireEvent:(NSString *)eventName params:(NSDictionary *)params domChanges:(NSDictionary *)domChanges
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSTimeInterval timeSp = [[NSDate date] timeIntervalSince1970] * 1000;
[dict setObject:@(timeSp) forKey:@"timestamp"];
if (params) {
[dict addEntriesFromDictionary:params];
}
[[WXSDKManager bridgeMgr] fireEvent:self.weexInstance.instanceId ref:self.ref type:eventName params:dict domChanges:domChanges];
}複製代碼
上述就是兩個方法的具體實現。能夠看到fireEvent:params:方法就是調用了fireEvent:params:domChanges:方法,只不過最後的domChanges參數傳了nil。
在fireEvent:params:domChanges:方法中會對params字典作了一次加工,加上了timestamp的鍵值。最終仍是會調用WXBridgeManager 裏面的fireEvent:ref: type:params:domChanges:方法。
在WXBridgeManager中具體實現了上述的兩個方法。
- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref type:(NSString *)type params:(NSDictionary *)params
{
[self fireEvent:instanceId ref:ref type:type params:params domChanges:nil];
}
- (void)fireEvent:(NSString *)instanceId ref:(NSString *)ref type:(NSString *)type params:(NSDictionary *)params domChanges:(NSDictionary *)domChanges
{
if (!type || !ref) {
WXLogError(@"Event type and component ref should not be nil");
return;
}
NSArray *args = @[ref, type, params?:@{}, domChanges?:@{}];
WXSDKInstance *instance = [WXSDKManager instanceForID:instanceId];
WXCallJSMethod *method = [[WXCallJSMethod alloc] initWithModuleName:nil methodName:@"fireEvent" arguments:args instance:instance];
[self callJsMethod:method];
}複製代碼
入參ref, type, params, domChanges封裝到最終的args參數數組裏面,最後會封裝出WXCallJSMethod方法,經過WXBridgeManager的callJsMethod調用到JS的fireEvent方法。
這裏能夠舉個例子:
假設一個場景,用戶點擊了一張圖片,因而就會改變label上的一段文字。
首先圖片是imageComponent,用戶點擊會觸發該Component的onclick:方法
組件裏面會調用fireEvent:params:方法:
[self fireEvent:@"click" params:@{@"position":position}];複製代碼
最終經過fireEvent:params:domChanges:方法,發送給JS的參數字典大概以下:
args:(
0,
(
{
args = (
3,
click,
{
position = {
height = "199.8792270531401";
width = "199.8792270531401";
x = "274.7584541062802";
y = "115.9420289855072";
};
timestamp = "1489932655404.133";
},
{
}
);
method = fireEvent;
module = "";
}
)
)複製代碼
JSFramework收到了fireEvent方法調用之後,處理完,知道label須要更新,因而又會開始call Native,調用Native的方法。調用Native的callNative方法,發過來的參數以下:
(
{
args = (
4,
{
value = "\U56fe\U7247\U88ab\U70b9\U51fb";
}
);
method = updateAttrs;
module = dom;
}
)複製代碼
最終會調用Dom的updateAttrs方法,會去更新id爲4的value,id爲4對應的就是label,更新它的值就是刷新label。
接着JSFramework還會繼續調用Native的callNative方法,發過來的參數以下:
(
{
args = (
);
method = updateFinish;
module = dom;
}
)複製代碼
調用Dom的updateFinish方法,即頁面刷新完畢。
至此,Weex從View的建立,到渲染,產生事件回調JSFramework,這一系列的流程源碼都解析完成了。
中間涉及到了3個子線程,mainThread,com.taobao.weex.component,com.taobao.weex.bridge,分別是UI主線程,DOM線程,JSbridge線程。
Native端目前還差神祕的JSFramework的源碼解析。請你們多多指點。