[ReactNative] Implement proper event coalescing

This commit is contained in:
Tadeu Zagallo 2015-05-27 20:15:33 -07:00
parent 17e9cd6297
commit 4fc15dbf17
7 changed files with 289 additions and 77 deletions

View File

@ -95,6 +95,11 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method;
/**
* The event dispatcher is a wrapper around -enqueueJSCall:args: that provides a
* higher-level interface for sending UI events such as touches and text input.
*
* NOTE: RCTEventDispatcher is now a bridge module, this is implemented as a
* category but remains declared in the bridge to avoid breaking changes
*
* To be moved.
*/
@property (nonatomic, readonly) RCTEventDispatcher *eventDispatcher;

View File

@ -18,7 +18,6 @@
#import "RCTContextExecutor.h"
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTJavaScriptLoader.h"
#import "RCTKeyCommands.h"
#import "RCTLog.h"
@ -211,7 +210,6 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void)
@property (nonatomic, strong) RCTBatchedBridge *batchedBridge;
@property (nonatomic, strong) RCTBridgeModuleProviderBlock moduleProvider;
@property (nonatomic, strong, readwrite) RCTEventDispatcher *eventDispatcher;
- (void)_invokeAndProcessModule:(NSString *)module
method:(NSString *)method
@ -875,11 +873,6 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
return _batchedBridge.modules;
}
- (RCTEventDispatcher *)eventDispatcher
{
return _eventDispatcher ?: _batchedBridge.eventDispatcher;
}
#define RCT_INNER_BRIDGE_ONLY(...) \
- (void)__VA_ARGS__ \
{ \
@ -943,11 +936,6 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin
_javaScriptExecutor = RCTCreateExecutor(executorClass);
_latestJSExecutor = _javaScriptExecutor;
/**
* Setup event dispatcher before initializing modules to allow init calls
*/
self.eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self];
/**
* Initialize and register bridge modules *before* adding the display link
* so we don't have threading issues

View File

@ -9,7 +9,7 @@
#import <UIKit/UIKit.h>
@class RCTBridge;
#import "RCTBridge.h"
typedef NS_ENUM(NSInteger, RCTTextEventType) {
RCTTextEventTypeFocus,
@ -28,14 +28,36 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
RCTScrollEventTypeEndAnimation,
};
@protocol RCTEvent <NSObject>
@required
@property (nonatomic, strong, readonly) NSNumber *viewTag;
@property (nonatomic, copy, readonly) NSString *eventName;
@property (nonatomic, copy, readonly) NSDictionary *body;
@property (nonatomic, assign, readonly) uint16_t coalescingKey;
- (BOOL)canCoalesce;
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent;
+ (NSString *)moduleDotMethod;
@end
@interface RCTBaseEvent : NSObject <RCTEvent>
- (instancetype)initWithViewTag:(NSNumber *)viewTag
eventName:(NSString *)eventName
body:(NSDictionary *)body NS_DESIGNATED_INITIALIZER;
@end
/**
* This class wraps the -[RCTBridge enqueueJSCall:args:] method, and
* provides some convenience methods for generating event calls.
*/
@interface RCTEventDispatcher : NSObject
- (instancetype)initWithBridge:(RCTBridge *)bridge;
/**
* Send an application-specific event that does not relate to a specific
* view, e.g. a navigation or data update notification.
@ -61,13 +83,6 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) {
reactTag:(NSNumber *)reactTag
text:(NSString *)text;
/**
* Send a scroll event.
* (You can send a fake scroll event by passing nil for scrollView).
*/
- (void)sendScrollEventWithType:(RCTScrollEventType)type
reactTag:(NSNumber *)reactTag
scrollView:(UIScrollView *)scrollView
userData:(NSDictionary *)userData;
- (void)sendEvent:(id<RCTEvent>)event;
@end

View File

