react-native-reanimated 4.5.3 → 4.5.4

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.
@@ -25,7 +25,7 @@ mergeProps(const ShadowNode &shadowNode, const PropsMap &propsMap, const ShadowN
25
25
  if (propsVector.size() > 1) {
26
26
  folly::dynamic newPropsDynamic = folly::dynamic::object;
27
27
  for (const auto &props : propsVector) {
28
- newPropsDynamic = folly::dynamic::merge(props.operator folly::dynamic(), newPropsDynamic);
28
+ newPropsDynamic = folly::dynamic::merge(newPropsDynamic, props.operator folly::dynamic());
29
29
  }
30
30
  return shadowNode.getComponentDescriptor().cloneProps(propsParserContext, newProps, RawProps(newPropsDynamic));
31
31
  }
@@ -62,11 +62,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
62
62
  const SharedComponentDescriptorRegistry &componentDescriptorRegistry,
63
63
  const std::shared_ptr<const ContextContainer> &contextContainer,
64
64
  jsi::Runtime &uiRuntime,
65
- const std::shared_ptr<UIScheduler> &uiScheduler
65
+ const std::shared_ptr<UIScheduler> &uiScheduler,
66
+ const std::shared_ptr<facebook::react::UIManager> &uiManager
66
67
  #ifdef ANDROID
67
68
  ,
68
69
  const PreserveMountedTagsFunction &filterUnmountedTagsFunction,
69
- const std::shared_ptr<facebook::react::UIManager> &uiManager,
70
70
  const std::shared_ptr<facebook::react::CallInvoker> &jsInvoker
71
71
  #endif
72
72
  )
@@ -74,11 +74,11 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
74
74
  contextContainer_(contextContainer),
75
75
  componentDescriptorRegistry_(componentDescriptorRegistry),
76
76
  uiRuntime_(uiRuntime),
77
- uiScheduler_(uiScheduler)
77
+ uiScheduler_(uiScheduler),
78
+ uiManager_(uiManager)
78
79
  #ifdef ANDROID
79
80
  ,
80
81
  preserveMountedTags_(filterUnmountedTagsFunction),
81
- uiManager_(uiManager),
82
82
  jsInvoker_(jsInvoker)
83
83
  #endif
84
84
  {
@@ -98,10 +98,10 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
98
98
  SharedComponentDescriptorRegistry componentDescriptorRegistry_;
99
99
  jsi::Runtime &uiRuntime_;
100
100
  const std::shared_ptr<UIScheduler> uiScheduler_;
101
+ std::shared_ptr<facebook::react::UIManager> uiManager_;
101
102
  PreserveMountedTagsFunction preserveMountedTags_;
102
103
 
103
104
  #ifdef ANDROID
104
- std::shared_ptr<facebook::react::UIManager> uiManager_;
105
105
  std::shared_ptr<facebook::react::CallInvoker> jsInvoker_;
106
106
 
107
107
  void restoreOpacityInCaseOfFlakyEnteringAnimation(SurfaceId surfaceId) const;
@@ -30,6 +30,8 @@ std::optional<MountingTransaction> LayoutAnimationsProxy_Experimental::pullTrans
30
30
  const std::vector<std::shared_ptr<MutationNode>> roots;
31
31
  const bool isInTransition = static_cast<bool>(transitionState_);
32
32
 
33
+ reconcileContradictedRemovals(mutations, filteredMutations);
34
+
33
35
  if (isInTransition) {
34
36
  updateLightTree(propsParserContext, mutations, filteredMutations);
35
37
  handleProgressTransition(filteredMutations, mutations, propsParserContext, surfaceId);
@@ -107,6 +109,67 @@ std::optional<MountingTransaction> LayoutAnimationsProxy_Experimental::pullTrans
107
109
  return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry};
108
110
  }
109
111
 
