react-native-reanimated 4.7.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp +34 -2
  2. package/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.h +15 -1
  3. package/Common/cpp/reanimated/Fabric/ReanimatedMountHook.cpp +9 -0
  4. package/Common/cpp/reanimated/Fabric/ReanimatedMountHook.h +3 -0
  5. package/Common/cpp/reanimated/Fabric/updates/SynchronousWritesTracker.cpp +103 -0
  6. package/Common/cpp/reanimated/Fabric/updates/SynchronousWritesTracker.h +74 -0
  7. package/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.cpp +15 -0
  8. package/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.h +5 -0
  9. package/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.cpp +10 -0
  10. package/Common/cpp/reanimated/Fabric/updates/UpdatesRegistryManager.h +3 -0
  11. package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.cpp +97 -54
  12. package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy.h +5 -1
  13. package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.cpp +23 -5
  14. package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +17 -1
  15. package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +9 -9
  16. package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsUtils.h +35 -8
  17. package/Common/cpp/reanimated/LayoutAnimations/PropsDiffer.h +2 -2
  18. package/Common/cpp/reanimated/LayoutAnimations/SharedTransitions.cpp +11 -11
  19. package/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +75 -2
  20. package/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.h +11 -1
  21. package/Common/cpp/reanimated/Tools/PlatformDepMethodsHolder.h +4 -1
  22. package/android/src/main/cpp/reanimated/android/NativeProxy.cpp +13 -3
  23. package/android/src/main/cpp/reanimated/android/NativeProxy.h +2 -0
  24. package/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt +84 -9
  25. package/android/src/main/java/com/swmansion/reanimated/NodesManager.kt +2 -4
  26. package/android/src/main/java/com/swmansion/reanimated/nativeProxy/EventHandler.kt +4 -3
  27. package/android/src/main/java/com/swmansion/reanimated/sensor/ReanimatedSensor.kt +1 -1
  28. package/android/src/main/java/com/swmansion/reanimated/sensor/ReanimatedSensorListener.kt +44 -33
  29. package/apple/reanimated/apple/REASynchronousPropsRewriter.h +18 -0
  30. package/apple/reanimated/apple/REASynchronousPropsRewriter.mm +37 -0
  31. package/apple/reanimated/apple/ReanimatedModule.mm +7 -0
  32. package/apple/reanimated/apple/keyboardObserver/REAKeyboardEventObserver.mm +20 -3
  33. package/apple/reanimated/apple/native/PlatformDepMethodsHolderImpl.mm +24 -1
  34. package/lib/module/animation/timing.js +15 -2
  35. package/lib/module/animation/timing.js.map +1 -1
  36. package/lib/module/platform-specific/jsVersion.js +1 -1
  37. package/lib/typescript/animation/timing.d.ts.map +1 -1
  38. package/lib/typescript/platform-specific/jsVersion.d.ts +1 -1
  39. package/package.json +3 -1
  40. package/src/animation/timing.ts +20 -2
  41. package/src/platform-specific/jsVersion.ts +1 -1
@@ -188,7 +188,9 @@ std::optional<MountingTransaction> LayoutAnimationsProxy::pullTransaction(
188
188
  // shared element, the issue should be gone with the new stack
189
189
  // implementation
190
190
  if (auto screen = findParentRNSScreen(afterTopScreen)) {
191
- forceScreenSnapshot_(screen->current.tag);
191
+ if (forceScreenSnapshot_(screen->current.tag)) {
192
+ snapshottedScreens_.insert(screen->current.tag);
193
+ }
192
194
  }
193
195
  #endif
194
196
  }
@@ -256,6 +258,7 @@ void LayoutAnimationsProxy::unmapLightNode(const std::shared_ptr<LightNode> &nod
256
258
  return;
257
259
  }
258
260
  lightNodes_.erase(it);
261
+ snapshottedScreens_.erase(node->current.tag);
259
262
  if (node == topScreen_) {
260
263
  topScreen_ = nullptr;
261
264
  }
@@ -288,17 +291,13 @@ void LayoutAnimationsProxy::reconcileContradictedRemovals(
288
291
  }
289
292
  const auto tag = mutation.newChildShadowView.tag;
290
293
  const auto it = lightNodes_.find(tag);
