diff --git a/Examples/UIExplorer/js/NativeAnimationsExample.js b/Examples/UIExplorer/js/NativeAnimationsExample.js index 62f7100f8..208f27a64 100644 --- a/Examples/UIExplorer/js/NativeAnimationsExample.js +++ b/Examples/UIExplorer/js/NativeAnimationsExample.js @@ -191,6 +191,7 @@ class EventExample extends React.Component { + ); + }, + }, { title: 'Internal Settings', render: function() { @@ -470,13 +479,4 @@ exports.examples = [ ); }, }, - { - title: 'Animated events', - platform: 'android', - render: function() { - return ( - - ); - }, - }, ]; diff --git a/Libraries/NativeAnimation/Drivers/RCTEventAnimation.h b/Libraries/NativeAnimation/Drivers/RCTEventAnimation.h new file mode 100644 index 000000000..4b8eefd1f --- /dev/null +++ b/Libraries/NativeAnimation/Drivers/RCTEventAnimation.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTValueAnimatedNode.h" +#import "RCTEventDispatcher.h" + +@interface RCTEventAnimation : NSObject + +@property (nonatomic, readonly, weak) RCTValueAnimatedNode *valueNode; + +- (instancetype)initWithEventPath:(NSArray *)eventPath + valueNode:(RCTValueAnimatedNode *)valueNode; + +- (void)updateWithEvent:(id)event; + +@end diff --git a/Libraries/NativeAnimation/Drivers/RCTEventAnimation.m b/Libraries/NativeAnimation/Drivers/RCTEventAnimation.m new file mode 100644 index 000000000..dd7dfc9cf --- /dev/null +++ b/Libraries/NativeAnimation/Drivers/RCTEventAnimation.m @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTEventAnimation.h" + +@implementation RCTEventAnimation +{ + NSArray *_eventPath; +} + +- (instancetype)initWithEventPath:(NSArray *)eventPath + valueNode:(RCTValueAnimatedNode *)valueNode +{ + if ((self = [super init])) { + _eventPath = eventPath; + _valueNode = valueNode; + } + return self; +} + +- (void)updateWithEvent:(id)event +{ + NSArray *args = event.arguments; + // Supported events args are in the following order: viewTag, eventName, eventData. + id currentValue = args[2]; + for (NSString *key in _eventPath) { + currentValue = [currentValue valueForKey:key]; + } + + _valueNode.value = ((NSNumber *)currentValue).doubleValue; + [_valueNode setNeedsUpdate]; +} + +@end diff --git a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj index 657150b28..aec9ef048 100644 --- a/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj +++ b/Libraries/NativeAnimation/RCTAnimation.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 13E501EF1D07A6C9005F35D8 /* RCTTransformAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E51D07A6C9005F35D8 /* RCTTransformAnimatedNode.m */; }; 13E501F01D07A6C9005F35D8 /* RCTValueAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */; }; 193F64F41D776EC6004D1CAA /* RCTDiffClampAnimatedNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 193F64F31D776EC6004D1CAA /* RCTDiffClampAnimatedNode.m */; }; + 19F00F221DC8847500113FEE /* RCTEventAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = 19F00F211DC8847500113FEE /* RCTEventAnimation.m */; }; + 19F00F231DC8848E00113FEE /* RCTEventAnimation.m in Sources */ = {isa = PBXBuildFile; fileRef = 19F00F211DC8847500113FEE /* RCTEventAnimation.m */; }; 2D3B5EF21D9B0B3100451313 /* RCTAnimationUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501B81D07A644005F35D8 /* RCTAnimationUtils.m */; }; 2D3B5EF31D9B0B3400451313 /* RCTViewPropertyMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501C81D07A644005F35D8 /* RCTViewPropertyMapper.m */; }; 2D3B5EF41D9B0B3700451313 /* RCTNativeAnimatedModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E501BE1D07A644005F35D8 /* RCTNativeAnimatedModule.m */; }; @@ -88,6 +90,8 @@ 13E501E71D07A6C9005F35D8 /* RCTValueAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTValueAnimatedNode.m; sourceTree = ""; }; 193F64F21D776EC6004D1CAA /* RCTDiffClampAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDiffClampAnimatedNode.h; sourceTree = ""; }; 193F64F31D776EC6004D1CAA /* RCTDiffClampAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDiffClampAnimatedNode.m; sourceTree = ""; }; + 19F00F201DC8847500113FEE /* RCTEventAnimation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTEventAnimation.h; sourceTree = ""; }; + 19F00F211DC8847500113FEE /* RCTEventAnimation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTEventAnimation.m; sourceTree = ""; }; 2D2A28201D9B03D100D4039D /* libRCTAnimation-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRCTAnimation-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 5C9894931D999639008027DB /* RCTDivisionAnimatedNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDivisionAnimatedNode.h; sourceTree = ""; }; 5C9894941D999639008027DB /* RCTDivisionAnimatedNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDivisionAnimatedNode.m; sourceTree = ""; }; @@ -175,6 +179,8 @@ isa = PBXGroup; children = ( 94C1294A1D4069170025F25C /* RCTAnimationDriver.h */, + 19F00F201DC8847500113FEE /* RCTEventAnimation.h */, + 19F00F211DC8847500113FEE /* RCTEventAnimation.m */, 94C1294C1D4069170025F25C /* RCTFrameAnimation.h */, 94C1294D1D4069170025F25C /* RCTFrameAnimation.m */, 94C1294E1D4069170025F25C /* RCTSpringAnimation.h */, @@ -266,6 +272,7 @@ 2D3B5EF21D9B0B3100451313 /* RCTAnimationUtils.m in Sources */, 2D3B5EF51D9B0B4800451313 /* RCTDivisionAnimatedNode.m in Sources */, 2D3B5EF71D9B0B4800451313 /* RCTAdditionAnimatedNode.m in Sources */, + 19F00F231DC8848E00113FEE /* RCTEventAnimation.m in Sources */, 2D3B5EF41D9B0B3700451313 /* RCTNativeAnimatedModule.m in Sources */, 2D3B5EF61D9B0B4800451313 /* RCTDiffClampAnimatedNode.m in Sources */, 2D3B5EF81D9B0B4800451313 /* RCTAnimatedNode.m in Sources */, @@ -289,6 +296,7 @@ 13E501F01D07A6C9005F35D8 /* RCTValueAnimatedNode.m in Sources */, 94DAE3F91D7334A70059942F /* RCTModuloAnimatedNode.m in Sources */, 193F64F41D776EC6004D1CAA /* RCTDiffClampAnimatedNode.m in Sources */, + 19F00F221DC8847500113FEE /* RCTEventAnimation.m in Sources */, 13E501EE1D07A6C9005F35D8 /* RCTStyleAnimatedNode.m in Sources */, 13E501CC1D07A644005F35D8 /* RCTAnimationUtils.m in Sources */, 13E501CF1D07A644005F35D8 /* RCTNativeAnimatedModule.m in Sources */, diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.h b/Libraries/NativeAnimation/RCTNativeAnimatedModule.h index da7831769..55cdf705f 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.h +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.h @@ -9,7 +9,8 @@ #import "RCTBridgeModule.h" #import "RCTValueAnimatedNode.h" #import "RCTEventEmitter.h" +#import "RCTEventDispatcher.h" -@interface RCTNativeAnimatedModule : RCTEventEmitter +@interface RCTNativeAnimatedModule : RCTEventEmitter @end diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.m b/Libraries/NativeAnimation/RCTNativeAnimatedModule.m index f08824395..8426d8bb8 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.m +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.m @@ -15,6 +15,7 @@ #import "RCTAnimationUtils.h" #import "RCTBridge.h" #import "RCTConvert.h" +#import "RCTEventAnimation.h" #import "RCTInterpolationAnimatedNode.h" #import "RCTLog.h" #import "RCTDiffClampAnimatedNode.h" @@ -30,6 +31,7 @@ { NSMutableDictionary *_animationNodes; NSMutableDictionary> *_animationDrivers; + NSMutableDictionary *_eventAnimationDrivers; NSMutableSet> *_activeAnimations; NSMutableSet> *_finishedAnimations; NSMutableSet *_updatedValueNodes; @@ -45,10 +47,18 @@ RCT_EXPORT_MODULE() _animationNodes = [NSMutableDictionary new]; _animationDrivers = [NSMutableDictionary new]; + _eventAnimationDrivers = [NSMutableDictionary new]; _activeAnimations = [NSMutableSet new]; _finishedAnimations = [NSMutableSet new]; _updatedValueNodes = [NSMutableSet new]; _propAnimationNodes = [NSMutableSet new]; + + [bridge.eventDispatcher addDispatchObserver:self]; +} + +- (void)dealloc +{ + [self.bridge.eventDispatcher removeDispatchObserver:self]; } @@ -157,7 +167,7 @@ RCT_EXPORT_METHOD(startAnimatingNode:(nonnull NSNumber *)animationId [_activeAnimations addObject:animationDriver]; _animationDrivers[animationId] = animationDriver; [animationDriver startAnimation]; - [self startAnimation]; + [self startAnimationLoopIfNeeded]; } RCT_EXPORT_METHOD(stopAnimation:(nonnull NSNumber *)animationId) @@ -263,12 +273,64 @@ RCT_EXPORT_METHOD(stopListeningToAnimatedNodeValue:(nonnull NSNumber *)tag) } } +RCT_EXPORT_METHOD(addAnimatedEventToView:(nonnull NSNumber *)viewTag + eventName:(nonnull NSString *)eventName + eventMapping:(NSDictionary *)eventMapping) +{ + NSNumber *nodeTag = [RCTConvert NSNumber:eventMapping[@"animatedValueTag"]]; + RCTAnimatedNode *node = _animationNodes[nodeTag]; + + if (!node) { + RCTLogError(@"Animated node with tag %@ does not exists", nodeTag); + return; + } + + if (![node isKindOfClass:[RCTValueAnimatedNode class]]) { + RCTLogError(@"Animated node connected to event should be of type RCTValueAnimatedNode"); + return; + } + + NSArray *eventPath = [RCTConvert NSStringArray:eventMapping[@"nativeEventPath"]]; + + RCTEventAnimation *driver = + [[RCTEventAnimation alloc] initWithEventPath:eventPath valueNode:(RCTValueAnimatedNode *)node]; + + _eventAnimationDrivers[[NSString stringWithFormat:@"%@%@", viewTag, eventName]] = driver; +} + +RCT_EXPORT_METHOD(removeAnimatedEventFromView:(nonnull NSNumber *)viewTag + eventName:(nonnull NSString *)eventName) +{ + [_eventAnimationDrivers removeObjectForKey:[NSString stringWithFormat:@"%@%@", viewTag, eventName]]; +} + - (void)animatedNode:(RCTValueAnimatedNode *)node didUpdateValue:(CGFloat)value { [self sendEventWithName:@"onAnimatedValueUpdate" body:@{@"tag": node.nodeTag, @"value": @(value)}]; } +- (BOOL)eventDispatcherWillDispatchEvent:(id)event +{ + // Native animated events only work for events dispatched from the main queue. + if (!RCTIsMainQueue() || _eventAnimationDrivers.count == 0) { + return NO; + } + + NSString *key = [NSString stringWithFormat:@"%@%@", event.viewTag, event.eventName]; + RCTEventAnimation *driver = _eventAnimationDrivers[key]; + + if (driver) { + [driver updateWithEvent:event]; + [self updateViewsProps]; + [driver.valueNode cleanupAnimationUpdate]; + + return YES; + } + + return NO; +} + - (void)updateViewsProps { for (RCTPropsAnimatedNode *propsNode in _propAnimationNodes) { @@ -278,14 +340,22 @@ RCT_EXPORT_METHOD(stopListeningToAnimatedNodeValue:(nonnull NSNumber *)tag) #pragma mark -- Animation Loop -- (void)startAnimation +- (void)startAnimationLoopIfNeeded { - if (!_displayLink && _activeAnimations.count > 0) { + if (!_displayLink && (_activeAnimations.count > 0 || _updatedValueNodes.count > 0)) { _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimations)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } } +- (void)stopAnimationLoopIfNeeded +{ + if (_displayLink && _activeAnimations.count == 0 && _updatedValueNodes.count == 0) { + [_displayLink invalidate]; + _displayLink = nil; + } +} + - (void)updateAnimations { // Step Current active animations @@ -321,10 +391,7 @@ RCT_EXPORT_METHOD(stopListeningToAnimatedNodeValue:(nonnull NSNumber *)tag) } [_finishedAnimations removeAllObjects]; - if (_activeAnimations.count == 0) { - [_displayLink invalidate]; - _displayLink = nil; - } + [self stopAnimationLoopIfNeeded]; } @end diff --git a/React.podspec b/React.podspec index 70e3f1a9d..8cd94c058 100644 --- a/React.podspec +++ b/React.podspec @@ -56,7 +56,7 @@ Pod::Spec.new do |s| s.subspec 'RCTAnimation' do |ss| ss.dependency 'React/Core' - ss.source_files = "Libraries/NativeAnimation/{Nodes/*,*}.{h,m}" + ss.source_files = "Libraries/NativeAnimation/{Drivers/*,Nodes/*,*}.{h,m}" end s.subspec 'RCTCameraRoll' do |ss| diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 6b5b448c1..b6a4d41bf 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -51,6 +51,20 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); @end +/** + * This protocol allows observing events dispatched by RCTEventDispatcher. + */ +@protocol RCTEventDispatcherObserver + +/** + * Called before dispatching an event, on the same thread the event was + * dispatched from. Return YES if the event was handled and must not be + * sent to JS. + */ +- (BOOL)eventDispatcherWillDispatchEvent:(id)event; + +@end + /** * This class wraps the -[RCTBridge enqueueJSCall:args:] method, and @@ -93,6 +107,16 @@ __deprecated_msg("Use RCTDirectEventBlock or RCTBubblingEventBlock instead"); */ - (void)sendEvent:(id)event; +/** + * Add an event dispatcher observer. + */ +- (void)addDispatchObserver:(id)observer; + +/** + * Remove an event dispatcher observer. + */ +- (void)removeDispatchObserver:(id)observer; + @end @interface RCTBridge (RCTEventDispatcher) diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 6fe1d4e33..381aa265c 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -46,6 +46,8 @@ static NSNumber *RCTGetEventID(id event) // This array contains ids of events in order they come in, so we can emit them to JS in the exact same order. NSMutableArray *_eventQueue; BOOL _eventsDispatchScheduled; + NSMutableArray> *_observers; + NSLock *_observersLock; } @synthesize bridge = _bridge; @@ -59,6 +61,8 @@ RCT_EXPORT_MODULE() _eventQueue = [NSMutableArray new]; _eventQueueLock = [NSLock new]; _eventsDispatchScheduled = NO; + _observers = [NSMutableArray new]; + _observersLock = [NSLock new]; } - (void)sendAppEventWithName:(NSString *)name body:(id)body @@ -140,6 +144,21 @@ RCT_EXPORT_MODULE() - (void)sendEvent:(id)event { + [_observersLock lock]; + + BOOL eventHandled = NO; + for (id observer in _observers) { + if ([observer eventDispatcherWillDispatchEvent:event]) { + eventHandled = YES; + } + } + + [_observersLock unlock]; + + if (eventHandled) { + return; + } + [_eventQueueLock lock]; NSNumber *eventID = RCTGetEventID(event); @@ -171,6 +190,20 @@ RCT_EXPORT_MODULE() } } +- (void)addDispatchObserver:(id)observer +{ + [_observersLock lock]; + [_observers addObject:observer]; + [_observersLock unlock]; +} + +- (void)removeDispatchObserver:(id)observer +{ + [_observersLock lock]; + [_observers removeObject:observer]; + [_observersLock unlock]; +} + - (void)dispatchEvent:(id)event { [_bridge enqueueJSCall:[[event class] moduleDotMethod] args:[event arguments]];