112
+ // If React re-creates or re-inserts a tag whose exiting removal we are still
113
+ // withholding, it has contradicted that withheld removal. Flush it now instead
114
+ // of letting the stale node linger: updateLightTree would overwrite its
115
+ // lightNodes_ entry (the "LightNode already exists" assert is compiled out in
116
+ // release), orphaning the still-mounted exiting view, and the eventual
117
+ // endLayoutAnimation would then remove the wrong, live view and crash the
118
+ // mounting layer.
119
+ //
120
+ // This must run before updateLightTree (so the tag is re-registered cleanly)
121
+ // and before addOngoingAnimations (which would otherwise emit an Update for a
122
+ // tag we are about to Delete this frame).
123
+ void LayoutAnimationsProxy_Experimental::reconcileContradictedRemovals(
124
+ const ShadowViewMutationList &mutations,
125
+ ShadowViewMutationList &filteredMutations) const {
126
+ for (const auto &mutation : mutations) {
127
+ if (mutation.type != ShadowViewMutation::Type::Create && mutation.type != ShadowViewMutation::Type::Insert) {
128
+ continue;
129
+ }
130
+ const auto tag = mutation.newChildShadowView.tag;
131
+ std::shared_ptr<LightNode> node;
132
+ if (const auto it = lightNodes_.find(tag); it != lightNodes_.end() && it->second->state != UNDEFINED) {
133
+ node = it->second;
134
+ lightNodes_.erase(it);
135
+ if (node->state == DELETED) {
136
+ // already unmounted — only the stale map entry had to go
137
+ continue;
138
+ }
139
+ } else {
140
+ // A settled exiting view (state DEAD) has already left lightNodes_ but is
141
+ // still mounted, awaiting the deadNodes cleanup in handleRemovals. That
142
+ // cleanup runs at the end of the transaction — after this Create would
143
+ // have re-registered the tag in the mounting layer's view registry — so
144
+ // it must be flushed now instead.
145
+ const auto deadIt = std::find_if(
146
+ deadNodes.begin(), deadNodes.end(), [tag](const auto &deadNode) { return deadNode->current.tag == tag; });
147
+ if (deadIt == deadNodes.end()) {
148
+ continue;
149
+ }
150
+ node = *deadIt;
151
+ deadNodes.erase(deadIt);
152
+ if (node->state == DELETED) {
153
+ continue;
154
+ }
155
+ }
156
+ // Flush the withheld removal for this tag (and its withheld subtree) right
157
+ // now, mirroring the deadNodes cleanup in handleRemovals.
158
+ const auto parent = node->parent.lock();
159
+ react_native_assert(parent && "Parent node is nullptr");
160
+ if (!parent) {
161
+ continue;
162
+ }
163
+ const auto index = parent->removeChild(node);
164
+ react_native_assert(index != -1 && "Exiting node not found");
165
+ if (index == -1) {
166
+ continue;
167
+ }
168
+ endAnimationsRecursively(node, index, filteredMutations);
169
+ maybeDropAncestors(parent, filteredMutations);
170
+ }
171
+ }
172
+
110
173
  bool LayoutAnimationsProxy_Experimental::shouldOverridePullTransaction() const {
111
174
  // we need to listen to every possible mutation to keep the light tree updated
112
175
  return true;
@@ -305,11 +368,18 @@ std::optional<SurfaceId> LayoutAnimationsProxy_Experimental::endLayoutAnimation(
305
368
  return surfaceId;
306
369
  }
307
370
 
308
- auto node = lightNodes_[tag];
309
- react_native_assert(node && "LightNode not found");
371
+ const auto nodeIt = lightNodes_.find(tag);
372
+ // the withheld removal may have already been flushed (e.g. reconciled after
373
+ // React re-created the tag) — the assert alone is compiled out in release
374
+ // and operator[] would insert a null node here
375
+ if (nodeIt == lightNodes_.end() || !nodeIt->second) {
376
+ react_native_assert(false && "LightNode not found");
377
+ return surfaceId;
378
+ }
379
+ auto node = nodeIt->second;
310
380
 
311
381
  node->state = DEAD;
312
- lightNodes_.erase(tag);
382
+ lightNodes_.erase(nodeIt);
313
383
  deadNodes.insert(node);
314
384
 
315
385
  return surfaceId;
@@ -349,6 +419,7 @@ void LayoutAnimationsProxy_Experimental::handleRemovals(
349
419
  parent->children.push_back(node);
350
420
  if (node->state == UNDEFINED) {
351
421
  node->state = WAITING;
422
+ lightNodes_[node->current.tag] = node;
352
423
  }
353
424
  } else {
354
425
  maybeCancelAnimation(node->current.tag);
@@ -432,6 +503,10 @@ void LayoutAnimationsProxy_Experimental::endAnimationsRecursively(
432
503
  ShadowViewMutationList &mutations) const {
433
504
  maybeCancelAnimation(node->current.tag);
434
505
  node->state = DELETED;
506
+ // drop the tag mapping unless it was already re-registered for a new node
507
+ if (const auto it = lightNodes_.find(node->current.tag); it != lightNodes_.end() && it->second == node) {
508
+ lightNodes_.erase(it);
509
+ }
435
510
  // iterate from the end, so that children
436
511
  // with higher indices appear first in the mutations list
437
512
 
@@ -463,6 +538,9 @@ void LayoutAnimationsProxy_Experimental::maybeDropAncestors(
463
538
  react_native_assert(index != -1 && "Child node not found");
464
539
 
465
540
  node->state = DELETED;
541
+ if (const auto it = lightNodes_.find(node->current.tag); it != lightNodes_.end() && it->second == node) {
542
+ lightNodes_.erase(it);
543
+ }
466
544
  maybeCancelAnimation(node->current.tag);
467
545
  cleanupMutations.push_back(ShadowViewMutation::RemoveMutation(parent->current.tag, node->current, index));
468
546
  cleanupMutations.push_back(ShadowViewMutation::DeleteMutation(node->current));
@@ -515,6 +593,9 @@ bool LayoutAnimationsProxy_Experimental::startAnimationsRecursively(
515
593
  mutations.push_back(ShadowViewMutation::DeleteMutation(subNode->current));
516
594
  } else {
517
595
  subNode->state = WAITING;
596
+ // register withheld subtree members, so that reconcileContradictedRemovals
597
+ // can find them when React re-creates their tags
598
+ lightNodes_[subNode->current.tag] = subNode;
518
599
  }
519
600
  }
520
601
 
@@ -67,11 +67,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
67
67
  const SharedComponentDescriptorRegistry &componentDescriptorRegistry,
68
68
  const std::shared_ptr<const ContextContainer> &contextContainer,
69
69
  jsi::Runtime &uiRuntime,
70
- const std::shared_ptr<UIScheduler> &uiScheduler
70
+ const std::shared_ptr<UIScheduler> &uiScheduler,
71
+ const std::shared_ptr<UIManager> &uiManager
71
72
  #ifdef ANDROID
72
73
  ,
73
74
  const PreserveMountedTagsFunction &filterUnmountedTagsFunction,
74
- const std::shared_ptr<UIManager> &uiManager,
75
75
  const std::shared_ptr<CallInvoker> &jsInvoker
76
76
  #endif
77
77
  )
@@ -80,11 +80,11 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
80
80
  componentDescriptorRegistry,
81
81
  contextContainer,
82
82
  uiRuntime,
83
- uiScheduler
83
+ uiScheduler,
84
+ uiManager
84
85
  #ifdef ANDROID
85
86
  ,
86
87
  filterUnmountedTagsFunction,
87
- uiManager,
88
88
  jsInvoker
89
89
  #endif
90
90
  ),
@@ -109,6 +109,9 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
109
109
  const ShadowViewMutationList &mutations,
110
110
  ShadowViewMutationList &filteredMutations) const;
