#import "REANodesManager.h" #import #import "Nodes/REANode.h" #import "Nodes/REAPropsNode.h" #import "Nodes/REAStyleNode.h" #import "Nodes/REATransformNode.h" #import "Nodes/REAValueNode.h" #import "Nodes/REABlockNode.h" #import "Nodes/REACondNode.h" #import "Nodes/REAOperatorNode.h" #import "Nodes/REASetNode.h" #import "Nodes/READebugNode.h" #import "Nodes/REAClockNodes.h" #import "Nodes/REAJSCallNode.h" #import "Nodes/REABezierNode.h" #import "Nodes/REAEventNode.h" #import "REAModule.h" #import "Nodes/REAAlwaysNode.h" #import "Nodes/REAConcatNode.h" #import "Nodes/REAParamNode.h" #import "Nodes/REAFunctionNode.h" #import "Nodes/REACallFuncNode.h" // Interface below has been added in order to use private methods of RCTUIManager, // RCTUIManager#UpdateView is a React Method which is exported to JS but in // Objective-C it stays private // RCTUIManager#setNeedsLayout is a method which updated layout only which // in its turn will trigger relayout if no batch has been activated @interface RCTUIManager () - (void)updateView:(nonnull NSNumber *)reactTag viewName:(NSString *)viewName props:(NSDictionary *)props; - (void)setNeedsLayout; @end @implementation REANodesManager { NSMutableDictionary *_nodes; NSMapTable *_eventMapping; NSMutableArray> *_eventQueue; CADisplayLink *_displayLink; REAUpdateContext *_updateContext; BOOL _wantRunUpdates; BOOL _processingDirectEvent; NSMutableArray *_onAnimationCallbacks; NSMutableArray *_operationsInBatch; } - (instancetype)initWithModule:(REAModule *)reanimatedModule uiManager:(RCTUIManager *)uiManager { if ((self = [super init])) { _reanimatedModule = reanimatedModule; _uiManager = uiManager; _nodes = [NSMutableDictionary new]; _eventMapping = [NSMapTable strongToWeakObjectsMapTable]; _eventQueue = [NSMutableArray new]; _updateContext = [REAUpdateContext new]; _wantRunUpdates = NO; _onAnimationCallbacks = [NSMutableArray new]; _operationsInBatch = [NSMutableArray new]; } return self; } - (void)invalidate { [self stopUpdatingOnAnimationFrame]; } - (void)operationsBatchDidComplete { if (_displayLink) { // if display link is set it means some of the operations that have run as a part of the batch // requested updates. We want updates to be run in the same frame as in which operations have // been scheduled as it may mean the new view has just been mounted and expects its initial // props to be calculated. // Unfortunately if the operation has just scheduled animation callback it won't run until the // next frame, so it's being triggered manually. _wantRunUpdates = YES; [self performOperations]; } } - (REANode *)findNodeByID:(REANodeID)nodeID { return _nodes[nodeID]; } - (void)postOnAnimation:(REAOnAnimationCallback)clb { [_onAnimationCallbacks addObject:clb]; [self startUpdatingOnAnimationFrame]; } - (void)postRunUpdatesAfterAnimation { _wantRunUpdates = YES; if (!_processingDirectEvent) { [self startUpdatingOnAnimationFrame]; } } - (void)startUpdatingOnAnimationFrame { if (!_displayLink) { // Setting _currentAnimationTimestamp here is connected with manual triggering of performOperations // in operationsBatchDidComplete. If new node has been created and clock has not been started, // _displayLink won't be initialized soon enough and _displayLink.timestamp will be 0. // However, CADisplayLink is using CACurrentMediaTime so if there's need to perform one more // evaluation, it could be used it here. In usual case, CACurrentMediaTime is not being used in // favor of setting it with _displayLink.timestamp in onAnimationFrame method. _currentAnimationTimestamp = CACurrentMediaTime(); _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onAnimationFrame:)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } } - (void)stopUpdatingOnAnimationFrame { if (_displayLink) { [_displayLink invalidate]; _displayLink = nil; } } - (void)onAnimationFrame:(CADisplayLink *)displayLink { // We process all enqueued events first _currentAnimationTimestamp = _displayLink.timestamp; for (NSUInteger i = 0; i < _eventQueue.count; i++) { id event = _eventQueue[i]; [self processEvent:event]; } [_eventQueue removeAllObjects]; NSArray *callbacks = _onAnimationCallbacks; _onAnimationCallbacks = [NSMutableArray new]; // When one of the callbacks would postOnAnimation callback we don't want // to process it until the next frame. This is why we cpy the array before // we iterate over it for (REAOnAnimationCallback block in callbacks) { block(displayLink); } [self performOperations]; if (_onAnimationCallbacks.count == 0) { [self stopUpdatingOnAnimationFrame]; } } - (void)performOperations { if (_wantRunUpdates) { [REANode runPropUpdates:_updateContext]; } if (_operationsInBatch.count != 0) { NSMutableArray *copiedOperationsQueue = _operationsInBatch; _operationsInBatch = [NSMutableArray new]; RCTExecuteOnUIManagerQueue(^{ for (int i = 0; i < copiedOperationsQueue.count; i++) { copiedOperationsQueue[i](self.uiManager); } [self.uiManager setNeedsLayout]; }); } _wantRunUpdates = NO; } - (void)enqueueUpdateViewOnNativeThread:(nonnull NSNumber *)reactTag viewName:(NSString *) viewName nativeProps:(NSMutableDictionary *)nativeProps { [_operationsInBatch addObject:^(RCTUIManager *uiManager) { [uiManager updateView:reactTag viewName:viewName props:nativeProps]; }]; } - (void)getValue:(REANodeID)nodeID callback:(RCTResponseSenderBlock)callback { id val = _nodes[nodeID].value; if (val) { callback(@[val]); } else { // NULL is not an object and it's not possible to pass it as callback's argument callback(@[[NSNull null]]); } } #pragma mark -- Graph - (void)createNode:(REANodeID)nodeID config:(NSDictionary *)config { static NSDictionary *map; static dispatch_once_t mapToken; dispatch_once(&mapToken, ^{ map = @{@"props": [REAPropsNode class], @"style": [REAStyleNode class], @"transform": [REATransformNode class], @"value": [REAValueNode class], @"block": [REABlockNode class], @"cond": [REACondNode class], @"op": [REAOperatorNode class], @"set": [REASetNode class], @"debug": [READebugNode class], @"clock": [REAClockNode class], @"clockStart": [REAClockStartNode class], @"clockStop": [REAClockStopNode class], @"clockTest": [REAClockTestNode class], @"call": [REAJSCallNode class], @"bezier": [REABezierNode class], @"event": [REAEventNode class], @"always": [REAAlwaysNode class], @"concat": [REAConcatNode class], @"param": [REAParamNode class], @"func": [REAFunctionNode class], @"callfunc": [REACallFuncNode class], // @"listener": nil, }; }); NSString *nodeType = [RCTConvert NSString:config[@"type"]]; Class nodeClass = map[nodeType]; if (!nodeClass) { RCTLogError(@"Animated node type %@ not supported natively", nodeType); return; } REANode *node = [[nodeClass alloc] initWithID:nodeID config:config]; node.nodesManager = self; node.updateContext = _updateContext; _nodes[nodeID] = node; } - (void)dropNode:(REANodeID)nodeID { REANode *node = _nodes[nodeID]; if (node) { [_nodes removeObjectForKey:nodeID]; } } - (void)connectNodes:(nonnull NSNumber *)parentID childID:(nonnull REANodeID)childID { RCTAssertParam(parentID); RCTAssertParam(childID); REANode *parentNode = _nodes[parentID]; REANode *childNode = _nodes[childID]; RCTAssertParam(childNode); [parentNode addChild:childNode]; } - (void)disconnectNodes:(REANodeID)parentID childID:(REANodeID)childID { RCTAssertParam(parentID); RCTAssertParam(childID); REANode *parentNode = _nodes[parentID]; REANode *childNode = _nodes[childID]; RCTAssertParam(childNode); [parentNode removeChild:childNode]; } - (void)connectNodeToView:(REANodeID)nodeID viewTag:(NSNumber *)viewTag viewName:(NSString *)viewName { RCTAssertParam(nodeID); REANode *node = _nodes[nodeID]; RCTAssertParam(node); if ([node isKindOfClass:[REAPropsNode class]]) { [(REAPropsNode *)node connectToView:viewTag viewName:viewName]; } } - (void)disconnectNodeFromView:(REANodeID)nodeID viewTag:(NSNumber *)viewTag { RCTAssertParam(nodeID); REANode *node = _nodes[nodeID]; RCTAssertParam(node); if ([node isKindOfClass:[REAPropsNode class]]) { [(REAPropsNode *)node disconnectFromView:viewTag]; } } - (void)attachEvent:(NSNumber *)viewTag eventName:(NSString *)eventName eventNodeID:(REANodeID)eventNodeID { RCTAssertParam(eventNodeID); REANode *eventNode = _nodes[eventNodeID]; RCTAssert([eventNode isKindOfClass:[REAEventNode class]], @"Event node is of an invalid type"); NSString *key = [NSString stringWithFormat:@"%@%@", viewTag, RCTNormalizeInputEventName(eventName)]; RCTAssert([_eventMapping objectForKey:key] == nil, @"Event handler already set for the given view and event type"); [_eventMapping setObject:eventNode forKey:key]; } - (void)detachEvent:(NSNumber *)viewTag eventName:(NSString *)eventName eventNodeID:(REANodeID)eventNodeID { NSString *key = [NSString stringWithFormat:@"%@%@", viewTag, RCTNormalizeInputEventName(eventName)]; [_eventMapping removeObjectForKey:key]; } - (void)processEvent:(id)event { NSString *key = [NSString stringWithFormat:@"%@%@", event.viewTag, RCTNormalizeInputEventName(event.eventName)]; REAEventNode *eventNode = [_eventMapping objectForKey:key]; [eventNode processEvent:event]; } - (void)processDirectEvent:(id)event { _processingDirectEvent = YES; [self processEvent:event]; [self performOperations]; _processingDirectEvent = NO; } - (BOOL)isDirectEvent:(id)event { static NSArray *directEventNames; static dispatch_once_t directEventNamesToken; dispatch_once(&directEventNamesToken, ^{ directEventNames = @[ @"topContentSizeChange", @"topMomentumScrollBegin", @"topMomentumScrollEnd", @"topScroll", @"topScrollBeginDrag", @"topScrollEndDrag" ]; }); return [directEventNames containsObject:RCTNormalizeInputEventName(event.eventName)]; } - (void)dispatchEvent:(id)event { NSString *key = [NSString stringWithFormat:@"%@%@", event.viewTag, RCTNormalizeInputEventName(event.eventName)]; REANode *eventNode = [_eventMapping objectForKey:key]; if (eventNode != nil) { if ([self isDirectEvent:event]) { // Bypass the event queue/animation frames and process scroll events // immediately to avoid getting out of sync with the scroll position [self processDirectEvent:event]; } else { // enqueue node to be processed [_eventQueue addObject:event]; [self startUpdatingOnAnimationFrame]; } } } - (void)configureProps:(NSSet *)nativeProps uiProps:(NSSet *)uiProps { _uiProps = uiProps; _nativeProps = nativeProps; } - (void)setValueForNodeID:(nonnull NSNumber *)nodeID value:(nonnull NSNumber *)newValue { RCTAssertParam(nodeID); REANode *node = _nodes[nodeID]; REAValueNode *valueNode = (REAValueNode *)node; [valueNode setValue:newValue]; } @end