diff --git a/apps/common-app/src/examples/LayoutAnimations/BBExample.tsx b/apps/common-app/src/examples/LayoutAnimations/BBExample.tsx
new file mode 100644
index 00000000000..2280ffca6eb
--- /dev/null
+++ b/apps/common-app/src/examples/LayoutAnimations/BBExample.tsx
@@ -0,0 +1,71 @@
+import { Button, StyleSheet, View } from 'react-native';
+import Animated, {
+ BounceIn,
+ FadeInRight,
+ FadeInUp,
+ FadeOutLeft,
+ LinearTransition,
+ RotateOutDownLeft,
+} from 'react-native-reanimated';
+import React, { useState } from 'react';
+
+export default function BBExample() {
+ const [show, setShow] = useState(true);
+ const [show2, setShow2] = useState(false);
+
+ return (
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ marginTop: 200,
+ backgroundColor: 'pink',
+ },
+ box: {
+ marginTop: 10,
+ width: 200,
+ height: 100,
+ backgroundColor: 'tomato',
+ },
+ innerBox: {
+ marginTop: 10,
+ width: 100,
+ height: 50,
+ backgroundColor: 'green',
+ },
+ refresher: {
+ marginTop: 10,
+ width: 100,
+ height: 100,
+ backgroundColor: 'blue',
+ },
+});
diff --git a/apps/common-app/src/examples/LayoutAnimations/DefaultAnimations.tsx b/apps/common-app/src/examples/LayoutAnimations/DefaultAnimations.tsx
index 6fd6a0a2715..173c948e1f3 100644
--- a/apps/common-app/src/examples/LayoutAnimations/DefaultAnimations.tsx
+++ b/apps/common-app/src/examples/LayoutAnimations/DefaultAnimations.tsx
@@ -104,9 +104,12 @@ const AnimatedBlock = ({
{show ? (
setShow(!show)}>
-
- {name}
-
+ {/* Workaround for TouchableWithoutFeedback overwriting the nativeID */}
+
+
+ {name}
+
+
) : null}
{!show ? (
diff --git a/apps/common-app/src/examples/index.ts b/apps/common-app/src/examples/index.ts
index 53c67b915b0..8a271f99436 100644
--- a/apps/common-app/src/examples/index.ts
+++ b/apps/common-app/src/examples/index.ts
@@ -121,6 +121,7 @@ import WithClampExample from './WithClampExample';
import WorkletFactoryCrash from './WorkletFactoryCrashExample';
import RuntimeTestsExample from './RuntimeTests/RuntimeTestsExample';
import HabitsExample from './LayoutAnimations/HabitsExample';
+import BBExample from './LayoutAnimations/BBExample';
import MemoExample from './MemoExample';
import PerformanceMonitorExample from './PerfomanceMonitorExample';
import ScreenTransitionExample from './ScreenTransitionExample';
@@ -502,7 +503,6 @@ export const EXAMPLES: Record = {
icon: '🧑💻',
title: 'Habits',
screen: HabitsExample,
- missingOnFabric: true,
},
PerformanceMonitorExample: {
icon: '⏱️',
@@ -524,6 +524,11 @@ export const EXAMPLES: Record = {
title: 'Composed handler internal merging',
screen: ComposedHandlerInternalMergingExample,
},
+ BBExample: {
+ icon: '💀',
+ title: 'BB',
+ screen: BBExample,
+ },
// Old examples
@@ -598,127 +603,102 @@ export const EXAMPLES: Record = {
DeleteAncestorOfExiting: {
title: '[LA] Deleting view with an exiting animation',
screen: DeleteAncestorOfExiting,
- missingOnFabric: true,
},
NestedNativeStacksWithLayout: {
title: '[LA] Nested NativeStacks with layout',
screen: NestedNativeStacksWithLayout,
- missingOnFabric: true,
},
BasicLayoutAnimation: {
title: '[LA] Basic layout animation',
screen: BasicLayoutAnimation,
- missingOnFabric: true,
},
BasicNestedAnimation: {
title: '[LA] Basic nested animation',
screen: BasicNestedAnimation,
- missingOnFabric: true,
},
BasicNestedLayoutAnimation: {
title: '[LA] Basic nested layout animation',
screen: BasicNestedLayoutAnimation,
- missingOnFabric: true,
},
NestedLayoutAnimations: {
title: '[LA] Nested layout animations',
screen: NestedTest,
- missingOnFabric: true,
},
CombinedLayoutAnimations: {
title: '[LA] Entering and Exiting with Layout',
screen: CombinedTest,
- missingOnFabric: true,
},
DefaultAnimations: {
title: '[LA] Default layout animations',
screen: DefaultAnimations,
- missingOnFabric: true,
},
DefaultTransitions: {
title: '[LA] Default layout transitions',
screen: WaterfallGridExample,
- missingOnFabric: true,
},
KeyframeAnimation: {
title: '[LA] Keyframe animation',
screen: KeyframeAnimation,
- missingOnFabric: true,
},
ParticipantList: {
title: '[LA] Participant List',
screen: AnimatedListExample,
- missingOnFabric: true,
},
OlympicAnimation: {
title: '[LA] Olympic animation',
screen: OlympicAnimation,
- missingOnFabric: true,
},
CustomLayoutAnimation: {
title: '[LA] Custom layout animation',
screen: CustomLayoutAnimationScreen,
- missingOnFabric: true,
},
ModalNewAPI: {
title: '[LA] ModalNewAPI',
screen: ModalNewAPI,
- missingOnFabric: true,
},
SpringLayoutAnimation: {
title: '[LA] Spring Layout Animation',
screen: SpringLayoutAnimation,
- missingOnFabric: true,
},
MountingUnmounting: {
title: '[LA] Mounting Unmounting',
screen: MountingUnmounting,
- missingOnFabric: true,
},
ReactionsCounterExample: {
title: '[LA] Reactions counter',
screen: ReactionsCounterExample,
- missingOnFabric: true,
},
SwipeableList: {
title: '[LA] Swipeable list',
screen: SwipeableList,
- missingOnFabric: true,
},
Modal: {
title: '[LA] Modal',
screen: Modal,
- missingOnFabric: true,
},
NativeModals: {
title: '[LA] Native modals (RN and Screens)',
screen: NativeModals,
- missingOnFabric: true,
},
Carousel: {
title: '[LA] Carousel',
screen: Carousel,
- missingOnFabric: true,
},
ReducedMotionLayoutExample: {
title: '[LA] Reduced Motion',
screen: ReducedMotionLayoutExample,
- missingOnFabric: true,
},
NestedLayoutAnimationConfig: {
title: '[LA] Nested LayoutAnimationConfig',
screen: NestedLayoutAnimationConfig,
- missingOnFabric: true,
},
FlatListSkipEnteringExiting: {
title: '[LA] FlatList skip entering & exiting',
screen: FlatListSkipEnteringExiting,
- missingOnFabric: true,
},
ChangeTheme: {
title: '[LA] Change theme',
screen: ChangeThemeExample,
- missingOnFabric: true,
},
// Shared Element Transitions
diff --git a/packages/docs-reanimated/docs/layout-animations/entering-exiting-animations.mdx b/packages/docs-reanimated/docs/layout-animations/entering-exiting-animations.mdx
index e28ce87546d..84dfff1f31a 100644
--- a/packages/docs-reanimated/docs/layout-animations/entering-exiting-animations.mdx
+++ b/packages/docs-reanimated/docs/layout-animations/entering-exiting-animations.mdx
@@ -11,6 +11,9 @@ Reanimated comes with a bunch of predefined animations you can customize. For mo
## Remarks
- We recommend using layout animation builders outside of components or with `useMemo` to ensure the best performance.
+- On the New Architecture:
+ - `nativeID` is used internally to configure entering animations, so overwriting it will result in entering animations not running. Some components (e.g. TouchableWithoutFeedback) overwrite `nativeID` of its children. To work around this issue wrap your animated children with a `View`.
+ - removing a non-animated view will trigger exiting animations in its children, but the non-animated view will not wait for the children's animations to finish. This is due to view flattening and can be mitigated by using `collapsable={false}`.
## Fade
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp
index 7bec48e950f..97c1228ff3d 100644
--- a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp
@@ -20,6 +20,12 @@ void LayoutAnimationsManager::configureAnimationBatch(
clearSharedTransitionConfig(tag);
sharedTransitionConfigs.push_back(std::move(layoutAnimationConfig));
} else {
+#ifdef RCT_NEW_ARCH_ENABLED
+ if (type == ENTERING){
+ enteringAnimationsForNativeID_[tag] = config;
+ continue;
+ }
+#endif
if (config == nullptr) {
getConfigsForType(type).erase(tag);
} else {
@@ -159,6 +165,19 @@ int LayoutAnimationsManager::findPrecedingViewTagForTransition(const int tag) {
return -1;
}
+#ifdef RCT_NEW_ARCH_ENABLED
+void LayoutAnimationsManager::transferConfigFromNativeID(
+ const int nativeId,
+ const int tag) {
+ auto lock = std::unique_lock(animationsMutex_);
+ auto config = enteringAnimationsForNativeID_[nativeId];
+ if (config) {
+ enteringAnimations_.insert_or_assign(tag, config);
+ }
+ enteringAnimationsForNativeID_.erase(nativeId);
+}
+#endif
+
#ifndef NDEBUG
std::string LayoutAnimationsManager::getScreenSharedTagPairString(
const int screenTag,
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h
index c5e3e8fd528..470e6d7b96a 100644
--- a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h
@@ -42,6 +42,9 @@ class LayoutAnimationsManager {
void clearLayoutAnimationConfig(const int tag);
void clearSharedTransitionConfig(const int tag);
void cancelLayoutAnimation(jsi::Runtime &rt, const int tag) const;
+#ifdef RCT_NEW_ARCH_ENABLED
+ void transferConfigFromNativeID(const int nativeId, const int tag);
+#endif
int findPrecedingViewTagForTransition(const int tag);
#ifndef NDEBUG
std::string getScreenSharedTagPairString(
@@ -63,6 +66,9 @@ class LayoutAnimationsManager {
std::unordered_map viewsScreenSharedTagMap_;
#endif
+#ifdef RCT_NEW_ARCH_ENABLED
+ std::unordered_map> enteringAnimationsForNativeID_;
+#endif
std::unordered_map> enteringAnimations_;
std::unordered_map> exitingAnimations_;
std::unordered_map> layoutAnimations_;
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsProxy.cpp b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsProxy.cpp
new file mode 100644
index 00000000000..eb2389e506a
--- /dev/null
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsProxy.cpp
@@ -0,0 +1,717 @@
+#ifdef RCT_NEW_ARCH_ENABLED
+
+#include "LayoutAnimationsProxy.h"
+#include
+#include
+#include
+#include
+#include "NativeReanimatedModule.h"
+
+namespace reanimated {
+
+// We never modify the Shadow Tree, we just send some additional
+// mutations to the mounting layer.
+// When animations finish, the Host Tree will represent the most recent Shadow
+// Tree
+// On android this code will be sometimes executed on the JS thread.
+// That's why we have to schedule some of animation manager function on the UI thread
+std::optional LayoutAnimationsProxy::pullTransaction(
+ SurfaceId surfaceId,
+ MountingTransaction::Number transactionNumber,
+ const TransactionTelemetry &telemetry,
+ ShadowViewMutationList mutations) const {
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "\npullTransaction " << std::this_thread::get_id() << " "
+ << surfaceId << std::endl;
+#endif
+ auto lock = std::unique_lock(mutex);
+ PropsParserContext propsParserContext{surfaceId, *contextContainer_};
+ ShadowViewMutationList filteredMutations;
+
+ std::vector> roots;
+ std::unordered_map movedViews;
+
+ parseRemoveMutations(movedViews, mutations, roots);
+
+ handleRemovals(filteredMutations, roots);
+
+ handleUpdatesAndEnterings(
+ filteredMutations, movedViews, mutations, propsParserContext, surfaceId);
+
+ addOngoingAnimations(surfaceId, filteredMutations);
+
+ return MountingTransaction{
+ surfaceId, transactionNumber, std::move(filteredMutations), telemetry};
+}
+
+std::optional LayoutAnimationsProxy::progressLayoutAnimation(
+ int tag,
+ const jsi::Object &newStyle) {
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "progress layout animation for tag " << tag << std::endl;
+#endif
+ auto lock = std::unique_lock(mutex);
+ auto layoutAnimationIt = layoutAnimations_.find(tag);
+
+ if (layoutAnimationIt == layoutAnimations_.end()) {
+ return {};
+ }
+
+ auto &layoutAnimation = layoutAnimationIt->second;
+
+ maybeRestoreOpacity(layoutAnimation, newStyle);
+
+ auto rawProps =
+ std::make_shared(uiRuntime_, jsi::Value(uiRuntime_, newStyle));
+
+ PropsParserContext propsParserContext{
+ layoutAnimation.finalView->surfaceId, *contextContainer_};
+#ifdef ANDROID
+ rawProps = std::make_shared(folly::dynamic::merge(
+ layoutAnimation.finalView->props->rawProps, (folly::dynamic)*rawProps));
+#endif
+ auto newProps =
+ getComponentDescriptorForShadowView(*layoutAnimation.finalView)
+ .cloneProps(
+ propsParserContext,
+ layoutAnimation.finalView->props,
+ std::move(*rawProps));
+ auto &updateMap =
+ surfaceManager.getUpdateMap(layoutAnimation.finalView->surfaceId);
+ updateMap.insert_or_assign(
+ tag, UpdateValues{newProps, Frame(uiRuntime_, newStyle)});
+
+ return layoutAnimation.finalView->surfaceId;
+}
+
+std::optional LayoutAnimationsProxy::endLayoutAnimation(
+ int tag,
+ bool shouldRemove) {
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "end layout animation for " << tag << " - should remove "
+ << shouldRemove << std::endl;
+#endif
+ auto lock = std::unique_lock(mutex);
+ auto layoutAnimationIt = layoutAnimations_.find(tag);
+
+ if (layoutAnimationIt == layoutAnimations_.end()) {
+ return {};
+ }
+
+ auto &layoutAnimation = layoutAnimationIt->second;
+
+ // multiple layout animations can be triggered for a view
+ // one after the other, so we need to keep count of how many
+ // were actually triggered, so that we don't cleanup necessary
+ // structures too early
+ if (layoutAnimation.count > 1) {
+ layoutAnimation.count--;
+ return {};
+ }
+
+ auto surfaceId = layoutAnimation.finalView->surfaceId;
+ auto &updateMap = surfaceManager.getUpdateMap(surfaceId);
+ layoutAnimations_.erase(tag);
+ updateMap.erase(tag);
+
+ if (!shouldRemove || !nodeForTag_.contains(tag)) {
+ return {};
+ }
+
+ auto node = nodeForTag_[tag];
+ auto mutationNode = std::static_pointer_cast(node);
+ mutationNode->state = DEAD;
+ deadNodes.insert(mutationNode);
+
+ return surfaceId;
+}
+
+/**
+ Organizes removed views into a tree structure, allowing for convenient
+ traversals and index maintenance
+ */
+void LayoutAnimationsProxy::parseRemoveMutations(
+ std::unordered_map &movedViews,
+ ShadowViewMutationList &mutations,
+ std::vector> &roots) const {
+ std::set deletedViews;
+ std::unordered_map>>
+ childrenForTag, unflattenedChildrenForTag;
+
+ // iterate from the end, so that parents appear before children
+ for (auto it = mutations.rbegin(); it != mutations.rend(); it++) {
+ auto &mutation = *it;
+ if (mutation.type == ShadowViewMutation::Delete) {
+ deletedViews.insert(mutation.oldChildShadowView.tag);
+ }
+ if (mutation.type == ShadowViewMutation::Remove) {
+ updateIndexForMutation(mutation);
+ auto tag = mutation.oldChildShadowView.tag;
+ auto parentTag = mutation.parentShadowView.tag;
+ auto unflattenedParentTag = parentTag; // temporary
+
+ std::shared_ptr mutationNode;
+ std::shared_ptr node = nodeForTag_[tag],
+ parent = nodeForTag_[parentTag],
+ unflattenedParent = nodeForTag_[unflattenedParentTag];
+
+ if (!node) {
+ mutationNode = std::make_shared(mutation);
+ } else {
+ mutationNode =
+ std::make_shared(mutation, std::move(*node));
+ for (auto &subNode : mutationNode->children) {
+ subNode->parent = mutationNode;
+ }
+ for (auto &subNode : mutationNode->unflattenedChildren) {
+ subNode->unflattenedParent = mutationNode;
+ }
+ }
+ if (!deletedViews.contains(mutation.oldChildShadowView.tag)) {
+ mutationNode->state = MOVED;
+ movedViews.insert_or_assign(
+ mutation.oldChildShadowView.tag, mutation.oldChildShadowView);
+ }
+ nodeForTag_[tag] = mutationNode;
+
+ if (!parent) {
+ parent = std::make_shared(parentTag);
+ nodeForTag_[parentTag] = parent;
+ }
+
+ if (!unflattenedParent){
+ if (parentTag == unflattenedParentTag){
+ unflattenedParent = parent;
+ } else {
+ unflattenedParent = std::make_shared(unflattenedParentTag);
+ nodeForTag_[unflattenedParentTag] = unflattenedParent;
+ }
+ }
+
+ if (!unflattenedParent->parent) {
+ roots.push_back(mutationNode);
+ }
+
+ childrenForTag[parentTag].push_back(mutationNode);
+ unflattenedChildrenForTag[unflattenedParentTag].push_back(mutationNode);
+ mutationNode->parent = parent;
+ mutationNode->unflattenedParent = unflattenedParent;
+ }
+ if (mutation.type == ShadowViewMutation::Update &&
+ movedViews.contains(mutation.newChildShadowView.tag)) {
+ auto node = nodeForTag_[mutation.newChildShadowView.tag];
+ auto mutationNode = std::static_pointer_cast(node);
+ mutationNode->mutation.oldChildShadowView = mutation.oldChildShadowView;
+ movedViews[mutation.newChildShadowView.tag] = mutation.oldChildShadowView;
+ }
+ }
+
+ for (auto &[parentTag, children] : childrenForTag) {
+ nodeForTag_[parentTag]->insertChildren(children);
+ }
+ for (auto &[unflattenedParentTag, children] : unflattenedChildrenForTag) {
+ nodeForTag_[unflattenedParentTag]->insertUnflattenedChildren(children);
+ }
+}
+
+void LayoutAnimationsProxy::handleRemovals(
+ ShadowViewMutationList &filteredMutations,
+ std::vector> &roots) const {
+ // iterate from the end, so that children
+ // with higher indices appear first in the mutations list
+ for (auto it = roots.rbegin(); it != roots.rend(); it++) {
+ auto &node = *it;
+ if (!startAnimationsRecursively(
+ node, true, true, false, filteredMutations)) {
+ filteredMutations.push_back(node->mutation);
+ nodeForTag_.erase(node->tag);
+ node->unflattenedParent->removeChildFromUnflattenedTree(node);//???
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "delete " << node->tag << std::endl;
+#endif
+ if (node->state != MOVED) {
+ maybeCancelAnimation(node->tag);
+ filteredMutations.push_back(ShadowViewMutation::DeleteMutation(
+ node->mutation.oldChildShadowView));
+ }
+ }
+ }
+
+ for (auto node : deadNodes) {
+ if (node->state != DELETED) {
+ endAnimationsRecursively(node, filteredMutations);
+ maybeDropAncestors(node->unflattenedParent, node, filteredMutations);
+ }
+ }
+ deadNodes.clear();
+}
+
+void LayoutAnimationsProxy::handleUpdatesAndEnterings(
+ ShadowViewMutationList &filteredMutations,
+ const std::unordered_map &movedViews,
+ ShadowViewMutationList &mutations,
+ const PropsParserContext &propsParserContext,
+ SurfaceId surfaceId) const {
+ for (auto &mutation : mutations) {
+ maybeUpdateWindowDimensions(mutation, surfaceId);
+
+ Tag tag = mutation.type == ShadowViewMutation::Type::Create ||
+ mutation.type == ShadowViewMutation::Type::Insert
+ ? mutation.newChildShadowView.tag
+ : mutation.oldChildShadowView.tag;
+
+ switch (mutation.type) {
+ case ShadowViewMutation::Type::Create: {
+ filteredMutations.push_back(mutation);
+ break;
+ }
+ case ShadowViewMutation::Type::Insert: {
+ updateIndexForMutation(mutation);
+ if (nodeForTag_.contains(mutation.parentShadowView.tag)) {
+ nodeForTag_[mutation.parentShadowView.tag]->applyMutationToIndices(mutation);
+ }
+
+ if (movedViews.contains(tag)) {
+ auto layoutAnimationIt = layoutAnimations_.find(tag);
+ if (layoutAnimationIt == layoutAnimations_.end()) {
+ filteredMutations.push_back(mutation);
+ continue;
+ }
+
+ auto oldView = *layoutAnimationIt->second.currentView;
+ filteredMutations.push_back(ShadowViewMutation::InsertMutation(
+ mutation.parentShadowView, oldView, mutation.index));
+ continue;
+ }
+
+ transferConfigFromNativeID(
+ mutation.newChildShadowView.props->nativeId,
+ mutation.newChildShadowView.tag);
+ if (!layoutAnimationsManager_->hasLayoutAnimation(tag, ENTERING)) {
+ filteredMutations.push_back(mutation);
+ continue;
+ }
+
+ startEnteringAnimation(tag, mutation);
+ filteredMutations.push_back(mutation);
+
+ // temporarily set opacity to 0 to prevent flickering on android
+ std::shared_ptr newView = cloneViewWithoutOpacity(mutation, propsParserContext);
+
+ filteredMutations.push_back(ShadowViewMutation::UpdateMutation(
+ mutation.newChildShadowView, *newView, mutation.parentShadowView));
+ break;
+ }
+
+ case ShadowViewMutation::Type::Update: {
+ auto shouldAnimate = hasLayoutChanged(mutation);
+ if (!layoutAnimationsManager_->hasLayoutAnimation(tag, LAYOUT) || (!shouldAnimate && !layoutAnimations_.contains(tag))) {
+ // We should cancel any ongoing animation here to ensure that the
+ // proper final state is reached for this view However, due to how
+ // RNSScreens handle adding headers (a second commit is triggered to
+ // offset all the elements by the header height) this would lead to
+ // all entering animations being cancelled when a screen with a header
+ // is pushed onto a stack
+ // TODO: find a better solution for this problem
+ filteredMutations.push_back(mutation);
+ continue;
+ } else if (!shouldAnimate){
+ updateOngoingAnimationTarget(tag, mutation);
+ continue;
+ }
+ startLayoutAnimation(tag, mutation);
+ break;
+ }
+
+ case ShadowViewMutation::Type::Remove:
+ case ShadowViewMutation::Type::Delete: {
+ break;
+ }
+
+ default:
+ filteredMutations.push_back(mutation);
+ }
+ }
+}
+
+void LayoutAnimationsProxy::addOngoingAnimations(
+ SurfaceId surfaceId,
+ ShadowViewMutationList &mutations) const {
+ auto &updateMap = surfaceManager.getUpdateMap(surfaceId);
+ for (auto &[tag, updateValues] : updateMap) {
+ auto layoutAnimationIt = layoutAnimations_.find(tag);
+
+ if (layoutAnimationIt == layoutAnimations_.end()) {
+ continue;
+ }
+
+ auto &layoutAnimation = layoutAnimationIt->second;
+
+ auto newView = std::make_shared(*layoutAnimation.finalView);
+ newView->props = updateValues.newProps;
+ updateLayoutMetrics(newView->layoutMetrics, updateValues.frame);
+
+ mutations.push_back(ShadowViewMutation::UpdateMutation(
+ *layoutAnimation.currentView, *newView, *layoutAnimation.parentView));
+ layoutAnimation.currentView = newView;
+ }
+ updateMap.clear();
+}
+
+void LayoutAnimationsProxy::endAnimationsRecursively(
+ std::shared_ptr node,
+ ShadowViewMutationList &mutations) const {
+ maybeCancelAnimation(node->tag);
+ node->state = DELETED;
+ // iterate from the end, so that children
+ // with higher indices appear first in the mutations list
+ for (auto it = node->unflattenedChildren.rbegin(); it != node->unflattenedChildren.rend(); it++) {
+ auto &subNode = *it;
+ if (subNode->state != DELETED) {
+ endAnimationsRecursively(subNode, mutations);
+ }
+ }
+ mutations.push_back(node->mutation);
+ nodeForTag_.erase(node->tag);
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "delete " << node->tag << std::endl;
+#endif
+ mutations.push_back(
+ ShadowViewMutation::DeleteMutation(node->mutation.oldChildShadowView));
+}
+
+void LayoutAnimationsProxy::maybeDropAncestors(
+ std::shared_ptr parent,
+ std::shared_ptr child,
+ ShadowViewMutationList &cleanupMutations) const {
+ parent->removeChildFromUnflattenedTree(child);
+ if (!parent->isMutationMode()) {
+ return;
+ }
+
+ auto node = std::static_pointer_cast(parent);
+ node->animatedChildren.erase(child->tag);
+
+ if (node->animatedChildren.empty() && node->state != ANIMATING) {
+ nodeForTag_.erase(node->tag);
+ cleanupMutations.push_back(node->mutation);
+ maybeCancelAnimation(node->tag);
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "delete " << node->tag << std::endl;
+#endif
+ cleanupMutations.push_back(
+ ShadowViewMutation::DeleteMutation(node->mutation.oldChildShadowView));
+ maybeDropAncestors(node->unflattenedParent, node, cleanupMutations);
+ }
+}
+
+const ComponentDescriptor &
+LayoutAnimationsProxy::getComponentDescriptorForShadowView(
+ const ShadowView &shadowView) const {
+ return componentDescriptorRegistry_->at(shadowView.componentHandle);
+}
+
+bool LayoutAnimationsProxy::startAnimationsRecursively(
+ std::shared_ptr node,
+ bool shouldRemoveSubviewsWithoutAnimations,
+ bool shouldAnimate,
+ bool isScreenPop,
+ ShadowViewMutationList &mutations) const {
+ if (isRNSScreen(node)) {
+ isScreenPop = true;
+ }
+
+ shouldAnimate = !isScreenPop &&
+ layoutAnimationsManager_->shouldAnimateExiting(node->tag, shouldAnimate);
+
+ bool hasExitAnimation = shouldAnimate &&
+ layoutAnimationsManager_->hasLayoutAnimation(
+ node->tag, LayoutAnimationType::EXITING);
+ bool hasAnimatedChildren = false;
+
+ shouldRemoveSubviewsWithoutAnimations =
+ shouldRemoveSubviewsWithoutAnimations && !hasExitAnimation;
+ std::vector> toBeRemoved;
+
+ // iterate from the end, so that children
+ // with higher indices appear first in the mutations list
+ for (auto it = node->unflattenedChildren.rbegin(); it != node->unflattenedChildren.rend(); it++) {
+ auto &subNode = *it;
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "child " << subNode->tag << " "
+ << " " << shouldAnimate << " "
+ << shouldRemoveSubviewsWithoutAnimations << std::endl;
+#endif
+ if (subNode->state != UNDEFINED && subNode->state != MOVED) {
+ if (shouldAnimate && subNode->state != DEAD) {
+ node->animatedChildren.insert(subNode->tag);
+ hasAnimatedChildren = true;
+ } else {
+ endAnimationsRecursively(subNode, mutations);
+ }
+ } else if (startAnimationsRecursively(
+ subNode,
+ shouldRemoveSubviewsWithoutAnimations,
+ shouldAnimate,
+ isScreenPop,
+ mutations)) {
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "child " << subNode->tag
+ << " start animations returned true " << std::endl;
+#endif
+ node->animatedChildren.insert(subNode->tag);
+ hasAnimatedChildren = true;
+ } else if (subNode->state == MOVED) {
+ mutations.push_back(subNode->mutation);
+ nodeForTag_.erase(subNode->tag);
+ } else if (shouldRemoveSubviewsWithoutAnimations) {
+ maybeCancelAnimation(subNode->tag);
+ mutations.push_back(subNode->mutation);
+ toBeRemoved.push_back(subNode);
+ subNode->state = DELETED;
+ nodeForTag_.erase(subNode->tag);
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "delete " << subNode->tag << std::endl;
+#endif
+ mutations.push_back(ShadowViewMutation::DeleteMutation(
+ subNode->mutation.oldChildShadowView));
+ } else {
+ subNode->state = WAITING;
+ }
+ }
+
+ for (auto &subNode : toBeRemoved) {
+ node->removeChildFromUnflattenedTree(subNode);
+ }
+
+ if (node->state == MOVED) {
+ return false;
+ }
+
+ bool wantAnimateExit = hasExitAnimation || hasAnimatedChildren;
+
+ if (hasExitAnimation) {
+ node->state = ANIMATING;
+ startExitingAnimation(node->tag, node->mutation);
+ } else {
+ layoutAnimationsManager_->clearLayoutAnimationConfig(node->tag);
+ }
+
+ if (!wantAnimateExit) {
+ return false;
+ }
+
+ return true;
+}
+
+void LayoutAnimationsProxy::updateIndexForMutation(
+ ShadowViewMutation &mutation) const {
+ if (mutation.index == -1) {
+ return;
+ }
+ if (!nodeForTag_.contains(mutation.parentShadowView.tag)) {
+ return;
+ }
+
+ auto parent = nodeForTag_[mutation.parentShadowView.tag];
+
+ int size = 0, prevIndex = -1, offset = 0;
+
+ for (auto &subNode : parent->children) {
+ size += subNode->mutation.index - prevIndex - 1;
+ if (mutation.index < size) {
+ break;
+ }
+ offset++;
+ prevIndex = subNode->mutation.index;
+ }
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ int tag = mutation.type == ShadowViewMutation::Insert
+ ? mutation.newChildShadowView.tag
+ : mutation.oldChildShadowView.tag;
+ LOG(INFO) << "update index for " << tag << " in "
+ << mutation.parentShadowView.tag << ": " << mutation.index << " -> "
+ << mutation.index + offset << std::endl;
+#endif
+ mutation.index += offset;
+}
+
+bool LayoutAnimationsProxy::shouldOverridePullTransaction() const {
+ return true;
+}
+
+void LayoutAnimationsProxy::createLayoutAnimation(const ShadowViewMutation &mutation, ShadowView &oldView, const SurfaceId &surfaceId, const int tag) const {
+
+ int count = 1;
+ auto layoutAnimationIt = layoutAnimations_.find(tag);
+
+ if (layoutAnimationIt != layoutAnimations_.end()) {
+ auto &layoutAnimation = layoutAnimationIt->second;
+ oldView = *layoutAnimation.currentView;
+ count = layoutAnimation.count + 1;
+ }
+
+ auto finalView = std::make_shared(mutation.type == ShadowViewMutation::Remove ? mutation.oldChildShadowView : mutation.newChildShadowView);
+ auto currentView = std::make_shared(oldView);
+ auto parentView = std::make_shared(mutation.parentShadowView);
+ layoutAnimations_.insert_or_assign(tag, LayoutAnimation{finalView, currentView, parentView, {}, count});
+}
+
+void LayoutAnimationsProxy::startEnteringAnimation(
+ const int tag,
+ ShadowViewMutation &mutation) const {
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "start entering animation for tag " << tag << std::endl;
+#endif
+ auto finalView = std::make_shared(mutation.newChildShadowView);
+ auto current = std::make_shared(mutation.oldChildShadowView);
+ auto parent = std::make_shared(mutation.parentShadowView);
+
+ auto &viewProps =
+ static_cast(*mutation.newChildShadowView.props);
+ layoutAnimations_.insert_or_assign(
+ tag, LayoutAnimation{finalView, current, parent, viewProps.opacity});
+
+ Snapshot values(
+ mutation.newChildShadowView,
+ surfaceManager.getWindow(mutation.newChildShadowView.surfaceId));
+ uiScheduler_->scheduleOnUI([values, this, tag]() {
+ jsi::Object yogaValues(uiRuntime_);
+ yogaValues.setProperty(uiRuntime_, "targetOriginX", values.x);
+ yogaValues.setProperty(uiRuntime_, "targetGlobalOriginX", values.x);
+ yogaValues.setProperty(uiRuntime_, "targetOriginY", values.y);
+ yogaValues.setProperty(uiRuntime_, "targetGlobalOriginY", values.y);
+ yogaValues.setProperty(uiRuntime_, "targetWidth", values.width);
+ yogaValues.setProperty(uiRuntime_, "targetHeight", values.height);
+ yogaValues.setProperty(uiRuntime_, "windowWidth", values.windowWidth);
+ yogaValues.setProperty(uiRuntime_, "windowHeight", values.windowHeight);
+ layoutAnimationsManager_->startLayoutAnimation(
+ uiRuntime_, tag, LayoutAnimationType::ENTERING, yogaValues);
+ });
+}
+
+void LayoutAnimationsProxy::startExitingAnimation(
+ const int tag,
+ ShadowViewMutation &mutation) const {
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "start exiting animation for tag " << tag << std::endl;
+#endif
+ auto surfaceId = mutation.oldChildShadowView.surfaceId;
+ auto oldView = mutation.oldChildShadowView;
+ createLayoutAnimation(mutation, oldView, surfaceId, tag);
+
+ Snapshot values(oldView, surfaceManager.getWindow(surfaceId));
+
+ uiScheduler_->scheduleOnUI([values, this, tag]() {
+ jsi::Object yogaValues(uiRuntime_);
+ yogaValues.setProperty(uiRuntime_, "currentOriginX", values.x);
+ yogaValues.setProperty(uiRuntime_, "currentGlobalOriginX", values.x);
+ yogaValues.setProperty(uiRuntime_, "currentOriginY", values.y);
+ yogaValues.setProperty(uiRuntime_, "currentGlobalOriginY", values.y);
+ yogaValues.setProperty(uiRuntime_, "currentWidth", values.width);
+ yogaValues.setProperty(uiRuntime_, "currentHeight", values.height);
+ yogaValues.setProperty(uiRuntime_, "windowWidth", values.windowWidth);
+ yogaValues.setProperty(uiRuntime_, "windowHeight", values.windowHeight);
+ layoutAnimationsManager_->startLayoutAnimation(
+ uiRuntime_, tag, LayoutAnimationType::EXITING, yogaValues);
+ layoutAnimationsManager_->clearLayoutAnimationConfig(tag);
+ });
+}
+
+void LayoutAnimationsProxy::startLayoutAnimation(
+ const int tag,
+ const ShadowViewMutation &mutation) const {
+#ifdef LAYOUT_ANIMATIONS_LOGS
+ LOG(INFO) << "start layout animation for tag " << tag << std::endl;
+#endif
+ auto surfaceId = mutation.oldChildShadowView.surfaceId;
+ auto oldView = mutation.oldChildShadowView;
+ createLayoutAnimation(mutation, oldView, surfaceId, tag);
+
+ Snapshot currentValues(oldView, surfaceManager.getWindow(surfaceId));
+ Snapshot targetValues(
+ mutation.newChildShadowView, surfaceManager.getWindow(surfaceId));
+
+ uiScheduler_->scheduleOnUI([currentValues, targetValues, this, tag]() {
+ jsi::Object yogaValues(uiRuntime_);
+ yogaValues.setProperty(uiRuntime_, "currentOriginX", currentValues.x);
+ yogaValues.setProperty(uiRuntime_, "currentGlobalOriginX", currentValues.x);
+ yogaValues.setProperty(uiRuntime_, "currentOriginY", currentValues.y);
+ yogaValues.setProperty(uiRuntime_, "currentGlobalOriginY", currentValues.y);
+ yogaValues.setProperty(uiRuntime_, "currentWidth", currentValues.width);
+ yogaValues.setProperty(uiRuntime_, "currentHeight", currentValues.height);
+ yogaValues.setProperty(uiRuntime_, "targetOriginX", targetValues.x);
+ yogaValues.setProperty(uiRuntime_, "targetGlobalOriginX", targetValues.x);
+ yogaValues.setProperty(uiRuntime_, "targetOriginY", targetValues.y);
+ yogaValues.setProperty(uiRuntime_, "targetGlobalOriginY", targetValues.y);
+ yogaValues.setProperty(uiRuntime_, "targetWidth", targetValues.width);
+ yogaValues.setProperty(uiRuntime_, "targetHeight", targetValues.height);
+ yogaValues.setProperty(uiRuntime_, "windowWidth", targetValues.windowWidth);
+ yogaValues.setProperty(uiRuntime_, "windowHeight", targetValues.windowHeight);
+ layoutAnimationsManager_->startLayoutAnimation(
+ uiRuntime_, tag, LayoutAnimationType::LAYOUT, yogaValues);
+ });
+}
+
+void LayoutAnimationsProxy::updateOngoingAnimationTarget(
+ const int tag,
+ const ShadowViewMutation &mutation) const {
+ layoutAnimations_[tag].finalView = std::make_shared(mutation.newChildShadowView);
+}
+
+void LayoutAnimationsProxy::maybeCancelAnimation(const int tag) const {
+ if (!layoutAnimations_.contains(tag)) {
+ return;
+ }
+ layoutAnimations_.erase(tag);
+ uiScheduler_->scheduleOnUI([this, tag]() {
+ layoutAnimationsManager_->cancelLayoutAnimation(uiRuntime_, tag);
+ });
+}
+
+void LayoutAnimationsProxy::transferConfigFromNativeID(
+ const std::string nativeIdString,
+ const int tag) const {
+ if (nativeIdString.empty()) {
+ return;
+ }
+ try {
+ auto nativeId = stoi(nativeIdString);
+ layoutAnimationsManager_->transferConfigFromNativeID(nativeId, tag);
+ } catch (std::invalid_argument) {
+ }
+}
+
+// When entering animations start, we temporarily set opacity to 0
+// so that we can immediately insert the view at the right position
+// and schedule the animation on the UI thread
+std::shared_ptr LayoutAnimationsProxy::cloneViewWithoutOpacity(facebook::react::ShadowViewMutation &mutation, const PropsParserContext &propsParserContext) const {
+ auto newView =
+ std::make_shared(mutation.newChildShadowView);
+ folly::dynamic opacity = folly::dynamic::object("opacity", 0);
+ auto newProps = getComponentDescriptorForShadowView(*newView).cloneProps(propsParserContext, newView->props, RawProps(opacity));
+ newView->props = newProps;
+ return newView;
+}
+
+void LayoutAnimationsProxy::maybeRestoreOpacity(LayoutAnimation &layoutAnimation, const jsi::Object &newStyle) const{
+ if (layoutAnimation.opacity && !newStyle.hasProperty(uiRuntime_, "opacity")) {
+ newStyle.setProperty(uiRuntime_, "opacity", jsi::Value(*layoutAnimation.opacity));
+ layoutAnimation.opacity.reset();
+ }
+}
+
+void LayoutAnimationsProxy::maybeUpdateWindowDimensions(facebook::react::ShadowViewMutation &mutation, SurfaceId surfaceId) const {
+ // This is a hacky way to obtain the window dimensions.
+ // We can identify the root, by checking if its tag is equal to the surfaceId
+ if (mutation.parentShadowView.tag == surfaceId) {
+ surfaceManager.updateWindow(
+ surfaceId,
+ mutation.parentShadowView.layoutMetrics.frame.size.width,
+ mutation.parentShadowView.layoutMetrics.frame.size.height);
+ }
+}
+
+} // namespace reanimated
+
+#endif // RCT_NEW_ARCH_ENABLED
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsProxy.h b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsProxy.h
new file mode 100644
index 00000000000..f8a01f9ca33
--- /dev/null
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsProxy.h
@@ -0,0 +1,128 @@
+#pragma once
+#ifdef RCT_NEW_ARCH_ENABLED
+
+#include "LayoutAnimationsManager.h"
+#include "PropsRegistry.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "LayoutAnimationsUtils.h"
+#include "UIScheduler.h"
+
+namespace reanimated {
+
+class NativeReanimatedModule;
+
+using namespace facebook;
+
+struct LayoutAnimation {
+ std::shared_ptr finalView, currentView, parentView;
+ std::optional opacity;
+ int count = 1;
+ LayoutAnimation &operator=(const LayoutAnimation &other) = default;
+};
+
+struct LayoutAnimationsProxy : public MountingOverrideDelegate {
+ mutable std::unordered_map> nodeForTag_;
+ mutable std::unordered_map layoutAnimations_;
+ mutable std::recursive_mutex mutex;
+ mutable SurfaceManager surfaceManager;
+ mutable std::unordered_set> deadNodes;
+ mutable std::unordered_map leastRemoved;
+ std::shared_ptr layoutAnimationsManager_;
+ ContextContainer::Shared contextContainer_;
+ SharedComponentDescriptorRegistry componentDescriptorRegistry_;
+ jsi::Runtime& uiRuntime_;
+ const std::shared_ptr uiScheduler_;
+ LayoutAnimationsProxy(
+ std::shared_ptr layoutAnimationsManager,
+ SharedComponentDescriptorRegistry componentDescriptorRegistry,
+ ContextContainer::Shared contextContainer,
+ jsi::Runtime& uiRuntime_,
+ const std::shared_ptr uiScheduler)
+ : layoutAnimationsManager_(layoutAnimationsManager),
+ contextContainer_(contextContainer),
+ componentDescriptorRegistry_(componentDescriptorRegistry),
+ uiRuntime_(uiRuntime_),
+ uiScheduler_(uiScheduler) {}
+
+ void startEnteringAnimation(const int tag, ShadowViewMutation &mutation)
+ const;
+ void startExitingAnimation(const int tag, ShadowViewMutation &mutation) const;
+ void startLayoutAnimation(const int tag, const ShadowViewMutation &mutation)
+ const;
+
+ void transferConfigFromNativeID(const std::string nativeId, const int tag)
+ const;
+ std::optional progressLayoutAnimation(
+ int tag,
+ const jsi::Object &newStyle);
+ std::optional endLayoutAnimation(int tag, bool shouldRemove);
+ void maybeCancelAnimation(const int tag) const;
+
+ void parseRemoveMutations(
+ std::unordered_map &movedViews,
+ ShadowViewMutationList &mutations,
+ std::vector> &roots) const;
+ void handleRemovals(
+ ShadowViewMutationList &filteredMutations,
+ std::vector> &roots) const;
+
+ void handleUpdatesAndEnterings(
+ ShadowViewMutationList &filteredMutations,
+ const std::unordered_map &movedViews,
+ ShadowViewMutationList &mutations,
+ const PropsParserContext &propsParserContext,
+ SurfaceId surfaceId) const;
+ void addOngoingAnimations(
+ SurfaceId surfaceId,
+ ShadowViewMutationList &mutations) const;
+ void updateOngoingAnimationTarget(
+ const int tag,
+ const ShadowViewMutation& mutation) const;
+ std::shared_ptr cloneViewWithoutOpacity(facebook::react::ShadowViewMutation &mutation, const PropsParserContext &propsParserContext) const;
+ void maybeRestoreOpacity(LayoutAnimation& layoutAnimation, const jsi::Object &newStyle) const;
+ void maybeUpdateWindowDimensions(facebook::react::ShadowViewMutation &mutation, SurfaceId surfaceId) const;
+ void createLayoutAnimation(const ShadowViewMutation &mutation, ShadowView &oldView, const SurfaceId &surfaceId, const int tag) const;
+
+ void updateIndexForMutation(ShadowViewMutation &mutation) const;
+
+ void removeRecursively(
+ std::shared_ptr node,
+ ShadowViewMutationList &mutations) const;
+ bool startAnimationsRecursively(
+ std::shared_ptr node,
+ const bool shouldRemoveSubviewsWithoutAnimations,
+ const bool shouldAnimate,
+ const bool isScreenPop,
+ ShadowViewMutationList &mutations) const;
+ void endAnimationsRecursively(
+ std::shared_ptr node,
+ ShadowViewMutationList &mutations) const;
+ void maybeDropAncestors(
+ std::shared_ptr node,
+ std::shared_ptr child,
+ ShadowViewMutationList &cleanupMutations) const;
+
+ const ComponentDescriptor &getComponentDescriptorForShadowView(
+ const ShadowView &shadowView) const;
+
+ // MountingOverrideDelegate
+
+ bool shouldOverridePullTransaction() const override;
+ std::optional pullTransaction(
+ SurfaceId surfaceId,
+ MountingTransaction::Number number,
+ const TransactionTelemetry &telemetry,
+ ShadowViewMutationList mutations) const override;
+};
+
+} // namespace reanimated
+
+#endif // RCT_NEW_ARCH_ENABLED
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsUtils.cpp b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsUtils.cpp
new file mode 100644
index 00000000000..a272f1a3a11
--- /dev/null
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsUtils.cpp
@@ -0,0 +1,81 @@
+#ifdef RCT_NEW_ARCH_ENABLED
+#include "LayoutAnimationsUtils.h"
+
+namespace reanimated {
+
+std::unordered_map &SurfaceManager::getUpdateMap(SurfaceId surfaceId) {
+ auto props = props_.find(surfaceId);
+ if (props != props_.end()) {
+ return *props->second;
+ }
+
+ auto newProps = std::make_shared>();
+ props_.insert_or_assign(surfaceId, newProps);
+ return *newProps;
+}
+
+void SurfaceManager::updateWindow(const SurfaceId surfaceId, const double windowWidth, const double windowHeight) {
+ windows_.insert_or_assign(surfaceId, Rect{windowWidth, windowHeight});
+}
+
+Rect SurfaceManager::getWindow(SurfaceId surfaceId) {
+ auto windowIt = windows_.find(surfaceId);
+ if (windowIt != windows_.end()) {
+ return windowIt->second;
+ }
+ return Rect{0, 0};
+}
+
+void Node::applyMutationToIndices(ShadowViewMutation mutation) {
+ if (tag != mutation.parentShadowView.tag) {
+ return;
+ }
+
+ int delta = mutation.type == ShadowViewMutation::Insert ? 1 : -1;
+ for (int i = children.size() - 1; i >= 0; i--) {
+ if (children[i]->mutation.index < mutation.index) {
+ return;
+ }
+ children[i]->mutation.index += delta;
+ }
+}
+
+// Should only be called on unflattened parents
+void Node::removeChildFromUnflattenedTree(std::shared_ptr child) {
+ for (int i = unflattenedChildren.size() - 1; i >= 0; i--) {
+ if (unflattenedChildren[i]->tag == child->tag) {
+ unflattenedChildren.erase(unflattenedChildren.begin() + i);
+ break;
+ }
+ }
+
+ auto& flattenedChildren = child->parent->children;
+ for (int i = flattenedChildren.size() - 1; i >= 0; i--) {
+ if (flattenedChildren[i]->tag == child->tag) {
+ flattenedChildren.erase(flattenedChildren.begin() + i);
+ return;
+ }
+ flattenedChildren[i]->mutation.index--;
+ }
+}
+
+void Node::insertChildren(
+ std::vector> &newChildren) {
+ mergeAndSwap(children, newChildren);
+}
+
+void Node::insertUnflattenedChildren(
+ std::vector> &newChildren) {
+ mergeAndSwap(unflattenedChildren, newChildren);
+}
+
+bool Node::isMutationMode(){
+ return false;
+}
+
+bool MutationNode::isMutationMode(){
+ return true;
+}
+}
+
+#endif
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsUtils.h b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsUtils.h
new file mode 100644
index 00000000000..d8056b9cd87
--- /dev/null
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsUtils.h
@@ -0,0 +1,164 @@
+#pragma once
+
+#include "LayoutAnimationsManager.h"
+#include "PropsRegistry.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace reanimated {
+
+struct Rect {
+ double width, height;
+};
+
+struct Frame {
+ std::optional x, y, width, height;
+ Frame(jsi::Runtime &runtime, const jsi::Object &newStyle) {
+ if (newStyle.hasProperty(runtime, "originX")) {
+ x = newStyle.getProperty(runtime, "originX").asNumber();
+ }
+ if (newStyle.hasProperty(runtime, "originY")) {
+ y = newStyle.getProperty(runtime, "originY").asNumber();
+ }
+ if (newStyle.hasProperty(runtime, "width")) {
+ width = newStyle.getProperty(runtime, "width").asNumber();
+ }
+ if (newStyle.hasProperty(runtime, "height")) {
+ height = newStyle.getProperty(runtime, "height").asNumber();
+ }
+ }
+};
+
+struct UpdateValues {
+ Props::Shared newProps;
+ Frame frame;
+};
+
+struct Snapshot {
+ double x, y, width, height, windowWidth, windowHeight;
+ Snapshot(const ShadowView &shadowView, Rect window) {
+ const auto &frame = shadowView.layoutMetrics.frame;
+ x = frame.origin.x;
+ y = frame.origin.y;
+ width = frame.size.width;
+ height = frame.size.height;
+ windowWidth = window.width;
+ windowHeight = window.height;
+ }
+};
+
+typedef enum ExitingState {
+ UNDEFINED = 1,
+ WAITING = 2,
+ ANIMATING = 4,
+ DEAD = 8,
+ MOVED = 16,
+ DELETED = 32,
+} ExitingState;
+
+struct MutationNode;
+
+/**
+ Represents a view that was either removed or had a child removed from the
+ ShadowTree
+ */
+struct Node {
+ std::vector> children, unflattenedChildren;
+ std::shared_ptr parent, unflattenedParent;
+ Tag tag;
+ void removeChildFromUnflattenedTree(std::shared_ptr child);
+ void applyMutationToIndices(ShadowViewMutation mutation);
+ void insertChildren(std::vector> &newChildren);
+ void insertUnflattenedChildren(std::vector> &newChildren);
+ virtual bool isMutationMode();
+ explicit Node(const Tag tag) : tag(tag) {}
+ Node(Node &&node) : children(std::move(node.children)), unflattenedChildren(std::move(node.unflattenedChildren)), tag(node.tag) {}
+ virtual ~Node() = default;
+};
+
+/**
+ Represents a view that was removed from the ShadowTree
+ */
+struct MutationNode : public Node {
+ ShadowViewMutation mutation;
+ std::unordered_set animatedChildren;
+ ExitingState state = UNDEFINED;
+ explicit MutationNode(ShadowViewMutation &mutation)
+ : Node(mutation.oldChildShadowView.tag), mutation(mutation) {}
+ MutationNode(ShadowViewMutation &mutation, Node &&node)
+ : Node(std::move(node)), mutation(mutation) {}
+ bool isMutationMode() override;
+};
+
+struct SurfaceManager {
+ mutable std::unordered_map<
+ SurfaceId,
+ std::shared_ptr>>
+ props_;
+ mutable std::unordered_map windows_;
+
+ std::unordered_map &getUpdateMap(SurfaceId surfaceId);
+ void updateWindow(SurfaceId surfaceId, double windowWidth, double windowHeight);
+ Rect getWindow(SurfaceId surfaceId);
+};
+
+static inline void updateLayoutMetrics(
+ LayoutMetrics &layoutMetrics,
+ Frame &frame) {
+ // we use optional's here to avoid overwriting non-animated values
+ if (frame.width) {
+ layoutMetrics.frame.size.width = *frame.width;
+ }
+ if (frame.height) {
+ layoutMetrics.frame.size.height = *frame.height;
+ }
+ if (frame.x) {
+ layoutMetrics.frame.origin.x = *frame.x;
+ }
+ if (frame.y) {
+ layoutMetrics.frame.origin.y = *frame.y;
+ }
+}
+
+static inline bool isRNSScreen(std::shared_ptr node) {
+ return !std::strcmp(
+ node->mutation.oldChildShadowView.componentName,
+ "RNSScreenStack") ||
+ !std::strcmp(
+ node->mutation.oldChildShadowView.componentName, "RNSScreen");
+}
+
+static inline bool hasLayoutChanged(const ShadowViewMutation& mutation){
+ return mutation.oldChildShadowView.layoutMetrics.frame != mutation.newChildShadowView.layoutMetrics.frame;
+}
+
+static inline void mergeAndSwap(std::vector>& A, std::vector>& B){
+ std::vector> merged;
+ auto it1 = A.begin(), it2 = B.begin();
+ while (it1 != A.end() && it2 != B.end()) {
+ if ((*it1)->mutation.index < (*it2)->mutation.index) {
+ merged.push_back(*it1);
+ it1++;
+ } else {
+ merged.push_back(*it2);
+ it2++;
+ }
+ }
+ while (it1 != A.end()) {
+ merged.push_back(*it1);
+ it1++;
+ }
+ while (it2 != B.end()) {
+ merged.push_back(*it2);
+ it2++;
+ }
+ std::swap(A, merged);
+}
+
+} // namespace reanimated
diff --git a/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.cpp b/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.cpp
index a1ace04a3f6..acbc1a476f8 100644
--- a/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.cpp
+++ b/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.cpp
@@ -17,6 +17,7 @@
#include
#ifdef RCT_NEW_ARCH_ENABLED
+#include
#include "ReanimatedCommitMarker.h"
#include "ShadowTreeCloner.h"
#endif
@@ -74,7 +75,8 @@ NativeReanimatedModule::NativeReanimatedModule(
}),
animatedSensorModule_(platformDepMethodsHolder),
jsLogger_(std::make_shared(jsScheduler_)),
- layoutAnimationsManager_(jsLogger_),
+ layoutAnimationsManager_(
+ std::make_shared(jsLogger_)),
#ifdef RCT_NEW_ARCH_ENABLED
synchronouslyUpdateUIPropsFunction_(
platformDepMethodsHolder.synchronouslyUpdateUIPropsFunction),
@@ -120,6 +122,32 @@ void NativeReanimatedModule::commonInit(
const jsi::Value &argsValue) {
this->dispatchCommand(rt, shadowNodeValue, commandNameValue, argsValue);
};
+ ProgressLayoutAnimationFunction progressLayoutAnimation =
+ [this](jsi::Runtime &rt, int tag, const jsi::Object &newStyle, bool) {
+ auto surfaceId =
+ layoutAnimationsProxy_->progressLayoutAnimation(tag, newStyle);
+ if (!surfaceId) {
+ return;
+ }
+ uiManager_->getShadowTreeRegistry().visit(
+ *surfaceId, [](const ShadowTree &shadowTree) {
+ shadowTree.notifyDelegatesOfUpdates();
+ });
+ };
+
+ EndLayoutAnimationFunction endLayoutAnimation =
+ [this](int tag, bool shouldRemove) {
+ auto surfaceId =
+ layoutAnimationsProxy_->endLayoutAnimation(tag, shouldRemove);
+ if (!surfaceId) {
+ return;
+ }
+
+ uiManager_->getShadowTreeRegistry().visit(
+ *surfaceId, [](const ShadowTree &shadowTree) {
+ shadowTree.notifyDelegatesOfUpdates();
+ });
+ };
auto obtainProp = [this](
jsi::Runtime &rt,
@@ -148,8 +176,13 @@ void NativeReanimatedModule::commonInit(
requestAnimationFrame,
platformDepMethodsHolder.getAnimationTimestamp,
platformDepMethodsHolder.setGestureStateFunction,
+#ifdef RCT_NEW_ARCH_ENABLED
+ progressLayoutAnimation,
+ endLayoutAnimation,
+#else
platformDepMethodsHolder.progressLayoutAnimation,
platformDepMethodsHolder.endLayoutAnimation,
+#endif
platformDepMethodsHolder.maybeFlushUIUpdatesQueueFunction);
}
@@ -423,7 +456,7 @@ jsi::Value NativeReanimatedModule::configureLayoutAnimationBatch(
batch[i].sharedTransitionTag = sharedTransitionTag.asString(rt).utf8(rt);
}
}
- layoutAnimationsManager_.configureAnimationBatch(batch);
+ layoutAnimationsManager_->configureAnimationBatch(batch);
return jsi::Value::undefined();
}
@@ -431,7 +464,7 @@ void NativeReanimatedModule::setShouldAnimateExiting(
jsi::Runtime &rt,
const jsi::Value &viewTag,
const jsi::Value &shouldAnimate) {
- layoutAnimationsManager_.setShouldAnimateExiting(
+ layoutAnimationsManager_->setShouldAnimateExiting(
viewTag.asNumber(), shouldAnimate.getBool());
}
@@ -811,6 +844,9 @@ jsi::Value NativeReanimatedModule::measure(
void NativeReanimatedModule::initializeFabric(
const std::shared_ptr &uiManager) {
uiManager_ = uiManager;
+
+ initializeLayoutAnimations();
+
commitHook_ =
std::make_shared(propsRegistry_, uiManager_);
#if REACT_NATIVE_MINOR_VERSION >= 73
@@ -818,6 +854,30 @@ void NativeReanimatedModule::initializeFabric(
std::make_shared(propsRegistry_, uiManager_);
#endif
}
+
+void NativeReanimatedModule::initializeLayoutAnimations() {
+ 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(),
+ uiWorkletRuntime_->getJSIRuntime(),
+ uiScheduler_);
+ uiManager_->getShadowTreeRegistry().enumerate(
+ [this](const ShadowTree &shadowTree, bool &stop) {
+ shadowTree.getMountingCoordinator()->setMountingOverrideDelegate(
+ layoutAnimationsProxy_);
+ });
+ }
+}
#endif // RCT_NEW_ARCH_ENABLED
jsi::Value NativeReanimatedModule::subscribeForKeyboardEvents(
diff --git a/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.h b/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.h
index 86e86800774..f4cde385cdd 100644
--- a/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.h
+++ b/packages/react-native-reanimated/Common/cpp/NativeModules/NativeReanimatedModule.h
@@ -20,6 +20,7 @@
#include "UIScheduler.h"
#ifdef RCT_NEW_ARCH_ENABLED
+#include "LayoutAnimationsProxy.h"
#include "PropsRegistry.h"
#include "ReanimatedCommitHook.h"
#if REACT_NATIVE_MINOR_VERSION >= 73
@@ -135,6 +136,8 @@ class NativeReanimatedModule : public NativeReanimatedModuleSpec {
jsi::Value measure(jsi::Runtime &rt, const jsi::Value &shadowNodeValue);
void initializeFabric(const std::shared_ptr &uiManager);
+
+ void initializeLayoutAnimations();
std::string obtainPropFromShadowNode(
jsi::Runtime &rt,
@@ -161,7 +164,7 @@ class NativeReanimatedModule : public NativeReanimatedModuleSpec {
const jsi::Value &listenerId) override;
inline LayoutAnimationsManager &layoutAnimationsManager() {
- return layoutAnimationsManager_;
+ return *layoutAnimationsManager_;
}
inline jsi::Runtime &getUIRuntime() {
@@ -198,7 +201,7 @@ class NativeReanimatedModule : public NativeReanimatedModuleSpec {
const std::function onRenderCallback_;
AnimatedSensorModule animatedSensorModule_;
const std::shared_ptr jsLogger_;
- LayoutAnimationsManager layoutAnimationsManager_;
+ std::shared_ptr layoutAnimationsManager_;
#ifdef RCT_NEW_ARCH_ENABLED
const SynchronouslyUpdateUIPropsFunction synchronouslyUpdateUIPropsFunction_;
@@ -207,6 +210,7 @@ class NativeReanimatedModule : public NativeReanimatedModuleSpec {
std::unordered_set
animatablePropNames_; // filled by configureProps
std::shared_ptr uiManager_;
+ std::shared_ptr layoutAnimationsProxy_;
// After app reload, surfaceId on iOS is still 1 but on Android it's 11.
// We can store surfaceId of the most recent ShadowNode as a workaround.
diff --git a/packages/react-native-reanimated/android/CMakeLists.txt b/packages/react-native-reanimated/android/CMakeLists.txt
index 42a058b411d..83c52a36450 100644
--- a/packages/react-native-reanimated/android/CMakeLists.txt
+++ b/packages/react-native-reanimated/android/CMakeLists.txt
@@ -82,6 +82,8 @@ target_include_directories(
"${REACT_NATIVE_DIR}/ReactCommon/react/renderer/graphics/platform/cxx"
"${REACT_NATIVE_DIR}/ReactCommon/runtimeexecutor"
"${REACT_NATIVE_DIR}/ReactCommon/yoga"
+ "${REACT_NATIVE_DIR}/ReactCommon/react/renderer/componentregistry"
+ "${REACT_NATIVE_DIR}/ReactCommon/react/renderer/core"
)
# build shared lib
@@ -152,6 +154,7 @@ if(${IS_NEW_ARCHITECTURE_ENABLED})
ReactAndroid::react_debug
ReactAndroid::react_render_core
ReactAndroid::react_render_mounting
+ ReactAndroid::react_render_componentregistry
ReactAndroid::react_render_scheduler
ReactAndroid::react_render_uimanager
ReactAndroid::rrc_view
diff --git a/packages/react-native-reanimated/cspell.json b/packages/react-native-reanimated/cspell.json
index c2871525896..0d54b449648 100644
--- a/packages/react-native-reanimated/cspell.json
+++ b/packages/react-native-reanimated/cspell.json
@@ -15,6 +15,7 @@
"collapsable",
"devs",
"easings",
+ "enterings",
"gesturehandler",
"inout",
"ispreview",
@@ -29,6 +30,7 @@
"runtimes",
"snapshotting",
"springify",
+ "subviews",
"superview",
"swmansion",
"systrace",
@@ -44,7 +46,7 @@
"workletized",
"workletizes",
"worklets",
- "xcworkspace"
+ "xcworkspace",
"runtimes",
"overdamped",
"automagically",
diff --git a/packages/react-native-reanimated/src/UpdateLayoutAnimations.ts b/packages/react-native-reanimated/src/UpdateLayoutAnimations.ts
index e28463712aa..2c34153adf6 100644
--- a/packages/react-native-reanimated/src/UpdateLayoutAnimations.ts
+++ b/packages/react-native-reanimated/src/UpdateLayoutAnimations.ts
@@ -1,5 +1,5 @@
'use strict';
-import { shouldBeUseWeb } from './PlatformChecker';
+import { isFabric, shouldBeUseWeb } from './PlatformChecker';
import {
configureLayoutAnimationBatch,
makeShareableCloneRecursive,
@@ -29,7 +29,7 @@ function createUpdateManager() {
animations.push(batchItem);
}
if (animations.length + deferredAnimations.length === 1) {
- setImmediate(this.flush);
+ isFabric() ? this.flush() : setImmediate(this.flush);
}
},
flush(this: void) {
diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
index 1b8d767806e..6c3d81d4791 100644
--- a/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
+++ b/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
@@ -99,6 +99,8 @@ export function createAnimatedComponent(
options?: Options
): ComponentClass>>;
+let id = 0;
+
export function createAnimatedComponent(
Component: ComponentType,
options?: Options
@@ -128,12 +130,21 @@ export function createAnimatedComponent(
static displayName: string;
static contextType = SkipEnteringContext;
context!: React.ContextType;
+ reanimatedID = id++;
constructor(props: AnimatedComponentProps) {
super(props);
if (isJest()) {
this.jestAnimatedStyle = { value: {} };
}
+ const entering = this.props.entering;
+ if (entering && isFabric()) {
+ updateLayoutAnimations(
+ this.reanimatedID,
+ LayoutAnimationType.ENTERING,
+ maybeBuild(entering, this.props?.style, AnimatedComponent.displayName)
+ );
+ }
}
componentDidMount() {
@@ -203,7 +214,7 @@ export function createAnimatedComponent(
this._component as HTMLElement,
LayoutAnimationType.EXITING
);
- } else if (exiting && !IS_WEB) {
+ } else if (exiting && !IS_WEB && !isFabric()) {
const reduceMotionInExiting =
'getReduceMotion' in exiting &&
typeof exiting.getReduceMotion === 'function'
@@ -488,6 +499,24 @@ export function createAnimatedComponent(
if (sharedTransitionTag) {
this._configureSharedTransition();
}
+ if (exiting && isFabric()) {
+ const reduceMotionInExiting =
+ 'getReduceMotion' in exiting &&
+ typeof exiting.getReduceMotion === 'function'
+ ? getReduceMotionFromConfig(exiting.getReduceMotion())
+ : getReduceMotionFromConfig();
+ if (!reduceMotionInExiting) {
+ updateLayoutAnimations(
+ tag as number,
+ LayoutAnimationType.EXITING,
+ maybeBuild(
+ exiting,
+ this.props?.style,
+ AnimatedComponent.displayName
+ )
+ );
+ }
+ }
const skipEntering = this.context?.current;
if (entering && !skipEntering && !IS_WEB) {
@@ -551,8 +580,13 @@ export function createAnimatedComponent(
default: { collapsable: false },
});
+ const skipEntering = this.context?.current;
+ const nativeID =
+ skipEntering || !isFabric() ? undefined : `${this.reanimatedID}`;
+
return (