diff --git a/.github/workflows/close-when-stale.yml b/.github/workflows/close-when-stale.yml
index 9de60eafacb..490ebfce177 100644
--- a/.github/workflows/close-when-stale.yml
+++ b/.github/workflows/close-when-stale.yml
@@ -31,4 +31,4 @@ jobs:
uses: ./close-when-stale
with:
close-when-stale-label: "Close when stale"
- days-to-close: 20
+ days-to-close: 14
diff --git a/apps/common-app/package.json b/apps/common-app/package.json
index f1751734a4b..93f75b3ea92 100644
--- a/apps/common-app/package.json
+++ b/apps/common-app/package.json
@@ -15,6 +15,7 @@
"@react-native-community/slider": "*",
"@react-native-masked-view/masked-view": "*",
"@react-native-picker/picker": "*",
+ "@react-navigation/bottom-tabs": "*",
"@react-navigation/native": "*",
"@react-navigation/native-stack": "*",
"@react-navigation/stack": "*",
@@ -38,6 +39,7 @@
"@react-native-community/slider": "^4.5.0",
"@react-native-masked-view/masked-view": "^0.3.1",
"@react-native-picker/picker": "^2.5.1",
+ "@react-navigation/bottom-tabs": "^6.5.20",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"@react-navigation/stack": "^6.3.18",
diff --git a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx
index c5ba46013d6..6f9aeaf84a3 100644
--- a/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx
+++ b/apps/common-app/src/examples/RuntimeTests/RuntimeTestsExample.tsx
@@ -4,13 +4,7 @@ import RuntimeTestsRunner from './ReanimatedRuntimeTestsRunner/RuntimeTestsRunne
// load tests
import './tests/TestsOfTestingFramework.test';
-import './tests/animations/withTiming/arrays.test';
-import './tests/animations/withTiming/basic.test';
-import './tests/animations/withTiming/colors.test';
-import './tests/animations/withTiming/easing.test';
-import './tests/animations/withTiming/transformMatrices.test';
-import './tests/animations/withDecay/basic.test';
-import './tests/animations/withSpring/variousConfig.test';
+import './tests/animations';
import './tests/core/cancelAnimation.test';
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/index.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/index.tsx
new file mode 100644
index 00000000000..a0dad3ec900
--- /dev/null
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/index.tsx
@@ -0,0 +1,25 @@
+import { describe } from '../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';
+
+describe('*****ANIMATIONS*****', () => {
+ describe('****withTiming**** ⏰', () => {
+ require('./withTiming/arrays.test');
+ require('./withTiming/basic.test');
+ require('./withTiming/objects.test');
+ require('./withTiming/colors.test');
+ require('./withTiming/easing.test');
+ require('./withTiming/transformMatrices.test');
+ });
+ describe('****withSpring****', () => {
+ require('./withSpring/variousConfig.test');
+ });
+ describe('****withDecay****', () => {
+ require('./withDecay/basic.test');
+ });
+ describe('****withSequence****', () => {
+ require('./withSequence/callbackCascade.test');
+ require('./withSequence/cancelAnimation.test');
+ require('./withSequence/numbers.test');
+ require('./withSequence/arrays.test');
+ require('./withSequence/colors.test');
+ });
+});
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withDecay/basic.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withDecay/basic.test.tsx
index 3d1b42f4d0f..6c242b575f5 100644
--- a/apps/common-app/src/examples/RuntimeTests/tests/animations/withDecay/basic.test.tsx
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withDecay/basic.test.tsx
@@ -49,8 +49,8 @@ describe('withDecay animation, test various config', () => {
[900, { velocity: 900, deceleration: 0.997 }],
[400, { velocity: 900, clamp: [0, 150] }],
[900, { velocity: 900, clamp: [0, 150], rubberBandEffect: true }],
- [700, { velocity: 2000, clamp: [0, 150], rubberBandEffect: true }],
- [400, { velocity: 2000, clamp: [0, 150], rubberBandEffect: true, rubberBandFactor: 2 }],
+ [800, { velocity: 2000, clamp: [0, 150], rubberBandEffect: true }],
+ [500, { velocity: 2000, clamp: [0, 150], rubberBandEffect: true, rubberBandFactor: 2 }],
] as Array<[number, WithDecayConfig]>)('Config ${1}', async ([duration, config]) => {
const snapshotName =
'decay_' +
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/arrays.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/arrays.test.tsx
new file mode 100644
index 00000000000..e3731e70a5b
--- /dev/null
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/arrays.test.tsx
@@ -0,0 +1,142 @@
+import React, { useEffect } from 'react';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+ withSequence,
+ withSpring,
+ Easing,
+ withDelay,
+} from 'react-native-reanimated';
+import {
+ describe,
+ test,
+ render,
+ wait,
+ useTestRef,
+ getTestComponent,
+ expect,
+} from '../../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';
+import { View, StyleSheet } from 'react-native';
+import { ComparisonMode } from '../../../ReanimatedRuntimeTestsRunner/types';
+
+type TestCase = {
+ startValues: [number, number, number];
+ middleValues: [number, number, number];
+ finalValues: [number, number, number];
+ animationNumber: number;
+};
+
+describe('withSequence animation of number', () => {
+ const COMPONENT_REF = {
+ first: 'firstComponent',
+ second: 'secondComponent',
+ third: 'thirdComponent',
+ };
+
+ const DELAY = 50;
+ const WidthComponent = ({ startValues, middleValues, finalValues, animationNumber }: TestCase) => {
+ const lefts = useSharedValue<[number, number, number]>(startValues);
+ const ref0 = useTestRef(COMPONENT_REF.first);
+ const ref1 = useTestRef(COMPONENT_REF.second);
+ const ref2 = useTestRef(COMPONENT_REF.third);
+
+ function animateValue(finalValues: [number, number, number]) {
+ 'worklet';
+ const finalValuesPlus20 = finalValues.map(val => val + 20);
+ switch (animationNumber) {
+ case 0:
+ return withSequence(
+ withTiming(finalValues, { duration: 200 }),
+ withDelay(DELAY, withTiming(middleValues, { duration: 300, easing: Easing.exp })),
+ withDelay(DELAY, withTiming(finalValuesPlus20, { duration: 200 })),
+ );
+ case 1:
+ return withSequence(
+ withSpring(finalValues, { duration: 200, dampingRatio: 1 }),
+ withDelay(DELAY, withSpring(middleValues, { duration: 300, dampingRatio: 1.5 })),
+ withDelay(DELAY, withSpring(finalValuesPlus20, { duration: 200, dampingRatio: 0.9 })),
+ );
+ case 2:
+ return withSequence(
+ withSpring(finalValues, { duration: 200, dampingRatio: 1 }),
+ withDelay(DELAY, withTiming(middleValues, { duration: 300 })),
+ withDelay(DELAY, withSpring(finalValuesPlus20, { duration: 200, dampingRatio: 1 })),
+ );
+ }
+ return [0, 0, 0];
+ }
+
+ const style0 = useAnimatedStyle(() => {
+ return { left: lefts.value[0] };
+ });
+ const style1 = useAnimatedStyle(() => {
+ return { left: lefts.value[1] };
+ });
+ const style2 = useAnimatedStyle(() => {
+ return { left: lefts.value[2] };
+ });
+
+ useEffect(() => {
+ lefts.value = animateValue(finalValues) as [number, number, number];
+ });
+ return (
+
+
+
+
+
+ );
+ };
+
+ test.each([
+ { startValues: [0, 10, 20], middleValues: [0, 100, 210], finalValues: [20, 10, 30], animationNumber: 0 },
+ { startValues: [0, 10, 20], middleValues: [0, 150, 160], finalValues: [40, 10, 30], animationNumber: 1 },
+ { startValues: [0, 10, 20], middleValues: [0, 150, 160], finalValues: [40, 10, 30], animationNumber: 2 },
+ { startValues: [30, 10, 55], middleValues: [0, -10, 60], finalValues: [40, 10, 30], animationNumber: 0 },
+ { startValues: [30, 10, 55], middleValues: [0, -10, 60], finalValues: [40, 10, 30], animationNumber: 1 },
+ { startValues: [30, 10, 55], middleValues: [0, -10, 60], finalValues: [40, 10, 30], animationNumber: 2 },
+ ] as Array)(
+ 'Animate ${startValues} → ${finalValues} → ${middleValues} → ${finalValues}, animation nr ${animationNumber}',
+ async ({ startValues, middleValues, finalValues, animationNumber }) => {
+ await render(
+ ,
+ );
+ const componentOne = getTestComponent(COMPONENT_REF.first);
+ const componentTwo = getTestComponent(COMPONENT_REF.second);
+ const componentThree = getTestComponent(COMPONENT_REF.third);
+ const margin = 30;
+
+ await wait(200 + DELAY / 2);
+ expect(await componentOne.getAnimatedStyle('left')).toBe(finalValues[0] + margin, ComparisonMode.DISTANCE);
+ expect(await componentTwo.getAnimatedStyle('left')).toBe(finalValues[1] + margin, ComparisonMode.DISTANCE);
+ expect(await componentThree.getAnimatedStyle('left')).toBe(finalValues[2] + margin, ComparisonMode.DISTANCE);
+ await wait(300 + DELAY / 2);
+ expect(await componentOne.getAnimatedStyle('left')).toBe(middleValues[0] + margin, ComparisonMode.DISTANCE);
+ expect(await componentTwo.getAnimatedStyle('left')).toBe(middleValues[1] + margin, ComparisonMode.DISTANCE);
+ expect(await componentThree.getAnimatedStyle('left')).toBe(middleValues[2] + margin, ComparisonMode.DISTANCE);
+ await wait(200 + DELAY);
+ expect(await componentOne.getAnimatedStyle('left')).toBe(finalValues[0] + 20 + margin, ComparisonMode.DISTANCE);
+ expect(await componentTwo.getAnimatedStyle('left')).toBe(finalValues[1] + 20 + margin, ComparisonMode.DISTANCE);
+ expect(await componentThree.getAnimatedStyle('left')).toBe(finalValues[2] + 20 + margin, ComparisonMode.DISTANCE);
+ },
+ );
+});
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ animatedBox: {
+ width: 80,
+ height: 80,
+ borderRadius: 10,
+ margin: 30,
+ backgroundColor: 'steelblue',
+ },
+});
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/callbackCascade.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/callbackCascade.test.tsx
new file mode 100644
index 00000000000..b4476c0ea35
--- /dev/null
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/callbackCascade.test.tsx
@@ -0,0 +1,236 @@
+import React, { useEffect } from 'react';
+import { View, StyleSheet } from 'react-native';
+import Animated, {
+ useSharedValue,
+ withTiming,
+ withSequence,
+ withSpring,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+import {
+ describe,
+ test,
+ expect,
+ render,
+ wait,
+ callTracker,
+ getTrackerCallCount,
+ mockAnimationTimer,
+ recordAnimationUpdates,
+ getRegisteredValue,
+ registerValue,
+} from '../../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';
+import { Snapshots } from './snapshots.snapshot';
+
+describe(`Cascade of callbacks`, () => {
+ enum Tracker {
+ callbackAnimation = 'callbackAnimation',
+ interruptedAnimation = 'interruptedAnimationTracker',
+ animationNotExecuted = 'animationNotExecuted',
+ }
+ enum SV {
+ callbackArgument0 = 'callbackArgument0',
+ callbackArgument1 = 'callbackArgument1',
+ }
+ const CallbackComponent = () => {
+ const callbackArgument0 = useSharedValue(null);
+ const callbackArgument1 = useSharedValue(false);
+ registerValue(SV.callbackArgument0, callbackArgument0);
+ registerValue(SV.callbackArgument1, callbackArgument1);
+
+ const sv0 = useSharedValue(0);
+ const sv1 = useSharedValue(0);
+ const sv2 = useSharedValue(0);
+
+ useEffect(() => {
+ sv0.value = withSequence(
+ withTiming(100, { duration: 400 }, () => {
+ sv1.value = withSequence(
+ withTiming(20, { duration: 600 }, (finished?: boolean) => {
+ // this animation gets interrupted
+ callbackArgument0.value = finished;
+ callTracker(Tracker.interruptedAnimation);
+ }),
+ withTiming(1000, { duration: 600 }, (finished?: boolean) => {
+ // execution of this animation never starts
+ callTracker(Tracker.animationNotExecuted);
+ callbackArgument1.value = finished;
+ }),
+ withTiming(1000, { duration: 600 }, (finished?: boolean) => {
+ // execution of this animation never starts
+ callTracker(Tracker.animationNotExecuted);
+ callbackArgument1.value = callbackArgument1.value || finished;
+ }),
+ );
+ }),
+
+ withTiming(20, { duration: 300 }, () => {
+ sv1.value = withSpring(150, { duration: 500 }, () => {
+ callTracker(Tracker.callbackAnimation);
+ });
+ sv2.value = withSequence(
+ withSpring(150, { duration: 300, dampingRatio: 2 }, () => {
+ callTracker(Tracker.callbackAnimation);
+ }),
+ withSpring(10, { duration: 300, dampingRatio: 2 }, () => {
+ callTracker(Tracker.callbackAnimation);
+ }),
+ );
+ }),
+ withTiming(200, { duration: 400 }),
+ );
+ });
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return { height: 20 + sv0.value, width: 20 + sv1.value, top: sv2.value };
+ });
+
+ return (
+
+
+
+ );
+ };
+
+ test('Test that all callbacks have been called a correct number of times', async () => {
+ await mockAnimationTimer();
+ const updatesContainerActive = await recordAnimationUpdates();
+
+ await render();
+ await wait(1400);
+ const updates = updatesContainerActive.getUpdates();
+ const nativeUpdates = await updatesContainerActive.getNativeSnapshots();
+
+ expect(updates).toMatchSnapshots(Snapshots.CallbackCascade);
+ expect(updates).toMatchNativeSnapshots(nativeUpdates);
+
+ // TODO Fix tests to support boolean values
+ expect((await getRegisteredValue(SV.callbackArgument0)).onJS).toBe(0);
+ expect((await getRegisteredValue(SV.callbackArgument1)).onJS).toBe(0);
+
+ (
+ [
+ [Tracker.animationNotExecuted, 2],
+ [Tracker.interruptedAnimation, 1],
+ [Tracker.callbackAnimation, 3],
+ ] as const
+ ).forEach(([trackerRef, counts]) => {
+ expect(getTrackerCallCount(trackerRef)).toBeCalled(counts);
+ expect(getTrackerCallCount(trackerRef)).toBeCalledUI(counts);
+ expect(getTrackerCallCount(trackerRef)).toBeCalledJS(0);
+ });
+ });
+});
+
+describe(`Test all callbacks have been called in valid order`, () => {
+ const SV_REF = 'SV_REF';
+
+ const CallbackComponent = () => {
+ const callbackArray = useSharedValue>([]);
+ registerValue(SV_REF, callbackArray);
+
+ const sv0 = useSharedValue(0);
+ const sv1 = useSharedValue(0);
+ const sv2 = useSharedValue(0);
+
+ useEffect(() => {
+ sv0.value = withSequence(
+ // finishes at 100
+ withTiming(200, { duration: 100 }, () => {
+ callbackArray.value = [...callbackArray.value, 'ONE'];
+
+ sv1.value = withSequence(
+ // finishes at 200
+ withTiming(100, { duration: 100 }, () => {
+ callbackArray.value = [...callbackArray.value, 'TWO'];
+ }),
+
+ // cancelled at 600
+ withTiming(50, { duration: 600 }, () => {
+ callbackArray.value = [...callbackArray.value, 'SIX'];
+ }),
+ // cancelled at 600
+ withTiming(100, { duration: 600 }, () => {
+ callbackArray.value = [...callbackArray.value, 'SEVEN'];
+ }),
+ );
+ }),
+ // finishes at 300
+ withTiming(100, { duration: 200 }, () => {
+ callbackArray.value = [...callbackArray.value, 'THREE'];
+
+ // finishes at 450
+ sv2.value = withSequence(
+ withTiming(150, { duration: 150 }, () => {
+ callbackArray.value = [...callbackArray.value, 'FOUR'];
+ }),
+
+ // finishes at 600
+ withTiming(150, { duration: 100 }, () => {
+ callbackArray.value = [...callbackArray.value];
+
+ // cancels all sv1 animations at 600, finishes at 800
+ sv1.value = withTiming(200, { duration: 100 }, () => {
+ callbackArray.value = [...callbackArray.value, 'EIGHT'];
+ });
+ }),
+ );
+ }),
+ // finishes at 500
+ withTiming(200, { duration: 200 }, () => {
+ callbackArray.value = [...callbackArray.value, 'FIVE'];
+ }),
+ // finishes at 900
+ withTiming(200, { duration: 400 }, () => {
+ callbackArray.value = [...callbackArray.value, 'NINE'];
+ }),
+ );
+ });
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return { height: 20 + sv0.value, width: 20 + sv1.value, top: sv2.value };
+ });
+
+ return (
+
+
+
+ );
+ };
+
+ test('Test order of cascade of callback (no direct nesting nesting)', async () => {
+ await mockAnimationTimer();
+ const updatesContainerActive = await recordAnimationUpdates();
+ await render();
+ await wait(1400);
+ const updates = updatesContainerActive.getUpdates();
+ const nativeUpdates = await updatesContainerActive.getNativeSnapshots();
+ expect(updates).toMatchSnapshots(Snapshots.CallbackOrder);
+ expect(updates).toMatchNativeSnapshots(nativeUpdates);
+
+ expect((await getRegisteredValue(SV_REF)).onJS).toBe([
+ 'ONE',
+ 'TWO',
+ 'THREE',
+ 'FOUR',
+ 'FIVE',
+ 'SIX',
+ 'SEVEN',
+ 'EIGHT',
+ 'NINE',
+ ]);
+ });
+});
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ animatedBox: {
+ width: 0,
+ backgroundColor: 'darkorange',
+ height: 80,
+ marginLeft: 30,
+ },
+});
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/cancelAnimation.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/cancelAnimation.test.tsx
new file mode 100644
index 00000000000..90382cd32f3
--- /dev/null
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/cancelAnimation.test.tsx
@@ -0,0 +1,79 @@
+import React, { useEffect } from 'react';
+import { View, StyleSheet } from 'react-native';
+import Animated, { useSharedValue, withSequence, cancelAnimation, withTiming } from 'react-native-reanimated';
+import {
+ describe,
+ test,
+ expect,
+ render,
+ wait,
+ getTestComponent,
+ useTestRef,
+} from '../../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';
+import { ComparisonMode } from '../../../ReanimatedRuntimeTestsRunner/types';
+
+describe(`Test cancelling animation `, () => {
+ const COMPONENT_REF = 'COMPONENT_REF';
+ const CancelComponent = ({
+ shouldCancelAnimation,
+ shouldStartNewAnimation,
+ }: {
+ shouldCancelAnimation?: boolean;
+ shouldStartNewAnimation?: boolean;
+ }) => {
+ const width = useSharedValue(0);
+ const ref = useTestRef(COMPONENT_REF);
+ useEffect(() => {
+ width.value = withSequence(
+ withTiming(100, { duration: 130 }),
+ withTiming(300, { duration: 130 }),
+ withTiming(50, { duration: 130 }),
+ );
+ });
+ useEffect(() => {
+ setTimeout(() => {
+ if (shouldCancelAnimation) {
+ cancelAnimation(width);
+ } else if (shouldStartNewAnimation) {
+ width.value = 0;
+ }
+ }, 200);
+ });
+ return (
+
+
+
+ );
+ };
+
+ test('Test animation running without interruption', async () => {
+ await render();
+ await wait(500);
+ const component = getTestComponent(COMPONENT_REF);
+ expect(await component.getAnimatedStyle('width')).toBe(50, ComparisonMode.DISTANCE);
+ });
+ test('Cancelling animation with *****cancelAnimation***** finishes the whole sequence', async () => {
+ await render();
+ await wait(500);
+ const component = getTestComponent(COMPONENT_REF);
+ expect(await component.getAnimatedStyle('width')).not.toBe(50, ComparisonMode.DISTANCE);
+ });
+ test('Cancelling animation by *****starting new animation***** finishes the whole sequence', async () => {
+ await render();
+ await wait(500);
+ const component = getTestComponent(COMPONENT_REF);
+ expect(await component.getAnimatedStyle('width')).not.toBe(50, ComparisonMode.DISTANCE);
+ });
+});
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ animatedBox: {
+ backgroundColor: 'darkorange',
+ height: 80,
+ margin: 30,
+ },
+});
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/colors.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/colors.test.tsx
new file mode 100644
index 00000000000..ce93bfc2672
--- /dev/null
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/colors.test.tsx
@@ -0,0 +1,198 @@
+import React, { useEffect } from 'react';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+ withSequence,
+ withDelay,
+} from 'react-native-reanimated';
+import {
+ describe,
+ test,
+ render,
+ wait,
+ useTestRef,
+ getTestComponent,
+ expect,
+} from '../../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';
+import { View, StyleSheet } from 'react-native';
+import { ComparisonMode } from '../../../ReanimatedRuntimeTestsRunner/types';
+
+type TestCase = {
+ startColor: string;
+ middleColor: string;
+ finalColor: string;
+};
+
+describe('withSequence animation of number', () => {
+ enum Component {
+ ACTIVE = 'ACTIVE',
+ PASSIVE = 'PASSIVE',
+ }
+ const DELAY = 75;
+ const WidthComponent = ({ startColor, middleColor, finalColor }: TestCase) => {
+ const colorActiveSV = useSharedValue(startColor);
+ const colorPassiveSV = useSharedValue(startColor);
+
+ const refActive = useTestRef(Component.ACTIVE);
+ const refPassive = useTestRef(Component.PASSIVE);
+
+ const styleActive = useAnimatedStyle(() => {
+ return {
+ backgroundColor: withSequence(
+ withDelay(DELAY, withTiming(colorActiveSV.value, { duration: 200 })),
+ withDelay(DELAY, withTiming(middleColor, { duration: 300 })),
+ withDelay(DELAY, withTiming(colorActiveSV.value, { duration: 200 })),
+ ),
+ };
+ });
+ const stylePassive = useAnimatedStyle(() => {
+ return {
+ backgroundColor: colorPassiveSV.value,
+ };
+ });
+
+ useEffect(() => {
+ colorActiveSV.value = finalColor;
+ }, [colorActiveSV, finalColor]);
+
+ useEffect(() => {
+ colorPassiveSV.value = withSequence(
+ withDelay(DELAY, withTiming(finalColor, { duration: 200 })),
+ withDelay(DELAY, withTiming(middleColor, { duration: 300 })),
+ withDelay(DELAY, withTiming(finalColor, { duration: 200 })),
+ );
+ }, [colorPassiveSV, finalColor, middleColor]);
+
+ return (
+
+
+
+
+ );
+ };
+
+ test.each([
+ {
+ startColor: 'gold',
+ startColorHex: '#ffd700ff',
+ middleColor: 'forestgreen',
+ middleColorHex: '#228b22ff',
+ finalColor: 'darkblue',
+ finalColorHex: '#00008bff',
+ },
+ {
+ startColor: '#ffd700ab',
+ startColorHex: '#ffd700ab',
+ middleColor: 'forestgreen',
+ middleColorHex: '#228b22ff',
+ finalColor: 'darkblue',
+ finalColorHex: '#00008bff',
+ },
+ {
+ startColor: '#ffd700ab',
+ startColorHex: '#ffd700ab',
+ middleColor: 'forestgreen',
+ middleColorHex: '#228b22',
+ finalColor: '#88bbcc44',
+ finalColorHex: '#88bbcc44',
+ },
+ {
+ startColor: 'gold',
+ startColorHex: '#ffd700ff',
+ middleColor: 'hsl(180, 50%, 50%)',
+ middleColorHex: '#40bfbfff',
+ finalColor: 'hsl(120,100%,50%)',
+ finalColorHex: '#00ff00ff',
+ },
+ {
+ startColor: 'gold',
+ startColorHex: '#ffd700ff',
+ middleColor: 'hsl(70, 100%, 75%)',
+ middleColorHex: '#eaff80ff',
+ finalColor: 'hsl(120,100%,50%)',
+ finalColorHex: '#00ff00ff',
+ },
+ {
+ startColor: 'hwb(70, 50%, 0%)',
+ startColorHex: '#eaff80ff',
+ middleColor: 'hsl(180, 50%, 50%)',
+ middleColorHex: '#40bfbfff',
+ finalColor: 'hsl(120,100%,50%)',
+ finalColorHex: '#00ff00ff',
+ },
+ {
+ startColor: 'hwb(70, 50%, 0%)',
+ startColorHex: '#eaff80ff',
+ middleColor: 'hsl(180, 50%, 50%)',
+ middleColorHex: '#40bfbfff',
+ finalColor: 'hsl(120,100%,50%)',
+ finalColorHex: '#00ff00ff',
+ },
+ {
+ startColor: 'hwb(70, 50%, 0%)',
+ startColorHex: '#eaff80ff',
+ middleColor: 'hsl(180, 50%, 50%)',
+ middleColorHex: '#40bfbfff',
+ finalColor: 'rgb(101,255,50)',
+ finalColorHex: '#65ff32ff',
+ },
+ {
+ startColor: 'hwb(70, 50%, 0%)',
+ startColorHex: '#eaff80ff',
+ middleColor: 'hsl(180, 50%, 50%)',
+ middleColorHex: '#40bfbfff',
+ finalColor: 'hsla( 120 , 100% , 50%, 0.5 )',
+ finalColorHex: '#00ff0080',
+ },
+ {
+ startColor: 'hwb(70, 50%, 0%)',
+ startColorHex: '#eaff80ff',
+ middleColor: 'hsl(180, 50%, 50%)',
+ middleColorHex: '#40bfbfff',
+ finalColor: 'rgb(101,255,50)',
+ finalColorHex: '#65ff32ff',
+ },
+ {
+ startColor: 'hwb(70, 50%, 0%)',
+ startColorHex: '#eaff80ff',
+ middleColor: 'hsl(180, 50%, 50%)',
+ middleColorHex: '#40bfbfff',
+ finalColor: 'rgba(100,255,50,0.5)',
+ finalColorHex: '#64ff3280',
+ },
+ ])(
+ 'Animate ${startColor} → ${finalColor} → ${middleColor} → ${finalColor}',
+ async ({ startColor, startColorHex, middleColor, middleColorHex, finalColor, finalColorHex }) => {
+ await render();
+ const activeComponent = getTestComponent(Component.ACTIVE);
+ const passiveComponent = getTestComponent(Component.PASSIVE);
+
+ await wait(DELAY / 2);
+ // TODO Decide what should be the starting value of activeComponent
+ expect(await activeComponent.getAnimatedStyle('backgroundColor')).not.toBe(startColorHex, ComparisonMode.COLOR);
+ expect(await passiveComponent.getAnimatedStyle('backgroundColor')).toBe(startColorHex, ComparisonMode.COLOR);
+ await wait(200 + DELAY);
+ expect(await activeComponent.getAnimatedStyle('backgroundColor')).toBe(finalColorHex, ComparisonMode.COLOR);
+ expect(await passiveComponent.getAnimatedStyle('backgroundColor')).toBe(finalColorHex, ComparisonMode.COLOR);
+ await wait(300 + DELAY);
+ expect(await activeComponent.getAnimatedStyle('backgroundColor')).toBe(middleColorHex, ComparisonMode.COLOR);
+ expect(await passiveComponent.getAnimatedStyle('backgroundColor')).toBe(middleColorHex, ComparisonMode.COLOR);
+ await wait(200 + DELAY);
+ expect(await activeComponent.getAnimatedStyle('backgroundColor')).toBe(finalColorHex, ComparisonMode.COLOR);
+ expect(await passiveComponent.getAnimatedStyle('backgroundColor')).toBe(finalColorHex, ComparisonMode.COLOR);
+ },
+ );
+});
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ animatedBox: {
+ width: 80,
+ height: 80,
+ borderRadius: 10,
+ margin: 30,
+ },
+});
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/numbers.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/numbers.test.tsx
new file mode 100644
index 00000000000..cbcf8d0533a
--- /dev/null
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/numbers.test.tsx
@@ -0,0 +1,158 @@
+import React, { useCallback, useEffect } from 'react';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+ withSequence,
+ withSpring,
+ Easing,
+ withDelay,
+} from 'react-native-reanimated';
+import {
+ describe,
+ test,
+ render,
+ wait,
+ useTestRef,
+ getTestComponent,
+ expect,
+} from '../../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';
+import { View, StyleSheet } from 'react-native';
+import { ComparisonMode } from '../../../ReanimatedRuntimeTestsRunner/types';
+
+type TestCase = {
+ startValue: number;
+ middleValue: number;
+ finalValue: number;
+ animationNumber: number;
+};
+
+describe('WithSequence animation of number', () => {
+ enum Component {
+ ACTIVE = 'ONE',
+ PASSIVE = 'TWO',
+ }
+ const DELAY = 50;
+ const WidthComponent = ({ startValue, middleValue, finalValue, animationNumber }: TestCase) => {
+ const leftActiveSV = useSharedValue(startValue);
+ const leftPassiveSV = useSharedValue(startValue);
+
+ const refOne = useTestRef(Component.ACTIVE);
+ const refTwo = useTestRef(Component.PASSIVE);
+
+ const animateValueFn = useCallback(
+ function animateValue(finalValue: number) {
+ 'worklet';
+ switch (animationNumber) {
+ case 0:
+ return withDelay(
+ DELAY,
+ withSequence(
+ withTiming(finalValue, { duration: 200 }),
+ withDelay(DELAY, withTiming(middleValue, { duration: 300, easing: Easing.exp })),
+ withDelay(DELAY, withTiming(finalValue + 20, { duration: 200 })),
+ ),
+ );
+ case 1:
+ return withSequence(
+ withDelay(DELAY, withSpring(finalValue, { duration: 200, dampingRatio: 1 })),
+ withDelay(DELAY, withSpring(middleValue, { duration: 300, dampingRatio: 1.5 })),
+ withDelay(DELAY, withSpring(finalValue + 20, { duration: 200, dampingRatio: 0.9 })),
+ );
+ case 2:
+ return withDelay(
+ DELAY,
+ withSequence(
+ withSpring(finalValue, { duration: 200, dampingRatio: 1 }),
+ withDelay(DELAY, withTiming(middleValue, { duration: 300 })),
+ withDelay(DELAY, withSpring(finalValue + 20, { duration: 200, dampingRatio: 1 })),
+ ),
+ );
+ }
+ return 0;
+ },
+ [animationNumber, middleValue],
+ );
+
+ const styleActive = useAnimatedStyle(() => {
+ return {
+ left: animateValueFn(leftActiveSV.value),
+ };
+ });
+ const stylePassive = useAnimatedStyle(() => {
+ return {
+ left: leftPassiveSV.value,
+ };
+ });
+
+ useEffect(() => {
+ leftActiveSV.value = finalValue;
+ }, [leftActiveSV, finalValue]);
+
+ useEffect(() => {
+ leftPassiveSV.value = animateValueFn(finalValue);
+ }, [leftPassiveSV, finalValue, animateValueFn]);
+
+ return (
+
+
+
+
+ );
+ };
+
+ test.each([
+ [0, -10, 100, 0],
+ [0, -10, 100, 1],
+ [0, -10, 100, 2],
+ [100, 50, 0, 0],
+ [100, 50, 0, 0],
+ [0, 100, 100, 2],
+ [100, 100, 0, 1],
+ [75, 0, 75, 1],
+ [0, 75, 0, 2],
+ ])(
+ 'Animate ${0} → ${2} → ${1} → (${2} + 20), animation nr ${3}',
+ async ([startValue, middleValue, finalValue, animationNumber]) => {
+ await render(
+ ,
+ );
+ const activeComponent = getTestComponent(Component.ACTIVE);
+ const passiveComponent = getTestComponent(Component.PASSIVE);
+
+ const margin = 30;
+ const stopValues = [startValue, finalValue, middleValue, finalValue + 20].map(value => value + margin);
+
+ await wait(DELAY / 2);
+ // TODO The condition below is not fulfilled, decide whether its bug or expected behavior
+ // expect(await activeComponent.getAnimatedStyle('left')).toBe(stopValues[0], ComparisonMode.DISTANCE);
+ expect(await passiveComponent.getAnimatedStyle('left')).toBe(stopValues[0], ComparisonMode.DISTANCE);
+ await wait(200 + DELAY);
+ expect(await activeComponent.getAnimatedStyle('left')).toBe(stopValues[1], ComparisonMode.DISTANCE);
+ expect(await passiveComponent.getAnimatedStyle('left')).toBe(stopValues[1], ComparisonMode.DISTANCE);
+ await wait(300 + DELAY);
+ expect(await activeComponent.getAnimatedStyle('left')).toBe(stopValues[2], ComparisonMode.DISTANCE);
+ expect(await passiveComponent.getAnimatedStyle('left')).toBe(stopValues[2], ComparisonMode.DISTANCE);
+ await wait(200 + DELAY);
+ expect(await activeComponent.getAnimatedStyle('left')).toBe(stopValues[3], ComparisonMode.DISTANCE);
+ expect(await passiveComponent.getAnimatedStyle('left')).toBe(stopValues[3], ComparisonMode.DISTANCE);
+ },
+ );
+});
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ animatedBox: {
+ width: 80,
+ height: 80,
+ borderRadius: 10,
+ margin: 30,
+ },
+});
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/snapshots.snapshot.ts b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/snapshots.snapshot.ts
new file mode 100644
index 00000000000..a32a831d600
--- /dev/null
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withSequence/snapshots.snapshot.ts
@@ -0,0 +1,4 @@
+export const Snapshots = {
+ CallbackCascade: [{"height":20.32,"top":0,"width":20},{"height":21.28,"top":0,"width":20},{"height":22.88,"top":0,"width":20},{"height":25.12,"top":0,"width":20},{"height":28,"top":0,"width":20},{"height":31.52,"top":0,"width":20},{"height":35.68,"top":0,"width":20},{"height":40.480000000000004,"top":0,"width":20},{"height":45.92,"top":0,"width":20},{"height":52.00000000000001,"top":0,"width":20},{"height":58.72,"top":0,"width":20},{"height":66.08,"top":0,"width":20},{"height":73.92,"top":0,"width":20},{"height":81.28,"top":0,"width":20},{"height":88,"top":0,"width":20},{"height":94.08,"top":0,"width":20},{"height":99.52000000000001,"top":0,"width":20},{"height":104.32,"top":0,"width":20},{"height":108.48,"top":0,"width":20},{"height":112,"top":0,"width":20},{"height":114.88,"top":0,"width":20},{"height":117.11999999999999,"top":0,"width":20},{"height":118.72,"top":0,"width":20},{"height":119.68,"top":0,"width":20},{"height":120,"top":0,"width":20},{"height":119.54488888888889,"top":0,"width":20.028444444444446},{"height":118.17955555555555,"top":0,"width":20.113777777777777},{"height":115.904,"top":0,"width":20.256},{"height":112.71822222222222,"top":0,"width":20.455111111111112},{"height":108.62222222222222,"top":0,"width":20.711111111111112},{"height":103.616,"top":0,"width":21.024},{"height":97.69955555555555,"top":0,"width":21.39377777777778},{"height":90.87288888888888,"top":0,"width":21.820444444444444},{"height":83.136,"top":0,"width":22.304},{"height":74.84444444444445,"top":0,"width":22.844444444444445},{"height":67.33511111111112,"top":0,"width":23.44177777777778},{"height":60.736,"top":0,"width":24.096},{"height":55.04711111111111,"top":0,"width":24.807111111111112},{"height":50.26844444444444,"top":0,"width":25.575111111111113},{"height":46.39999999999999,"top":0,"width":26.400000000000002},{"height":43.44177777777777,"top":0,"width":27.28177777777778},{"height":41.393777777777785,"top":0,"width":28.220444444444443},{"height":40.256,"top":0,"width":29.216},{"height":40,"top":0,"width":30.26488888888889},{"height":40.576,"top":8.11355102947627,"width":65.78968999558386},{"height":42.304,"top":25.637413972281905,"width":127.66265235959303},{"height":45.184,"top":45.94949907449062,"width":173.39386881650358},{"height":49.216,"top":65.63156033870584,"width":191.7333034401837},{"height":54.400000000000006,"top":83.1184254598458,"width":190.0070325072947},{"height":60.736000000000004,"top":97.87705282784722,"width":180.33957821769178},{"height":68.224,"top":109.91665760527567,"width":171.53403630546734},{"height":76.864,"top":119.50348550522719,"width":167.02769751854723},{"height":86.656,"top":127.00039429751021,"width":166.3963583104561},{"height":97.60000000000001,"top":132.7812481647999,"width":167.72860081921837},{"height":109.696,"top":137.18914038581056,"width":169.31940520394326},{"height":122.944,"top":140.51948592000025,"width":170.31335735425753},{"height":137.05599999999998,"top":143.01657138088473,"width":170.60516092612443},{"height":150.304,"top":144.87683812149274,"width":170.46208642898188},{"height":162.39999999999998,"top":146.2550516590432,"width":170.19400330604017},{"height":173.344,"top":147.27124979015753,"width":169.9911502240215},{"height":183.13600000000002,"top":148.01739192451524,"width":169.90703504305213},{"height":191.77599999999998,"top":148.56322592499936,"width":169.91195887531606},{"height":199.264,"top":150,"width":169.95334038302818},{"height":205.6,"top":142.88008590592372,"width":169.99210275310344},{"height":210.784,"top":126.8795893968295,"width":170.01244994003898},{"height":214.816,"top":108.13990792374766,"width":170.0157332766819},{"height":217.696,"top":89.86315950081254,"width":170.01014554042317},{"height":219.424,"top":73.53975858870585,"width":170.00319752429016},{"height":220,"top":59.69876710887795,"width":169.9987560561899},{"height":220,"top":48.35849832702202,"width":169.9973824676853},{"height":220,"top":39.29080883214692,"width":169.99795293962688},{"height":220,"top":32.17108147552973,"width":169.99911305581472},{"height":220,"top":26.65922856419261,"width":170.00001202276414},{"height":220,"top":22.439969656980633,"width":170.0003969114859},{"height":220,"top":19.239780156329363,"width":170.00038707239574},{"height":220,"top":16.831056166389086,"width":170},{"height":220,"top":15.02975953569973,"width":170},{"height":220,"top":13.69016208374916,"width":170},{"height":220,"top":12.698697852752636,"width":170},{"height":220,"top":11.967970412620131,"width":170},{"height":220,"top":11.431402205594173,"width":170},{"height":220,"top":10,"width":170}],
+ CallbackOrder: [{"height": 30.240000000000002, "top": 0, "width": 20}, {"height": 60.96, "top": 0, "width": 20}, {"height": 112.16, "top": 0, "width": 20}, {"height": 168.16, "top": 0, "width": 20}, {"height": 204, "top": 0, "width": 20}, {"height": 219.36, "top": 0, "width": 20}, {"height": 220, "top": 0, "width": 20}, {"height": 218.72, "top": 0, "width": 25.12}, {"height": 214.88, "top": 0, "width": 40.480000000000004}, {"height": 208.48, "top": 0, "width": 66.08}, {"height": 199.52, "top": 0, "width": 94.08}, {"height": 188, "top": 0, "width": 112}, {"height": 173.92000000000002, "top": 0, "width": 119.68}, {"height": 158.72, "top": 0, "width": 120}, {"height": 145.92000000000002, "top": 0, "width": 119.92888888888889}, {"height": 135.68, "top": 0, "width": 119.71555555555555}, {"height": 128, "top": 0, "width": 119.36}, {"height": 122.88000000000001, "top": 0, "width": 118.86222222222221}, {"height": 120.32, "top": 0, "width": 118.22222222222223}, {"height": 120, "top": 0, "width": 117.44}, {"height": 121.28, "top": 3.413333333333334, "width": 116.51555555555555}, {"height": 125.12, "top": 13.653333333333336, "width": 115.44888888888889}, {"height": 131.51999999999998, "top": 30.720000000000002, "width": 114.24}, {"height": 140.48000000000002, "top": 54.613333333333344, "width": 112.88888888888889}, {"height": 152, "top": 84.66666666666666, "width": 111.39555555555556}, {"height": 166.07999999999998, "top": 111.12, "width": 109.76}, {"height": 181.28, "top": 130.74666666666667, "width": 107.98222222222222}, {"height": 194.07999999999998, "top": 143.54666666666668, "width": 106.06222222222222}, {"height": 204.32, "top": 149.52, "width": 104}, {"height": 212, "top": 150, "width": 101.79555555555555}, {"height": 217.12, "top": 150, "width": 99.44888888888889}, {"height": 219.68, "top": 150, "width": 96.96000000000001}, {"height": 220, "top": 150, "width": 94.33777777777777}, {"height": 220, "top": 150, "width": 91.77777777777777}, {"height": 220, "top": 150, "width": 89.36}, {"height": 220, "top": 150, "width": 87.08444444444444}, {"height": 220, "top": 150, "width": 84.95111111111112}, {"height": 220, "top": 150, "width": 91.86561422222223}, {"height": 220, "top": 150, "width": 112.60912355555556}, {"height": 220, "top": 150, "width": 147.1816391111111}, {"height": 220, "top": 150, "width": 184.995328}, {"height": 220, "top": 150, "width": 209.19608888888888}, {"height": 220, "top": 150, "width": 219.56784355555556}, {"height": 220, "top": 150, "width": 220}],
+ }
\ No newline at end of file
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/basic.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/basic.test.tsx
index eb7d9d51839..d5754097b5d 100644
--- a/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/basic.test.tsx
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/basic.test.tsx
@@ -41,7 +41,7 @@ describe('withTiming animation of WIDTH', () => {
});
const stylePassive = useAnimatedStyle(() => {
return {
- width: withTiming(widthActiveSV.value, { duration: 500 }),
+ width: widthPassiveSV.value,
};
});
diff --git a/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/easing.test.tsx b/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/easing.test.tsx
index 128c459cda7..864ebe9f5e8 100644
--- a/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/easing.test.tsx
+++ b/apps/common-app/src/examples/RuntimeTests/tests/animations/withTiming/easing.test.tsx
@@ -155,26 +155,19 @@ describe('withTiming snapshots 📸, test EASING', () => {
}
});
- test.each([
- Easing.bounce,
- Easing.circle,
- Easing.cubic,
- Easing.ease,
- Easing.exp,
- Easing.linear,
- Easing.quad,
- Easing.sin,
- ])('Easing.%p', async easing => {
- const [activeUpdates, activeNativeUpdates, passiveUpdates] = await getSnapshotUpdates(easing);
- expect(activeUpdates).toMatchSnapshots(EasingSnapshots[easing.name as keyof typeof EasingSnapshots]);
- expect(passiveUpdates).toMatchSnapshots(EasingSnapshots[easing.name as keyof typeof EasingSnapshots]);
- expect(activeUpdates).toMatchNativeSnapshots(activeNativeUpdates, true);
- });
-
+ test.each([Easing.bounce, Easing.circle, Easing.cubic, Easing.ease, Easing.linear, Easing.quad, Easing.sin])(
+ 'Easing.%p',
+ async easing => {
+ const [activeUpdates, activeNativeUpdates, passiveUpdates] = await getSnapshotUpdates(easing);
+ expect(activeUpdates).toMatchSnapshots(EasingSnapshots[easing.name as keyof typeof EasingSnapshots]);
+ expect(passiveUpdates).toMatchSnapshots(EasingSnapshots[easing.name as keyof typeof EasingSnapshots]);
+ expect(activeUpdates).toMatchNativeSnapshots(activeNativeUpdates, true);
+ },
+ );
test('Easing.exp', async () => {
const [activeUpdates, activeNativeUpdates, passiveUpdates] = await getSnapshotUpdates(Easing.exp);
-
expect(activeUpdates).toMatchSnapshots(EasingSnapshots.exp);
+ // TODO Investigate why easing.exp works different than other easings
expect(passiveUpdates).toMatchSnapshots([{ width: 0 }, ...EasingSnapshots.exp]);
expect(activeUpdates).toMatchNativeSnapshots(activeNativeUpdates, true);
});
diff --git a/apps/common-app/src/examples/SharedElementTransitions/TabNavigatorExample.tsx b/apps/common-app/src/examples/SharedElementTransitions/TabNavigatorExample.tsx
new file mode 100644
index 00000000000..e7df372be4c
--- /dev/null
+++ b/apps/common-app/src/examples/SharedElementTransitions/TabNavigatorExample.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import { StyleSheet, View, Button, Text } from 'react-native';
+import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
+import type { NativeStackScreenProps } from '@react-navigation/native-stack';
+import { createNativeStackNavigator } from '@react-navigation/native-stack';
+import Animated from 'react-native-reanimated';
+
+function getStyle(index: number) {
+ switch (index) {
+ case 0:
+ return styles.box1;
+ case 1:
+ return styles.box2;
+ default:
+ return styles.box3;
+ }
+}
+
+type ScreenProps = {
+ [key: string]: {
+ id?: number;
+ showButtons?: boolean;
+ };
+};
+function Screen({ navigation, route }: NativeStackScreenProps) {
+ const id = route.params?.id ?? 0;
+ const showButtons = !!route.params?.showButtons;
+ return (
+
+ Current id: {id}
+ {showButtons && id < 2 && (
+
+ );
+}
+
+function createStack() {
+ const Stack = createNativeStackNavigator();
+ return () => (
+
+
+
+ );
+}
+
+const Tab = createBottomTabNavigator();
+const StackA = createStack();
+const StackB = createStack();
+
+function TabNavigatorExample() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ paddingTop: 10,
+ },
+ box1: {
+ width: 100,
+ height: 100,
+ backgroundColor: 'green',
+ },
+ box2: {
+ width: 200,
+ height: 100,
+ backgroundColor: 'blue',
+ },
+ box3: {
+ width: 150,
+ height: 250,
+ backgroundColor: 'red',
+ marginLeft: 50,
+ },
+});
+
+export default TabNavigatorExample;
diff --git a/apps/common-app/src/examples/index.ts b/apps/common-app/src/examples/index.ts
index 8a271f99436..10f7e41c72b 100644
--- a/apps/common-app/src/examples/index.ts
+++ b/apps/common-app/src/examples/index.ts
@@ -130,6 +130,7 @@ import ComposedHandlerDifferentEventsExample from './ComposedHandlerDifferentEve
import ComposedHandlerInternalMergingExample from './ComposedHandlerInternalMergingExample';
import BorderRadiiExample from './SharedElementTransitions/BorderRadii';
import FreezingShareablesExample from './ShareableFreezingExample';
+import TabNavigatorExample from './SharedElementTransitions/TabNavigatorExample';
interface Example {
icon?: string;
@@ -771,13 +772,21 @@ export const EXAMPLES: Record = {
ChangeThemeSharedExample: {
title: '[SET] Change theme',
screen: ChangeThemeSharedExample,
+ missingOnFabric: true,
},
NestedRotationSharedExample: {
title: '[SET] Nested Transforms',
screen: NestedRotationExample,
+ missingOnFabric: true,
},
BorderRadiiExample: {
title: '[SET] Border Radii',
screen: BorderRadiiExample,
+ missingOnFabric: true,
+ },
+ TabNavigatorExample: {
+ title: '[SET] Tab Navigator',
+ screen: TabNavigatorExample,
+ missingOnFabric: true,
},
} as const;
diff --git a/packages/docs-reanimated/docs/core/useSharedValue.mdx b/packages/docs-reanimated/docs/core/useSharedValue.mdx
index 70d2b7ec82d..f3c5e3969f0 100644
--- a/packages/docs-reanimated/docs/core/useSharedValue.mdx
+++ b/packages/docs-reanimated/docs/core/useSharedValue.mdx
@@ -67,6 +67,8 @@ import SharedValueSrc from '!!raw-loader!@site/src/examples/SharedValue';
- When you change the `sv.value` Reanimated will update the styles and keep the shared value in sync between the threads. However, this won't trigger a typical React re-render because a shared value is a plain JavaScript object.
+- When you read the `sv.value` on the [JavaScript thread](/docs/fundamentals/glossary#javascript-thread), the thread will get blocked until the value is fetched from the [UI thread](/docs/fundamentals/glossary#ui-thread). In most cases it will be negligible, but if the UI thread is busy or you are reading a value multiple times, the wait time needed to synchronize both threads may significantly increase.
+
- When you change the `sv.value` the update will happen synchronously on the [UI thread](/docs/fundamentals/glossary#ui-thread). On the other hand, on the [JavaScript thread](/docs/fundamentals/glossary#javascript-thread) the update is asynchronous. This means when you try to immediately log the `value` after the change it will log the previously stored value.
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp
index 1f3598284bd..b81720ca47d 100644
--- a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.cpp
@@ -165,6 +165,12 @@ int LayoutAnimationsManager::findPrecedingViewTagForTransition(const int tag) {
return -1;
}
+const std::vector &LayoutAnimationsManager::getSharedGroup(
+ const int viewTag) {
+ const auto &groupSharedTag = viewTagToSharedTag_[viewTag];
+ return sharedTransitionGroups_[groupSharedTag];
+}
+
#ifdef RCT_NEW_ARCH_ENABLED
void LayoutAnimationsManager::transferConfigFromNativeID(
const int nativeId,
diff --git a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h
index d382cbbe08b..c853af74761 100644
--- a/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h
+++ b/packages/react-native-reanimated/Common/cpp/LayoutAnimations/LayoutAnimationsManager.h
@@ -46,6 +46,7 @@ class LayoutAnimationsManager {
void transferConfigFromNativeID(const int nativeId, const int tag);
#endif
int findPrecedingViewTagForTransition(const int tag);
+ const std::vector &getSharedGroup(const int viewTag);
#ifndef NDEBUG
std::string getScreenSharedTagPairString(
const int screenTag,
diff --git a/packages/react-native-reanimated/Common/cpp/ReanimatedRuntime/WorkletRuntimeDecorator.cpp b/packages/react-native-reanimated/Common/cpp/ReanimatedRuntime/WorkletRuntimeDecorator.cpp
index ccf91720de4..f5c8e93b4f7 100644
--- a/packages/react-native-reanimated/Common/cpp/ReanimatedRuntime/WorkletRuntimeDecorator.cpp
+++ b/packages/react-native-reanimated/Common/cpp/ReanimatedRuntime/WorkletRuntimeDecorator.cpp
@@ -4,6 +4,8 @@
#include "Shareables.h"
#include "WorkletRuntime.h"
+#include
+
#ifdef ANDROID
#include "Logger.h"
#else
@@ -110,13 +112,11 @@ void WorkletRuntimeDecorator::decorate(
auto argsArray =
shareableArgs->toJSValue(rt).asObject(rt).asArray(rt);
auto argsSize = argsArray.size(rt);
- // number of arguments is typically relatively small so it is ok to
- // to use VLAs here, hence disabling the lint rule
- jsi::Value args[argsSize]; // NOLINT(runtime/arrays)
+ std::vector args(argsSize);
for (size_t i = 0; i < argsSize; i++) {
args[i] = argsArray.getValueAtIndex(rt, i);
}
- remoteFun.asObject(rt).asFunction(rt).call(rt, args, argsSize);
+ remoteFun.asObject(rt).asFunction(rt).call(rt, const_cast(args.data()), args.size());
}
});
});
diff --git a/packages/react-native-reanimated/__tests__/InterpolateColor.test.tsx b/packages/react-native-reanimated/__tests__/InterpolateColor.test.tsx
index 01ef7a3c4fb..c44f49b916a 100644
--- a/packages/react-native-reanimated/__tests__/InterpolateColor.test.tsx
+++ b/packages/react-native-reanimated/__tests__/InterpolateColor.test.tsx
@@ -123,6 +123,14 @@ describe('colors interpolation', () => {
expect(interpolatedColor).toBe(`rgba(96, 144, 32, ${112 / 255})`);
});
+ it('handles tiny values', () => {
+ const colors = ['#00000000', '#ff802001'];
+
+ // We don't want output like "rgba(4, 2, 0, 3.921568627450981e-7)":
+ const interpolatedColor = interpolateColor(0.0001, [0, 1], colors);
+ expect(interpolatedColor).toBe(`rgba(4, 2, 0, 0)`);
+ });
+
function TestComponent() {
const color = useSharedValue('#105060');
diff --git a/packages/react-native-reanimated/android/CMakeLists.txt b/packages/react-native-reanimated/android/CMakeLists.txt
index 83c52a36450..e6fae24cbb6 100644
--- a/packages/react-native-reanimated/android/CMakeLists.txt
+++ b/packages/react-native-reanimated/android/CMakeLists.txt
@@ -15,7 +15,7 @@ add_compile_options(${folly_FLAGS})
string(APPEND CMAKE_CXX_FLAGS " -DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION} -DREANIMATED_VERSION=${REANIMATED_VERSION} -DHERMES_ENABLE_DEBUGGER=${HERMES_ENABLE_DEBUGGER}")
-string(APPEND CMAKE_CXX_FLAGS " -fexceptions -fno-omit-frame-pointer -frtti -fstack-protector-all -std=c++${CMAKE_CXX_STANDARD} -Wall -Werror")
+string(APPEND CMAKE_CXX_FLAGS " -fexceptions -fno-omit-frame-pointer -frtti -fstack-protector-all -std=c++${CMAKE_CXX_STANDARD} -Wall -Wpedantic -Werror")
if(${IS_NEW_ARCHITECTURE_ENABLED})
string(APPEND CMAKE_CXX_FLAGS " -DRCT_NEW_ARCH_ENABLED")
diff --git a/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.cpp b/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.cpp
index acd19f8a384..920fb2bd8b3 100644
--- a/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.cpp
+++ b/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.cpp
@@ -1,4 +1,5 @@
#include "LayoutAnimations.h"
+#include
#include "FeaturesConfig.h"
#include "Logger.h"
@@ -100,10 +101,22 @@ void LayoutAnimations::setFindPrecedingViewTagForTransition(
findPrecedingViewTagForTransitionBlock;
}
+void LayoutAnimations::setGetSharedGroupBlock(
+ GetSharedGroupBlock getSharedGroupBlock) {
+ getSharedGroupBlock_ = getSharedGroupBlock;
+}
+
int LayoutAnimations::findPrecedingViewTagForTransition(int tag) {
return findPrecedingViewTagForTransitionBlock_(tag);
}
+jni::local_ref LayoutAnimations::getSharedGroup(const int tag) {
+ const auto &group = getSharedGroupBlock_(tag);
+ auto jGroup = JArrayInt::newArray(group.size());
+ jGroup->setRegion(0, group.size(), group.data());
+ return jGroup;
+}
+
void LayoutAnimations::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", LayoutAnimations::initHybrid),
@@ -124,6 +137,7 @@ void LayoutAnimations::registerNatives() {
makeNativeMethod(
"findPrecedingViewTagForTransition",
LayoutAnimations::findPrecedingViewTagForTransition),
+ makeNativeMethod("getSharedGroup", LayoutAnimations::getSharedGroup),
#ifndef NDEBUG
makeNativeMethod(
"checkDuplicateSharedTag", LayoutAnimations::checkDuplicateSharedTag),
diff --git a/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.h b/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.h
index 053c8948ea2..31fabd4f25e 100644
--- a/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.h
+++ b/packages/react-native-reanimated/android/src/main/cpp/LayoutAnimations.h
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include "JNIHelper.h"
namespace reanimated {
@@ -22,6 +23,7 @@ class LayoutAnimations : public jni::HybridClass {
using ClearAnimationConfigBlock = std::function;
using CancelAnimationBlock = std::function;
using FindPrecedingViewTagForTransitionBlock = std::function;
+ using GetSharedGroupBlock = std::function(const int)>;
public:
static auto constexpr kJavaDescriptor =
@@ -53,6 +55,7 @@ class LayoutAnimations : public jni::HybridClass {
void setFindPrecedingViewTagForTransition(
FindPrecedingViewTagForTransitionBlock
findPrecedingViewTagForTransitionBlock);
+ void setGetSharedGroupBlock(const GetSharedGroupBlock getSharedGroupBlock);
void progressLayoutAnimation(
int tag,
@@ -62,6 +65,7 @@ class LayoutAnimations : public jni::HybridClass {
void clearAnimationConfigForTag(int tag);
void cancelAnimationForTag(int tag);
int findPrecedingViewTagForTransition(int tag);
+ jni::local_ref getSharedGroup(const int tag);
private:
friend HybridBase;
@@ -73,6 +77,7 @@ class LayoutAnimations : public jni::HybridClass {
CancelAnimationBlock cancelAnimationBlock_;
FindPrecedingViewTagForTransitionBlock
findPrecedingViewTagForTransitionBlock_;
+ GetSharedGroupBlock getSharedGroupBlock_;
#ifndef NDEBUG
CheckDuplicateSharedTag checkDuplicateSharedTag_;
#endif
diff --git a/packages/react-native-reanimated/android/src/main/cpp/NativeProxy.cpp b/packages/react-native-reanimated/android/src/main/cpp/NativeProxy.cpp
index fcd4a8ad60a..b29134aa4b9 100644
--- a/packages/react-native-reanimated/android/src/main/cpp/NativeProxy.cpp
+++ b/packages/react-native-reanimated/android/src/main/cpp/NativeProxy.cpp
@@ -652,6 +652,16 @@ void NativeProxy::setupLayoutAnimations() {
return -1;
}
});
+
+ layoutAnimations_->cthis()->setGetSharedGroupBlock(
+ [weakNativeReanimatedModule](int tag) -> std::vector {
+ if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
+ return nativeReanimatedModule->layoutAnimationsManager()
+ .getSharedGroup(tag);
+ } else {
+ return {};
+ }
+ });
}
} // namespace reanimated
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java
index 6af987f55f8..d0d0f5405e0 100644
--- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/AnimationsManager.java
@@ -494,7 +494,7 @@ public boolean shouldAnimateExiting(int tag, boolean shouldAnimate) {
}
public boolean hasAnimationForTag(int tag, int type) {
- return mNativeMethodsHolder.hasAnimation(tag, type);
+ return mNativeMethodsHolder != null && mNativeMethodsHolder.hasAnimation(tag, type);
}
public boolean isLayoutAnimationEnabled() {
@@ -680,7 +680,7 @@ private void cancelAnimationsInSubviews(ViewGroup view) {
}
}
- private View resolveView(int tag) {
+ protected View resolveView(int tag) {
if (mExitingViews.containsKey(tag)) {
return mExitingViews.get(tag);
} else {
@@ -718,6 +718,14 @@ public void screenDidLayout(View view) {
mSharedTransitionManager.screenDidLayout(view);
}
+ public void navigationTabChanged(View previousTab, View newTab) {
+ mSharedTransitionManager.navigationTabChanged(previousTab, newTab);
+ }
+
+ public void visitNativeTreeAndMakeSnapshot(View view) {
+ mSharedTransitionManager.visitNativeTreeAndMakeSnapshot(view);
+ }
+
public void viewDidLayout(View view) {
mSharedTransitionManager.viewDidLayout(view);
}
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java
index c9126fe4216..f31f97010ff 100644
--- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/LayoutAnimations.java
@@ -52,6 +52,8 @@ public LayoutAnimations(ReactApplicationContext context) {
public native int findPrecedingViewTagForTransition(int tag);
+ public native int[] getSharedGroup(int tag);
+
private void endLayoutAnimation(int tag, boolean removeView) {
AnimationsManager animationsManager = getAnimationsManager();
if (animationsManager == null) {
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java
index 803cb5295d6..5df03aba32a 100644
--- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/NativeMethodsHolder.java
@@ -18,4 +18,6 @@ public interface NativeMethodsHolder {
int findPrecedingViewTagForTransition(int tag);
void checkDuplicateSharedTag(int viewTag, int screenTag);
+
+ int[] getSharedGroup(int viewTag);
}
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java
index 98d1d61f807..ab0e79bdd79 100644
--- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/ReanimatedNativeHierarchyManager.java
@@ -214,12 +214,14 @@ public class ReanimatedNativeHierarchyManager extends NativeViewHierarchyManager
private final ReaLayoutAnimator mReaLayoutAnimator;
private final HashMap> mPendingDeletionsForTag = new HashMap<>();
private boolean initOk = true;
+ private final TabNavigatorObserver mTabNavigatorObserver;
public ReanimatedNativeHierarchyManager(
ViewManagerRegistry viewManagers, ReactApplicationContext reactContext) {
super(viewManagers);
mReaLayoutAnimator = new ReaLayoutAnimator(reactContext, this);
+ mTabNavigatorObserver = new TabNavigatorObserver(mReaLayoutAnimator);
Class> clazz = this.getClass().getSuperclass();
if (clazz == null) {
@@ -293,6 +295,14 @@ public synchronized void updateLayout(
if (!hasHeader || !container.isLayoutRequested()) {
mReaLayoutAnimator.getAnimationsManager().screenDidLayout(container);
}
+ View screen = resolveView(tag);
+ View screenFragmentManager = (View) screen.getParent();
+ if (screenFragmentManager != null) {
+ View screenHolder = (View) screenFragmentManager.getParent();
+ if (ScreensHelper.isScreenContainer(screenHolder)) {
+ mTabNavigatorObserver.handleScreenContainerUpdate(screen);
+ }
+ }
}
View view = resolveView(tag);
if (view != null && mReaLayoutAnimator != null) {
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/ScreensHelper.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/ScreensHelper.java
new file mode 100644
index 00000000000..63eb9075d3f
--- /dev/null
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/ScreensHelper.java
@@ -0,0 +1,83 @@
+package com.swmansion.reanimated.layoutReanimation;
+
+import android.util.Log;
+import android.view.View;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class ScreensHelper {
+
+ public static View getTabNavigator(View view) {
+ View currentView = view;
+ while (currentView != null) {
+ if (isScreenContainer(currentView)) {
+ return currentView;
+ }
+ if (isScreen(currentView) && isScreensCoordinatorLayout(currentView.getParent())) {
+ View screen = currentView;
+ Class> screenClass = screen.getClass();
+ try {
+ Method getContainer = screenClass.getMethod("getContainer");
+ currentView = (View) getContainer.invoke(screen);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ String message =
+ e.getMessage() != null ? e.getMessage() : "Unable to invoke the getContainer method";
+ Log.e("[Reanimated]", message);
+ break;
+ }
+ } else if (currentView.getParent() instanceof View) {
+ currentView = (View) currentView.getParent();
+ } else {
+ break;
+ }
+ }
+ return null;
+ }
+
+ public static boolean isViewChildOfScreen(View view, View screen) {
+ View currentView = view;
+ while (currentView != null) {
+ if (currentView == screen) {
+ return true;
+ }
+ if (!(currentView.getParent() instanceof View)) {
+ return false;
+ }
+ currentView = (View) currentView.getParent();
+ }
+ return false;
+ }
+
+ public static View getTopScreenForStack(View view) {
+ if (isScreenStack(view)) {
+ View stack = view;
+ Class> screenStackClass = stack.getClass();
+ try {
+ Method getTopScreen = screenStackClass.getMethod("getTopScreen");
+ return (View) getTopScreen.invoke(stack);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ignored) {
+ }
+ }
+ return view;
+ }
+
+ public static boolean isScreen(Object maybeView) {
+ return isInstanceOf(maybeView, "Screen");
+ }
+
+ public static boolean isScreenStack(Object maybeView) {
+ return isInstanceOf(maybeView, "ScreenStack");
+ }
+
+ public static boolean isScreenContainer(Object maybeView) {
+ return isInstanceOf(maybeView, "ScreenContainer");
+ }
+
+ public static boolean isScreensCoordinatorLayout(Object maybeView) {
+ return isInstanceOf(maybeView, "ScreensCoordinatorLayout");
+ }
+
+ private static boolean isInstanceOf(Object maybeView, String className) {
+ return maybeView != null && maybeView.getClass().getSimpleName().equals(className);
+ }
+}
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java
index 59703ce618f..cb658542aff 100644
--- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/SharedTransitionManager.java
@@ -193,6 +193,11 @@ protected boolean prepareSharedTransition(List sharedViews, boolean withNe
}
protected void onScreenWillDisappear() {
+ for (Integer tag : mTagsToCleanup) {
+ mNativeMethodsHolder.clearAnimationConfig(tag);
+ }
+ mTagsToCleanup.clear();
+
if (!mIsTransitionPrepared) {
return;
}
@@ -205,11 +210,6 @@ protected void onScreenWillDisappear() {
}
startPreparedTransitions();
-
- for (Integer tag : mTagsToCleanup) {
- mNativeMethodsHolder.clearAnimationConfig(tag);
- }
- mTagsToCleanup.clear();
}
private boolean tryStartSharedTransitionForViews(
@@ -265,17 +265,22 @@ private List getSharedElementsForCurrentTransition(
mNativeMethodsHolder.findPrecedingViewTagForTransition(sharedView.getId());
}
}
+
boolean bothAreRemoved = !addedNewScreen && viewTags.contains(targetViewTag);
if (targetViewTag < 0) {
continue;
}
+
+ View siblingView = reanimatedNativeHierarchyManager.resolveView(targetViewTag);
+ siblingView = maybeOverrideSiblingForTabNavigator(sharedView, siblingView);
+
View viewSource, viewTarget;
if (addedNewScreen) {
- viewSource = reanimatedNativeHierarchyManager.resolveView(targetViewTag);
+ viewSource = siblingView;
viewTarget = sharedView;
} else {
viewSource = sharedView;
- viewTarget = reanimatedNativeHierarchyManager.resolveView(targetViewTag);
+ viewTarget = siblingView;
}
if (bothAreRemoved) {
// case for nested stack
@@ -293,33 +298,43 @@ private List getSharedElementsForCurrentTransition(
continue;
}
- ViewGroup stack = (ViewGroup) findStack(viewSourceScreen);
- if (stack == null) {
+ ViewGroup sourceStack = (ViewGroup) findStack(viewSourceScreen);
+ if (sourceStack == null) {
continue;
}
-
- ViewGroupManager stackViewGroupManager =
- (ViewGroupManager) reanimatedNativeHierarchyManager.resolveViewManager(stack.getId());
- int screensCount = stackViewGroupManager.getChildCount(stack);
-
- if (screensCount < 2) {
- continue;
+ int stackId = sourceStack.getId();
+ ViewGroupManager stackViewManager =
+ (ViewGroupManager) reanimatedNativeHierarchyManager.resolveViewManager(stackId);
+ boolean isInSameStack = false;
+ for (int i = 0; i < stackViewManager.getChildCount(sourceStack); i++) {
+ if (stackViewManager.getChildAt(sourceStack, i) == viewTargetScreen) {
+ isInSameStack = true;
+ }
}
+ if (isInSameStack) {
+ ViewGroupManager stackViewGroupManager =
+ (ViewGroupManager)
+ reanimatedNativeHierarchyManager.resolveViewManager(sourceStack.getId());
+ int screensCount = stackViewGroupManager.getChildCount(sourceStack);
+ if (screensCount < 2) {
+ continue;
+ }
- View topScreen = stackViewGroupManager.getChildAt(stack, screensCount - 1);
- View secondScreen = stackViewGroupManager.getChildAt(stack, screensCount - 2);
- boolean isValidConfiguration;
- if (addedNewScreen) {
- isValidConfiguration =
- secondScreen.getId() == viewSourceScreen.getId()
- && topScreen.getId() == viewTargetScreen.getId();
- } else {
- isValidConfiguration =
- topScreen.getId() == viewSourceScreen.getId()
- && secondScreen.getId() == viewTargetScreen.getId();
- }
- if (!isValidConfiguration) {
- continue;
+ View topScreen = stackViewGroupManager.getChildAt(sourceStack, screensCount - 1);
+ View secondScreen = stackViewGroupManager.getChildAt(sourceStack, screensCount - 2);
+ boolean isValidConfiguration;
+ if (addedNewScreen) {
+ isValidConfiguration =
+ secondScreen.getId() == viewSourceScreen.getId()
+ && topScreen.getId() == viewTargetScreen.getId();
+ } else {
+ isValidConfiguration =
+ topScreen.getId() == viewSourceScreen.getId()
+ && secondScreen.getId() == viewTargetScreen.getId();
+ }
+ if (!isValidConfiguration) {
+ continue;
+ }
}
}
@@ -340,7 +355,7 @@ private List getSharedElementsForCurrentTransition(
}
Snapshot targetViewSnapshot = mSnapshotRegistry.get(viewTarget.getId());
if (targetViewSnapshot == null) {
- continue;
+ makeSnapshot(viewTarget);
}
newTransitionViews.add(viewSource);
@@ -379,6 +394,33 @@ private List getSharedElementsForCurrentTransition(
return sharedElements;
}
+ private View maybeOverrideSiblingForTabNavigator(View sharedView, View siblingView) {
+ View maybeTabNavigatorForSharedView = ScreensHelper.getTabNavigator(sharedView);
+
+ if (maybeTabNavigatorForSharedView == null) {
+ return siblingView;
+ }
+
+ int siblingTag = siblingView.getId();
+ int[] sharedGroup = mNativeMethodsHolder.getSharedGroup(sharedView.getId());
+ int siblingIndex = -1;
+ for (int i = 0; i < sharedGroup.length; i++) {
+ if (sharedGroup[i] == siblingTag) {
+ siblingIndex = i;
+ }
+ }
+
+ for (int i = siblingIndex; i >= 0; i--) {
+ int viewTag = sharedGroup[i];
+ View view = mAnimationsManager.resolveView(viewTag);
+ if (maybeTabNavigatorForSharedView == ScreensHelper.getTabNavigator(view)) {
+ return view;
+ }
+ }
+
+ return siblingView;
+ }
+
private void setupTransitionContainer() {
if (mTransitionContainer == null) {
ReactContext context = mAnimationsManager.getContext();
@@ -640,6 +682,7 @@ private void visitTree(View view, TreeVisitor treeVisitor) {
}
void visitNativeTreeAndMakeSnapshot(View view) {
+ view = ScreensHelper.getTopScreenForStack(view);
if (!(view instanceof ViewGroup)) {
return;
}
@@ -696,4 +739,60 @@ void orderByAnimationTypes(List sharedElements) {
}
}
}
+
+ public void navigationTabChanged(View previousTab, View newTab) {
+ mAddedSharedViews.clear();
+ List sharedElements = new ArrayList<>();
+ List sharedViews = new ArrayList<>();
+ findSharedViewsForScreen(previousTab, sharedViews);
+ sortViewsByTags(sharedViews);
+ for (View sharedView : sharedViews) {
+ int[] sharedGroup = mNativeMethodsHolder.getSharedGroup(sharedView.getId());
+ for (int i = sharedGroup.length - 1; i >= 0; i--) {
+ View targetView = mAnimationsManager.resolveView(sharedGroup[i]);
+ if (!ScreensHelper.isViewChildOfScreen(targetView, newTab)) {
+ continue;
+ }
+ Snapshot sourceViewSnapshot = mSnapshotRegistry.get(sharedView.getId());
+ if (sourceViewSnapshot == null) {
+ // This is just to ensure that we have a snapshot and to prevent
+ // a theoretically possible NullPointerException.
+ continue;
+ }
+ SharedElement sharedElement =
+ new SharedElement(sharedView, sourceViewSnapshot, targetView, new Snapshot(targetView));
+ sharedElements.add(sharedElement);
+ break;
+ }
+ }
+ if (sharedElements.isEmpty()) {
+ return;
+ }
+ mSharedElements = sharedElements;
+ mSharedElementsWithAnimation.clear();
+ for (SharedElement sharedElement : sharedElements) {
+ mSharedElementsLookup.put(sharedElement.sourceView.getId(), sharedElement);
+ mSharedElementsWithAnimation.add(sharedElement);
+ }
+ setupTransitionContainer();
+ reparentSharedViewsForCurrentTransition(sharedElements);
+ startSharedTransition(
+ mSharedElementsWithAnimation, LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION);
+ }
+
+ private void findSharedViewsForScreen(View view, List sharedViews) {
+ view = ScreensHelper.getTopScreenForStack(view);
+ if (!(view instanceof ViewGroup)) {
+ return;
+ }
+ ViewGroup viewGroup = (ViewGroup) view;
+ if (mAnimationsManager.hasAnimationForTag(
+ view.getId(), LayoutAnimations.Types.SHARED_ELEMENT_TRANSITION)) {
+ sharedViews.add(view);
+ }
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ View child = viewGroup.getChildAt(i);
+ findSharedViewsForScreen(child, sharedViews);
+ }
+ }
}
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java
index 9ebb402914a..920029cd8a1 100644
--- a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/Snapshot.java
@@ -6,6 +6,8 @@
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.ViewManager;
import com.swmansion.reanimated.ReactNativeUtils;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -117,9 +119,43 @@ public class Snapshot {
borderRadii = new ReactNativeUtils.BorderRadii(0, 0, 0, 0, 0);
}
+ private int[] tryGetRealPosition(View view) {
+ int[] location = new int[2];
+ View currentView = view;
+ while (currentView != null) {
+ location[0] += currentView.getX();
+ location[1] += currentView.getY();
+ if (ScreensHelper.isScreen(currentView)
+ && ScreensHelper.isScreensCoordinatorLayout(currentView.getParent())) {
+ View screen = currentView;
+ Class> screenClass = screen.getClass();
+ try {
+ Method getContainer = screenClass.getMethod("getContainer");
+ currentView = (View) getContainer.invoke(screen);
+ } catch (NoSuchMethodException
+ | InvocationTargetException
+ | IllegalAccessException ignored) {
+ }
+ } else if (currentView.getParent() instanceof View) {
+ currentView = (View) currentView.getParent();
+ } else {
+ break;
+ }
+ }
+ return location;
+ }
+
public Snapshot(View view) {
int[] location = new int[2];
view.getLocationOnScreen(location);
+ if (location[0] == 0 && location[1] == 0) {
+ /*
+ In certain cases, when a view is correctly attached to the screen and has computed
+ the correct layout, but is not visible on the screen, `getLocationOnScreen` may return
+ incorrect values [0, 0]. This behavior can occur during tab changes in bottom tabs.
+ */
+ location = tryGetRealPosition(view);
+ }
originX = location[0];
originY = location[1];
width = view.getWidth();
diff --git a/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/TabNavigatorObserver.java b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/TabNavigatorObserver.java
new file mode 100644
index 00000000000..9c51d694b0e
--- /dev/null
+++ b/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/layoutReanimation/TabNavigatorObserver.java
@@ -0,0 +1,128 @@
+package com.swmansion.reanimated.layoutReanimation;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class TabNavigatorObserver {
+ private final Set mFragmentsWithListenerRegistry = new HashSet<>();
+ private final ReaLayoutAnimator mReaLayoutAnimator;
+
+ public TabNavigatorObserver(ReaLayoutAnimator reaLayoutAnimator) {
+ mReaLayoutAnimator = reaLayoutAnimator;
+ }
+
+ public void handleScreenContainerUpdate(View screen) {
+ try {
+ Class> screenClass = screen.getClass();
+ Method getScreenFragment = screenClass.getMethod("getFragment");
+ Fragment fragment = (Fragment) getScreenFragment.invoke(screen);
+ int fragmentTag = fragment.getId();
+ if (!mFragmentsWithListenerRegistry.contains(fragmentTag)) {
+ mFragmentsWithListenerRegistry.add(fragmentTag);
+ fragment
+ .getParentFragmentManager()
+ .registerFragmentLifecycleCallbacks(new FragmentLifecycleCallbacks(fragment), true);
+ }
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ String message = e.getMessage() != null ? e.getMessage() : "Unable to get screen fragment";
+ Log.e("[Reanimated]", message);
+ }
+ }
+
+ class FragmentLifecycleCallbacks extends FragmentManager.FragmentLifecycleCallbacks {
+ private View firstScreen;
+ private Method getScreen;
+ private Method getActivityState;
+ private final Set screenTagsWithListener = new HashSet<>();
+ private final List nextTransition = new ArrayList<>();
+
+ public FragmentLifecycleCallbacks(Fragment fragment) {
+ try {
+ Class> screenFragmentClass = fragment.getClass();
+ getScreen = screenFragmentClass.getMethod("getScreen");
+ View screen = (View) getScreen.invoke(fragment);
+ getActivityState = screen.getClass().getMethod("getActivityState");
+ addScreenListener(screen);
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+ String message =
+ e.getMessage() != null ? e.getMessage() : "Unable to get screen activity state";
+ Log.e("[Reanimated]", message);
+ }
+ }
+
+ private void addScreenListener(View screen)
+ throws InvocationTargetException, IllegalAccessException {
+ if (screenTagsWithListener.contains(screen.getId())) {
+ return;
+ }
+ screenTagsWithListener.add(screen.getId());
+ screen.addOnAttachStateChangeListener(new OnAttachStateChangeListener());
+ screen.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if (nextTransition.isEmpty()) {
+ return;
+ }
+ AnimationsManager animationsManager = mReaLayoutAnimator.getAnimationsManager();
+ animationsManager.navigationTabChanged(nextTransition.get(0), nextTransition.get(1));
+ nextTransition.clear();
+ });
+ }
+
+ public void onFragmentAttached(
+ FragmentManager fragmentManager, Fragment fragment, Context context) {
+ onFragmentUpdate(fragment, true);
+ }
+
+ public void onFragmentDetached(FragmentManager fragmentManager, Fragment fragment) {
+ onFragmentUpdate(fragment, false);
+ }
+
+ private void onFragmentUpdate(Fragment fragment, boolean isAttaching) {
+ try {
+ View screen = (View) getScreen.invoke(fragment);
+ if (getActivityState.invoke(screen) == null) {
+ return;
+ }
+ addScreenListener(screen);
+
+ if (firstScreen == null) {
+ firstScreen = screen;
+ return;
+ }
+
+ if (isAttaching) {
+ nextTransition.add(firstScreen);
+ nextTransition.add(screen);
+ } else {
+ nextTransition.add(screen);
+ nextTransition.add(firstScreen);
+ }
+ firstScreen = null;
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ String message = e.getMessage() != null ? e.getMessage() : "Unable to get screen view";
+ Log.e("[Reanimated]", message);
+ }
+ }
+ }
+
+ class OnAttachStateChangeListener implements View.OnAttachStateChangeListener {
+ @Override
+ public void onViewAttachedToWindow(@NonNull View screen) {}
+
+ @Override
+ public void onViewDetachedFromWindow(@NonNull View screen) {
+ AnimationsManager animationsManager = mReaLayoutAnimator.getAnimationsManager();
+ animationsManager.visitNativeTreeAndMakeSnapshot(screen);
+ }
+ }
+}
diff --git a/packages/react-native-reanimated/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java b/packages/react-native-reanimated/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java
index 82c53a2c5c1..89f31ebf554 100644
--- a/packages/react-native-reanimated/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java
+++ b/packages/react-native-reanimated/android/src/paper/java/com/swmansion/reanimated/NativeProxy.java
@@ -135,6 +135,14 @@ public void checkDuplicateSharedTag(int viewTag, int screenTag) {
layoutAnimations.checkDuplicateSharedTag(viewTag, screenTag);
}
}
+
+ public int[] getSharedGroup(int viewTag) {
+ LayoutAnimations layoutAnimations = weakLayoutAnimations.get();
+ if (layoutAnimations != null) {
+ return layoutAnimations.getSharedGroup(viewTag);
+ }
+ return new int[]{};
+ }
};
}
}
diff --git a/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/73/com/swmansion/reanimated/NativeProxy.java b/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/73/com/swmansion/reanimated/NativeProxy.java
index a4d85fb2181..67478c49c26 100644
--- a/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/73/com/swmansion/reanimated/NativeProxy.java
+++ b/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/73/com/swmansion/reanimated/NativeProxy.java
@@ -111,6 +111,12 @@ public void cancelAnimation(int tag) {
public void checkDuplicateSharedTag(int viewTag, int screenTag) {
// NOT IMPLEMENTED
}
+
+ @Override
+ public int[] getSharedGroup(int viewTag) {
+ // NOT IMPLEMENTED
+ return new int[]{};
+ }
};
}
}
diff --git a/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/latest/com/swmansion/reanimated/NativeProxy.java b/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/latest/com/swmansion/reanimated/NativeProxy.java
index 56fb4bb218d..3902e82c2a7 100644
--- a/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/latest/com/swmansion/reanimated/NativeProxy.java
+++ b/packages/react-native-reanimated/android/src/reactNativeVersionPatch/RuntimeExecutor/latest/com/swmansion/reanimated/NativeProxy.java
@@ -138,6 +138,12 @@ public void cancelAnimation(int tag) {
public void checkDuplicateSharedTag(int viewTag, int screenTag) {
// NOT IMPLEMENTED
}
+
+ @Override
+ public int[] getSharedGroup(int viewTag) {
+ // NOT IMPLEMENTED
+ return new int[]{};
+ }
};
}
}
diff --git a/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.h b/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.h
index d2fe0a6d459..a61ca4d609e 100644
--- a/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.h
+++ b/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.h
@@ -26,6 +26,7 @@ typedef void (^REACheckDuplicateSharedTagBlock)(REAUIView *view, NSNumber *_Nonn
#endif
typedef void (^REACancelAnimationBlock)(NSNumber *_Nonnull tag);
typedef NSNumber *_Nullable (^REAFindPrecedingViewTagForTransitionBlock)(NSNumber *_Nonnull tag);
+typedef NSArray *_Nullable (^REAGetSharedGroupBlock)(NSNumber *_Nonnull tag);
typedef int (^REATreeVisitor)(id);
BOOL REANodeFind(id view, int (^block)(id));
@@ -45,6 +46,7 @@ BOOL REANodeFind(id view, int (^block)(id));
isSharedTransition:(BOOL)isSharedTransition;
- (void)setFindPrecedingViewTagForTransitionBlock:
(REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition;
+- (void)setGetSharedGroupBlock:(REAGetSharedGroupBlock)getSharedGroupBlock;
- (void)setCancelAnimationBlock:(REACancelAnimationBlock)animationCancellingBlock;
- (void)endLayoutAnimationForTag:(NSNumber *_Nonnull)tag removeView:(BOOL)removeView;
- (void)endAnimationsRecursive:(REAUIView *)view;
diff --git a/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.m b/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.m
index 16288e5557b..bd47daf28a8 100644
--- a/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.m
+++ b/packages/react-native-reanimated/apple/LayoutReanimation/REAAnimationsManager.m
@@ -600,6 +600,11 @@ - (void)setFindPrecedingViewTagForTransitionBlock:
[_sharedTransitionManager setFindPrecedingViewTagForTransitionBlock:findPrecedingViewTagForTransition];
}
+- (void)setGetSharedGroupBlock:(REAGetSharedGroupBlock)getSharedGroupBlock
+{
+ [_sharedTransitionManager setGetSharedGroupBlock:getSharedGroupBlock];
+}
+
- (void)setCancelAnimationBlock:(REACancelAnimationBlock)animationCancellingBlock
{
[_sharedTransitionManager setCancelAnimationBlock:animationCancellingBlock];
diff --git a/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.h b/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.h
index 02c17326154..cca9c631be5 100644
--- a/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.h
+++ b/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.h
@@ -3,7 +3,9 @@
|| (RCT_NEW_ARCH_ENABLED && __has_include() && __cplusplus))
#if LOAD_SCREENS_HEADERS
+#import
#import
+#import
#import
#endif
@@ -17,5 +19,9 @@
+ (REAUIView *)getScreenWrapper:(REAUIView *)view;
+ (int)getScreenType:(REAUIView *)screen;
+ (bool)isRNSScreenType:(REAUIView *)screen;
++ (REAUIView *)findTopScreenInChildren:(REAUIView *)screen;
++ (REAUIView *)getActiveTabForTabNavigator:(REAUIView *)tabNavigator;
++ (bool)isView:(REAUIView *)view DescendantOfScreen:(REAUIView *)screen;
++ (bool)isViewOnTopOfScreenStack:(REAUIView *)view;
@end
diff --git a/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.m b/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.m
index 2f565efdfa8..2c9e0169795 100644
--- a/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.m
+++ b/packages/react-native-reanimated/apple/LayoutReanimation/REAScreensHelper.m
@@ -18,6 +18,9 @@ + (REAUIView *)getScreenForView:(REAUIView *)view
+ (REAUIView *)getStackForView:(REAUIView *)view
{
+ if (view == nil) {
+ return nil;
+ }
if ([view isKindOfClass:[RNSScreenView class]]) {
if (view.reactSuperview != nil) {
if ([view.reactSuperview isKindOfClass:[RNSScreenStackView class]]) {
@@ -25,11 +28,19 @@ + (REAUIView *)getStackForView:(REAUIView *)view
}
}
}
- while (view != nil && ![view isKindOfClass:[RNSScreenStackView class]] && view.superview != nil) {
- view = view.superview;
+ REAUIView *currentView = view;
+ while (currentView.reactSuperview != nil) {
+ if ([currentView isKindOfClass:[RNSScreenStackView class]]) {
+ return currentView;
+ }
+ currentView = currentView.reactSuperview;
}
- if ([view isKindOfClass:[RNSScreenStackView class]]) {
- return view;
+ currentView = view;
+ while (currentView.superview != nil) {
+ if ([currentView isKindOfClass:[RNSScreenStackView class]]) {
+ return currentView;
+ }
+ currentView = currentView.superview;
}
return nil;
}
@@ -69,6 +80,83 @@ + (bool)isRNSScreenType:(REAUIView *)view
return [view isKindOfClass:[RNSScreen class]] == YES;
}
++ (REAUIView *)getActiveTabForTabNavigator:(REAUIView *)tabNavigator
+{
+ NSArray *screenTabs = tabNavigator.reactSubviews;
+ for (RNSScreenView *tab in screenTabs) {
+ if (tab.activityState == RNSActivityStateOnTop) {
+ return tab;
+ }
+ }
+ return nil;
+}
+
++ (REAUIView *)findTopScreenInChildren:(REAUIView *)view
+{
+ for (REAUIView *child in view.reactSubviews) {
+ if ([child isKindOfClass:[RNSScreenStackView class]]) {
+ int screenCount = [child.reactSubviews count];
+ if (screenCount != 0) {
+ REAUIView *topScreen = child.reactSubviews[[child.reactSubviews count] - 1];
+ REAUIView *maybeChildScreen = [REAScreensHelper findTopScreenInChildren:topScreen];
+ if (maybeChildScreen) {
+ return maybeChildScreen;
+ }
+ if (topScreen) {
+ return topScreen;
+ }
+ }
+ }
+ REAUIView *topScreen = [REAScreensHelper findTopScreenInChildren:child];
+ if (topScreen != nil) {
+ return topScreen;
+ }
+ }
+ if ([view isKindOfClass:[RNSScreenView class]]) {
+ return view;
+ }
+ return nil;
+}
+
++ (bool)isView:(REAUIView *)view DescendantOfScreen:(REAUIView *)screen
+{
+ REAUIView *currentView = view;
+ while (currentView.reactSuperview) {
+ if (currentView == screen) {
+ return true;
+ }
+ currentView = currentView.reactSuperview;
+ }
+ return false;
+}
+
++ (bool)isViewOnTopOfScreenStack:(REAUIView *)view
+{
+ NSMutableArray *screens = [NSMutableArray new];
+ REAUIView *currentView = view;
+ while (currentView.reactSuperview != nil) {
+ if ([currentView isKindOfClass:[RNSScreenView class]]) {
+ [screens addObject:currentView];
+ }
+ currentView = currentView.reactSuperview;
+ }
+ for (int i = 0; i < [screens count]; i++) {
+ REAUIView *screen = screens[i];
+ REAUIView *container = screen.reactSuperview;
+ if ([container isKindOfClass:[RNSScreenStackView class]]) {
+ if (screen.reactSuperview.reactSubviews.lastObject != screen) {
+ return false;
+ }
+ }
+ if ([container isKindOfClass:[RNSScreenNavigationContainerView class]]) {
+ if ([REAScreensHelper getActiveTabForTabNavigator:container] != screen) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
#else
+ (REAUIView *)getScreenForView:(REAUIView *)view
diff --git a/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.h b/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.h
index 8f507f64443..961737d2337 100644
--- a/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.h
+++ b/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.h
@@ -10,6 +10,7 @@
- (void)setFindPrecedingViewTagForTransitionBlock:
(REAFindPrecedingViewTagForTransitionBlock)findPrecedingViewTagForTransition;
- (void)setCancelAnimationBlock:(REACancelAnimationBlock)cancelAnimationBlock;
+- (void)setGetSharedGroupBlock:(REAGetSharedGroupBlock)getSharedGroupBlock;
- (instancetype)initWithAnimationsManager:(REAAnimationsManager *)animationManager;
- (REAUIView *)getTransitioningView:(NSNumber *)tag;
- (NSDictionary *)prepareDataForWorklet:(NSMutableDictionary *)currentValues
diff --git a/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.m b/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.m
index 619d6679068..4cc7d020fea 100644
--- a/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.m
+++ b/packages/react-native-reanimated/apple/LayoutReanimation/REASharedTransitionManager.m
@@ -11,6 +11,7 @@ @implementation REASharedTransitionManager {
NSMutableDictionary *_currentSharedTransitionViews;
REAFindPrecedingViewTagForTransitionBlock _findPrecedingViewTagForTransition;
REACancelAnimationBlock _cancelLayoutAnimation;
+ REAGetSharedGroupBlock _getSharedGroupBlock;
REAUIView *_transitionContainer;
NSMutableArray *_addedSharedViews;
BOOL _isSharedTransitionActive;
@@ -29,7 +30,8 @@ @implementation REASharedTransitionManager {
BOOL _isAsyncSharedTransitionConfigured;
BOOL _clearScreen;
BOOL _isInteractive;
- REAUIView *_disappearingScreen;
+ NSMutableArray *_disappearingScreens;
+ BOOL _isTabNavigator;
}
/*
@@ -68,6 +70,21 @@ - (instancetype)initWithAnimationsManager:(REAAnimationsManager *)animationManag
_reattachedViews = [NSMutableSet new];
_isAsyncSharedTransitionConfigured = NO;
_isConfigured = NO;
+ _disappearingScreens = [NSMutableArray new];
+ _isTabNavigator = NO;
+ _findPrecedingViewTagForTransition = ^NSNumber *(NSNumber *tag)
+ {
+ // default implementation, this block will be replaced by a setter
+ return nil;
+ };
+ _cancelLayoutAnimation = ^(NSNumber *tag) {
+ // default implementation, this block will be replaced by a setter
+ };
+ _getSharedGroupBlock = ^NSArray *(NSNumber *tag)
+ {
+ // default implementation, this block will be replaced by a setter
+ return nil;
+ };
[self swizzleScreensMethods];
}
return self;
@@ -240,6 +257,8 @@ - (NSArray *)sortViewsByTags:(NSArray *)views
}
} while (siblingView == nil && siblingViewTag != nil);
+ siblingView = [self maybeOverrideSiblingForTabNavigator:sharedView siblingView:siblingView];
+
if (siblingView == nil) {
// the sibling of shared view doesn't exist yet
continue;
@@ -276,14 +295,18 @@ - (NSArray *)sortViewsByTags:(NSArray *)views
// check valid target screen configuration
int screensCount = [stack.reactSubviews count];
if (addedNewScreen && !isModal) {
- // is under top
- if (screensCount < 2) {
- continue;
- }
- REAUIView *viewSourceParentScreen = [REAScreensHelper getScreenForView:viewSource];
- REAUIView *screenUnderStackTop = stack.reactSubviews[screensCount - 2];
- if (![screenUnderStackTop.reactTag isEqual:viewSourceParentScreen.reactTag] && !isInCurrentTransition) {
- continue;
+ REAUIView *sourceStack = [REAScreensHelper getStackForView:viewSource];
+ REAUIView *targetStack = [REAScreensHelper getStackForView:viewTarget];
+ if (sourceStack == targetStack) {
+ // is under top
+ if (screensCount < 2) {
+ continue;
+ }
+ REAUIView *viewSourceParentScreen = [REAScreensHelper getScreenForView:viewSource];
+ REAUIView *screenUnderStackTop = stack.reactSubviews[screensCount - 2];
+ if (![screenUnderStackTop.reactTag isEqual:viewSourceParentScreen.reactTag] && !isInCurrentTransition) {
+ continue;
+ }
}
} else if (!addedNewScreen && !isModal) {
// is on top
@@ -358,6 +381,49 @@ - (NSArray *)sortViewsByTags:(NSArray *)views
return newSharedElements;
}
+- (REAUIView *)maybeOverrideSiblingForTabNavigator:(REAUIView *)sharedView siblingView:(REAUIView *)siblingView
+{
+ REAUIView *maybeTabNavigatorForSharedView = [self getTabNavigator:sharedView];
+ REAUIView *maybeTabNavigatorForSiblingView = [self getTabNavigator:siblingView];
+
+ if (!(maybeTabNavigatorForSharedView && maybeTabNavigatorForSiblingView) ||
+ maybeTabNavigatorForSharedView != maybeTabNavigatorForSiblingView) {
+ return siblingView;
+ }
+
+ NSArray *sharedGroup = _getSharedGroupBlock(sharedView.reactTag);
+ int siblingIndex = [sharedGroup indexOfObject:siblingView.reactTag];
+ REAUIView *activeTab = [REAScreensHelper getActiveTabForTabNavigator:maybeTabNavigatorForSharedView];
+ for (int i = siblingIndex; i >= 0; i--) {
+ NSNumber *viewTag = sharedGroup[i];
+ REAUIView *view = [_animationManager viewForTag:viewTag];
+ if ([REAScreensHelper isView:view DescendantOfScreen:activeTab]) {
+ return view;
+ }
+ }
+ return nil;
+}
+
+- (REAUIView *)getTabNavigator:(REAUIView *)view
+{
+ REAUIView *currentView = view;
+ while (currentView.superview) {
+ if ([currentView isKindOfClass:NSClassFromString(@"RNSScreenNavigationContainerView")]) {
+ return currentView;
+ }
+ currentView = (REAUIView *)currentView.superview;
+ }
+
+ currentView = view;
+ while (currentView.reactSuperview) {
+ if ([currentView isKindOfClass:NSClassFromString(@"RNSScreenNavigationContainerView")]) {
+ return currentView;
+ }
+ currentView = (REAUIView *)currentView.reactSuperview;
+ }
+ return nil;
+}
+
/*
Method swizzling is used to get notification from react-native-screens
about push or pop screen from stack.
@@ -365,19 +431,19 @@ - (NSArray *)sortViewsByTags:(NSArray *)views
- (void)swizzleScreensMethods
{
#if LOAD_SCREENS_HEADERS
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- SEL viewDidLayoutSubviewsSelector = @selector(viewDidLayoutSubviews);
- SEL notifyWillDisappearSelector = @selector(notifyWillDisappear);
- SEL viewIsAppearingSelector = @selector(viewIsAppearing:);
- Class screenClass = [RNSScreen class];
- Class screenViewClass = [RNSScreenView class];
- BOOL allSelectorsAreAvailable = [RNSScreen instancesRespondToSelector:viewDidLayoutSubviewsSelector] &&
- [RNSScreenView instancesRespondToSelector:notifyWillDisappearSelector] &&
- [RNSScreen instancesRespondToSelector:viewIsAppearingSelector] &&
- [RNSScreenView instancesRespondToSelector:@selector(isModal)]; // used by REAScreenHelper
-
- if (allSelectorsAreAvailable) {
+ SEL viewDidLayoutSubviewsSelector = @selector(viewDidLayoutSubviews);
+ SEL notifyWillDisappearSelector = @selector(notifyWillDisappear);
+ SEL viewIsAppearingSelector = @selector(viewIsAppearing:);
+ Class screenClass = [RNSScreen class];
+ Class screenViewClass = [RNSScreenView class];
+ BOOL allSelectorsAreAvailable = [RNSScreen instancesRespondToSelector:viewDidLayoutSubviewsSelector] &&
+ [RNSScreenView instancesRespondToSelector:notifyWillDisappearSelector] &&
+ [RNSScreen instancesRespondToSelector:viewIsAppearingSelector] &&
+ [RNSScreenView instancesRespondToSelector:@selector(isModal)]; // used by REAScreenHelper
+
+ if (allSelectorsAreAvailable) {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
[REAUtils swizzleMethod:viewDidLayoutSubviewsSelector
forClass:screenClass
with:@selector(reanimated_viewDidLayoutSubviews)
@@ -390,21 +456,46 @@ - (void)swizzleScreensMethods
forClass:screenClass
with:@selector(reanimated_viewIsAppearing:)
fromClass:[self class]];
- _isConfigured = YES;
- }
- });
+ });
+ _isConfigured = YES;
+ }
#endif
}
- (void)setDisappearingScreen:(REAUIView *)view
{
- _disappearingScreen = view;
+ if (view == nil) {
+ [_disappearingScreens removeAllObjects];
+ } else {
+ [_disappearingScreens addObject:view];
+ }
_isInteractive = [_sharedTransitionManager isInteractiveScreenChange:view];
}
-- (REAUIView *)getDisappearingScreen
+- (REAUIView *)getLastDisappearingScreen
{
- return _disappearingScreen;
+ int count = [_disappearingScreens count];
+ if (count == 0) {
+ return nil;
+ } else {
+ return _disappearingScreens[count - 1];
+ }
+}
+
+- (NSMutableArray *)getDisappearingScreens
+{
+ return _disappearingScreens;
+}
+
+- (void)dismissAsyncTransition
+{
+ _isAsyncSharedTransitionConfigured = NO;
+ [_sharedElements removeAllObjects];
+}
+
+- (BOOL)isTabNavigator
+{
+ return _isTabNavigator;
}
- (void)setIsInteractive:(BOOL)isInteractive
@@ -422,7 +513,12 @@ - (void)reanimated_viewDidLayoutSubviews
// call original method from react-native-screens, self == RNScreen
[self reanimated_viewDidLayoutSubviews];
REAUIView *screen = [self valueForKey:@"screenView"];
- [_sharedTransitionManager screenAddedToStack:screen];
+ if ([_sharedTransitionManager isTabNavigator]) {
+ [_sharedTransitionManager dismissAsyncTransition];
+ [_sharedTransitionManager handleTabNavigatorChange:screen];
+ } else {
+ [_sharedTransitionManager screenAddedToStack:screen];
+ }
}
- (void)reanimated_notifyWillDisappear
@@ -443,20 +539,129 @@ - (void)reanimated_viewIsAppearing:(BOOL)animated
{
// call original method from react-native-screens, self == RNSScreen
[self reanimated_viewIsAppearing:animated];
- REAUIView *disappearingScreen = [_sharedTransitionManager getDisappearingScreen];
+ REAUIView *disappearingScreen = [_sharedTransitionManager getLastDisappearingScreen];
REAUIView *targetScreen = [self valueForKey:@"screenView"];
- if (disappearingScreen != NULL) {
- [_sharedTransitionManager screenRemovedFromStack:disappearingScreen
- withOffsetX:-targetScreen.superview.frame.origin.x
- withOffsetY:-targetScreen.superview.frame.origin.y];
+
+ if (disappearingScreen == nil) {
+ [_sharedTransitionManager setDisappearingScreen:nil];
+ return;
}
- [_sharedTransitionManager setDisappearingScreen:NULL];
+
+ NSArray *disappearingScreens = [_sharedTransitionManager getDisappearingScreens];
+ REAUIView *firstScreen = disappearingScreens[0];
+ if ([firstScreen.reactSuperview isKindOfClass:NSClassFromString(@"RNSScreenNavigationContainerView")]) {
+ [_sharedTransitionManager handleTabNavigatorChange:nil];
+ return;
+ }
+ float transitionViewOffsetX = 0;
+ if ([REAScreensHelper getStackForView:disappearingScreen] != [REAScreensHelper getStackForView:targetScreen]) {
+ transitionViewOffsetX = [_sharedTransitionManager getTransitionViewOffset:targetScreen];
+ }
+ [_sharedTransitionManager screenRemovedFromStack:disappearingScreen
+ withOffsetX:-(targetScreen.superview.frame.origin.x + transitionViewOffsetX)
+ withOffsetY:-(targetScreen.superview.frame.origin.y)];
+ [_sharedTransitionManager setDisappearingScreen:nil];
+}
+
+- (void)handleTabNavigatorChange:(REAUIView *)layoutedScreen
+{
+ if (_isAsyncSharedTransitionConfigured) {
+ // this is a new screen, let wait until header will be attached to a screen to make a proper snapshots
+ _isTabNavigator = YES;
+ return;
+ }
+
+ REAUIView *navTabScreen = _disappearingScreens[0];
+ REAUIView *sourceScreen = _disappearingScreens[[_disappearingScreens count] - 1];
+ REAUIView *targetTabScreen = [REAScreensHelper getActiveTabForTabNavigator:navTabScreen.reactSuperview];
+ REAUIView *targetScreen = [REAScreensHelper findTopScreenInChildren:targetTabScreen];
+ if (!layoutedScreen && _isTabNavigator) {
+ // just wait for the next layout computation for your screen
+ return;
+ }
+
+ NSMutableArray *sharedViews = [NSMutableArray new];
+ REANodeFind(sourceScreen, ^int(id view) {
+ if ([self->_animationManager hasAnimationForTag:view.reactTag type:SHARED_ELEMENT_TRANSITION]) {
+ [sharedViews addObject:(REAUIView *)view];
+ }
+ return false;
+ });
+ sharedViews = (NSMutableArray *)[self sortViewsByTags:sharedViews];
+
+ for (REAUIView *sharedView in sharedViews) {
+ NSArray *groupTags = _getSharedGroupBlock(sharedView.reactTag);
+ if (![groupTags containsObject:sharedView.reactTag]) {
+ continue;
+ }
+ REAUIView *siblingView;
+ for (NSNumber *tag in groupTags) {
+ REAUIView *currentView = [_animationManager viewForTag:tag];
+ if ([REAScreensHelper isView:currentView DescendantOfScreen:targetScreen]) {
+ siblingView = currentView;
+ REAUIView *siblingScreen = [REAScreensHelper getScreenForView:siblingView];
+ if (layoutedScreen && siblingScreen != layoutedScreen) {
+ // just wait for the next layout computation for your screen
+ return;
+ }
+ break;
+ }
+ }
+
+ if (siblingView == nil) {
+ return;
+ }
+
+ REAUIView *viewSource = sharedView;
+ REAUIView *viewTarget = siblingView;
+ REASnapshot *sourceViewSnapshot = _snapshotRegistry[viewSource.reactTag];
+ if (navTabScreen.superview) {
+ sourceViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewSource];
+ }
+ REASnapshot *targetViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewTarget];
+ REASharedElement *sharedElement = [[REASharedElement alloc] initWithSourceView:viewSource
+ sourceViewSnapshot:sourceViewSnapshot
+ targetView:viewTarget
+ targetViewSnapshot:targetViewSnapshot];
+ sharedElement.animationType = SHARED_ELEMENT_TRANSITION;
+ [_sharedElements addObject:sharedElement];
+
+ _snapshotRegistry[viewSource.reactTag] = sourceViewSnapshot;
+ _snapshotRegistry[viewTarget.reactTag] = targetViewSnapshot;
+ _sharedElementsLookup[viewSource.reactTag] = sharedElement;
+ }
+
+ [self setDisappearingScreen:nil];
+ _isTabNavigator = NO;
+
+ if ([_sharedElements count] == 0) {
+ return;
+ }
+
+ [self configureTransitionContainer];
+ [self reparentSharedViewsForCurrentTransition:_sharedElements];
+ [self startSharedTransition:_sharedElements];
+}
+
+- (float)getTransitionViewOffset:(REAUIView *)screen
+{
+ float x = 0;
+ REAUIView *currentView = screen;
+ while (currentView.superview != nil) {
+ REAUIView *maybeView = (REAUIView *)currentView.superview.superview;
+ if ([maybeView isKindOfClass:NSClassFromString(@"UINavigationTransitionView")]) {
+ CGPoint transitionViewOffset = currentView.frame.origin;
+ x += transitionViewOffset.x;
+ }
+ currentView = currentView.superview;
+ }
+ return x;
}
- (void)screenAddedToStack:(REAUIView *)screen
{
if (screen.superview != nil) {
- [self runAsyncSharedTransition];
+ [self runAsyncSharedTransition:screen];
}
}
@@ -469,9 +674,9 @@ - (void)screenRemovedFromStack:(REAUIView *)screen withOffsetX:(double)offsetX w
bool isInteractive = [self getIsInteractive];
if ((stack != nil || isModal) && !isRemovedInParentStack) {
// screen is removed from React tree (navigation.navigate())
- bool isScreenRemovedFromReactTree = [self isScreen:screen outsideStack:stack];
+ bool isScreenRemovedFromReactTree = screen.reactSuperview == nil;
// click on button goBack on native header
- bool isTriggeredByGoBackButton = [self isScreen:screen onTopOfStack:stack];
+ bool isTriggeredByGoBackButton = [REAScreensHelper isViewOnTopOfScreenStack:screen];
bool shouldRunTransition = (isScreenRemovedFromReactTree || isTriggeredByGoBackButton) &&
!(isInteractive && [_currentSharedTransitionViews count] > 0);
if (shouldRunTransition) {
@@ -526,22 +731,6 @@ - (void)clearConfigForStackNow:(REAUIView *)stack
}
}
-- (BOOL)isScreen:(REAUIView *)screen outsideStack:(REAUIView *)stack
-{
- for (REAUIView *child in stack.reactSubviews) {
- if ([child.reactTag isEqual:screen.reactTag]) {
- return NO;
- }
- }
- return YES;
-}
-
-- (BOOL)isScreen:(REAUIView *)screen onTopOfStack:(REAUIView *)stack
-{
- int screenCount = stack.reactSubviews.count;
- return screenCount > 0 && screen == stack.reactSubviews.lastObject;
-}
-
- (BOOL)isRemovedFromHigherStack:(REAUIView *)screen
{
REAUIView *stack = screen.reactSuperview;
@@ -588,13 +777,18 @@ - (void)runSharedTransitionForSharedViewsOnScreen:(REAUIView *)screen
}
}
-- (void)runAsyncSharedTransition
+- (void)runAsyncSharedTransition:(REAUIView *)screen
{
if ([_sharedElements count] == 0 || !_isAsyncSharedTransitionConfigured) {
return;
}
for (REASharedElement *sharedElement in _sharedElements) {
REAUIView *viewTarget = sharedElement.targetView;
+ REAUIView *viewScreen = [REAScreensHelper getScreenForView:viewTarget];
+ if (viewScreen != screen) {
+ // just wait for the next layout computation for your screen
+ return;
+ }
REASnapshot *targetViewSnapshot = [[REASnapshot alloc] initWithAbsolutePosition:viewTarget];
_snapshotRegistry[viewTarget.reactTag] = targetViewSnapshot;
sharedElement.targetViewSnapshot = targetViewSnapshot;
@@ -743,6 +937,11 @@ - (void)setCancelAnimationBlock:(REACancelAnimationBlock)cancelAnimationBlock
_cancelLayoutAnimation = cancelAnimationBlock;
}
+- (void)setGetSharedGroupBlock:(REAGetSharedGroupBlock)getSharedGroupBlock
+{
+ _getSharedGroupBlock = getSharedGroupBlock;
+}
+
- (void)clearAllSharedConfigsForViewTag:(NSNumber *)viewTag
{
if (viewTag != nil) {
diff --git a/packages/react-native-reanimated/apple/native/NativeProxy.mm b/packages/react-native-reanimated/apple/native/NativeProxy.mm
index a703bdf99c6..a5002bcc47d 100644
--- a/packages/react-native-reanimated/apple/native/NativeProxy.mm
+++ b/packages/react-native-reanimated/apple/native/NativeProxy.mm
@@ -211,6 +211,18 @@ void setupLayoutAnimationCallbacks(
}
return nil;
}];
+
+ [animationsManager setGetSharedGroupBlock:^NSArray *_Nullable(NSNumber *_Nonnull tag) {
+ if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
+ const auto &results = nativeReanimatedModule->layoutAnimationsManager().getSharedGroup([tag intValue]);
+ NSMutableArray *convertedResult = [NSMutableArray new];
+ for (const int tag : results) {
+ [convertedResult addObject:@(tag)];
+ }
+ return convertedResult;
+ }
+ return nil;
+ }];
#ifndef NDEBUG
[animationsManager setCheckDuplicateSharedTagBlock:^(REAUIView *view, NSNumber *_Nonnull viewTag) {
if (auto nativeReanimatedModule = weakNativeReanimatedModule.lock()) {
diff --git a/packages/react-native-reanimated/src/Colors.ts b/packages/react-native-reanimated/src/Colors.ts
index 2b01b32668a..cd577a68c54 100644
--- a/packages/react-native-reanimated/src/Colors.ts
+++ b/packages/react-native-reanimated/src/Colors.ts
@@ -522,7 +522,9 @@ export const rgbaColor = (
): number | string => {
'worklet';
if (IS_WEB || !_WORKLET) {
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+ // Replace tiny values like 1.234e-11 with 0:
+ const safeAlpha = alpha < 0.001 ? 0 : alpha;
+ return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
}
const c =
@@ -701,9 +703,10 @@ export function convertToRGBA(color: unknown): ParsedColorArray {
export function rgbaArrayToRGBAColor(RGBA: ParsedColorArray): string {
'worklet';
+ const alpha = RGBA[3] < 0.001 ? 0 : RGBA[3];
return `rgba(${Math.round(RGBA[0] * 255)}, ${Math.round(
RGBA[1] * 255
- )}, ${Math.round(RGBA[2] * 255)}, ${RGBA[3]})`;
+ )}, ${Math.round(RGBA[2] * 255)}, ${alpha})`;
}
export function toLinearSpace(
diff --git a/yarn.lock b/yarn.lock
index 1371f2abb5a..fd25939cb72 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5876,6 +5876,23 @@ __metadata:
languageName: node
linkType: hard
+"@react-navigation/bottom-tabs@npm:^6.5.20":
+ version: 6.5.20
+ resolution: "@react-navigation/bottom-tabs@npm:6.5.20"
+ dependencies:
+ "@react-navigation/elements": "npm:^1.3.30"
+ color: "npm:^4.2.3"
+ warn-once: "npm:^0.1.0"
+ peerDependencies:
+ "@react-navigation/native": ^6.0.0
+ react: "*"
+ react-native: "*"
+ react-native-safe-area-context: ">= 3.0.0"
+ react-native-screens: ">= 3.0.0"
+ checksum: 10/327c5a6706a5e990295aa56e3ebda96b196ab67f9edbaa6eb6b80adbe131dddbb1eb564f95c6c0003bfd1e28e4040315a0a7cfd22bd7a79230b41ed6f9ba276d
+ languageName: node
+ linkType: hard
+
"@react-navigation/core@npm:^6.4.16":
version: 6.4.16
resolution: "@react-navigation/core@npm:6.4.16"
@@ -8597,6 +8614,7 @@ __metadata:
"@react-native-community/slider": "npm:^4.5.0"
"@react-native-masked-view/masked-view": "npm:^0.3.1"
"@react-native-picker/picker": "npm:^2.5.1"
+ "@react-navigation/bottom-tabs": "npm:^6.5.20"
"@react-navigation/native": "npm:^6.1.9"
"@react-navigation/native-stack": "npm:^6.9.17"
"@react-navigation/stack": "npm:^6.3.18"
@@ -8624,6 +8642,7 @@ __metadata:
"@react-native-community/slider": "*"
"@react-native-masked-view/masked-view": "*"
"@react-native-picker/picker": "*"
+ "@react-navigation/bottom-tabs": "*"
"@react-navigation/native": "*"
"@react-navigation/native-stack": "*"
"@react-navigation/stack": "*"