From db5acfe6e31ec018c350b1caf23df0885686f73b Mon Sep 17 00:00:00 2001 From: Oleksii Bulenok Date: Thu, 9 Jul 2026 19:47:45 +0200 Subject: [PATCH] fix input field in the wrong place when backgrounding an app for a while --- patches/react-native-reanimated@4.3.2.patch | 115 ++++++++++++++++++ .../react-native-reanimated@4.3.2.patch.md | 39 ++++++ 2 files changed, 154 insertions(+) diff --git a/patches/react-native-reanimated@4.3.2.patch b/patches/react-native-reanimated@4.3.2.patch index baaed85216..3cea7735c5 100644 --- a/patches/react-native-reanimated@4.3.2.patch +++ b/patches/react-native-reanimated@4.3.2.patch @@ -1,3 +1,118 @@ +diff --git a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp +index 531f0dc7b4eeb9b29cb2255d8444da02a74c35b7..64a903157d8831fcf882c0d5198e777414808a27 100644 +--- a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp ++++ b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp +@@ -2,6 +2,7 @@ + #include + + #include ++#include + #include + + namespace reanimated { +@@ -25,7 +26,13 @@ void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operation + addUpdatesToBatch(shadowNode, jsi::dynamicFromValue(rt, updates)); + + if constexpr (StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS")) { +- timestampMap_[shadowNode->getTag()] = timestamp; ++ const auto tag = shadowNode->getTag(); ++ timestampMap_[tag] = timestamp; ++ // If JS already has a `settledProps` snapshot for this tag, it is now ++ // stale — schedule a refresh on the next `getUpdatesOlderThanTimestamp`. ++ if (syncedTags_.erase(tag) > 0) { ++ invalidatedTags_.insert(tag); ++ } + } + } + } +@@ -37,22 +44,40 @@ jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp( + std::lock_guard lock{mutex_}; + removeUpdatesOlderThanTimestamp(cleanupTimestamp); + +- std::vector>> updates; ++ // Each returned tag is paired with whether it came from the settled path — ++ // only settled-path tags are tracked as "synced" so that an ongoing animation ++ // doesn't re-trigger an invalidation/sync on every GC tick. ++ std::vector, bool>> updates; + + for (const auto &[viewTag, pair] : updatesRegistry_) { + auto it = timestampMap_.find(viewTag); +- if (it != timestampMap_.end() && it->second < timestamp) { +- updates.emplace_back(viewTag, std::cref(pair.second)); ++ if (it == timestampMap_.end()) { ++ continue; ++ } ++ const bool isSettled = it->second < timestamp; ++ const auto invalidatedIt = invalidatedTags_.find(viewTag); ++ const bool isStaleSynced = invalidatedIt != invalidatedTags_.end(); ++ if (isSettled || isStaleSynced) { ++ updates.emplace_back(viewTag, std::cref(pair.second), isSettled); ++ if (isStaleSynced) { ++ // Only erase serviced invalidations; if a tag was invalidated but the ++ // matching update batch hasn't been flushed into updatesRegistry_ yet, ++ // we leave the entry so the next sync picks it up. ++ invalidatedTags_.erase(invalidatedIt); ++ } + } + } + + const jsi::Array array(rt, updates.size()); + size_t i = 0; +- for (const auto &[viewTag, styleProps] : updates) { ++ for (const auto &[viewTag, styleProps, isSettled] : updates) { + const jsi::Object item(rt); + item.setProperty(rt, "viewTag", viewTag); + item.setProperty(rt, "styleProps", jsi::valueFromDynamic(rt, styleProps.get())); + array.setValueAtIndex(rt, i++, item); ++ if (isSettled) { ++ syncedTags_.insert(viewTag); ++ } + } + + return jsi::Value(rt, array); +@@ -62,7 +87,8 @@ void AnimatedPropsRegistry::removeUpdatesOlderThanTimestamp(const double timesta + for (auto it = timestampMap_.begin(); it != timestampMap_.end();) { + const auto viewTag = it->first; + const auto viewTimestamp = it->second; +- if (viewTimestamp < timestamp) { ++ ++ if (viewTimestamp < timestamp && syncedTags_.count(viewTag) > 0) { + it = timestampMap_.erase(it); + updatesRegistry_.erase(viewTag); + } else { +@@ -74,6 +100,8 @@ void AnimatedPropsRegistry::removeUpdatesOlderThanTimestamp(const double timesta + void AnimatedPropsRegistry::removeTag(const Tag tag) { + updatesRegistry_.erase(tag); + timestampMap_.erase(tag); ++ syncedTags_.erase(tag); ++ invalidatedTags_.erase(tag); + } + + } // namespace reanimated +diff --git a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h +index 2c6c0e13604c9421e147d7eea7f4a4752288011c..da69e9119f200bb11878455232cf975f6f2c0042 100644 +--- a/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h ++++ b/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.h +@@ -7,6 +7,7 @@ + #include + #include + #include ++#include + #include + + namespace reanimated { +@@ -20,6 +21,11 @@ class AnimatedPropsRegistry : public UpdatesRegistry { + + private: + std::unordered_map timestampMap_; // viewTag -> timestamp, protected by `mutex_` ++ // Tags whose latest values have already been pushed to React `settledProps`. ++ std::unordered_set syncedTags_; ++ // Tags that were synced to React but received a fresh worklet update since; ++ // their `settledProps` are stale and need to be refreshed on the next sync. ++ std::unordered_set invalidatedTags_; + + void removeUpdatesOlderThanTimestamp(double timestamp); + void removeTag(Tag tag) override; diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h index 096218ab9659955cd6272c97181bce3c893ed591..1a8e25fc8295b3ac943130709bf063ea41a50585 100644 --- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h diff --git a/patches/react-native-reanimated@4.3.2.patch.md b/patches/react-native-reanimated@4.3.2.patch.md index c97309ca0b..9b0a89d9a3 100644 --- a/patches/react-native-reanimated@4.3.2.patch.md +++ b/patches/react-native-reanimated@4.3.2.patch.md @@ -1,5 +1,13 @@ # react-native-reanimated@4.3.2.patch +Contains two independent changes: + +1. Backport of PR 9901 (`LayoutAnimation.configureNext` compatibility) +2. Backport of PR 9527 plus an eviction guard in `AnimatedPropsRegistry` + (stale `settledProps` applied after app resume) + +## 1. Backport of PR 9901 + Backport of https://github.com/software-mansion/react-native-reanimated/pull/9901 ("refactor(LayoutAnimations): stop taking over UIManagerAnimationDelegate"). @@ -20,3 +28,34 @@ Only the `packages/react-native-reanimated` part of the PR is included (the `apps/fabric-example` hunk is not part of the published package), and the include hunk in `LayoutAnimationsProxy_Legacy.cpp` was adjusted to the 4.3.2 release sources. + +## 2. Stale `settledProps` after app resume (AnimatedPropsRegistry) + +Backport of https://github.com/software-mansion/react-native-reanimated/pull/9527 +("Fix stale settledProps on worklet re-animation") plus an additional eviction +guard on top of it. Fixes the Android DM composer "phantom jump" +(https://github.com/software-mansion/react-native-reanimated/issues/9574). + +Background: with `FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS`, once an +animation settles its final props are handed to JS (polled every 500 ms by +`PropsRegistryGarbageCollector`) and stored in React component state +(`settledProps`). The native registry entry is then evicted ~2 s after the +last worklet write, at which point `ReanimatedCommitHook` no longer covers the +view and the React-side snapshot becomes the sole owner of the value. + +The PR 9527 part adds `syncedTags_` / `invalidatedTags_` so that when a +previously-synced view re-animates, its now-stale React snapshot is refreshed +on the next GC tick instead of waiting up to ~1.5 s for the new value to +settle. + +The additional guard fixes the handoff itself. Upstream, eviction relies on a +timing assumption: the settled value is returnable between 1 s and 2 s of age, +and a 500 ms JS timer is assumed to tick inside that window. If no tick lands +there (app backgrounded while a keyboard-driven animation finishes, JS thread +blocked for >1 s), the first tick after resume destroys the entry inside +`getUpdatesOlderThanTimestamp` *before* the collection loop can return it - +the settled value is lost, React state keeps the pre-background value, and the +next React commit snaps the view back (the phantom jump). The guard makes +`removeUpdatesOlderThanTimestamp` only evict tags present in `syncedTags_`, +i.e. values that were actually handed to JS; an unsynced stale entry survives +one more tick, gets returned as settled, and is evicted on the following tick.