@ -11,16 +11,76 @@
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTSparseArray.h"
static uint64_t RCTGetEventID(id<RCTEvent> event)
{
return (
[event.viewTag intValue] |
(((uint64_t)event.eventName.hash & 0xFFFF) << 32) |
(((uint64_t)event.coalescingKey) << 48)
);
}
@implementation RCTBaseEvent
@synthesize viewTag = _viewTag;
@synthesize eventName = _eventName;
@synthesize body = _body;
- (instancetype)initWithViewTag:(NSNumber *)viewTag
eventName:(NSString *)eventName
body:(NSDictionary *)body
{
if ((self = [super init])) {
_viewTag = viewTag;
_eventName = eventName;
_body = body;
}
return self;
}
- (uint16_t)coalescingKey
{
return 0;
}
- (BOOL)canCoalesce
{
return YES;
}
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent
{
return newEvent;
}
+ (NSString *)moduleDotMethod
{
return nil;
}
@end
@interface RCTEventDispatcher() <RCTBridgeModule, RCTFrameUpdateObserver>
@end
@implementation RCTEventDispatcher
{
RCTBridge __weak *_bridge;
RCTSparseArray *_eventQueue;
NSLock *_eventQueueLock;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
- (instancetype)init
{
if ((self = [super init])) {
_bridge = bridge;
_eventQueue = [[RCTSparseArray alloc] init];
_eventQueueLock = [[NSLock alloc] init];
}
return self;
}
@ -70,58 +130,71 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveEvent);
}];
}
/**
* TODO: throttling
* NOTE: the old system used a per-scrollview throttling
* which would be fairly easy to re-implement if needed,
* but this is non-optimal as it leads to degradation in
* scroll responsiveness. A better solution would be to
* coalesce multiple scroll events into a single batch.
*/
- (void)sendScrollEventWithType:(RCTScrollEventType)type
reactTag:(NSNumber *)reactTag
scrollView:(UIScrollView *)scrollView
userData:(NSDictionary *)userData
- (void)sendEvent:(id<RCTEvent>)event
{
static NSString *events[] = {
@"topScrollBeginDrag",
@"topScroll",
@"topScrollEndDrag",
@"topMomentumScrollBegin",
@"topMomentumScrollEnd",
@"topScrollAnimationEnd",
};
NSDictionary *body = @{
@"contentOffset": @{
@"x": @(scrollView.contentOffset.x),
@"y": @(scrollView.contentOffset.y)
},
@"contentInset": @{
@"top": @(scrollView.contentInset.top),
@"left": @(scrollView.contentInset.left),
@"bottom": @(scrollView.contentInset.bottom),
@"right": @(scrollView.contentInset.right)
},
@"contentSize": @{
@"width": @(scrollView.contentSize.width),
@"height": @(scrollView.contentSize.height)
},
@"layoutMeasurement": @{
@"width": @(scrollView.frame.size.width),
@"height": @(scrollView.frame.size.height)
},
@"zoomScale": @(scrollView.zoomScale ?: 1),
@"target": reactTag
};
if (userData) {
NSMutableDictionary *mutableBody = [body mutableCopy];
[mutableBody addEntriesFromDictionary:userData];
body = mutableBody;
if (!event.canCoalesce) {
[self dispatchEvent:event];
return;
}
[self sendInputEventWithName:events[type] body:body];
[_eventQueueLock lock];
uint64_t eventID = RCTGetEventID(event);
id<RCTEvent> previousEvent = _eventQueue[eventID];
if (previousEvent) {
event = [previousEvent coalesceWithEvent:event];
}
_eventQueue[eventID] = event;
[_eventQueueLock unlock];
}
- (void)dispatchEvent:(id<RCTEvent>)event
{
NSMutableArray *arguments = [[NSMutableArray alloc] init];
if (event.viewTag) {
[arguments addObject:event.viewTag];
}
[arguments addObject:event.eventName];
if (event.body) {
[arguments addObject:event.body];
}
[_bridge enqueueJSCall:[[event class] moduleDotMethod]
args:arguments];
}
- (dispatch_queue_t)methodQueue
{
return RCTJSThread;
}
- (void)didUpdateFrame:(RCTFrameUpdate *)update
{
RCTSparseArray *eventQueue;
[_eventQueueLock lock];
eventQueue = _eventQueue;
_eventQueue = [[RCTSparseArray alloc] init];
[_eventQueueLock unlock];
for (id<RCTEvent> event in eventQueue.allObjects) {
[self dispatchEvent:event];
}
}
@end
@implementation RCTBridge (RCTEventDispatcher)
- (RCTEventDispatcher *)eventDispatcher
{
return self.modules[RCTBridgeModuleNameForClass([RCTEventDispatcher class])];
}
@end

View File

@ -15,6 +15,7 @@
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTNavItem.h"
#import "RCTScrollView.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "RCTWrapperViewController.h"

View File

@ -10,13 +10,12 @@
#import <UIKit/UIScrollView.h>
#import "RCTAutoInsetsProtocol.h"
#import "RCTEventDispatcher.h"
#import "RCTScrollableProtocol.h"
#import "RCTView.h"
@protocol UIScrollViewDelegate;
@class RCTEventDispatcher;
@interface RCTScrollView : RCTView <UIScrollViewDelegate, RCTScrollableProtocol, RCTAutoInsetsProtocol>
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
@ -48,3 +47,16 @@
@property (nonatomic, copy) NSIndexSet *stickyHeaderIndices;
@end
@interface RCTEventDispatcher (RCTScrollView)
/**
* Send a scroll event.
* (You can send a fake scroll event by passing nil for scrollView).
*/
- (void)sendScrollEventWithType:(RCTScrollEventType)type
reactTag:(NSNumber *)reactTag
scrollView:(UIScrollView *)scrollView
userData:(NSDictionary *)userData;
@end

View File

@ -21,6 +21,107 @@
CGFloat const ZINDEX_DEFAULT = 0;
CGFloat const ZINDEX_STICKY_HEADER = 50;
@interface RCTScrollEvent : NSObject <RCTEvent>
- (instancetype)initWithType:(RCTScrollEventType)type
reactTag:(NSNumber *)reactTag
scrollView:(UIScrollView *)scrollView
userData:(NSDictionary *)userData NS_DESIGNATED_INITIALIZER;
@end
@implementation RCTScrollEvent
{
RCTScrollEventType _type;
UIScrollView *_scrollView;
NSDictionary *_userData;
}
@synthesize viewTag = _viewTag;
- (instancetype)initWithType:(RCTScrollEventType)type
reactTag:(NSNumber *)reactTag
scrollView:(UIScrollView *)scrollView
userData:(NSDictionary *)userData
{
if (self = [super init]) {
_type = type;
_viewTag = reactTag;
_scrollView = scrollView;
_userData = userData;
}
return self;
}
- (uint16_t)coalescingKey
{
return 0;
}
- (NSDictionary *)body
{
NSDictionary *body = @{
@"contentOffset": @{
@"x": @(_scrollView.contentOffset.x),
@"y": @(_scrollView.contentOffset.y)
},
@"contentInset": @{
@"top": @(_scrollView.contentInset.top),
@"left": @(_scrollView.contentInset.left),
@"bottom": @(_scrollView.contentInset.bottom),
@"right": @(_scrollView.contentInset.right)
},
@"contentSize": @{
@"width": @(_scrollView.contentSize.width),
@"height": @(_scrollView.contentSize.height)
},
@"layoutMeasurement": @{
@"width": @(_scrollView.frame.size.width),
@"height": @(_scrollView.frame.size.height)
},
@"zoomScale": @(_scrollView.zoomScale ?: 1),
};
if (_userData) {
NSMutableDictionary *mutableBody = [body mutableCopy];
[mutableBody addEntriesFromDictionary:_userData];
body = mutableBody;
}
return body;
}
- (NSString *)eventName
{
static NSString *events[] = {
@"topScrollBeginDrag",
@"topScroll",
@"topScrollEndDrag",
@"topMomentumScrollBegin",
@"topMomentumScrollEnd",
@"topScrollAnimationEnd",
};
return events[_type];
}
- (BOOL)canCoalesce
{
return YES;
}
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent
{
return newEvent;
}
+ (NSString *)moduleDotMethod
{
return @"RCTEventEmitter.receiveEvent";
}
@end
/**
* Include a custom scroll view subclass because we want to limit certain
* default UIKit behaviors such as textFields automatically scrolling
@ -442,6 +543,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove)
reactTag:self.reactTag
scrollView:scrollView
userData:userData];
// Update dispatch time
_lastScrollDispatchTime = now;
_allowNextScrollNoMatterWhat = NO;
@ -630,3 +732,19 @@ RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, UIEdgeInsets);
}
@end
@implementation RCTEventDispatcher (RCTScrollView)
- (void)sendScrollEventWithType:(RCTScrollEventType)type
reactTag:(NSNumber *)reactTag
scrollView:(UIScrollView *)scrollView
userData:(NSDictionary *)userData
{
RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithType:type
reactTag:reactTag
scrollView:scrollView
userData:userData];
[self sendEvent:scrollEvent];
}
@end