react-native-reanimated 4.5.2 → 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.
- package/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp +1 -1
- package/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp +54 -24
- package/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h +13 -6
- package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +5 -5
- package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp +84 -3
- package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.h +7 -4
- package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +159 -27
- package/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.h +40 -18
- package/Common/cpp/reanimated/NativeModules/ReanimatedModuleProxy.cpp +21 -14
- package/android/build.gradle.kts +9 -0
- package/apple/reanimated/apple/REANodesManager.mm +5 -1
- package/lib/module/PropsRegistryGarbageCollector.js +3 -6
- package/lib/module/PropsRegistryGarbageCollector.js.map +1 -1
- package/lib/module/animation/styleAnimation.js +4 -3
- package/lib/module/animation/styleAnimation.js.map +1 -1
- package/lib/module/createAnimatedComponent/AnimatedComponent.js +1 -1
- package/lib/module/createAnimatedComponent/AnimatedComponent.js.map +1 -1
- package/lib/module/layoutReanimation/animationsManager.js +9 -6
- package/lib/module/layoutReanimation/animationsManager.js.map +1 -1
- package/lib/module/platform-specific/jsVersion.js +1 -1
- package/lib/typescript/PropsRegistryGarbageCollector.d.ts +0 -1
- package/lib/typescript/PropsRegistryGarbageCollector.d.ts.map +1 -1
- package/lib/typescript/animation/styleAnimation.d.ts +1 -1
- package/lib/typescript/animation/styleAnimation.d.ts.map +1 -1
- package/lib/typescript/createAnimatedComponent/AnimatedComponent.d.ts.map +1 -1
- package/lib/typescript/layoutReanimation/animationsManager.d.ts.map +1 -1
- package/lib/typescript/platform-specific/jsVersion.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/reanimated_utils.rb +7 -0
- package/src/PropsRegistryGarbageCollector.ts +3 -6
- package/src/animation/styleAnimation.ts +5 -3
- package/src/createAnimatedComponent/AnimatedComponent.tsx +3 -1
- package/src/layoutReanimation/animationsManager.ts +9 -7
- package/src/platform-specific/jsVersion.ts +1 -1
|
@@ -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()
|
|
28
|
+
newPropsDynamic = folly::dynamic::merge(newPropsDynamic, props.operator folly::dynamic());
|
|
29
29
|
}
|
|
30
30
|
return shadowNode.getComponentDescriptor().cloneProps(propsParserContext, newProps, RawProps(newPropsDynamic));
|
|
31
31
|
}
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
#include <react/debug/react_native_assert.h>
|
|
7
7
|
|
|
8
|
+
#include <functional>
|
|
8
9
|
#include <memory>
|
|
9
10
|
#include <utility>
|
|
11
|
+
#include <vector>
|
|
10
12
|
|
|
11
13
|
namespace reanimated {
|
|
12
14
|
|
|
@@ -35,26 +37,65 @@ void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operation
|
|
|
35
37
|
addUpdatesToBatch(shadowNode->getFamilyShared(), jsi::dynamicFromValue(rt, updates));
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// When USE_ANIMATION_BACKEND is enabled, updates bypass `updatesRegistry_`,
|
|
41
|
+
// so entries added to `timestampMap_` would never be synced and thus never
|
|
42
|
+
// evicted, leaking until view unmount.
|
|
43
|
+
if constexpr (
|
|
44
|
+
StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS") &&
|
|
45
|
+
!StaticFeatureFlags::getFlag("USE_ANIMATION_BACKEND")) {
|
|
46
|
+
const auto tag = shadowNode->getTag();
|
|
47
|
+
timestampMap_[tag] = timestamp;
|
|
48
|
+
// If JS already has a `settledProps` snapshot for this tag, it is now
|
|
49
|
+
// stale — schedule a refresh on the next `collectSettledUpdates`.
|
|
50
|
+
if (syncedTags_.erase(tag) > 0) {
|
|
51
|
+
invalidatedTags_.insert(tag);
|
|
52
|
+
}
|
|
40
53
|
}
|
|
41
54
|
}
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
jsi::Value AnimatedPropsRegistry::
|
|
45
|
-
jsi::Runtime &rt,
|
|
46
|
-
const double timestamp,
|
|
47
|
-
const double cleanupTimestamp) {
|
|
57
|
+
jsi::Value AnimatedPropsRegistry::collectSettledUpdates(jsi::Runtime &rt, const double settledTimestamp) {
|
|
48
58
|
react_native_assert(UpdatesRegistryManager::isLockedByCurrentThread());
|
|
49
|
-
removeUpdatesOlderThanTimestamp(cleanupTimestamp);
|
|
50
59
|
|
|
51
60
|
std::vector<std::pair<Tag, std::reference_wrapper<const folly::dynamic>>> updates;
|
|
52
61
|
|
|
53
|
-
for (
|
|
54
|
-
auto
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
for (auto it = updatesRegistry_.begin(); it != updatesRegistry_.end();) {
|
|
63
|
+
const auto viewTag = it->first;
|
|
64
|
+
|
|
65
|
+
if (syncedTags_.contains(viewTag)) {
|
|
66
|
+
// React already has the latest value for this tag (synced on a previous
|
|
67
|
+
// call, so the `settledProps` state is committed by now) — the registry
|
|
68
|
+
// entry is redundant. `syncedTags_` is intentionally retained to detect
|
|
69
|
+
// re-animation staleness. Note that `syncedTags_` and `invalidatedTags_`
|
|
70
|
+
// are disjoint — `update()` moves tags from the former to the latter.
|
|
71
|
+
timestampMap_.erase(viewTag);
|
|
72
|
+
it = updatesRegistry_.erase(it);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const auto timestampIt = timestampMap_.find(viewTag);
|
|
77
|
+
if (timestampIt == timestampMap_.end()) {
|
|
78
|
+
++it;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const bool isSettled = timestampIt->second < settledTimestamp;
|
|
82
|
+
const auto invalidatedIt = invalidatedTags_.find(viewTag);
|
|
83
|
+
const bool isInvalidated = invalidatedIt != invalidatedTags_.end();
|
|
84
|
+
if (isSettled || isInvalidated) {
|
|
85
|
+
updates.emplace_back(viewTag, std::cref(it->second.second));
|
|
86
|
+
if (isSettled) {
|
|
87
|
+
// Only settled-path tags are tracked as "synced" so that an ongoing
|
|
88
|
+
// animation doesn't re-trigger an invalidation/sync on every GC tick.
|
|
89
|
+
syncedTags_.insert(viewTag);
|
|
90
|
+
}
|
|
91
|
+
if (isInvalidated) {
|
|
92
|
+
// Only erase serviced invalidations; if a tag was invalidated but the
|
|
93
|
+
// matching update batch hasn't been flushed into updatesRegistry_ yet,
|
|
94
|
+
// we leave the entry so the next sync picks it up.
|
|
95
|
+
invalidatedTags_.erase(invalidatedIt);
|
|
96
|
+
}
|
|
57
97
|
}
|
|
98
|
+
++it;
|
|
58
99
|
}
|
|
59
100
|
|
|
60
101
|
const jsi::Array array(rt, updates.size());
|
|
@@ -69,22 +110,11 @@ jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp(
|
|
|
69
110
|
return jsi::Value(rt, array);
|
|
70
111
|
}
|
|
71
112
|
|
|
72
|
-
void AnimatedPropsRegistry::removeUpdatesOlderThanTimestamp(const double timestamp) {
|
|
73
|
-
for (auto it = timestampMap_.begin(); it != timestampMap_.end();) {
|
|
74
|
-
const auto viewTag = it->first;
|
|
75
|
-
const auto viewTimestamp = it->second;
|
|
76
|
-
if (viewTimestamp < timestamp) {
|
|
77
|
-
it = timestampMap_.erase(it);
|
|
78
|
-
updatesRegistry_.erase(viewTag);
|
|
79
|
-
} else {
|
|
80
|
-
it++;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
113
|
void AnimatedPropsRegistry::removeTag(const Tag tag) {
|
|
86
114
|
updatesRegistry_.erase(tag);
|
|
87
115
|
timestampMap_.erase(tag);
|
|
116
|
+
syncedTags_.erase(tag);
|
|
117
|
+
invalidatedTags_.erase(tag);
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
} // namespace reanimated
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
#include <react/renderer/uimanager/UIManager.h>
|
|
6
6
|
|
|
7
|
-
#include <memory>
|
|
8
|
-
#include <string>
|
|
9
7
|
#include <unordered_map>
|
|
10
|
-
#include <
|
|
8
|
+
#include <unordered_set>
|
|
11
9
|
|
|
12
10
|
namespace reanimated {
|
|
13
11
|
|
|
@@ -15,13 +13,22 @@ class AnimatedPropsRegistry : public UpdatesRegistry {
|
|
|
15
13
|
public:
|
|
16
14
|
void update(jsi::Runtime &rt, const jsi::Value &operations, double timestamp);
|
|
17
15
|
|
|
18
|
-
///
|
|
19
|
-
|
|
16
|
+
/// Returns updates that settled (received no update since `settledTimestamp`)
|
|
17
|
+
/// or whose synced `settledProps` snapshot was invalidated by a fresh update.
|
|
18
|
+
/// Also evicts entries that have already been synced to React — by the time
|
|
19
|
+
/// of the next call, the corresponding `settledProps` state is guaranteed to
|
|
20
|
+
/// be committed, so the registry entries are redundant.
|
|
21
|
+
jsi::Value collectSettledUpdates(jsi::Runtime &rt, double settledTimestamp);
|
|
20
22
|
|
|
21
23
|
private:
|
|
22
24
|
std::unordered_map<Tag, double> timestampMap_;
|
|
25
|
+
// Tags whose latest values have already been pushed to React `settledProps`.
|
|
26
|
+
// Intentionally retained after eviction to detect re-animation staleness.
|
|
27
|
+
std::unordered_set<Tag> syncedTags_;
|
|
28
|
+
// Tags that were synced to React but received a fresh worklet update since;
|
|
29
|
+
// their `settledProps` are stale and need to be refreshed on the next sync.
|
|
30
|
+
std::unordered_set<Tag> invalidatedTags_;
|
|
23
31
|
|
|
24
|
-
void removeUpdatesOlderThanTimestamp(double timestamp);
|
|
25
32
|
void removeTag(Tag tag) override;
|
|
26
33
|
};
|
|
27
34
|
|
|
@@ -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
|
|
309
|
-
|
|
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(
|
|
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,11 +35,14 @@ 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 &
|
|
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;
|
|
40
43
|
|
|
44
|
+
reconcileContradictedRemovals(mutations, filteredMutations, surfaceId);
|
|
45
|
+
|
|
41
46
|
addOngoingAnimations(surfaceId, filteredMutations);
|
|
42
47
|
|
|
43
48
|
#ifdef ANDROID
|
|
@@ -58,17 +63,120 @@ std::optional<MountingTransaction> LayoutAnimationsProxy_Legacy::pullTransaction
|
|
|
58
63
|
|
|
59
64
|
parseRemoveMutations(movedViews, mutations, roots);
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
64
80
|
|
|
65
81
|
handleUpdatesAndEnterings(filteredMutations, movedViews, mutations, propsParserContext, surfaceId);
|
|
66
82
|
|
|
67
83
|
addOngoingAnimations(surfaceId, filteredMutations);
|
|
68
84
|
|
|
85
|
+
dropUpdatesForDeletedViews(filteredMutations);
|
|
86
|
+
|
|
69
87
|
return MountingTransaction{surfaceId, transactionNumber, std::move(filteredMutations), telemetry};
|
|
70
88
|
}
|
|
71
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
|
+
|
|
107
|
+
// If React re-creates or re-inserts a tag whose exiting removal we are still
|
|
108
|
+
// withholding, it has contradicted that withheld Remove/Delete. Flush it now
|
|
109
|
+
// instead of letting it fire later against a stale hierarchy (which would
|
|
110
|
+
// unmount the wrong, still-live view and crash the mounting layer).
|
|
111
|
+
//
|
|
112
|
+
// This must run before addOngoingAnimations (which would otherwise emit an
|
|
113
|
+
// Update for a tag we are about to Delete this frame) and before
|
|
114
|
+
// parseRemoveMutations, so the rest of the pipeline sees clean bookkeeping.
|
|
115
|
+
void LayoutAnimationsProxy_Legacy::reconcileContradictedRemovals(
|
|
116
|
+
ShadowViewMutationList &mutations,
|
|
117
|
+
ShadowViewMutationList &filteredMutations,
|
|
118
|
+
SurfaceId surfaceId) const {
|
|
119
|
+
auto &deadNodes = getSurfaceContext(surfaceId).deadNodes;
|
|
120
|
+
for (auto &mutation : mutations) {
|
|
121
|
+
if (mutation.type != ShadowViewMutation::Type::Create && mutation.type != ShadowViewMutation::Type::Insert) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
auto tag = mutation.newChildShadowView.tag;
|
|
125
|
+
auto it = nodeForTag_.find(tag);
|
|
126
|
+
// Only a MutationNode represents a withheld removal; a plain Node is just a
|
|
127
|
+
// live parent of some removed child and must be left untouched.
|
|
128
|
+
if (it == nodeForTag_.end() || !it->second->isMutationNode()) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
auto node = std::static_pointer_cast<MutationNode>(it->second);
|
|
132
|
+
// Flush the withheld Remove/Delete for this tag (and its withheld subtree)
|
|
133
|
+
// right now, mirroring the deadNodes cleanup in handleRemovals. This removes
|
|
134
|
+
// the stale view before React's Create/Insert re-registers the same tag.
|
|
135
|
+
endAnimationsRecursively(node, filteredMutations);
|
|
136
|
+
maybeDropAncestors(node->unflattenedParent, node, filteredMutations);
|
|
137
|
+
deadNodes.erase(node);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
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
|
+
|
|
72
180
|
std::optional<SurfaceId> LayoutAnimationsProxy_Legacy::progressLayoutAnimation(int tag, const jsi::Object &newStyle) {
|
|
73
181
|
#ifdef LAYOUT_ANIMATIONS_LOGS
|
|
74
182
|
LOG(INFO) << "progress layout animation for tag " << tag << std::endl;
|
|
@@ -127,10 +235,13 @@ std::optional<SurfaceId> LayoutAnimationsProxy_Legacy::endLayoutAnimation(int ta
|
|
|
127
235
|
}
|
|
128
236
|
|
|
129
237
|
auto node = nodeForTag_[tag];
|
|
130
|
-
|
|
238
|
+
if (!node->isMutationNode()) {
|
|
239
|
+
react_native_assert(false && "exiting tag must map to a MutationNode");
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
131
242
|
auto mutationNode = std::static_pointer_cast<MutationNode>(node);
|
|
132
243
|
mutationNode->state = ExitingState_Legacy::DEAD;
|
|
133
|
-
auto &
|
|
244
|
+
auto &deadNodes = getSurfaceContext(surfaceId).deadNodes;
|
|
134
245
|
deadNodes.insert(mutationNode);
|
|
135
246
|
|
|
136
247
|
return surfaceId;
|
|
@@ -242,12 +353,13 @@ void LayoutAnimationsProxy_Legacy::handleRemovals(
|
|
|
242
353
|
ShadowViewMutationList &filteredMutations,
|
|
243
354
|
std::vector<std::shared_ptr<MutationNode>> &roots,
|
|
244
355
|
std::unordered_set<std::shared_ptr<MutationNode>> &deadNodes,
|
|
245
|
-
bool
|
|
356
|
+
bool surfaceDropped,
|
|
357
|
+
bool flushDeadNodes) const {
|
|
246
358
|
// iterate from the end, so that children
|
|
247
359
|
// with higher indices appear first in the mutations list
|
|
248
360
|
for (auto it = roots.rbegin(); it != roots.rend(); it++) {
|
|
249
361
|
auto &node = *it;
|
|
250
|
-
if (!startAnimationsRecursively(node, true,
|
|
362
|
+
if (!startAnimationsRecursively(node, true, !surfaceDropped, false, filteredMutations)) {
|
|
251
363
|
filteredMutations.push_back(node->mutation);
|
|
252
364
|
node->unflattenedParent->removeChildFromUnflattenedTree(node); //???
|
|
253
365
|
if (node->state != ExitingState_Legacy::MOVED) {
|
|
@@ -262,6 +374,10 @@ void LayoutAnimationsProxy_Legacy::handleRemovals(
|
|
|
262
374
|
}
|
|
263
375
|
}
|
|
264
376
|
|
|
377
|
+
if (!flushDeadNodes) {
|
|
378
|
+
// Deferred - the host still has these views mounted, bookkeeping stays.
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
265
381
|
for (const auto &node : deadNodes) {
|
|
266
382
|
if (node->state != ExitingState_Legacy::DELETED) {
|
|
267
383
|
endAnimationsRecursively(node, filteredMutations);
|
|
@@ -674,6 +790,7 @@ void LayoutAnimationsProxy_Legacy::startEnteringAnimation(const int tag, ShadowV
|
|
|
674
790
|
auto &mutex = strongThis->mutex;
|
|
675
791
|
auto lock = std::unique_lock<std::recursive_mutex>(mutex);
|
|
676
792
|
#ifdef ANDROID
|
|
793
|
+
strongThis->uiThreadId_ = std::this_thread::get_id();
|
|
677
794
|
if (consumeIsCancelled(strongThis->pendingStarts_, tag, handle)) {
|
|
678
795
|
// the view was removed before this start could run
|
|
679
796
|
return;
|
|
@@ -734,12 +851,12 @@ void LayoutAnimationsProxy_Legacy::startExitingAnimation(const int tag, ShadowVi
|
|
|
734
851
|
auto &mutex = strongThis->mutex;
|
|
735
852
|
auto lock = std::unique_lock<std::recursive_mutex>(mutex);
|
|
736
853
|
#ifdef ANDROID
|
|
854
|
+
strongThis->uiThreadId_ = std::this_thread::get_id();
|
|
737
855
|
if (consumeIsCancelled(strongThis->pendingStarts_, tag, handle)) {
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
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.
|
|
743
860
|
return;
|
|
744
861
|
}
|
|
745
862
|
#endif
|
|
@@ -797,6 +914,7 @@ void LayoutAnimationsProxy_Legacy::startLayoutAnimation(const int tag, const Sha
|
|
|
797
914
|
auto &mutex = strongThis->mutex;
|
|
798
915
|
auto lock = std::unique_lock<std::recursive_mutex>(mutex);
|
|
799
916
|
#ifdef ANDROID
|
|
917
|
+
strongThis->uiThreadId_ = std::this_thread::get_id();
|
|
800
918
|
if (consumeIsCancelled(strongThis->pendingStarts_, tag, handle)) {
|
|
801
919
|
// the view was removed before this start could run
|
|
802
920
|
return;
|
|
@@ -959,23 +1077,37 @@ inline bool MutationNode::isMutationNode() {
|
|
|
959
1077
|
return true;
|
|
960
1078
|
}
|
|
961
1079
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const RawValue &config,
|
|
967
|
-
const jsi::Value &successCallbackValue,
|
|
968
|
-
const jsi::Value &failureCallbackValue) const {}
|
|
969
|
-
|
|
970
|
-
void LayoutAnimationsProxy_Legacy::setComponentDescriptorRegistry(
|
|
971
|
-
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
|
+
}
|
|
972
1084
|
|
|
973
|
-
|
|
974
|
-
|
|
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;
|
|
975
1092
|
}
|
|
976
1093
|
|
|
977
|
-
|
|
978
|
-
|
|
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;
|
|
979
1111
|
}
|
|
980
1112
|
|
|
981
1113
|
} // namespace reanimated
|