Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useState } from 'react';
import { Alert, Button, Pressable, StyleSheet, Text, View } from 'react-native';

// Final-state-first bench (Objective 06). The nativeID markers below make the
// Legacy proxy enqueue controlled prepared native requests on example-app
// builds with IOS_LAYOUT_ANIMATIONS_CORE_ANIMATION on; no production Layout
// Animations run here. With the flag on, the box slides 5 s (taps land at the
// final frame) and the delayed view holds invisible for 1 s, then fades in.
// With the flag off, both views jump.

const LAYOUT_BENCH_NATIVE_ID = 'fsf-bench-layout';
const ENTERING_BENCH_NATIVE_ID = 'fsf-bench-entering';

export default function FinalStateFirstBenchExample() {
const [moved, setMoved] = useState(false);
const [entered, setEntered] = useState(false);
const [lastTap, setLastTap] = useState('none');

return (
<View style={styles.container}>
<Text style={styles.header}>Final-state-first bench (Objective 06)</Text>

<Text style={styles.label}>Layout: A → B over 5 s</Text>
<Button title="Move box" onPress={() => setMoved((prev) => !prev)} />
<View style={styles.arena}>
<Pressable
nativeID={LAYOUT_BENCH_NATIVE_ID}
testID="fsf-layout-box"
onPress={() => {
setLastTap(moved ? 'B (final)' : 'A (final)');
Alert.alert('Tapped', 'The tap used the mounted final frame.');
}}
style={[styles.box, moved ? styles.boxMoved : null]}>
<Text style={styles.boxLabel}>tap me</Text>
</Pressable>
</View>
<Text style={styles.label}>Last registered tap position: {lastTap}</Text>

<Text style={styles.label}>Entering: 1 s delay, 2 s fade</Text>
<Button
title={entered ? 'Unmount delayed view' : 'Mount delayed view'}
onPress={() => setEntered((prev) => !prev)}
/>
<View style={styles.arena}>
{entered ? (
<View
nativeID={ENTERING_BENCH_NATIVE_ID}
testID="fsf-entering-box"
style={[styles.box, styles.enteringBox]}>
<Text style={styles.boxLabel}>entered</Text>
</View>
) : null}
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
gap: 8,
},
header: {
fontSize: 18,
fontWeight: 'bold',
},
label: {
fontSize: 14,
},
arena: {
height: 160,
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'gray',
justifyContent: 'flex-start',
alignItems: 'flex-start',
},
box: {
width: 100,
height: 60,
backgroundColor: 'navy',
justifyContent: 'center',
alignItems: 'center',
},
boxMoved: {
marginLeft: 220,
marginTop: 80,
},
enteringBox: {
backgroundColor: 'darkgreen',
marginLeft: 40,
marginTop: 40,
},
boxLabel: {
color: 'white',
},
});
8 changes: 8 additions & 0 deletions apps/common-app/src/apps/reanimated/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ const FlatListExample: React.FC = () =>
React.createElement(
require('./SharedElementTransitions/FlatList').default as React.FC
);
const FinalStateFirstBench: React.FC = () =>
React.createElement(
require('./LayoutAnimations/FinalStateFirstBenchExample').default
);
const FlatListSkipEnteringExiting: React.FC = () =>
React.createElement(
require('./LayoutAnimations/FlatListSkipEnteringExiting').default
Expand Down Expand Up @@ -1192,6 +1196,10 @@ export const EXAMPLES: Record<string, Example> = {
title: '[LA] Strict Mode Comparison',
screen: StrictModeComparison,
},
FinalStateFirstBench: {
title: '[LA] Final-state-first bench',
screen: FinalStateFirstBench,
},
ReactionsCounterExample: {
title: '[LA] Reactions counter',
screen: ReactionsCounterExample,
Expand Down
2 changes: 1 addition & 1 deletion apps/fabric-example/ios/Podfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/react-native-reanimated/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### 🎉 New features

- Add feature-flagged Shared Native Animation Boundary for CSS Transitions and future Layout Animations ([#10276](https://github.com/software-mansion/react-native-reanimated/pull/10276) by [@pawicao](https://github.com/pawicao))
- Add feature-flagged final-state-first mount delivery for future native Layout Animations on iOS (by [@pawicao](https://github.com/pawicao))

### 🐛 Bug fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,74 @@ size_t LayoutAnimationsProxyCommon::FrameDrivenLeaseKeyHash::operator()(const Fr
return facebook::react::hash_combine(key.surfaceId, key.tag);
}

namespace {

struct LayoutPendingViewSnapshot final : public PendingLayoutViewSnapshot {
LayoutPendingViewSnapshot(ShadowView oldView, ShadowView finalView)
: oldView(std::move(oldView)), finalView(std::move(finalView)) {}

ShadowView oldView;
ShadowView finalView;
};

} // namespace

std::shared_ptr<PendingNativeLayoutStarts> LayoutAnimationsProxyCommon::makePendingNativeLayoutStarts(
const std::shared_ptr<native_animation::NativeAnimationService> &nativeAnimationService,
const std::shared_ptr<LayoutMountBoundary> &layoutMountBoundary) {
if (!useNativeLayoutAnimations() || !nativeAnimationService || !layoutMountBoundary) {
return nullptr;
}
auto pendingStarts = std::make_shared<PendingNativeLayoutStarts>(nativeAnimationService, layoutMountBoundary);
layoutMountBoundary->setPostMountObserver(
[weakPendingStarts = std::weak_ptr<PendingNativeLayoutStarts>(pendingStarts)](const SurfaceId surfaceId) {
if (const auto pendingStarts = weakPendingStarts.lock()) {
pendingStarts->drainAfterMount(surfaceId);
}
});
return pendingStarts;
}

void LayoutAnimationsProxyCommon::enqueueNativeLayoutStart(
const LayoutAnimationType type,
const ShadowView &oldView,
const ShadowView &finalView,
const MountingTransaction::Number transactionNumber,
native_animation::AnimationRequest request,
native_animation::AnimationCallbacks callbacks) const {
react_native_assert(pendingNativeStarts_ != nullptr && "native layout start delivery is not available");
const auto handle = request.handle;
pendingNativeStarts_->enqueue({
handle,
transactionNumber,
type,
finalView.layoutMetrics.frame,
// The snapshot below keeps this props object alive while pending.
reinterpret_cast<uintptr_t>(finalView.props.get()),
std::make_unique<LayoutPendingViewSnapshot>(oldView, finalView),
std::move(request),
std::move(callbacks),
});
}

void LayoutAnimationsProxyCommon::cancelNativeLayoutStart(const native_animation::AnimationHandle &handle) const {
if (pendingNativeStarts_) {
pendingNativeStarts_->cancel(handle);
}
}

void LayoutAnimationsProxyCommon::notifyNativeStartsSurfaceStarted(const SurfaceId surfaceId) const {
if (pendingNativeStarts_) {
pendingNativeStarts_->notifySurfaceStarted(surfaceId);
}
}

void LayoutAnimationsProxyCommon::cancelNativeStartsSurface(const SurfaceId surfaceId) const {
if (pendingNativeStarts_) {
pendingNativeStarts_->cancelSurface(surfaceId);
}
}

std::optional<facebook::react::SurfaceId>
LayoutAnimationsProxyCommon::onTransitionProgress(int tag, double progress, bool isClosing, bool isGoingForward) {
return std::nullopt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include <reanimated/LayoutAnimations/FrameDrivenAnimationLease.h>
#include <reanimated/LayoutAnimations/LayoutAnimationType.h>
#include <reanimated/LayoutAnimations/LayoutAnimationsManager.h>
#include <reanimated/LayoutAnimations/LayoutMountBoundary.h>
#include <reanimated/LayoutAnimations/LayoutNativeAnimationAdapter.h>
#include <reanimated/LayoutAnimations/PendingNativeLayoutStarts.h>
#include <reanimated/NativeAnimations/NativeAnimationService.h>
#include <reanimated/Tools/FeatureFlags.h>
#include <reanimated/Tools/PlatformDepMethodsHolder.h>
Expand Down Expand Up @@ -78,6 +80,7 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
jsi::Runtime &uiRuntime,
const std::shared_ptr<UIScheduler> &uiScheduler,
const std::shared_ptr<native_animation::NativeAnimationService> &nativeAnimationService,
const std::shared_ptr<LayoutMountBoundary> &layoutMountBoundary,
const std::shared_ptr<facebook::react::UIManager> &uiManager
#ifdef ANDROID
,
Expand All @@ -98,7 +101,8 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
[uiScheduler](const native_animation::CallbackOperation &operation) {
scheduleOnUI(uiScheduler, operation);
})
: nullptr)
: nullptr),
pendingNativeStarts_(makePendingNativeLayoutStarts(nativeAnimationService, layoutMountBoundary))
#ifdef ANDROID
,
preserveMountedTags_(filterUnmountedTagsFunction),
Expand All @@ -125,7 +129,24 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
void releaseFrameDrivenLayoutAnimation(SurfaceId surfaceId, Tag tag, native_animation::AnimationOutcome outcome)
const;

// Final-state-first delivery: stores an owned prepared request during
// interception; the pending store submits it after the intended mount.
void enqueueNativeLayoutStart(
LayoutAnimationType type,
const ShadowView &oldView,
const ShadowView &finalView,
MountingTransaction::Number transactionNumber,
native_animation::AnimationRequest request,
native_animation::AnimationCallbacks callbacks) const;
void cancelNativeLayoutStart(const native_animation::AnimationHandle &handle) const;
void notifyNativeStartsSurfaceStarted(SurfaceId surfaceId) const;
void cancelNativeStartsSurface(SurfaceId surfaceId) const;

protected:
static std::shared_ptr<PendingNativeLayoutStarts> makePendingNativeLayoutStarts(
const std::shared_ptr<native_animation::NativeAnimationService> &nativeAnimationService,
const std::shared_ptr<LayoutMountBoundary> &layoutMountBoundary);

void transferConfigFromNativeID(const std::string &nativeId, const int tag) const;

mutable std::unordered_set<Tag> maybeSettledAnimationTags_;
Expand All @@ -137,6 +158,7 @@ class LayoutAnimationsProxyCommon : public facebook::react::MountingOverrideDele
const std::shared_ptr<UIScheduler> uiScheduler_;
std::shared_ptr<facebook::react::UIManager> uiManager_;
const std::shared_ptr<LayoutNativeAnimationAdapter> nativeAnimationAdapter_;
const std::shared_ptr<PendingNativeLayoutStarts> pendingNativeStarts_;
PreserveMountedTagsFunction preserveMountedTags_;

struct FrameDrivenLeaseKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
jsi::Runtime &uiRuntime,
const std::shared_ptr<UIScheduler> &uiScheduler,
const std::shared_ptr<native_animation::NativeAnimationService> &nativeAnimationService,
const std::shared_ptr<LayoutMountBoundary> &layoutMountBoundary,
const std::shared_ptr<UIManager> &uiManager
#ifdef ANDROID
,
Expand All @@ -83,6 +84,7 @@ struct LayoutAnimationsProxy_Experimental : public LayoutAnimationsProxyCommon,
uiRuntime,
uiScheduler,
nativeAnimationService,
layoutMountBoundary,
uiManager
#ifdef ANDROID
,
Expand Down
Loading
Loading