diff --git a/src/TransitionView.tsx b/src/TransitionView.tsx index f402b5c..a3e43c0 100644 --- a/src/TransitionView.tsx +++ b/src/TransitionView.tsx @@ -24,6 +24,7 @@ import type { WithTimingConfig, } from 'react-native-reanimated'; +import { resolveOverlayRadius } from './overlayRadius'; import { TransitionCoordinator } from './TransitionCoordinator'; import type { TransitionEntry } from './TransitionCoordinator'; import type { SharedTransitionEasing } from './types'; @@ -61,7 +62,10 @@ export function TransitionView({ entry }: { entry: TransitionEntry }) { const y = useSharedValue(from.y); const width = useSharedValue(from.width); const height = useSharedValue(from.height); - const radius = useSharedValue(config.morphBorderRadius ? fromRadius : 0); + const radius = useSharedValue( + resolveOverlayRadius(config.morphBorderRadius, fromRadius, entry.toRadius) + .start + ); const fade = useSharedValue(0); /** * Drives completion. It always travels 0 → 1, which matters: Reanimated's @@ -104,7 +108,9 @@ export function TransitionView({ entry }: { entry: TransitionEntry }) { y.value = animate(to.y); width.value = animate(to.width); height.value = animate(to.height); - radius.value = animate(cfg.morphBorderRadius ? toRadius : 0); + radius.value = animate( + resolveOverlayRadius(cfg.morphBorderRadius, fromRadius, toRadius).end + ); fade.value = animate(1); // Restart progress from 0 so a retarget re-runs the full settle window. diff --git a/src/__tests__/overlayRadius.test.ts b/src/__tests__/overlayRadius.test.ts new file mode 100644 index 0000000..1cca936 --- /dev/null +++ b/src/__tests__/overlayRadius.test.ts @@ -0,0 +1,28 @@ +import { resolveOverlayRadius } from '../overlayRadius'; + +describe('resolveOverlayRadius', () => { + it('morphs from the source radius to the target radius', () => { + expect(resolveOverlayRadius(true, 20, 110)).toEqual({ + start: 20, + end: 110, + }); + }); + + it('holds the source radius when morphing is disabled', () => { + expect(resolveOverlayRadius(false, 55, 12)).toEqual({ + start: 55, + end: 55, + }); + }); + + it('keeps a circular source circular for the whole flight', () => { + const { start, end } = resolveOverlayRadius(false, 32, 0); + expect(start).toBe(32); + expect(end).toBe(32); + }); + + it('leaves square endpoints square', () => { + expect(resolveOverlayRadius(false, 0, 0)).toEqual({ start: 0, end: 0 }); + expect(resolveOverlayRadius(true, 0, 0)).toEqual({ start: 0, end: 0 }); + }); +}); diff --git a/src/overlayRadius.ts b/src/overlayRadius.ts new file mode 100644 index 0000000..32361ae --- /dev/null +++ b/src/overlayRadius.ts @@ -0,0 +1,28 @@ +/** + * overlayRadius + * + * Corner radius resolution for the flying overlay. Pure and Reanimated-free + * so it can be unit tested without a renderer. + */ + +/** The overlay container's borderRadius at both ends of the flight. */ +export interface OverlayRadiusRange { + start: number; + end: number; +} + +/** + * `morphBorderRadius: false` means "do not animate the radius", not "drop it": + * the cloned child's own radius is reset, so the overlay container is the only + * thing rounding the corners and must hold the source radius for the flight. + */ +export function resolveOverlayRadius( + morphBorderRadius: boolean, + fromRadius: number, + toRadius: number +): OverlayRadiusRange { + return { + start: fromRadius, + end: morphBorderRadius ? toRadius : fromRadius, + }; +}