291
- if (it == lightNodes_.end() || it->second->state == UNDEFINED) {
294
+ if (it == lightNodes_.end() || it->second->state == LIVE) {
292
295
  continue;
293
296
  }
294
297
  const auto node = it->second;
295
298
  completedAnimations_.erase(tag);
296
299
  updateMap_.erase(tag);
297
300
  unmapLightNode(node);
298
- if (node->state == DELETED) {
299
- // already unmounted — only the stale map entry had to go
300
- continue;
301
- }
302
301
  const auto parent = node->parent.lock();
303
302
  react_native_assert(parent && "Parent node is nullptr");
304
303
  if (!parent) {
@@ -331,6 +330,7 @@ void LayoutAnimationsProxy::updateLightTree(
331
330
  std::unordered_map<Tag, IndexCursors> indexCursors;
332
331
  std::unordered_map<Tag, ShadowView> updatedViews;
333
332
  std::unordered_map<Tag, std::vector<AncestorOrigin>> oldChains;
333
+ std::unordered_set<Tag> removedSubtreeRoots;
334
334
  for (auto it = mutations.rbegin(); it != mutations.rend(); it++) {
335
335
  const auto &mutation = *it;
336
336
  switch (mutation.type) {
@@ -407,16 +407,21 @@ void LayoutAnimationsProxy::updateLightTree(
407
407
  break;
408
408
  }
409
409
  case ShadowViewMutation::Delete: {
410
- const auto it = lightNodes_.find(mutation.oldChildShadowView.tag);
411
- react_native_assert(it != lightNodes_.end() && "Delete mutation for an unknown node");
412
- const auto state = it->second->state;
413
- // View flattening emits a child's Delete after its parent's Remove, which may have already torn the child
414
- // down.
415
- if (state == UNDEFINED || state == DEAD || state == DELETED) {
410
+ const auto tag = mutation.oldChildShadowView.tag;
411
+ const auto it = lightNodes_.find(tag);
412
+ react_native_assert(
413
+ (it == lightNodes_.end() || it->second->isExiting()) &&
414
+ "Delete mutation for a view that React did not remove");
415
+ // The differ removes everything that moves out of a view before its Delete, and inserts into the parent
416
+ // of the view only after it.
417
+ if (removedSubtreeRoots.erase(tag)) {
418
+ react_native_assert(it != lightNodes_.end() && "Delete mutation for an unknown node");
416
419
  const auto node = it->second;
417
- unmapLightNode(node);
420
+ const auto parent = node->parent.lock();
421
+ react_native_assert(parent && "Parent node is nullptr");
422
+ handleSubtreeRemoval(node, parent, transaction);
418
423
  }
419
- staleSynchronousProps_.forget(mutation.oldChildShadowView.tag);
424
+ staleSynchronousProps_.forget(tag);
420
425
  break;
421
426
  }
422
427
  case ShadowViewMutation::Insert: {
@@ -430,10 +435,13 @@ void LayoutAnimationsProxy::updateLightTree(
430
435
  bool hasSharedTransition = false;
431
436
  if constexpr (StaticFeatureFlags::getFlag("ENABLE_SHARED_ELEMENT_TRANSITIONS")) {
432
437
  auto sharedTransitionLock = std::unique_lock<std::mutex>(sharedTransitionManager_->mutex_);
433
- hasSharedTransition = sharedTransitionManager_->tagToName_.contains(tag);
438
+ hasSharedTransition = mutation.newChildShadowView.traits.check(ShadowNodeTraits::Trait::ViewKind) &&
439
+ sharedTransitionManager_->tagToName_.contains(tag);
434
440
  }
435
441
  const auto layoutConfig = layoutAnimationsManager_->getLayoutAnimationConfig(tag, LAYOUT);
436
- const auto enteringConfig = layoutAnimationsManager_->getLayoutAnimationConfig(tag, ENTERING);
442
+ const auto enteringConfig = isViewKind(mutation.newChildShadowView)
443
+ ? layoutAnimationsManager_->getLayoutAnimationConfig(tag, ENTERING)
444
+ : nullptr;
437
445
  if (moved.contains(tag)) {
438
446
  const auto offset = reparentOffset(oldChains.at(tag), ancestorOrigins(parent, {}));
439
447
  node->previous.layoutMetrics.frame.origin += offset;
@@ -449,8 +457,13 @@ void LayoutAnimationsProxy::updateLightTree(
449
457
  filteredMutations.push_back(
450
458
  ShadowViewMutation::InsertMutation(mutation.parentTag, node->previous, hostIndex));
451
459
  } else {
452
- filteredMutations.push_back(
453
- ShadowViewMutation::InsertMutation(mutation.parentTag, mutation.newChildShadowView, hostIndex));
460
+ auto view = mutation.newChildShadowView;
461
+ const auto updatedViewIt = updatedViews.find(tag);
462
+ const bool propsChanged = updatedViewIt != updatedViews.end() && updatedViewIt->second.props != view.props;
463
+ if (!propsChanged) {
464
+ view.props = propsOfMountedView(view);
465
+ }
466
+ filteredMutations.push_back(ShadowViewMutation::InsertMutation(mutation.parentTag, view, hostIndex));
454
467
  }
455
468
  } else if (enteringConfig) {
456
469
  transaction.entering.push_back({node, enteringConfig});
@@ -476,6 +489,7 @@ void LayoutAnimationsProxy::updateLightTree(
476
489
  const auto tag = node->current.tag;
477
490
  const auto parentTag = mutation.parentTag;
478
491
  const auto &parent = lightNodes_[parentTag];
492
+ react_native_assert(parent && "Remove mutation from an unknown parent");
479
493
  const auto hostIndex = parent->toHostIndexForRemove(mutation.index, indexCursors[parentTag]);
480
494
  react_native_assert(
481
495
  hostIndex < static_cast<int>(parent->children.size()) &&
@@ -490,8 +504,11 @@ void LayoutAnimationsProxy::updateLightTree(
490
504
  filteredMutations.push_back(
491
505
  ShadowViewMutation::RemoveMutation(parentTag, mutation.oldChildShadowView, hostIndex));
492
506
  parent->children.erase(parent->children.begin() + hostIndex);
493
- } else if (!deleted.contains(parentTag)) {
494
- handleSubtreeRemoval(node, parent, hostIndex, transaction);
507
+ } else {
508
+ node->setExitingState(DECISION_PENDING);
509
+ if (!deleted.contains(parentTag)) {
510
+ removedSubtreeRoots.insert(tag);
511
+ }
495
512
  }
496
513
  break;
497
514
  }
@@ -501,6 +518,7 @@ void LayoutAnimationsProxy::updateLightTree(
501
518
  }
502
519
  }
503
520
  }
521
+ react_native_assert(removedSubtreeRoots.empty() && "React removed a view without deleting it");
504
522
  }
505
523
 
506
524
  void LayoutAnimationsProxy::applyInitialMutationsToLightTree(const ShadowViewMutationList &mutations) const {
@@ -656,14 +674,16 @@ void LayoutAnimationsProxy::initializeLightTree(const ShadowTreeRevision &baseRe
656
674
 
657
675
  const auto lock = std::unique_lock<std::recursive_mutex>(mutex);
658
676
  react_native_assert(!isLightTreeInitialized() && "Light tree is already initialized");
677
+ const auto root = std::make_shared<LightNode>();
659
678
  if (baseRevision.rootShadowNode) {
660
679
  const auto &size = baseRevision.rootShadowNode->getLayoutMetrics().frame.size;
661
680
  window_ = {size.width, size.height};
681
+ root->current = ShadowView(*baseRevision.rootShadowNode);
682
+ } else {
683
+ root->current.componentName = "RootView";
684
+ root->current.tag = surfaceId_;
685
+ root->current.props = std::make_shared<BaseViewProps>();
662
686
  }
663
- const auto root = std::make_shared<LightNode>();
664
- root->current.componentName = "RootView";
665
- root->current.tag = surfaceId_;
666
- root->current.props = std::make_shared<BaseViewProps>();
667
687
  lightNodes_[surfaceId_] = root;
668
688
  applyInitialMutationsToLightTree(initialMutations);
669
689
  for (const auto &[revisionNumber, mutations] : pendingTransactions_) {
@@ -703,37 +723,57 @@ std::optional<SurfaceId> LayoutAnimationsProxy::endLayoutAnimation(int tag, bool
703
723
  react_native_assert(false && "LightNode not found");
704
724
  return surfaceId_;
705
725
  }
706
- nodeIt->second->setExitingState(DEAD);
726
+ nodeIt->second->setExitingState(COMPLETED);
707
727
 
708
728
  return surfaceId_;
709
729
  }
710
730
 
711
731
  // A subtree that animates keeps its place in the host tree, so nothing is emitted for its root.
712
- // A subtree that does not animate emits its Remove in stream order. Its teardown mounts at the
713
- // end of the transaction, so native code that reads a view on unmount still sees its children.
732
+ // A subtree that does not animate is removed at its current host index after its descendants, like React
733
+ // removes it. On iOS a subtree that holds a screen React Native Screens snapshots is removed before its
734
+ // descendants instead, and their teardown mounts at the end of the transaction.
714
735
  void LayoutAnimationsProxy::handleSubtreeRemoval(
715
736
  const std::shared_ptr<LightNode> &node,
716
737
  const std::shared_ptr<LightNode> &parent,
717
- const int hostIndex,
718
738
  TransactionMeta &transaction) const {
719
739
  ReanimatedSystraceSection s("handleSubtreeRemoval");
740
+ const bool defersTeardown = holdsSnapshottedScreen(node);
720
741
  const StartAnimationsRecursivelyConfig config = {
721
742
  .shouldRemoveSubviewsWithoutAnimations = true,
722
743
  .shouldAnimate = !transaction.surfaceDropped,
723
744
  .isScreenPop = false,
745
+ .defersTeardown = defersTeardown,
724
746
  };
725
747
  if (startAnimationsRecursively(node, transaction, config)) {
726
748
  return;
727
749
  }
728
- react_native_assert(!node->isExiting() && "A subtree that does not animate must stay UNDEFINED");
750
+ react_native_assert(node->state == DECISION_PENDING && "A subtree that does not animate stays DECISION_PENDING");
751
+ const auto hostIndex = parent->removeChild(node);
752
+ react_native_assert(hostIndex != -1 && "Removed node not found in its parent");
753
+ node->setExitingState(TORN_DOWN);
754
+ unmapLightNode(node);
729
755
  cancelLayoutAnimation(node->current.tag);
730
756
  if constexpr (StaticFeatureFlags::getFlag("ENABLE_SHARED_ELEMENT_TRANSITIONS")) {
731
757
  hiddenViewTags_.erase(node->current.tag);
732
758
  }
733
759
  transaction.filteredMutations.push_back(
734
760
  ShadowViewMutation::RemoveMutation(parent->current.tag, node->current, hostIndex));
735
- transaction.teardownMutations.push_back(ShadowViewMutation::DeleteMutation(node->current));
736
- parent->children.erase(parent->children.begin() + hostIndex);
761
+ (defersTeardown ? transaction.teardownMutations : transaction.filteredMutations)
762
+ .push_back(ShadowViewMutation::DeleteMutation(node->current));
763
+ }
764
+
765
+ // React Native Screens snapshots a screen in snapshottedScreens_ after pending updates when the screen is removed,
766
+ // so its content must still be mounted then.
767
+ bool LayoutAnimationsProxy::holdsSnapshottedScreen([[maybe_unused]] const std::shared_ptr<LightNode> &node) const {
768
+ #ifdef __APPLE__
769
+ if constexpr (StaticFeatureFlags::getFlag("ENABLE_SHARED_ELEMENT_TRANSITIONS")) {
770
+ return std::ranges::any_of(snapshottedScreens_, [&](const Tag tag) {
771
+ const auto it = lightNodes_.find(tag);
772
+ return it != lightNodes_.end() && isInSubtree(it->second, node);
773
+ });
774
+ }
775
+ #endif
776
+ return false;
737
777
  }
738
778
 
739
779
  void LayoutAnimationsProxy::flushCompletedRemovals(ShadowViewMutationList &filteredMutations) const {
@@ -755,14 +795,14 @@ void LayoutAnimationsProxy::flushCompletedRemovals(ShadowViewMutationList &filte
755
795
  continue;
756
796
  }
757
797
  const auto nodeIt = lightNodes_.find(tag);
758
- if (nodeIt == lightNodes_.end() || nodeIt->second->state != DEAD) {
798
+ if (nodeIt == lightNodes_.end() || nodeIt->second->state != COMPLETED) {
759
799
  continue;
760
800
  }
761
801
  const auto node = nodeIt->second;
762
802
  auto parent = node->parent.lock();
763
803
  react_native_assert(parent && "Parent node is nullptr");
764
804
  auto index = parent->removeChild(node);
765
- react_native_assert(index != -1 && "Dead node not found");
805
+ react_native_assert(index != -1 && "Completed node not found");
766
806
 
767
807
  endAnimationsRecursively(node, index, filteredMutations);
768
808
  maybeDropAncestors(parent, filteredMutations);
@@ -824,7 +864,7 @@ void LayoutAnimationsProxy::addOngoingAnimations(ShadowViewMutationList &mutatio
824
864
  mutations.push_back(
825
865
  ShadowViewMutation::UpdateMutation(layoutAnimation.currentView, newView, layoutAnimation.parentTag));
826
866
  layoutAnimation.currentView = newView;
827
- if (layoutAnimation.opacity && static_cast<const ViewProps &>(*newView.props).opacity == *layoutAnimation.opacity) {
867
+ if (layoutAnimation.opacity && getViewProps(newView).opacity == *layoutAnimation.opacity) {
828
868
  layoutAnimation.opacity.reset();
829
869
  }
830
870
  }
@@ -837,7 +877,7 @@ void LayoutAnimationsProxy::endAnimationsRecursively(
837
877
  ShadowViewMutationList &mutations) const {
838
878
  const auto tag = node->current.tag;
839
879
  cancelLayoutAnimation(tag);
840
- node->setExitingState(DELETED);
880
+ node->setExitingState(TORN_DOWN);
841
881
  unmapLightNode(node);
842
882
  // iterate from the end, so that children
843
883
  // with higher indices appear first in the mutations list
@@ -845,7 +885,7 @@ void LayoutAnimationsProxy::endAnimationsRecursively(
845
885
  const int childrenSize = static_cast<int>(node->children.size());
846
886
  for (int i = childrenSize - 1; i >= 0; i--) {
847
887
  auto &subNode = node->children[i];
848
- if (subNode->state != DELETED) {
888
+ if (subNode->state != TORN_DOWN) {
849
889
  endAnimationsRecursively(subNode, i, mutations);
850
890
  }
851
891
  }
@@ -863,7 +903,7 @@ void LayoutAnimationsProxy::endAnimationsRecursively(
863
903
  void LayoutAnimationsProxy::maybeDropAncestors(
864
904
  const std::shared_ptr<LightNode> &node,
865
905
  ShadowViewMutationList &cleanupMutations) const {
866
- if (node->children.size() != 0 || node->state == ANIMATING || node->state == UNDEFINED) {
906
+ if (node->children.size() != 0 || node->state == ANIMATING || node->state == LIVE) {
867
907
  return;
868
908
  }
869
909
 
@@ -872,7 +912,7 @@ void LayoutAnimationsProxy::maybeDropAncestors(
872
912
  auto index = parent->removeChild(node);
873
913
  react_native_assert(index != -1 && "Child node not found");
874
914
 
875
- node->setExitingState(DELETED);
915
+ node->setExitingState(TORN_DOWN);
876
916
  unmapLightNode(node);
877
917
  cancelLayoutAnimation(node->current.tag);
878
918
  if constexpr (StaticFeatureFlags::getFlag("ENABLE_SHARED_ELEMENT_TRANSITIONS")) {
@@ -887,8 +927,9 @@ bool LayoutAnimationsProxy::startAnimationsRecursively(
887
927
  const std::shared_ptr<LightNode> &node,
888
928
  TransactionMeta &transaction,
889
929
  StartAnimationsRecursivelyConfig config) const {
890
- auto &mutations = transaction.teardownMutations;
891
- auto &[shouldRemoveSubviewsWithoutAnimations, shouldAnimate, isScreenPop] = config;
930
+ react_native_assert(node->state == DECISION_PENDING && "Subtree removal of a view that React does not delete");
931
+ auto &[shouldRemoveSubviewsWithoutAnimations, shouldAnimate, isScreenPop, defersTeardown] = config;
932
+ auto &mutations = defersTeardown ? transaction.teardownMutations : transaction.filteredMutations;
892
933
  if (isRNSScreenOrStack(node)) {
893
934
  isScreenPop = true;
894
935
  }
@@ -909,8 +950,9 @@ bool LayoutAnimationsProxy::startAnimationsRecursively(
909
950
  for (auto it = node->children.rbegin(); it != node->children.rend(); it++) {
910
951
  index--;
911
952
  auto &subNode = *it;
912
- if (subNode->state != UNDEFINED) {
913
- if (shouldAnimate && subNode->state != DEAD) {
953
+ react_native_assert(subNode->isExiting() && "Subtree removal of a view that React does not delete");
954
+ if (subNode->state != DECISION_PENDING) {
955
+ if (shouldAnimate && subNode->state != COMPLETED) {
914
956
  hasAnimatedChildren = true;
915
957
  } else {
916
958
  endAnimationsRecursively(subNode, index, mutations);
@@ -925,13 +967,11 @@ bool LayoutAnimationsProxy::startAnimationsRecursively(
925
967
  }
926
968
  mutations.push_back(ShadowViewMutation::RemoveMutation(node->current.tag, subNode->current, index));
927
969
  toBeRemoved.push_back(subNode);
928
- subNode->setExitingState(DELETED);
970
+ subNode->setExitingState(TORN_DOWN);
971
+ unmapLightNode(subNode);
929
972
  mutations.push_back(ShadowViewMutation::DeleteMutation(subNode->current));
930
973
  } else {
931
974
  subNode->setExitingState(WAITING);
932
- // register withheld subtree members, so that reconcileContradictedRemovals
933
- // can find them when React re-creates their tags
934
- lightNodes_[subNode->current.tag] = subNode;
935
975
  }
936
976
  }
937
977
 
@@ -943,7 +983,6 @@ bool LayoutAnimationsProxy::startAnimationsRecursively(
943
983
 
944
984
  if (hasExitAnimation) {
945
985
  node->setExitingState(ANIMATING);
946
- lightNodes_[node->current.tag] = node;
947
986
  transaction.exiting.push_back({node, exitConfig});
948
987
  } else {
949
988
  if (!shouldAnimate) {
@@ -951,7 +990,6 @@ bool LayoutAnimationsProxy::startAnimationsRecursively(
951
990
  }
952
991
  if (hasAnimatedChildren) {
953
992
  node->setExitingState(WAITING);
954
- lightNodes_[node->current.tag] = node;
955
993
  }
956
994
  }
957
995
 
@@ -967,6 +1005,7 @@ void LayoutAnimationsProxy::clearSurfaceState() const {
967
1005
  LayoutAnimationsProxyCommon::clearSurfaceState();
968
1006
  if constexpr (StaticFeatureFlags::getFlag("ENABLE_SHARED_ELEMENT_TRANSITIONS")) {
969
1007
  sharedContainers_.clear();
1008
+ snapshottedScreens_.clear();
970
1009
  transition_.reset();
971
1010
  uncommittedScreenPop_.reset();
972
1011
  }
@@ -981,6 +1020,7 @@ void LayoutAnimationsProxy::clearSurfaceState() const {
981
1020
  ShadowView LayoutAnimationsProxy::cloneViewWithoutOpacity(
982
1021
  const ShadowView &shadowView,
983
1022
  const PropsParserContext &propsParserContext) const {
1023
+ react_native_assert(isViewKind(shadowView) && "Only ViewKind views have ViewProps");
984
1024
  auto newView = shadowView;
985
1025
  folly::dynamic rawProps = folly::dynamic::object("opacity", 0);
986
1026
  #ifdef ANDROID
@@ -996,11 +1036,16 @@ ShadowView LayoutAnimationsProxy::cloneViewWithoutOpacity(
996
1036
 
997
1037
  // Android's push model applies JS-thread transactions asynchronously on the UI
998
1038
  // thread. A UI-thread pull can overtake them, so completed animations must not
999
- // add structural cleanup mutations there. This gate can go away with Android's
1000
- // pull model.
1039
+ // add structural cleanup mutations there.
1040
+ //
1041
+ // Under the pull model there is nothing left to overtake, because every
1042
+ // transaction is pulled from the UI thread's frame callback. Keeping the thread
1043
+ // check on its own there would mean this never returns true, so no exiting view
1044
+ // would ever have its removal flushed, and every one of them would stay mounted
1045
+ // for the lifetime of the surface.
1001
1046
  bool LayoutAnimationsProxy::shouldFlushStructuralMutations() const {
1002
1047
  #ifdef ANDROID
1003
- return !worklets::isOnUIThread(uiScheduler_);
1048
+ return isMountingCoordinatorPullModelEnabled() || !worklets::isOnUIThread(uiScheduler_);
1004
1049
  #else
1005
1050
  return true;
1006
1051
  #endif
@@ -1061,9 +1106,7 @@ void LayoutAnimationsProxy::startEnteringAnimation(
1061
1106
  const std::shared_ptr<Serializable> &config) const {
1062
1107
  resolveLightNodeProps(node);
1063
1108
  const auto &newChildShadowView = node->current;
1064
- const auto &props = newChildShadowView.props;
1065
- auto &viewProps = static_cast<const ViewProps &>(*props);
1066
- const auto opacity = viewProps.opacity;
1109
+ const auto opacity = getViewProps(newChildShadowView).opacity;
1067
1110
  const auto &parent = node->parent.lock();
1068
1111
  react_native_assert(parent && "Parent node is nullptr");
1069
1112
  enqueueLayoutAnimation(ManagedLayoutAnimationStart{
@@ -33,6 +33,7 @@ struct StartAnimationsRecursivelyConfig {
33
33
  bool shouldRemoveSubviewsWithoutAnimations;
34
34
  bool shouldAnimate;
35
35
  bool isScreenPop;
36
+ bool defersTeardown;
36
37
  };
37
38
 
38
39
  struct PendingNodeAnimation {
@@ -93,6 +94,9 @@ struct LayoutAnimationsProxy : public LayoutAnimationsProxyCommon {
93
94
  mutable std::optional<ProgressTransition> transition_;
94
95
  mutable std::optional<UncommittedScreenPop> uncommittedScreenPop_;
95
96
  mutable std::shared_ptr<LightNode> topScreen_;
97
+ // screens that forceScreenSnapshot_ switched to snapshots after updates; React Native Screens never switches them
98
+ // back
99
+ mutable std::unordered_set<Tag> snapshottedScreens_;
96
100
  mutable std::unordered_map<Tag, SharedContainer> sharedContainers_;
97
101
  mutable std::unordered_set<Tag> hiddenViewTags_;
98
102
  std::shared_ptr<SharedTransitionManager> sharedTransitionManager_;
@@ -244,8 +248,8 @@ struct LayoutAnimationsProxy : public LayoutAnimationsProxyCommon {
244
248
  void handleSubtreeRemoval(
245
249
  const std::shared_ptr<LightNode> &node,
246
250
  const std::shared_ptr<LightNode> &parent,
247
- int hostIndex,
248
251
  TransactionMeta &transaction) const;
252
+ bool holdsSnapshottedScreen(const std::shared_ptr<LightNode> &node) const;
249
253
  void flushCompletedRemovals(ShadowViewMutationList &filteredMutations) const;
250
254
 
251
255
  void addOngoingAnimations(ShadowViewMutationList &mutations) const;
@@ -156,6 +156,25 @@ void LayoutAnimationsProxyCommon::flushLayoutAnimationOperations(std::unique_loc
156
156
  flushLayoutAnimationOperationsLocked();
157
157
  }
158
158
 
159
+ // iOS, and Android with accumulated raw props, write the props of an Insert to the view. The shadow props of a view
160
+ // that only moves lack the values that were written synchronously.
161
+ Props::Shared LayoutAnimationsProxyCommon::propsOfMountedView(const ShadowView &view) const {
162
+ #ifdef __APPLE__
163
+ if (readMountedViewProps_) {
164
+ if (auto mountedProps = readMountedViewProps_(view.tag)) {
165
+ return mountedProps;
166
+ }
167
+ }
168
+ #elif defined(ANDROID)
169
+ if (readSynchronousProps_) {
170
+ if (const auto synchronousProps = readSynchronousProps_(view.tag); !synchronousProps.empty()) {
171
+ return mergeSynchronousProps(view, synchronousProps);
172
+ }
173
+ }
174
+ #endif
175
+ return view.props;
176
+ }
177
+
159
178
  Props::Shared LayoutAnimationsProxyCommon::mergeSynchronousProps(const ShadowView &view, const folly::dynamic &props)
160
179
  const {
161
180
  auto rawProps = props;
@@ -204,7 +223,7 @@ void LayoutAnimationsProxyCommon::applySynchronousPropsToLayoutAnimation(const T
204
223
  start.after.props = mergeSynchronousProps(start.after, props);
205
224
  if constexpr (std::is_same_v<Start, ManagedLayoutAnimationStart>) {
206
225
  if (propsIncludeOpacity && start.opacity) {
207
- start.opacity = static_cast<const ViewProps &>(*start.after.props).opacity;
226
+ start.opacity = getViewProps(start.after).opacity;
208
227
  }
209
228
  }
210
229
  }
@@ -226,7 +245,7 @@ void LayoutAnimationsProxyCommon::applySynchronousPropsToLayoutAnimation(const T
226
245
  animation->finalView.props = mergeSynchronousProps(animation->finalView, props);
227
246
  animation->currentView.props = mergeSynchronousProps(animation->currentView, props);
228
247
  if (propsIncludeOpacity && animation->opacity) {
229
- animation->opacity = static_cast<const ViewProps &>(*animation->finalView.props).opacity;
248
+ animation->opacity = getViewProps(animation->finalView).opacity;
230
249
  }
231
250
  if (const auto it = updateMap_.find(tag); it != updateMap_.end() && it->second.newProps) {
232
251
  auto pendingView = animation->finalView;
@@ -506,7 +525,6 @@ std::shared_ptr<Serializable> LayoutAnimationsProxyCommon::getRetargetLayoutAnim
506
525
 
507
526
  bool LayoutAnimationsProxyCommon::updateEnteringAnimationTarget(const Tag tag, const ShadowView &finalView) const {
508
527
  auto lock = std::unique_lock<std::recursive_mutex>(mutex);
509
- const auto opacity = static_cast<const ViewProps &>(*finalView.props).opacity;
510
528
  if (const auto pendingIt = pendingLayoutAnimations_.find(tag); pendingIt != pendingLayoutAnimations_.end()) {
511
529
  if (pendingIt->second.type != LayoutAnimationType::ENTERING) {
512
530
  return false;
@@ -515,7 +533,7 @@ bool LayoutAnimationsProxyCommon::updateEnteringAnimationTarget(const Tag tag, c
515
533
  react_native_assert(operationIndex < layoutAnimationOperations_.size());
516
534
  if (auto *start = std::get_if<ManagedLayoutAnimationStart>(&layoutAnimationOperations_[operationIndex])) {
517
535
  start->after = finalView;
518
- start->opacity = opacity;
536
+ start->opacity = getViewProps(finalView).opacity;
519
537
  return true;
520
538
  }
521
539
  react_native_assert(false && "Pending managed layout animation not found");
@@ -524,7 +542,7 @@ bool LayoutAnimationsProxyCommon::updateEnteringAnimationTarget(const Tag tag, c
524
542
  animationIt != layoutAnimations_.end() && animationIt->second.type == LayoutAnimationType::ENTERING) {
525
543
  animationIt->second.finalView = finalView;
526
544
  if (animationIt->second.opacity) {
527
- animationIt->second.opacity = opacity;
545
+ animationIt->second.opacity = getViewProps(finalView).opacity;
528
546
  }
529
547
  return true;
530
548
  }
@@ -63,6 +63,10 @@ struct LayoutAnimationCancellation {
63
63
  using LayoutAnimationOperation =
64
64
  std::variant<ManagedLayoutAnimationStart, ProgressLayoutAnimationStart, LayoutAnimationCancellation>;
65
65
 
66
+ #ifdef ANDROID
67
+ using SynchronousPropsReader = std::function<folly::dynamic(Tag tag)>;
68
+ #endif
69
+
66
70
  struct LayoutAnimationsProxyDependencies {
67
71
  std::shared_ptr<LayoutAnimationsManager> layoutAnimationsManager;
68
72
  SharedComponentDescriptorRegistry componentDescriptorRegistry;
@@ -74,9 +78,11 @@ struct LayoutAnimationsProxyDependencies {
74
78
  #ifdef ANDROID
75
79
  PreserveMountedTagsFunction filterUnmountedTagsFunction;
76
80
  std::shared_ptr<facebook::react::CallInvoker> jsInvoker;
81
+ SynchronousPropsReader readSynchronousProps;
77
82
  #endif
78
83
  #ifdef __APPLE__
79
84
  ForceScreenSnapshotFunction forceScreenSnapshot;
85
+ ReadMountedViewPropsFunction readMountedViewProps;
80
86
  #endif
81
87
  };
82
88
 
@@ -95,7 +101,12 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
95
101
  #ifdef ANDROID
96
102
  ,
97
103
  preserveMountedTags_(dependencies.filterUnmountedTagsFunction),
98
- jsInvoker_(dependencies.jsInvoker)
104
+ jsInvoker_(dependencies.jsInvoker),
105
+ readSynchronousProps_(dependencies.readSynchronousProps)
106
+ #endif
107
+ #ifdef __APPLE__
108
+ ,
109
+ readMountedViewProps_(dependencies.readMountedViewProps)
99
110
  #endif
100
111
  {
101
112
  }
@@ -115,6 +126,7 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
115
126
  void flushLayoutAnimationOperations() const;
116
127
 
117
128
  protected:
129
+ Props::Shared propsOfMountedView(const ShadowView &view) const;
118
130
  Props::Shared mergeSynchronousProps(const ShadowView &view, const folly::dynamic &props) const;
119
131
  bool hasLayoutAnimationRecords() const;
120
132
  void applySynchronousPropsToLayoutAnimation(Tag tag, const folly::dynamic &props) const;
@@ -169,6 +181,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
169
181
  #ifdef ANDROID
170
182
  PreserveMountedTagsFunction preserveMountedTags_;
171
183
  std::shared_ptr<facebook::react::CallInvoker> jsInvoker_;
184
+ SynchronousPropsReader readSynchronousProps_;
185
+ #endif
186
+ #ifdef __APPLE__
187
+ ReadMountedViewPropsFunction readMountedViewProps_;
172
188
  #endif
173
189
 
174
190
  private:
@@ -134,12 +134,12 @@ void LayoutAnimationsProxy_Legacy::reconcileContradictedRemovals(
134
134
  }
135
135
  }
136
136
 
137
- // On android mutations that alter the view hierarchy are only produced on the JS thread (the push model), so to not
138
- // race with those, we apply the dead nodes cleanup only on the JS thread, unless there is a surface drop, in which case
139
- // we can safely cleanup on the UI thread since the surface is gone and no more mutations will be produced for it.
137
+ // With Android's push model, structural mutations from the JS thread may still be waiting to mount when a UI-thread
138
+ // pull runs, so dead nodes must be cleaned up on the JS thread. The pull model mounts transactions on the UI thread,
139
+ // where cleanup is safe. A dropped surface can also be cleaned up immediately.
140
140
  bool LayoutAnimationsProxy_Legacy::shouldFlushDeadNodes([[maybe_unused]] const bool surfaceDropped) const {
141
141
  #ifdef ANDROID
142
- return surfaceDropped || !worklets::isOnUIThread(uiScheduler_);
142
+ return surfaceDropped || isMountingCoordinatorPullModelEnabled() || !worklets::isOnUIThread(uiScheduler_);
143
143
  #else
144
144
  return true;
145
145
  #endif
@@ -356,8 +356,9 @@ void LayoutAnimationsProxy_Legacy::handleUpdatesAndEnterings(
356
356
  }
357
357
 
358
358
  transferConfigFromNativeID(mutation.newChildShadowView.props->nativeId, mutation.newChildShadowView.tag);
359
- const auto enteringConfig =
360
- layoutAnimationsManager_->getLayoutAnimationConfig(tag, LayoutAnimationType::ENTERING);
359
+ const auto enteringConfig = isViewKind(mutation.newChildShadowView)
360
+ ? layoutAnimationsManager_->getLayoutAnimationConfig(tag, LayoutAnimationType::ENTERING)
361
+ : nullptr;
361
362
  if (!enteringConfig) {
362
363
  filteredMutations.push_back(mutation);
363
364
  continue;
@@ -470,7 +471,7 @@ void LayoutAnimationsProxy_Legacy::addOngoingAnimations(ShadowViewMutationList &
470
471
  mutations.push_back(
471
472
  ShadowViewMutation::UpdateMutation(layoutAnimation.currentView, newView, layoutAnimation.parentTag));
472
473
  layoutAnimation.currentView = newView;
473
- if (layoutAnimation.opacity && static_cast<const ViewProps &>(*newView.props).opacity == *layoutAnimation.opacity) {
474
+ if (layoutAnimation.opacity && getViewProps(newView).opacity == *layoutAnimation.opacity) {
474
475
  layoutAnimation.opacity.reset();
475
476
  }
476
477
  }
@@ -653,14 +654,13 @@ void LayoutAnimationsProxy_Legacy::startEnteringAnimation(
653
654
  #ifdef LAYOUT_ANIMATIONS_LOGS
654
655
  LOG(INFO) << "start entering animation for tag " << tag << std::endl;
655
656
  #endif
656
- auto &viewProps = static_cast<const ViewProps &>(*mutation.newChildShadowView.props);
657
657
  enqueueLayoutAnimation(ManagedLayoutAnimationStart{
658
658
  .tag = tag,
659
659
  .type = LayoutAnimationType::ENTERING,
660
660
  .before = mutation.newChildShadowView,
661
661
  .after = mutation.newChildShadowView,
662
662
  .parentTag = mutation.parentTag,
663
- .opacity = viewProps.opacity,
663
+ .opacity = getViewProps(mutation.newChildShadowView).opacity,
664
664
  .config = config,
665
665
  });
666
666
  }