#include #include #include #include #include #include #include #include #include #ifdef __ANDROID__ #include #endif // __ANDROID__ #include #include #include #include #include #include #include #include #include namespace reanimated { #if REACT_NATIVE_MINOR_VERSION >= 81 static inline std::shared_ptr shadowNodeFromValue( jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper) { return Bridging>::fromJs( rt, shadowNodeWrapper); } #endif namespace { #ifdef ANDROID constexpr bool shouldUseSynchronousUpdatesInPerformOperations() { return StaticFeatureFlags::getFlag("ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS"); } #else constexpr bool shouldUseSynchronousUpdatesInPerformOperations() { return false; } #endif std::pair partitionUpdates( const UpdatesBatch &updatesBatch, const std::unordered_set &synchronousPropNames, const bool shouldRequireIntegerColors = false, const bool allowPartialViews = false) { UpdatesBatch synchronousUpdatesBatch; UpdatesBatch shadowTreeUpdatesBatch; for (const auto &[shadowNode, props] : updatesBatch) { if (allowPartialViews) { folly::dynamic synchronousProps = folly::dynamic::object(); folly::dynamic shadowTreeProps = folly::dynamic::object(); for (const auto &[key, value] : props.items()) { const auto keyStr = key.asString(); const bool isColorProp = keyStr == "color" || keyStr.find("Color") != std::string::npos; const bool isSynchronous = synchronousPropNames.contains(keyStr) && (!shouldRequireIntegerColors || !isColorProp || value.isInt()); if (isSynchronous) { synchronousProps[keyStr] = value; } else { shadowTreeProps[keyStr] = value; } } if (!synchronousProps.empty()) { synchronousUpdatesBatch.emplace_back( shadowNode, std::move(synchronousProps)); } if (!shadowTreeProps.empty()) { shadowTreeUpdatesBatch.emplace_back( shadowNode, std::move(shadowTreeProps)); } } else { bool hasOnlySynchronousProps = true; for (const auto &[key, value] : props.items()) { const auto keyStr = key.asString(); const bool isColorProp = keyStr == "color" || keyStr.find("Color") != std::string::npos; const bool isSynchronous = synchronousPropNames.contains(keyStr) && (!shouldRequireIntegerColors || !isColorProp || value.isInt()); if (!isSynchronous) { hasOnlySynchronousProps = false; break; } } if (hasOnlySynchronousProps) { synchronousUpdatesBatch.emplace_back(shadowNode, props); } else { shadowTreeUpdatesBatch.emplace_back(shadowNode, props); } } } return { std::move(synchronousUpdatesBatch), std::move(shadowTreeUpdatesBatch)}; } } // namespace ReanimatedModuleProxy::ReanimatedModuleProxy( const std::shared_ptr &workletsModuleProxy, jsi::Runtime &rnRuntime, const std::shared_ptr &jsCallInvoker, const PlatformDepMethodsHolder &platformDepMethodsHolder, const bool isReducedMotion) : ReanimatedModuleProxySpec(jsCallInvoker), isReducedMotion_(isReducedMotion), workletsModuleProxy_(workletsModuleProxy), eventHandlerRegistry_(std::make_unique()), requestRender_(platformDepMethodsHolder.requestRender), animatedSensorModule_(platformDepMethodsHolder), jsLogger_( std::make_shared(workletsModuleProxy->getJSScheduler())), layoutAnimationsManager_( std::make_shared(jsLogger_)), getAnimationTimestamp_(platformDepMethodsHolder.getAnimationTimestamp), animatedPropsRegistry_(std::make_shared()), staticPropsRegistry_(std::make_shared()), updatesRegistryManager_( std::make_shared(staticPropsRegistry_)), viewStylesRepository_(std::make_shared( staticPropsRegistry_, animatedPropsRegistry_)), cssAnimationKeyframesRegistry_( std::make_shared(viewStylesRepository_)), cssAnimationsRegistry_(std::make_shared()), cssTransitionsRegistry_(std::make_shared( staticPropsRegistry_, getAnimationTimestamp_)), #ifdef ANDROID synchronouslyUpdateUIPropsFunction_( platformDepMethodsHolder.synchronouslyUpdateUIPropsFunction), #endif // ANDROID subscribeForKeyboardEventsFunction_( platformDepMethodsHolder.subscribeForKeyboardEvents), unsubscribeFromKeyboardEventsFunction_( platformDepMethodsHolder.unsubscribeFromKeyboardEvents) { auto lock = updatesRegistryManager_->lock(); // Add registries in order of their priority (from the lowest to the // highest) // CSS transitions should be overriden by animated style animations; // animated style animations should be overriden by CSS animations updatesRegistryManager_->addRegistry(cssTransitionsRegistry_); updatesRegistryManager_->addRegistry(animatedPropsRegistry_); updatesRegistryManager_->addRegistry(cssAnimationsRegistry_); } void ReanimatedModuleProxy::init( const PlatformDepMethodsHolder &platformDepMethodsHolder) { auto onRenderCallback = [weakThis = weak_from_this()](const double timestampMs) { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->renderRequested_ = false; strongThis->onRender(timestampMs); }; onRenderCallback_ = std::move(onRenderCallback); auto updateProps = [weakThis = weak_from_this()]( jsi::Runtime &rt, const jsi::Value &operations) { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->animatedPropsRegistry_->update(rt, operations); }; auto measure = [weakThis = weak_from_this()]( jsi::Runtime &rt, const jsi::Value &shadowNodeValue) -> jsi::Value { auto strongThis = weakThis.lock(); if (!strongThis) { return jsi::Value::undefined(); } return strongThis->measure(rt, shadowNodeValue); }; auto dispatchCommand = [weakThis = weak_from_this()]( jsi::Runtime &rt, const jsi::Value &shadowNodeValue, const jsi::Value &commandNameValue, const jsi::Value &argsValue) { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->dispatchCommand( rt, shadowNodeValue, commandNameValue, argsValue); }; ProgressLayoutAnimationFunction progressLayoutAnimation = [weakThis = weak_from_this()]( jsi::Runtime &rt, int tag, const jsi::Object &newStyle) { auto strongThis = weakThis.lock(); if (!strongThis) { return; } auto surfaceId = strongThis->layoutAnimationsProxy_->progressLayoutAnimation( tag, newStyle); if (!surfaceId) { return; } strongThis->layoutAnimationFlushRequests_.insert(*surfaceId); }; auto requestLayoutAnimationRender = [weakThis = weak_from_this()](double) { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->layoutAnimationRenderRequested_ = false; }; EndLayoutAnimationFunction endLayoutAnimation = [weakThis = weak_from_this(), requestLayoutAnimationRender]( int tag, bool shouldRemove) { auto strongThis = weakThis.lock(); if (!strongThis) { return; } auto surfaceId = strongThis->layoutAnimationsProxy_->endLayoutAnimation( tag, shouldRemove); if (!strongThis->layoutAnimationRenderRequested_) { strongThis->layoutAnimationRenderRequested_ = true; // if an animation has duration 0, performOperations would not get // called for it so we call requestRender to have it called in the // next frame strongThis->requestRender_(requestLayoutAnimationRender); } if (!surfaceId) { return; } strongThis->layoutAnimationFlushRequests_.insert(*surfaceId); }; auto obtainProp = [weakThis = weak_from_this()]( jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &propName) { auto strongThis = weakThis.lock(); if (!strongThis) { return jsi::String::createFromUtf8(rt, ""); } return strongThis->obtainProp(rt, shadowNodeWrapper, propName); }; jsi::Runtime &uiRuntime = workletsModuleProxy_->getUIWorkletRuntime()->getJSIRuntime(); UIRuntimeDecorator::decorate( uiRuntime, obtainProp, updateProps, measure, dispatchCommand, platformDepMethodsHolder.getAnimationTimestamp, platformDepMethodsHolder.setGestureStateFunction, progressLayoutAnimation, endLayoutAnimation, platformDepMethodsHolder.maybeFlushUIUpdatesQueueFunction); } ReanimatedModuleProxy::~ReanimatedModuleProxy() { // event handler registry and frame callbacks store some JSI values from UI // runtime, so they have to go away before we tear down the runtime eventHandlerRegistry_.reset(); frameCallbacks_.clear(); } jsi::Value ReanimatedModuleProxy::registerEventHandler( jsi::Runtime &rt, const jsi::Value &worklet, const jsi::Value &eventName, const jsi::Value &emitterReactTag) { static uint64_t NEXT_EVENT_HANDLER_ID = 1; uint64_t newRegistrationId = NEXT_EVENT_HANDLER_ID++; auto eventNameStr = eventName.asString(rt).utf8(rt); auto handlerSerializable = extractSerializableOrThrow( rt, worklet, "[Reanimated] Event handler must be a serializable worklet."); int emitterReactTagInt = emitterReactTag.asNumber(); workletsModuleProxy_->getUIScheduler()->scheduleOnUI( [=, weakThis = weak_from_this()]() { auto strongThis = weakThis.lock(); if (!strongThis) { return; } auto handler = std::make_shared( newRegistrationId, eventNameStr, emitterReactTagInt, handlerSerializable); strongThis->eventHandlerRegistry_->registerEventHandler( std::move(handler)); }); return jsi::Value(static_cast(newRegistrationId)); } void ReanimatedModuleProxy::unregisterEventHandler( jsi::Runtime &, const jsi::Value ®istrationId) { uint64_t id = registrationId.asNumber(); workletsModuleProxy_->getUIScheduler()->scheduleOnUI( [=, weakThis = weak_from_this()]() { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->eventHandlerRegistry_->unregisterEventHandler(id); }); } std::string ReanimatedModuleProxy::obtainPropFromShadowNode( jsi::Runtime &rt, const std::string &propName, const std::shared_ptr &shadowNode) { auto newestCloneOfShadowNode = uiManager_->getNewestCloneOfShadowNode(*shadowNode); return PropValueProcessor::processPropValue( propName, newestCloneOfShadowNode, rt); } jsi::Value ReanimatedModuleProxy::getViewProp( jsi::Runtime &rnRuntime, const jsi::Value &shadowNodeWrapper, const jsi::Value &propName, const jsi::Value &callback) { const auto propNameStr = propName.asString(rnRuntime).utf8(rnRuntime); const auto funPtr = std::make_shared( callback.getObject(rnRuntime).asFunction(rnRuntime)); const auto shadowNode = shadowNodeFromValue(rnRuntime, shadowNodeWrapper); workletsModuleProxy_->getUIScheduler()->scheduleOnUI( [=, weakThis = weak_from_this()]() { auto strongThis = weakThis.lock(); if (!strongThis) { return; } jsi::Runtime &uiRuntime = strongThis->workletsModuleProxy_->getUIWorkletRuntime() ->getJSIRuntime(); const auto resultStr = strongThis->obtainPropFromShadowNode( uiRuntime, propNameStr, shadowNode); strongThis->workletsModuleProxy_->getJSScheduler()->scheduleOnJS( [=](jsi::Runtime &rnRuntime) { const auto resultValue = jsi::String::createFromUtf8(rnRuntime, resultStr); funPtr->call(rnRuntime, resultValue); }); }); return jsi::Value::undefined(); } jsi::Value ReanimatedModuleProxy::getStaticFeatureFlag( jsi::Runtime &rt, const jsi::Value &name) { return reanimated::StaticFeatureFlags::getFlag(name.asString(rt).utf8(rt)); } jsi::Value ReanimatedModuleProxy::setDynamicFeatureFlag( jsi::Runtime &rt, const jsi::Value &name, const jsi::Value &value) { reanimated::DynamicFeatureFlags::setFlag( name.asString(rt).utf8(rt), value.asBool()); return jsi::Value::undefined(); } jsi::Value ReanimatedModuleProxy::configureLayoutAnimationBatch( jsi::Runtime &rt, const jsi::Value &layoutAnimationsBatch) { auto array = layoutAnimationsBatch.asObject(rt).asArray(rt); size_t length = array.size(rt); std::vector batch(length); for (int i = 0; i < length; i++) { auto item = array.getValueAtIndex(rt, i).asObject(rt); auto &batchItem = batch[i]; batchItem.tag = item.getProperty(rt, "viewTag").asNumber(); batchItem.type = static_cast( item.getProperty(rt, "type").asNumber()); auto config = item.getProperty(rt, "config"); if (config.isUndefined()) { batchItem.config = nullptr; } else { batchItem.config = extractSerializableOrThrow( rt, config, "[Reanimated] Layout animation config must be an object."); } } layoutAnimationsManager_->configureAnimationBatch(batch); return jsi::Value::undefined(); } void ReanimatedModuleProxy::setShouldAnimateExiting( jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &shouldAnimate) { layoutAnimationsManager_->setShouldAnimateExiting( viewTag.asNumber(), shouldAnimate.getBool()); } bool ReanimatedModuleProxy::isAnyHandlerWaitingForEvent( const std::string &eventName, const int emitterReactTag) { return eventHandlerRegistry_->isAnyHandlerWaitingForEvent( eventName, emitterReactTag); } void ReanimatedModuleProxy::maybeRequestRender() { if (!renderRequested_) { renderRequested_ = true; requestRender_(onRenderCallback_); } } void ReanimatedModuleProxy::onRender(double timestampMs) { ReanimatedSystraceSection s("ReanimatedModuleProxy::onRender"); auto callbacks = std::move(frameCallbacks_); frameCallbacks_.clear(); jsi::Runtime &uiRuntime = workletsModuleProxy_->getUIWorkletRuntime()->getJSIRuntime(); jsi::Value timestamp{timestampMs}; for (const auto &callback : callbacks) { runOnRuntimeGuarded(uiRuntime, *callback, timestamp); } } jsi::Value ReanimatedModuleProxy::registerSensor( jsi::Runtime &rt, const jsi::Value &sensorType, const jsi::Value &interval, const jsi::Value &iosReferenceFrame, const jsi::Value &sensorDataHandler) { return animatedSensorModule_.registerSensor( rt, workletsModuleProxy_->getUIWorkletRuntime(), sensorType, interval, iosReferenceFrame, sensorDataHandler); } void ReanimatedModuleProxy::unregisterSensor( jsi::Runtime &, const jsi::Value &sensorId) { animatedSensorModule_.unregisterSensor(sensorId); } void ReanimatedModuleProxy::cleanupSensors() { animatedSensorModule_.unregisterAllSensors(); } void ReanimatedModuleProxy::setViewStyle( jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &viewStyle) { const auto tag = viewTag.asNumber(); staticPropsRegistry_->set(rt, tag, viewStyle); if (staticPropsRegistry_->hasObservers(tag)) { maybeRunCSSLoop(); } } void ReanimatedModuleProxy::markNodeAsRemovable( jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper) { auto lock = updatesRegistryManager_->lock(); auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper); updatesRegistryManager_->markNodeAsRemovable(shadowNode); } void ReanimatedModuleProxy::unmarkNodeAsRemovable( jsi::Runtime &rt, const jsi::Value &viewTag) { auto lock = updatesRegistryManager_->lock(); updatesRegistryManager_->unmarkNodeAsRemovable(viewTag.asNumber()); } void ReanimatedModuleProxy::registerCSSKeyframes( jsi::Runtime &rt, const jsi::Value &animationName, const jsi::Value &viewName, const jsi::Value &keyframesConfig) { // Convert react view name to Fabric component name const auto componentName = componentNameByReactViewName(viewName.asString(rt).utf8(rt)); cssAnimationKeyframesRegistry_->set( animationName.asString(rt).utf8(rt), componentName, parseCSSAnimationKeyframesConfig( rt, keyframesConfig, componentName, viewStylesRepository_)); } void ReanimatedModuleProxy::unregisterCSSKeyframes( jsi::Runtime &rt, const jsi::Value &animationName, const jsi::Value &viewName) { cssAnimationKeyframesRegistry_->remove( animationName.asString(rt).utf8(rt), componentNameByReactViewName(viewName.asString(rt).utf8(rt))); } void ReanimatedModuleProxy::applyCSSAnimations( jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &animationUpdates) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper); const auto timestamp = getCssTimestamp(); const auto updates = parseCSSAnimationUpdates(rt, animationUpdates); CSSAnimationsMap newAnimations; if (!updates.newAnimationSettings.empty()) { // animationNames always exists when newAnimationSettings is not empty const auto animationNames = updates.animationNames.value(); const auto animationNamesCount = animationNames.size(); for (const auto &[index, settings] : updates.newAnimationSettings) { if (index >= animationNamesCount) { throw std::invalid_argument( "[Reanimated] index is out of bounds of animationNames"); } const auto &animationName = animationNames[index]; const auto &keyframesConfig = cssAnimationKeyframesRegistry_->get( animationName, shadowNode->getComponentName()); newAnimations.emplace( index, std::make_shared( rt, shadowNode, animationName, keyframesConfig, settings, timestamp)); } } { auto lock = cssAnimationsRegistry_->lock(); cssAnimationsRegistry_->apply( rt, shadowNode, updates.animationNames, std::move(newAnimations), updates.settingsUpdates, timestamp); } maybeRunCSSLoop(); } void ReanimatedModuleProxy::unregisterCSSAnimations(const jsi::Value &viewTag) { auto lock = cssAnimationsRegistry_->lock(); cssAnimationsRegistry_->remove(viewTag.asNumber()); } void ReanimatedModuleProxy::registerCSSTransition( jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &transitionConfig) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper); auto transition = std::make_shared( std::move(shadowNode), parseCSSTransitionConfig(rt, transitionConfig), viewStylesRepository_); { auto lock = cssTransitionsRegistry_->lock(); cssTransitionsRegistry_->add(transition); } maybeRunCSSLoop(); } void ReanimatedModuleProxy::updateCSSTransition( jsi::Runtime &rt, const jsi::Value &viewTag, const jsi::Value &configUpdates) { auto lock = cssTransitionsRegistry_->lock(); cssTransitionsRegistry_->updateSettings( viewTag.asNumber(), parsePartialCSSTransitionConfig(rt, configUpdates)); maybeRunCSSLoop(); } void ReanimatedModuleProxy::unregisterCSSTransition( jsi::Runtime &rt, const jsi::Value &viewTag) { auto lock = cssTransitionsRegistry_->lock(); cssTransitionsRegistry_->remove(viewTag.asNumber()); } bool ReanimatedModuleProxy::handleEvent( const std::string &eventName, const int emitterReactTag, const jsi::Value &payload, double currentTime) { ReanimatedSystraceSection s("ReanimatedModuleProxy::handleEvent"); eventHandlerRegistry_->processEvent( workletsModuleProxy_->getUIWorkletRuntime(), currentTime, eventName, emitterReactTag, payload); // TODO: return true if Reanimated successfully handled the event // to avoid sending it to JavaScript return false; } bool ReanimatedModuleProxy::handleRawEvent( const RawEvent &rawEvent, double currentTime) { ReanimatedSystraceSection s("ReanimatedModuleProxy::handleRawEvent"); const EventTarget *eventTarget = rawEvent.eventTarget.get(); if (eventTarget == nullptr) { // after app reload scrollView is unmounted and its content offset is set // to 0 and view is thrown into recycle pool setting content offset // triggers scroll event eventTarget is null though, because it's // unmounting we can just ignore this event, because it's an event on // unmounted component return false; } int tag = eventTarget->getTag(); auto eventType = rawEvent.type; if (eventType.rfind("top", 0) == 0) { eventType = "on" + eventType.substr(3); } if (!isAnyHandlerWaitingForEvent(eventType, tag)) { return false; } jsi::Runtime &rt = workletsModuleProxy_->getUIWorkletRuntime()->getJSIRuntime(); const auto &eventPayload = rawEvent.eventPayload; jsi::Value payload = eventPayload->asJSIValue(rt); auto res = handleEvent(eventType, tag, std::move(payload), currentTime); // TODO: we should call performOperations conditionally if event is handled // (res == true), but for now handleEvent always returns false. Thankfully, // performOperations does not trigger a lot of code if there is nothing to // be done so this is fine for now. performOperations(); return res; } void ReanimatedModuleProxy::cssLoopCallback(const double /*timestampMs*/) { shouldUpdateCssAnimations_ = true; if (cssAnimationsRegistry_->hasUpdates() || cssTransitionsRegistry_->hasUpdates() #ifdef ANDROID || updatesRegistryManager_->hasPropsToRevert() #endif // ANDROID ) { requestRender_([weakThis = weak_from_this()](const double newTimestampMs) { if (auto strongThis = weakThis.lock()) { strongThis->cssLoopCallback(newTimestampMs); } }); } else { cssLoopRunning_ = false; } } void ReanimatedModuleProxy::maybeRunCSSLoop() { if (cssLoopRunning_) { return; } cssLoopRunning_ = true; workletsModuleProxy_->getUIScheduler()->scheduleOnUI( [weakThis = weak_from_this()]() { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->requestRender_([weakThis](const double timestampMs) { if (auto strongThis = weakThis.lock()) { strongThis->cssLoopCallback(timestampMs); } }); }); } double ReanimatedModuleProxy::getCssTimestamp() { if (cssLoopRunning_) { return currentCssTimestamp_; } currentCssTimestamp_ = getAnimationTimestamp_(); return currentCssTimestamp_; } void ReanimatedModuleProxy::performOperations() { ReanimatedSystraceSection s("ReanimatedModuleProxy::performOperations"); auto flushRequestsCopy = std::move(layoutAnimationFlushRequests_); for (const auto surfaceId : flushRequestsCopy) { uiManager_->getShadowTreeRegistry().visit( surfaceId, [](const ShadowTree &shadowTree) { shadowTree.notifyDelegatesOfUpdates(); }); } jsi::Runtime &rt = workletsModuleProxy_->getUIWorkletRuntime()->getJSIRuntime(); UpdatesBatch updatesBatch; { ReanimatedSystraceSection s2("ReanimatedModuleProxy::flushUpdates"); auto lock = updatesRegistryManager_->lock(); if (shouldUpdateCssAnimations_) { currentCssTimestamp_ = getAnimationTimestamp_(); auto lock = cssTransitionsRegistry_->lock(); // Update CSS transitions and flush updates cssTransitionsRegistry_->update(currentCssTimestamp_); cssTransitionsRegistry_->flushUpdates(updatesBatch); } { auto lock = animatedPropsRegistry_->lock(); // Flush all animated props updates animatedPropsRegistry_->flushUpdates(updatesBatch); } if (shouldUpdateCssAnimations_) { auto lock = cssAnimationsRegistry_->lock(); // Update CSS animations and flush updates cssAnimationsRegistry_->update(currentCssTimestamp_); cssAnimationsRegistry_->flushUpdates(updatesBatch); } shouldUpdateCssAnimations_ = false; if constexpr (shouldUseSynchronousUpdatesInPerformOperations()) { applySynchronousUpdates(updatesBatch); } if ((updatesBatch.size() > 0) && updatesRegistryManager_->shouldReanimatedSkipCommit()) { updatesRegistryManager_->pleaseCommitAfterPause(); } } if (updatesRegistryManager_->shouldReanimatedSkipCommit()) { // It may happen that `performOperations` is called on the UI thread // while React Native tries to commit a new tree on the JS thread. // In this case, we should skip the commit here and let React Native do // it. The commit will include the current values from the updates manager // which will be applied in ReanimatedCommitHook. return; } commitUpdates(rt, updatesBatch); // Clear the entire cache after the commit // (we don't know if the view is updated from outside of Reanimated // so we have to clear the entire cache) viewStylesRepository_->clearNodesCache(); } void ReanimatedModuleProxy::performNonLayoutOperations() { ReanimatedSystraceSection s( "ReanimatedModuleProxy::performNonLayoutOperations"); UpdatesBatch updatesBatch = animatedPropsRegistry_->getPendingUpdates(); applySynchronousUpdates(updatesBatch, true); } void ReanimatedModuleProxy::applySynchronousUpdates( UpdatesBatch &updatesBatch, const bool allowPartialUpdates) { #ifdef ANDROID static const std::unordered_set synchronousProps = { "opacity", "elevation", "zIndex", // "shadowOpacity", // not supported on Android // "shadowRadius", // not supported on Android "backgroundColor", // "color", // TODO: fix animating color of Animated.Text, "tintColor", "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderTopStartRadius", "borderTopEndRadius", "borderBottomLeftRadius", "borderBottomRightRadius", "borderBottomStartRadius", "borderBottomEndRadius", "borderStartStartRadius", "borderStartEndRadius", "borderEndStartRadius", "borderEndEndRadius", "borderColor", "borderTopColor", "borderBottomColor", "borderLeftColor", "borderRightColor", "borderStartColor", "borderEndColor", "transform", }; // NOTE: Keep in sync with NativeProxy.java static constexpr auto CMD_START_OF_VIEW = 1; static constexpr auto CMD_START_OF_TRANSFORM = 2; static constexpr auto CMD_END_OF_TRANSFORM = 3; static constexpr auto CMD_END_OF_VIEW = 4; static constexpr auto CMD_OPACITY = 10; static constexpr auto CMD_ELEVATION = 11; static constexpr auto CMD_Z_INDEX = 12; static constexpr auto CMD_SHADOW_OPACITY = 13; static constexpr auto CMD_SHADOW_RADIUS = 14; static constexpr auto CMD_BACKGROUND_COLOR = 15; static constexpr auto CMD_COLOR = 16; static constexpr auto CMD_TINT_COLOR = 17; static constexpr auto CMD_BORDER_RADIUS = 20; static constexpr auto CMD_BORDER_TOP_LEFT_RADIUS = 21; static constexpr auto CMD_BORDER_TOP_RIGHT_RADIUS = 22; static constexpr auto CMD_BORDER_TOP_START_RADIUS = 23; static constexpr auto CMD_BORDER_TOP_END_RADIUS = 24; static constexpr auto CMD_BORDER_BOTTOM_LEFT_RADIUS = 25; static constexpr auto CMD_BORDER_BOTTOM_RIGHT_RADIUS = 26; static constexpr auto CMD_BORDER_BOTTOM_START_RADIUS = 27; static constexpr auto CMD_BORDER_BOTTOM_END_RADIUS = 28; static constexpr auto CMD_BORDER_START_START_RADIUS = 29; static constexpr auto CMD_BORDER_START_END_RADIUS = 30; static constexpr auto CMD_BORDER_END_START_RADIUS = 31; static constexpr auto CMD_BORDER_END_END_RADIUS = 32; static constexpr auto CMD_BORDER_COLOR = 40; static constexpr auto CMD_BORDER_TOP_COLOR = 41; static constexpr auto CMD_BORDER_BOTTOM_COLOR = 42; static constexpr auto CMD_BORDER_LEFT_COLOR = 43; static constexpr auto CMD_BORDER_RIGHT_COLOR = 44; static constexpr auto CMD_BORDER_START_COLOR = 45; static constexpr auto CMD_BORDER_END_COLOR = 46; static constexpr auto CMD_TRANSFORM_TRANSLATE_X = 100; static constexpr auto CMD_TRANSFORM_TRANSLATE_Y = 101; static constexpr auto CMD_TRANSFORM_SCALE = 102; static constexpr auto CMD_TRANSFORM_SCALE_X = 103; static constexpr auto CMD_TRANSFORM_SCALE_Y = 104; static constexpr auto CMD_TRANSFORM_ROTATE = 105; static constexpr auto CMD_TRANSFORM_ROTATE_X = 106; static constexpr auto CMD_TRANSFORM_ROTATE_Y = 107; static constexpr auto CMD_TRANSFORM_ROTATE_Z = 108; static constexpr auto CMD_TRANSFORM_SKEW_X = 109; static constexpr auto CMD_TRANSFORM_SKEW_Y = 110; static constexpr auto CMD_TRANSFORM_MATRIX = 111; static constexpr auto CMD_TRANSFORM_PERSPECTIVE = 112; static constexpr auto CMD_UNIT_DEG = 200; static constexpr auto CMD_UNIT_RAD = 201; static constexpr auto CMD_UNIT_PX = 202; static constexpr auto CMD_UNIT_PERCENT = 203; const auto propNameToCommand = [](const std::string &name) { if (name == "opacity") return CMD_OPACITY; if (name == "elevation") return CMD_ELEVATION; if (name == "zIndex") return CMD_Z_INDEX; if (name == "shadowOpacity") return CMD_SHADOW_OPACITY; if (name == "shadowRadius") return CMD_SHADOW_RADIUS; if (name == "backgroundColor") return CMD_BACKGROUND_COLOR; if (name == "color") return CMD_COLOR; if (name == "tintColor") return CMD_TINT_COLOR; if (name == "borderRadius") return CMD_BORDER_RADIUS; if (name == "borderTopLeftRadius") return CMD_BORDER_TOP_LEFT_RADIUS; if (name == "borderTopRightRadius") return CMD_BORDER_TOP_RIGHT_RADIUS; if (name == "borderTopStartRadius") return CMD_BORDER_TOP_START_RADIUS; if (name == "borderTopEndRadius") return CMD_BORDER_TOP_END_RADIUS; if (name == "borderBottomLeftRadius") return CMD_BORDER_BOTTOM_LEFT_RADIUS; if (name == "borderBottomRightRadius") return CMD_BORDER_BOTTOM_RIGHT_RADIUS; if (name == "borderBottomStartRadius") return CMD_BORDER_BOTTOM_START_RADIUS; if (name == "borderBottomEndRadius") return CMD_BORDER_BOTTOM_END_RADIUS; if (name == "borderStartStartRadius") return CMD_BORDER_START_START_RADIUS; if (name == "borderStartEndRadius") return CMD_BORDER_START_END_RADIUS; if (name == "borderEndStartRadius") return CMD_BORDER_END_START_RADIUS; if (name == "borderEndEndRadius") return CMD_BORDER_END_END_RADIUS; if (name == "borderColor") return CMD_BORDER_COLOR; if (name == "borderTopColor") return CMD_BORDER_TOP_COLOR; if (name == "borderBottomColor") return CMD_BORDER_BOTTOM_COLOR; if (name == "borderLeftColor") return CMD_BORDER_LEFT_COLOR; if (name == "borderRightColor") return CMD_BORDER_RIGHT_COLOR; if (name == "borderStartColor") return CMD_BORDER_START_COLOR; if (name == "borderEndColor") return CMD_BORDER_END_COLOR; if (name == "transform") return CMD_START_OF_TRANSFORM; // TODO: use CMD_TRANSFORM? throw std::runtime_error("[Reanimated] Unsupported style: " + name); }; const auto transformNameToCommand = [](const std::string &name) { if (name == "translateX") return CMD_TRANSFORM_TRANSLATE_X; if (name == "translateY") return CMD_TRANSFORM_TRANSLATE_Y; if (name == "scale") return CMD_TRANSFORM_SCALE; if (name == "scaleX") return CMD_TRANSFORM_SCALE_X; if (name == "scaleY") return CMD_TRANSFORM_SCALE_Y; if (name == "rotate") return CMD_TRANSFORM_ROTATE; if (name == "rotateX") return CMD_TRANSFORM_ROTATE_X; if (name == "rotateY") return CMD_TRANSFORM_ROTATE_Y; if (name == "rotateZ") return CMD_TRANSFORM_ROTATE_Z; if (name == "skewX") return CMD_TRANSFORM_SKEW_X; if (name == "skewY") return CMD_TRANSFORM_SKEW_Y; if (name == "matrix") return CMD_TRANSFORM_MATRIX; if (name == "perspective") return CMD_TRANSFORM_PERSPECTIVE; throw std::runtime_error("[Reanimated] Unsupported transform: " + name); }; auto [synchronousUpdatesBatch, shadowTreeUpdatesBatch] = partitionUpdates( updatesBatch, synchronousProps, true, allowPartialUpdates); if (!synchronousUpdatesBatch.empty()) { std::vector intBuffer; std::vector doubleBuffer; intBuffer.reserve(1024); doubleBuffer.reserve(1024); for (const auto &[shadowNode, props] : synchronousUpdatesBatch) { intBuffer.push_back(CMD_START_OF_VIEW); intBuffer.push_back(shadowNode->getTag()); for (const auto &[key, value] : props.items()) { const auto command = propNameToCommand(key.getString()); switch (command) { case CMD_OPACITY: case CMD_ELEVATION: case CMD_Z_INDEX: case CMD_SHADOW_OPACITY: case CMD_SHADOW_RADIUS: intBuffer.push_back(command); doubleBuffer.push_back(value.asDouble()); break; case CMD_BACKGROUND_COLOR: case CMD_COLOR: case CMD_TINT_COLOR: case CMD_BORDER_COLOR: case CMD_BORDER_TOP_COLOR: case CMD_BORDER_BOTTOM_COLOR: case CMD_BORDER_LEFT_COLOR: case CMD_BORDER_RIGHT_COLOR: case CMD_BORDER_START_COLOR: case CMD_BORDER_END_COLOR: intBuffer.push_back(command); intBuffer.push_back(value.asInt()); break; case CMD_BORDER_RADIUS: case CMD_BORDER_TOP_LEFT_RADIUS: case CMD_BORDER_TOP_RIGHT_RADIUS: case CMD_BORDER_TOP_START_RADIUS: case CMD_BORDER_TOP_END_RADIUS: case CMD_BORDER_BOTTOM_LEFT_RADIUS: case CMD_BORDER_BOTTOM_RIGHT_RADIUS: case CMD_BORDER_BOTTOM_START_RADIUS: case CMD_BORDER_BOTTOM_END_RADIUS: case CMD_BORDER_START_START_RADIUS: case CMD_BORDER_START_END_RADIUS: case CMD_BORDER_END_START_RADIUS: case CMD_BORDER_END_END_RADIUS: intBuffer.push_back(command); if (value.isDouble()) { intBuffer.push_back(CMD_UNIT_PX); doubleBuffer.push_back(value.getDouble()); } else if (value.isString()) { const auto &valueStr = value.getString(); if (!valueStr.ends_with("%")) { throw std::runtime_error( "[Reanimated] Border radius string must be a percentage"); } intBuffer.push_back(CMD_UNIT_PERCENT); doubleBuffer.push_back(std::stof(valueStr.substr(0, -1))); } else { throw std::runtime_error( "[Reanimated] Border radius value must be either a number or a string"); } break; case CMD_START_OF_TRANSFORM: intBuffer.push_back(command); react_native_assert( value.isArray() && "[Reanimated] Transform value must be an array"); for (const auto &item : value) { react_native_assert( item.isObject() && "[Reanimated] Transform array item must be an object"); react_native_assert( item.size() == 1 && "[Reanimated] Transform array item must have exactly one key-value pair"); const auto transformCommand = transformNameToCommand(item.keys().begin()->getString()); const auto &transformValue = *item.values().begin(); switch (transformCommand) { case CMD_TRANSFORM_SCALE: case CMD_TRANSFORM_SCALE_X: case CMD_TRANSFORM_SCALE_Y: case CMD_TRANSFORM_PERSPECTIVE: { intBuffer.push_back(transformCommand); doubleBuffer.push_back(transformValue.asDouble()); break; } case CMD_TRANSFORM_TRANSLATE_X: case CMD_TRANSFORM_TRANSLATE_Y: { intBuffer.push_back(transformCommand); if (transformValue.isDouble()) { intBuffer.push_back(CMD_UNIT_PX); doubleBuffer.push_back(transformValue.getDouble()); } else if (transformValue.isString()) { const auto &transformValueStr = transformValue.getString(); if (!transformValueStr.ends_with("%")) { throw std::runtime_error( "[Reanimated] String translate must be a percentage"); } intBuffer.push_back(CMD_UNIT_PERCENT); doubleBuffer.push_back( std::stof(transformValueStr.substr(0, -1))); } else { throw std::runtime_error( "[Reanimated] Translate value must be either a number or a string"); } break; } case CMD_TRANSFORM_ROTATE: case CMD_TRANSFORM_ROTATE_X: case CMD_TRANSFORM_ROTATE_Y: case CMD_TRANSFORM_ROTATE_Z: case CMD_TRANSFORM_SKEW_X: case CMD_TRANSFORM_SKEW_Y: { const auto &transformValueStr = transformValue.getString(); intBuffer.push_back(transformCommand); if (transformValueStr.ends_with("deg")) { intBuffer.push_back(CMD_UNIT_DEG); } else if (transformValueStr.ends_with("rad")) { intBuffer.push_back(CMD_UNIT_RAD); } else { throw std::runtime_error( "[Reanimated] Unsupported rotation unit: " + transformValueStr); } doubleBuffer.push_back( std::stof(transformValueStr.substr(0, -3))); break; } case CMD_TRANSFORM_MATRIX: { intBuffer.push_back(transformCommand); react_native_assert( transformValue.isArray() && "[Reanimated] Matrix must be an array"); int size = transformValue.size(); intBuffer.push_back(size); for (int i = 0; i < size; i++) { doubleBuffer.push_back(transformValue[i].asDouble()); } break; } } } intBuffer.push_back(CMD_END_OF_TRANSFORM); break; } } intBuffer.push_back(CMD_END_OF_VIEW); } synchronouslyUpdateUIPropsFunction_(intBuffer, doubleBuffer); } updatesBatch = std::move(shadowTreeUpdatesBatch); #endif // ANDROID } void ReanimatedModuleProxy::requestFlushRegistry() { requestRender_([weakThis = weak_from_this()](const double timestamp) { if (auto strongThis = weakThis.lock()) { strongThis->shouldFlushRegistry_ = true; } }); } void ReanimatedModuleProxy::commitUpdates( jsi::Runtime &rt, const UpdatesBatch &updatesBatch) { ReanimatedSystraceSection s("ReanimatedModuleProxy::commitUpdates"); react_native_assert(uiManager_ != nullptr); const auto &shadowTreeRegistry = uiManager_->getShadowTreeRegistry(); std::unordered_map propsMapBySurface; #ifdef ANDROID updatesRegistryManager_->collectPropsToRevertBySurface(propsMapBySurface); #endif if (shouldFlushRegistry_) { shouldFlushRegistry_ = false; const auto propsMap = updatesRegistryManager_->collectProps(); for (auto const &[family, props] : propsMap) { const auto surfaceId = family->getSurfaceId(); auto &propsVector = propsMapBySurface[surfaceId][family]; for (const auto &prop : props) { propsVector.emplace_back(prop); } } } else { for (auto const &[shadowNode, props] : updatesBatch) { SurfaceId surfaceId = shadowNode->getSurfaceId(); auto family = &shadowNode->getFamily(); react_native_assert(family->getSurfaceId() == surfaceId); propsMapBySurface[surfaceId][family].emplace_back(std::move(props)); } } for (auto const &[surfaceId, propsMap] : propsMapBySurface) { shadowTreeRegistry.visit(surfaceId, [&](ShadowTree const &shadowTree) { const auto status = shadowTree.commit( [&](RootShadowNode const &oldRootShadowNode) -> RootShadowNode::Unshared { if (updatesRegistryManager_->shouldReanimatedSkipCommit()) { return nullptr; } auto rootNode = cloneShadowTreeWithNewProps(oldRootShadowNode, propsMap); // Mark the commit as Reanimated commit so that we can distinguish // it in ReanimatedCommitHook. auto reaShadowNode = std::reinterpret_pointer_cast( rootNode); reaShadowNode->setReanimatedCommitTrait(); return rootNode; }, {/* .enableStateReconciliation = */ false, /* .mountSynchronously = */ true}); #ifdef ANDROID if (status == ShadowTree::CommitStatus::Succeeded) { updatesRegistryManager_->clearPropsToRevert(surfaceId); } #endif }); } } void ReanimatedModuleProxy::dispatchCommand( jsi::Runtime &rt, const jsi::Value &shadowNodeValue, const jsi::Value &commandNameValue, const jsi::Value &argsValue) { const auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); std::string commandName = stringFromValue(rt, commandNameValue); folly::dynamic args = commandArgsFromValue(rt, argsValue); const auto &scheduler = static_cast(uiManager_->getDelegate()); if (!scheduler) { return; } const auto &schedulerDelegate = scheduler->getDelegate(); if (schedulerDelegate) { const auto shadowView = ShadowView(*shadowNode); schedulerDelegate->schedulerDidDispatchCommand( shadowView, commandName, args); } } jsi::String ReanimatedModuleProxy::obtainProp( jsi::Runtime &rt, const jsi::Value &shadowNodeWrapper, const jsi::Value &propName) { jsi::Runtime &uiRuntime = workletsModuleProxy_->getUIWorkletRuntime()->getJSIRuntime(); const auto propNameStr = propName.asString(rt).utf8(rt); const auto shadowNode = shadowNodeFromValue(rt, shadowNodeWrapper); const auto resultStr = obtainPropFromShadowNode(uiRuntime, propNameStr, shadowNode); return jsi::String::createFromUtf8(rt, resultStr); } jsi::Value ReanimatedModuleProxy::measure( jsi::Runtime &rt, const jsi::Value &shadowNodeValue) { // based on implementation from UIManagerBinding.cpp auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); auto layoutMetrics = uiManager_->getRelativeLayoutMetrics( *shadowNode, nullptr, {/* .includeTransform = */ true}); if (layoutMetrics == EmptyLayoutMetrics) { // Originally, in this case React Native returns `{0, 0, 0, 0, 0, 0}`, // most likely due to the type of measure callback function which accepts // just an array of numbers (not null). In Reanimated, `measure` returns // `MeasuredDimensions | null`. return jsi::Value::null(); } auto newestCloneOfShadowNode = uiManager_->getNewestCloneOfShadowNode(*shadowNode); auto layoutableShadowNode = dynamic_cast(newestCloneOfShadowNode.get()); facebook::react::Point originRelativeToParent = layoutableShadowNode != nullptr ? layoutableShadowNode->getLayoutMetrics().frame.origin : facebook::react::Point(); auto frame = layoutMetrics.frame; jsi::Object result(rt); result.setProperty( rt, "x", jsi::Value(static_cast(originRelativeToParent.x))); result.setProperty( rt, "y", jsi::Value(static_cast(originRelativeToParent.y))); result.setProperty( rt, "width", jsi::Value(static_cast(frame.size.width))); result.setProperty( rt, "height", jsi::Value(static_cast(frame.size.height))); result.setProperty( rt, "pageX", jsi::Value(static_cast(frame.origin.x))); result.setProperty( rt, "pageY", jsi::Value(static_cast(frame.origin.y))); return result; } void ReanimatedModuleProxy::initializeFabric( const std::shared_ptr &uiManager) { uiManager_ = uiManager; viewStylesRepository_->setUIManager(uiManager_); initializeLayoutAnimationsProxy(); const std::function request = [weakThis = weak_from_this()]() { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->requestFlushRegistry(); }; mountHook_ = std::make_shared( uiManager_, updatesRegistryManager_, request); commitHook_ = std::make_shared( uiManager_, updatesRegistryManager_, layoutAnimationsProxy_); } void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() { uiManager_->setAnimationDelegate(nullptr); auto scheduler = reinterpret_cast(uiManager_->getDelegate()); auto componentDescriptorRegistry = scheduler->getContextContainer() ->at>( "ComponentDescriptorRegistry_DO_NOT_USE_PRETTY_PLEASE") .lock(); if (componentDescriptorRegistry) { layoutAnimationsProxy_ = std::make_shared( layoutAnimationsManager_, componentDescriptorRegistry, scheduler->getContextContainer(), workletsModuleProxy_->getUIWorkletRuntime()->getJSIRuntime(), workletsModuleProxy_->getUIScheduler()); } } #ifdef IS_REANIMATED_EXAMPLE_APP std::string format(bool b) { return b ? "✅" : "❌"; } std::function ReanimatedModuleProxy::createRegistriesLeakCheck() { return [weakThis = weak_from_this()]() { auto strongThis = weakThis.lock(); if (!strongThis) { return std::string(""); } std::string result = ""; result += "AnimatedPropsRegistry: " + format(strongThis->animatedPropsRegistry_->isEmpty()); result += "\nCSSAnimationsRegistry: " + format(strongThis->cssAnimationsRegistry_->isEmpty()); result += "\nCSSTransitionsRegistry: " + format(strongThis->cssTransitionsRegistry_->isEmpty()); result += "\nStaticPropsRegistry: " + format(strongThis->staticPropsRegistry_->isEmpty()) + "\n"; return result; }; } #endif // IS_REANIMATED_EXAMPLE_APP jsi::Value ReanimatedModuleProxy::subscribeForKeyboardEvents( jsi::Runtime &rt, const jsi::Value &handlerWorklet, const jsi::Value &isStatusBarTranslucent, const jsi::Value &isNavigationBarTranslucent) { auto serializableHandler = extractSerializableOrThrow( rt, handlerWorklet, "[Reanimated] Keyboard event handler must be a worklet."); return subscribeForKeyboardEventsFunction_( [=, weakThis = weak_from_this()](int keyboardState, int height) { auto strongThis = weakThis.lock(); if (!strongThis) { return; } strongThis->workletsModuleProxy_->getUIWorkletRuntime()->runGuarded( serializableHandler, jsi::Value(keyboardState), jsi::Value(height)); }, isStatusBarTranslucent.getBool(), isNavigationBarTranslucent.getBool()); } void ReanimatedModuleProxy::unsubscribeFromKeyboardEvents( jsi::Runtime &, const jsi::Value &listenerId) { unsubscribeFromKeyboardEventsFunction_(listenerId.asNumber()); } } // namespace reanimated