111
111
 
112
+ void reconcileContradictedRemovals(const ShadowViewMutationList &mutations, ShadowViewMutationList &filteredMutations)
113
+ const;
114
+
112
115
  void handleSharedTransitionsStart(
113
116
  const std::shared_ptr<LightNode> &afterTopScreen,
114
117
  const std::shared_ptr<LightNode> &beforeTopScreen,
@@ -1,12 +1,14 @@
1
1
  #include <reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h>
2
2
 
3
3
  #include <react/debug/react_native_assert.h>
4
+ #include <react/renderer/mounting/ShadowTree.h>
4
5
  #include <react/renderer/mounting/ShadowViewMutation.h>
5
6
 
6
7
  #include <memory>
7
8
  #include <ranges>
8
9
  #include <set>
9
10
  #include <string>
11
+ #include <thread>
10
12
  #include <unordered_map>
11
13
  #include <unordered_set>
12
14
  #include <utility>
@@ -33,7 +35,8 @@ std::optional<MountingTransaction> LayoutAnimationsProxy_Legacy::pullTransaction
33
35
  auto lock = std::unique_lock<std::recursive_mutex>(mutex);
34
36
  PropsParserContext propsParserContext{surfaceId, *contextContainer_};
35
37
  ShadowViewMutationList filteredMutations;
36
- auto &[deadNodes] = surfaceContext_[surfaceId];
38
+ auto &surfaceCtx = getSurfaceContext(surfaceId);
39
+ auto &deadNodes = surfaceCtx.deadNodes;
37
40
 
38
41
  std::vector<std::shared_ptr<MutationNode>> roots;
39
42
  std::unordered_map<Tag, Tag> movedViews;
@@ -60,17 +63,47 @@ std::optional<MountingTransaction> LayoutAnimationsProxy_Legacy::pullTransaction
60
63
 
61
64
  parseRemoveMutations(movedViews, mutations, roots);
62
65
 
63
- auto shouldAnimate = !surfacesToRemove_.contains(surfaceId);
64
- surfacesToRemove_.erase(surfaceId);
65
- handleRemovals(filteredMutations, roots, deadNodes, shouldAnimate);
66
+ // We recognize dropped surfaces by the presence of a Remove mutation for a root child. This can produce false
67
+ // positives. Ideal solution will be to introduce an appropriate API in RN
68
+ auto surfaceDropped = false;
69
+ const auto removesRootChildren = std::ranges::any_of(mutations, [surfaceId](const auto &mutation) {
70
+ return mutation.type == ShadowViewMutation::Remove && mutation.parentTag == surfaceId;
71
+ });
72
+ if (removesRootChildren) {
73
+ surfaceDropped = surfacesToRemove_.erase(surfaceId) != 0;
74
+ }
75
+ const bool flushDeadNodes = shouldFlushDeadNodes(surfaceDropped);
76
+ handleRemovals(filteredMutations, roots, deadNodes, surfaceDropped, flushDeadNodes);
77
+ #ifdef ANDROID
78
+ maybeScheduleCleanupPull(surfaceCtx, surfaceId, flushDeadNodes);
79
+ #endif // ANDROID
66
80
 
67
81
  handleUpdatesAndEnterings(filteredMutations, movedViews, mutations, propsParserContext, surfaceId);
68
82
 
69
83
  addOngoingAnimations(surfaceId, filteredMutations);
70
84
 
85
+ dropUpdatesForDeletedViews(filteredMutations);
86
+
71
87
  return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry};
72
88
  }
73
89
 
90
+ // The LayoutAnimationDriver can pair a final keyframe update with the withheld
91
+ // Remove/Delete it replays in the same transaction; we emit removals first, so
92
+ // the update would reach the mounting layer after its view was deleted.
93
+ void LayoutAnimationsProxy_Legacy::dropUpdatesForDeletedViews(ShadowViewMutationList &filteredMutations) const {
94
+ std::unordered_set<Tag> deletedTags;
95
+ for (const auto &mutation : filteredMutations) {
96
+ if (mutation.type == ShadowViewMutation::Delete) {
97
+ deletedTags.insert(mutation.oldChildShadowView.tag);
98
+ }
99
+ }
100
+ if (!deletedTags.empty()) {
101
+ std::erase_if(filteredMutations, [&deletedTags](const auto &mutation) {
102
+ return mutation.type == ShadowViewMutation::Update && deletedTags.contains(mutation.newChildShadowView.tag);
103
+ });
104
+ }
105
+ }
106
+
74
107
  // If React re-creates or re-inserts a tag whose exiting removal we are still
75
108
  // withholding, it has contradicted that withheld Remove/Delete. Flush it now
76
109
  // instead of letting it fire later against a stale hierarchy (which would
@@ -83,7 +116,7 @@ void LayoutAnimationsProxy_Legacy::reconcileContradictedRemovals(
83
116
  ShadowViewMutationList &mutations,
84
117
  ShadowViewMutationList &filteredMutations,
85
118
  SurfaceId surfaceId) const {
86
- auto &[deadNodes] = surfaceContext_[surfaceId];
119
+ auto &deadNodes = getSurfaceContext(surfaceId).deadNodes;
87
120
  for (auto &mutation : mutations) {
88
121
  if (mutation.type != ShadowViewMutation::Type::Create && mutation.type != ShadowViewMutation::Type::Insert) {
89
122
  continue;
@@ -105,6 +138,45 @@ void LayoutAnimationsProxy_Legacy::reconcileContradictedRemovals(
105
138
  }
106
139
  }
107
140
 
141
+ // On android mutations that alter the view hierarchy are only produced on the JS thread (the push model), so to not
142
+ // race with those, we apply the dead nodes cleanup only on the JS thread, unless there is a surface drop, in which case
143
+ // we can safely cleanup on the UI thread since the surface is gone and no more mutations will be produced for it.
144
+ bool LayoutAnimationsProxy_Legacy::shouldFlushDeadNodes([[maybe_unused]] const bool surfaceDropped) const {
145
+ #ifdef ANDROID
146
+ return surfaceDropped || std::this_thread::get_id() != uiThreadId_;
147
+ #else
148
+ return true;
149
+ #endif
150
+ }
151
+
152
+ #ifdef ANDROID
153
+ // We schedule a pullTransaction call to happen on the JS thread so it can safely remove dead nodes after exiting
154
+ // finished
155
+ void LayoutAnimationsProxy_Legacy::maybeScheduleCleanupPull(
156
+ SurfaceContext &surfaceCtx,
157
+ const SurfaceId surfaceId,
158
+ const bool flushedDeadNodes) const {
159
+ if (flushedDeadNodes) {
160
+ surfaceCtx.cleanupPullScheduled = false;
161
+ } else if (!surfaceCtx.deadNodes.empty() && !surfaceCtx.cleanupPullScheduled) {
162
+ surfaceCtx.cleanupPullScheduled = true;
163
+ scheduleDeferredCleanupPull(surfaceId);
164
+ }
165
+ }
166
+
167
+ void LayoutAnimationsProxy_Legacy::scheduleDeferredCleanupPull(SurfaceId surfaceId) const {
168
+ const std::weak_ptr<UIManager> weakUiManager = uiManager_;
169
+ jsInvoker_->invokeAsync([weakUiManager, surfaceId](jsi::Runtime &) {
170
+ auto uiManager = weakUiManager.lock();
171
+ if (!uiManager) {
172
+ return;
173
+ }
174
+ uiManager->getShadowTreeRegistry().visit(
175
+ surfaceId, [](ShadowTree const &shadowTree) { shadowTree.notifyDelegatesOfUpdates(); });
176
+ });
177
+ }
178
+ #endif
179
+
108
180
  std::optional<SurfaceId> LayoutAnimationsProxy_Legacy::progressLayoutAnimation(int tag, const jsi::Object &newStyle) {
109
181
  #ifdef LAYOUT_ANIMATIONS_LOGS
110
182
  LOG(INFO) << "progress layout animation for tag " << tag << std::endl;
@@ -169,7 +241,7 @@ std::optional<SurfaceId> LayoutAnimationsProxy_Legacy::endLayoutAnimation(int ta
169
241
  }
170
242
  auto mutationNode = std::static_pointer_cast<MutationNode>(node);
171
243
  mutationNode->state = ExitingState_Legacy::DEAD;
172
- auto &[deadNodes] = surfaceContext_[surfaceId];
244
+ auto &deadNodes = getSurfaceContext(surfaceId).deadNodes;
173
245
  deadNodes.insert(mutationNode);
174
246
 
175
247
  return surfaceId;
@@ -281,12 +353,13 @@ void LayoutAnimationsProxy_Legacy::handleRemovals(
281
353
  ShadowViewMutationList &filteredMutations,
282
354
  std::vector<std::shared_ptr<MutationNode>> &roots,
283
355
  std::unordered_set<std::shared_ptr<MutationNode>> &deadNodes,
284
- bool shouldAnimate) const {
356
+ bool surfaceDropped,
357
+ bool flushDeadNodes) const {
285
358
  // iterate from the end, so that children
286
359
  // with higher indices appear first in the mutations list
287
360
  for (auto it = roots.rbegin(); it != roots.rend(); it++) {
288
361
  auto &node = *it;
289
- if (!startAnimationsRecursively(node, true, shouldAnimate, false, filteredMutations)) {
362
+ if (!startAnimationsRecursively(node, true, !surfaceDropped, false, filteredMutations)) {
290
363
  filteredMutations.push_back(node->mutation);
291
364
  node->unflattenedParent->removeChildFromUnflattenedTree(node); //???
292
365
  if (node->state != ExitingState_Legacy::MOVED) {
@@ -301,6 +374,10 @@ void LayoutAnimationsProxy_Legacy::handleRemovals(
301
374
  }
302
375
  }
303
376
 
377
+ if (!flushDeadNodes) {
378
+ // Deferred - the host still has these views mounted, bookkeeping stays.
379
+ return;
380
+ }
304
381
  for (const auto &node : deadNodes) {
305
382
  if (node->state != ExitingState_Legacy::DELETED) {
306
383
  endAnimationsRecursively(node, filteredMutations);
@@ -713,6 +790,7 @@ void LayoutAnimationsProxy_Legacy::startEnteringAnimation(const int tag, ShadowV
713
790
  auto &mutex = strongThis->mutex;
714
791
  auto lock = std::unique_lock<std::recursive_mutex>(mutex);
715
792
  #ifdef ANDROID
793
+ strongThis->uiThreadId_ = std::this_thread::get_id();
716
794
  if (consumeIsCancelled(strongThis->pendingStarts_, tag, handle)) {
717
795
  // the view was removed before this start could run
718
796
  return;
@@ -773,12 +851,12 @@ void LayoutAnimationsProxy_Legacy::startExitingAnimation(const int tag, ShadowVi
773
851
  auto &mutex = strongThis->mutex;
774
852
  auto lock = std::unique_lock<std::recursive_mutex>(mutex);
775
853
  #ifdef ANDROID
854
+ strongThis->uiThreadId_ = std::this_thread::get_id();
776
855
  if (consumeIsCancelled(strongThis->pendingStarts_, tag, handle)) {
777
- // the view was removed (e.g. its subtree was force-ended by a screen
778
- // pop) before this start could run its Remove+Delete are already on
779
- // their way to the mounting layer, so starting the animation now
780
- // would emit updates for a view that's about to be deleted
781
- strongThis->layoutAnimationsManager_->clearLayoutAnimationConfig(tag);
856
+ // The view was removed before this start could run. Deliberately no
857
+ // clearLayoutAnimationConfig: with tag reuse the config maps already
858
+ // describe the re-created view, and wiping them would leave it mounted
859
+ // at opacity 0. A dead tag's stale configs can never fire again.
782
860
  return;
783
861
  }
784
862
  #endif
@@ -836,6 +914,7 @@ void LayoutAnimationsProxy_Legacy::startLayoutAnimation(const int tag, const Sha
836
914
  auto &mutex = strongThis->mutex;
837
915
  auto lock = std::unique_lock<std::recursive_mutex>(mutex);
838
916
  #ifdef ANDROID
917
+ strongThis->uiThreadId_ = std::this_thread::get_id();
839
918
  if (consumeIsCancelled(strongThis->pendingStarts_, tag, handle)) {
840
919
  // the view was removed before this start could run
841
920
  return;
@@ -998,23 +1077,37 @@ inline bool MutationNode::isMutationNode() {
998
1077
  return true;
999
1078
  }
1000
1079
 
1001
- // UIManagerAnimationDelegate
1002
-
1003
- void LayoutAnimationsProxy_Legacy::uiManagerDidConfigureNextLayoutAnimation(
1004
- jsi::Runtime &runtime,
1005
- const RawValue &config,
1006
- const jsi::Value &successCallbackValue,
1007
- const jsi::Value &failureCallbackValue) const {}
1008
-
1009
- void LayoutAnimationsProxy_Legacy::setComponentDescriptorRegistry(
1010
- const SharedComponentDescriptorRegistry &componentDescriptorRegistry) {}
1080
+ void LayoutAnimationsProxy_Legacy::startSurface(const SurfaceId surfaceId) {
1081
+ auto lock = std::unique_lock<std::recursive_mutex>(mutex);
1082
+ surfaceContext_.try_emplace(surfaceId);
1083
+ }
1011
1084
 
1012
- bool LayoutAnimationsProxy_Legacy::shouldAnimateFrame() const {
1013
- return false;
1085
+ SurfaceContext &LayoutAnimationsProxy_Legacy::getSurfaceContext(const SurfaceId surfaceId) const {
1086
+ // startSurface() creates the entry for a surface before any other method
1087
+ // uses it. The proxy does not remove entries during its lifetime. Thus a
1088
+ // missing entry is an initialization bug, not a stopped surface.
1089
+ const auto it = surfaceContext_.find(surfaceId);
1090
+ react_native_assert(it != surfaceContext_.end() && "surface must be registered by startSurface");
1091
+ return it->second;
1014
1092
  }
1015
1093
 
1016
- void LayoutAnimationsProxy_Legacy::stopSurface(SurfaceId surfaceId) {
1017
- surfacesToRemove_.insert(surfaceId);
1094
+ // UIManagerCommitHook
1095
+
1096
+ // Surface teardown commits an empty root (SurfaceHandler::stop) before the
1097
+ // teardown transaction is pulled — mark it so pullTransaction skips exit
1098
+ // animations. Reading the ShadowTreeRegistry here instead would deadlock.
1099
+ RootShadowNode::Unshared LayoutAnimationsProxy_Legacy::shadowTreeWillCommit(
1100
+ const ShadowTree &shadowTree,
1101
+ const RootShadowNode::Shared & /*oldRootShadowNode*/,
1102
+ const RootShadowNode::Unshared &newRootShadowNode) noexcept {
1103
+ const auto surfaceId = shadowTree.getSurfaceId();
1104
+ auto lock = std::unique_lock<std::recursive_mutex>(mutex);
1105
+ if (newRootShadowNode->getChildren().empty()) {
1106
+ surfacesToRemove_.insert(surfaceId);
1107
+ } else {
1108
+ surfacesToRemove_.erase(surfaceId);
1109
+ }
1110
+ return newRootShadowNode;
1018
1111
  }
1019
1112
 
1020
1113
  } // namespace reanimated
@@ -3,8 +3,8 @@
3
3
  #include <react/renderer/componentregistry/ComponentDescriptorFactory.h>
4
4
  #include <react/renderer/mounting/MountingOverrideDelegate.h>
5
5
  #include <react/renderer/scheduler/Scheduler.h>
6
- #include <react/renderer/uimanager/UIManagerAnimationDelegate.h>
7
6
  #include <react/renderer/uimanager/UIManagerBinding.h>
7
+ #include <react/renderer/uimanager/UIManagerCommitHook.h>
8
8
  #include <reanimated/Compat/WorkletsApi.h>
9
9
  #include <reanimated/LayoutAnimations/LayoutAnimationsManager.h>
10
10
  #include <reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h>
@@ -13,6 +13,7 @@
13
13
 
14
14
  #include <memory>
15
15
  #include <string>
16
+ #include <thread>
16
17
  #include <unordered_map>
17
18
  #include <unordered_set>
18
19
  #include <utility>
@@ -97,12 +98,17 @@ static inline void mergeAndSwap(
97
98
  std::swap(A, merged);
98
99
  }
99
100
 
101
+ // Created by startSurface before the proxy can pull for that surface.
102
+ // Entries are never erased, so stray pulls after teardown always find state.
100
103
  struct SurfaceContext {
101
104
  mutable std::unordered_set<std::shared_ptr<MutationNode>> deadNodes;
105
+ #ifdef ANDROID
106
+ mutable bool cleanupPullScheduled = false;
107
+ #endif
102
108
  };
103
109
 
104
110
  struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
105
- public UIManagerAnimationDelegate,
111
+ public UIManagerCommitHook,
106
112
  public std::enable_shared_from_this<LayoutAnimationsProxy_Legacy> {
107
113
  mutable std::unordered_map<Tag, std::shared_ptr<Node>> nodeForTag_;
108
114
  mutable std::recursive_mutex mutex;
@@ -110,17 +116,24 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
110
116
  mutable std::unordered_map<SurfaceId, SurfaceContext> surfaceContext_;
111
117
  mutable std::unordered_map<Tag, int> leastRemoved;
112
118
  mutable std::unordered_set<SurfaceId> surfacesToRemove_;
119
+ bool shouldFlushDeadNodes(bool surfaceDropped) const;
120
+ #ifdef ANDROID
121
+ mutable std::thread::id uiThreadId_;
122
+
123
+ void maybeScheduleCleanupPull(SurfaceContext &surfaceCtx, SurfaceId surfaceId, bool flushedDeadNodes) const;
124
+ void scheduleDeferredCleanupPull(SurfaceId surfaceId) const;
125
+ #endif
113
126
 
114
127
  LayoutAnimationsProxy_Legacy(
115
128
  const std::shared_ptr<LayoutAnimationsManager> &layoutAnimationsManager,
116
129
  const SharedComponentDescriptorRegistry &componentDescriptorRegistry,
117
130
  const std::shared_ptr<const ContextContainer> &contextContainer,
118
131
  jsi::Runtime &uiRuntime,
119
- const std::shared_ptr<UIScheduler> &uiScheduler
132
+ const std::shared_ptr<UIScheduler> &uiScheduler,
133
+ const std::shared_ptr<UIManager> &uiManager
120
134
  #ifdef ANDROID
121
135
  ,
122
136
  const PreserveMountedTagsFunction &filterUnmountedTagsFunction,
123
- const std::shared_ptr<UIManager> &uiManager,
124
137
  const std::shared_ptr<CallInvoker> &jsInvoker
125
138
  #endif
126
139
  )
@@ -129,14 +142,19 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
129
142
  componentDescriptorRegistry,
130
143
  contextContainer,
131
144
  uiRuntime,
132
- uiScheduler
145
+ uiScheduler,
146
+ uiManager
133
147
  #ifdef ANDROID
134
148
  ,
135
149
  filterUnmountedTagsFunction,
136
- uiManager,
137
150
  jsInvoker
138
151
  #endif
139
152
  ) {
153
+ uiManager->registerCommitHook(*this);
154
+ }
155
+
156
+ ~LayoutAnimationsProxy_Legacy() override {
157
+ uiManager_->unregisterCommitHook(*this);
140
158
  }
141
159
 
142
160
  void startEnteringAnimation(const int tag, ShadowViewMutation &mutation) const;
@@ -146,6 +164,8 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
146
164
  void transferConfigFromNativeID(const std::string &nativeId, const int tag) const;
147
165
  std::optional<SurfaceId> progressLayoutAnimation(int tag, const jsi::Object &newStyle) override;
148
166
  std::optional<SurfaceId> endLayoutAnimation(int tag, bool shouldRemove) override;
167
+ void startSurface(const SurfaceId surfaceId) override;
168
+ SurfaceContext &getSurfaceContext(SurfaceId surfaceId) const;
149
169
  void maybeCancelAnimation(const int tag) const;
150
170
 
151
171
  void reconcileContradictedRemovals(
@@ -160,7 +180,8 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
160
180
  ShadowViewMutationList &filteredMutations,
161
181
  std::vector<std::shared_ptr<MutationNode>> &roots,
162
182
  std::unordered_set<std::shared_ptr<MutationNode>> &deadNodes,
163
- bool shouldAnimate) const;
183
+ bool surfaceDropped,
184
+ bool flushDeadNodes) const;
164
185
 
165
186
  void handleUpdatesAndEnterings(
166
187
  ShadowViewMutationList &filteredMutations,
@@ -169,6 +190,7 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
169
190
  const PropsParserContext &propsParserContext,
170
191
  SurfaceId surfaceId) const;
171
192
  void addOngoingAnimations(SurfaceId surfaceId, ShadowViewMutationList &mutations) const;
193
+ void dropUpdatesForDeletedViews(ShadowViewMutationList &filteredMutations) const;
172
194
  void updateOngoingAnimationTarget(const int tag, const ShadowViewMutation &mutation) const;
173
195
  std::shared_ptr<ShadowView> cloneViewWithoutOpacity(
174
196
  facebook::react::ShadowViewMutation &mutation,
@@ -206,19 +228,15 @@ struct LayoutAnimationsProxy_Legacy : public LayoutAnimationsProxyCommon,
206
228
  const TransactionTelemetry &telemetry,
207
229
  ShadowViewMutationList mutations) const override;
208
230
 
209
- // UIManagerAnimationDelegate
210
-
211
- void uiManagerDidConfigureNextLayoutAnimation(
212
- jsi::Runtime &runtime,
213
- const RawValue &config,
214
- const jsi::Value &successCallbackValue,
215
- const jsi::Value &failureCallbackValue) const override;
216
-
217
- void setComponentDescriptorRegistry(const SharedComponentDescriptorRegistry &componentDescriptorRegistry) override;
231
+ // UIManagerCommitHook
218
232
 
219
- bool shouldAnimateFrame() const override;
233
+ void commitHookWasRegistered(const UIManager &) noexcept override {}
234
+ void commitHookWasUnregistered(const UIManager &) noexcept override {}
220
235
 
221
- void stopSurface(SurfaceId surfaceId) override;
236
+ RootShadowNode::Unshared shadowTreeWillCommit(
237
+ const ShadowTree &shadowTree,
238
+ const RootShadowNode::Shared &oldRootShadowNode,
239
+ const RootShadowNode::Unshared &newRootShadowNode) noexcept override;
222
240
  };
223
241
 
224
242
  } // namespace reanimated
@@ -688,7 +688,19 @@ bool ReanimatedModuleProxy::handleRawEvent(const RawEvent &rawEvent, double curr
688
688
  return false;
689
689
  }
690
690
 
691
- int tag = eventTarget->getTag();
691
+ #if REACT_NATIVE_VERSION_MINOR >= 87
692
+ const auto tag = eventTarget->getTag();
693
+ #else
694
+ // A stale event dispatched during unmount may carry an EventTarget with a null
695
+ // InstanceHandle which getTag() would dereference (see #9925).
696
+ // Fixed in React Native 0.87 by https://github.com/facebook/react-native/pull/56763.
697
+ const auto shadowNodeFamily = rawEvent.shadowNodeFamily.lock();
698
+ if (shadowNodeFamily == nullptr) {
699
+ return false;
700
+ }
701
+ const auto tag = shadowNodeFamily->getTag();
702
+ #endif
703
+
692
704
  auto eventType = rawEvent.type;
693
705
  if (eventType.rfind("top", 0) == 0) {
694
706
  eventType = "on" + eventType.substr(3);
@@ -1222,11 +1234,11 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() {
1222
1234
  componentDescriptorRegistry,
1223
1235
  scheduler->getContextContainer(),
1224
1236
  getJSIRuntimeFromWorkletRuntime(uiRuntime_),
1225
- uiScheduler_
1237
+ uiScheduler_,
1238
+ uiManager_
1226
1239
  #ifdef ANDROID
1227
1240
  ,
1228
1241
  filterUnmountedTagsFunction_,
1229
- uiManager_,
1230
1242
  jsInvoker_
1231
1243
  #endif
1232
1244
  );
@@ -1235,22 +1247,19 @@ void ReanimatedModuleProxy::initializeLayoutAnimationsProxy() {
1235
1247
  #endif
1236
1248
  layoutAnimationsProxy_ = std::move(layoutAnimationsProxyExperimental);
1237
1249
  } else {
1238
- auto layoutAnimationsProxyLegacy = std::make_shared<LayoutAnimationsProxy_Legacy>(
1250
+ layoutAnimationsProxy_ = std::make_shared<LayoutAnimationsProxy_Legacy>(
1239
1251
  layoutAnimationsManager_,
1240
1252
  componentDescriptorRegistry,
1241
1253
  scheduler->getContextContainer(),
1242
1254
  getJSIRuntimeFromWorkletRuntime(uiRuntime_),
1243
- uiScheduler_
1255
+ uiScheduler_,
1256
+ uiManager_
1244
1257
  #ifdef ANDROID
1245
1258
  ,
1246
1259
  filterUnmountedTagsFunction_,
1247
- uiManager_,
1248
1260
  jsInvoker_
1249
1261
  #endif
1250
1262
  );
1251
- // TODO (future): support in experimental
1252
- uiManager_->setAnimationDelegate(layoutAnimationsProxyLegacy.get());
1253
- layoutAnimationsProxy_ = std::move(layoutAnimationsProxyLegacy);
1254
1263
  }
1255
1264
  }
1256
1265
  }
@@ -125,7 +125,11 @@ using namespace facebook::react;
125
125
  - (void)performOperations
126
126
  {
127
127
  RCTAssertMainQueue();
128
- _performOperations(); // calls ReanimatedModuleProxy::performOperations
128
+ REAPerformOperations performOperations = _performOperations;
129
+ if (performOperations == nil) {
130
+ return;
131
+ }
132
+ performOperations(); // calls ReanimatedModuleProxy::performOperations
129
133
  }
130
134
 
131
135
  - (void)dispatchEvent:(id<RCTEvent>)event
@@ -5,5 +5,5 @@
5
5
  * version used to build the native part of the library in runtime. Remember to
6
6
  * keep this in sync with the version declared in `package.json`
7
7
  */
8
- export const jsVersion = '4.5.3';
8
+ export const jsVersion = '4.5.4';
9
9
  //# sourceMappingURL=jsVersion.js.map
@@ -3,5 +3,5 @@
3
3
  * version used to build the native part of the library in runtime. Remember to
4
4
  * keep this in sync with the version declared in `package.json`
5
5
  */
6
- export declare const jsVersion = "4.5.3";
6
+ export declare const jsVersion = "4.5.4";
7
7
  //# sourceMappingURL=jsVersion.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-reanimated",
3
- "version": "4.5.3",
3
+ "version": "4.5.4",
4
4
  "description": "More powerful alternative to Animated library for React Native.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -4,4 +4,4 @@
4
4
  * version used to build the native part of the library in runtime. Remember to
5
5
  * keep this in sync with the version declared in `package.json`
6
6
  */
7
- export const jsVersion = '4.5.3';
7
+ export const jsVersion = '4.5.4';