From 18436751750ddb26671a07b04451ef16355f6792 Mon Sep 17 00:00:00 2001 From: Mykola Stanislavchuk Date: Thu, 7 Mar 2024 01:12:47 +0100 Subject: [PATCH] feat: add animation option for js stack (#11854) **Motivation** The JS stack of navigation doesn't have an animation option for easy configuration transition between screens **What was done** Now we can pass the option "animation" to the screen options and have default animation And here are the basic transitions that could be used: | 'default' | 'fade' | 'fade_from_bottom' | 'none' | 'reveal_from_bottom' | 'scale_from_center' | 'slide_from_bottom' | 'slide_from_right' | 'slide_from_left' --------- Co-authored-by: Satyajit Sahoo --- example/src/Screens/MixedHeaderMode.tsx | 5 +- .../CardStyleInterpolators.tsx | 13 +++ .../TransitionConfigs/TransitionPresets.tsx | 9 ++ packages/stack/src/index.tsx | 1 + packages/stack/src/types.tsx | 30 +++++- .../stack/src/views/Stack/CardContainer.tsx | 4 +- packages/stack/src/views/Stack/CardStack.tsx | 94 ++++++++++++++----- packages/stack/src/views/Stack/StackView.tsx | 2 +- 8 files changed, 121 insertions(+), 37 deletions(-) diff --git a/example/src/Screens/MixedHeaderMode.tsx b/example/src/Screens/MixedHeaderMode.tsx index 2f9bab54e5..965e21a1d7 100644 --- a/example/src/Screens/MixedHeaderMode.tsx +++ b/example/src/Screens/MixedHeaderMode.tsx @@ -4,7 +4,6 @@ import { createStackNavigator, HeaderStyleInterpolators, type StackScreenProps, - TransitionPresets, } from '@react-navigation/stack'; import * as React from 'react'; import { Platform, ScrollView, StyleSheet, View } from 'react-native'; @@ -112,7 +111,7 @@ export function MixedHeaderMode({ > @@ -134,7 +133,7 @@ export function MixedHeaderMode({ name="Albums" component={AlbumsScreen} options={{ - ...TransitionPresets.ModalSlideFromBottomIOS, + animation: 'slide_from_bottom', headerMode: 'screen', title: 'Albums', }} diff --git a/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx b/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx index ee99970f15..00dee4567c 100644 --- a/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx +++ b/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx @@ -63,6 +63,19 @@ export function forHorizontalIOS({ }; } +/** + * iOS-style slide in from the left. + */ +export function forHorizontalIOSInverted({ + inverted, + ...rest +}: StackCardInterpolationProps): StackCardInterpolatedStyle { + return forHorizontalIOS({ + ...rest, + inverted: Animated.multiply(inverted, -1), + }); +} + /** * Standard iOS-style slide in from the bottom (used for modals). */ diff --git a/packages/stack/src/TransitionConfigs/TransitionPresets.tsx b/packages/stack/src/TransitionConfigs/TransitionPresets.tsx index 1147006ac4..2d5b7d3926 100644 --- a/packages/stack/src/TransitionConfigs/TransitionPresets.tsx +++ b/packages/stack/src/TransitionConfigs/TransitionPresets.tsx @@ -6,6 +6,7 @@ import { forFadeFromBottomAndroid, forFadeFromCenter as forFadeCard, forHorizontalIOS, + forHorizontalIOSInverted, forModalPresentationIOS, forRevealFromBottomAndroid, forScaleFromCenterAndroid, @@ -150,3 +151,11 @@ export const ModalTransition = Platform.select({ ios: ModalPresentationIOS, default: BottomSheetAndroid, }); + +/** + * Slide from left transition. + */ +export const SlideFromLeftIOS: TransitionPreset = { + ...SlideFromRightIOS, + cardStyleInterpolator: forHorizontalIOSInverted, +}; diff --git a/packages/stack/src/index.tsx b/packages/stack/src/index.tsx index d8950353a4..9956d60283 100644 --- a/packages/stack/src/index.tsx +++ b/packages/stack/src/index.tsx @@ -36,6 +36,7 @@ export { useGestureHandlerRef } from './utils/useGestureHandlerRef'; * Types */ export type { + StackAnimationName, StackCardInterpolatedStyle, StackCardInterpolationProps, StackCardStyleInterpolator, diff --git a/packages/stack/src/types.tsx b/packages/stack/src/types.tsx index 94cb9eac6f..86926319ec 100644 --- a/packages/stack/src/types.tsx +++ b/packages/stack/src/types.tsx @@ -78,8 +78,19 @@ export type GestureDirection = | 'vertical' | 'vertical-inverted'; +export type StackAnimationName = + | 'default' + | 'fade' + | 'fade_from_bottom' + | 'none' + | 'reveal_from_bottom' + | 'scale_from_center' + | 'slide_from_bottom' + | 'slide_from_right' + | 'slide_from_left'; + type SceneOptionsDefaults = TransitionPreset & { - animationEnabled: boolean; + animation: StackAnimationName; gestureEnabled: boolean; cardOverlayEnabled: boolean; headerMode: StackHeaderMode; @@ -326,11 +337,20 @@ export type StackNavigationOptions = StackHeaderOptions & */ presentation?: 'card' | 'modal' | 'transparentModal'; /** - * Whether transition animation should be enabled the screen. - * If you set it to `false`, the screen won't animate when pushing or popping. - * Defaults to `true` on Android and iOS, `false` on Web. + * How the screen should animate when pushed or popped. + * + * Supported values: + * - 'none': don't animate the screen + * - 'default': use the platform default animation + * - 'fade': fade screen in or out + * - 'fade_from_bottom': fade screen in or out from bottom + * - 'slide_from_bottom': slide in the new screen from bottom + * - 'slide_from_right': slide in the new screen from right + * - 'slide_from_left': slide in the new screen from left + * - 'reveal_from_bottom': reveal screen in from bottom to top + * - 'scale_from_center': scale screen in from center */ - animationEnabled?: boolean; + animation?: StackAnimationName; /** * The type of animation to use when this screen replaces another screen. Defaults to `push`. * When `pop` is used, the `pop` animation is applied to the screen being replaced. diff --git a/packages/stack/src/views/Stack/CardContainer.tsx b/packages/stack/src/views/Stack/CardContainer.tsx index 4c1c707169..4178b617cb 100644 --- a/packages/stack/src/views/Stack/CardContainer.tsx +++ b/packages/stack/src/views/Stack/CardContainer.tsx @@ -190,7 +190,7 @@ function CardContainerInner({ const { presentation, - animationEnabled, + animation, cardOverlay, cardOverlayEnabled, cardShadowEnabled, @@ -275,7 +275,7 @@ function CardContainerInner({ display: // Hide unfocused screens when animation isn't enabled // This is also necessary for a11y on web - animationEnabled === false && + animation === 'none' && isNextScreenTransparent === false && detachCurrentScreen !== false && !focused diff --git a/packages/stack/src/views/Stack/CardStack.tsx b/packages/stack/src/views/Stack/CardStack.tsx index b774ced113..8d4e5bdfb8 100755 --- a/packages/stack/src/views/Stack/CardStack.tsx +++ b/packages/stack/src/views/Stack/CardStack.tsx @@ -23,18 +23,26 @@ import { forNoAnimation as forNoAnimationCard, } from '../../TransitionConfigs/CardStyleInterpolators'; import { + BottomSheetAndroid, DefaultTransition, + FadeFromBottomAndroid, ModalFadeTransition, + ModalSlideFromBottomIOS, ModalTransition, + RevealFromBottomAndroid, + ScaleFromCenterAndroid, + SlideFromLeftIOS, + SlideFromRightIOS, } from '../../TransitionConfigs/TransitionPresets'; import type { Layout, Scene, + StackAnimationName, StackCardStyleInterpolator, StackDescriptor, StackDescriptorMap, StackHeaderMode, - StackNavigationOptions, + TransitionPreset, } from '../../types'; import { findLastIndex } from '../../utils/findLastIndex'; import { getDistanceForDirection } from '../../utils/getDistanceForDirection'; @@ -86,6 +94,21 @@ type State = { headerHeights: Record; }; +const NAMED_TRANSITIONS_PRESETS = { + default: DefaultTransition, + fade: ModalFadeTransition, + fade_from_bottom: FadeFromBottomAndroid, + none: DefaultTransition, + reveal_from_bottom: RevealFromBottomAndroid, + scale_from_center: ScaleFromCenterAndroid, + slide_from_left: SlideFromLeftIOS, + slide_from_right: SlideFromRightIOS, + slide_from_bottom: Platform.select({ + ios: ModalSlideFromBottomIOS, + default: BottomSheetAndroid, + }), +} as const satisfies Record; + const EPSILON = 1e-5; const STATE_INACTIVE = 0; @@ -178,12 +201,22 @@ const getDistanceFromOptions = ( descriptor: StackDescriptor | undefined, isRTL: boolean ) => { - const { - presentation, - gestureDirection = presentation === 'modal' + if (descriptor?.options.gestureDirection) { + return getDistanceForDirection( + layout, + descriptor?.options.gestureDirection, + isRTL + ); + } + + const defaultGestureDirection = + descriptor?.options.presentation === 'modal' ? ModalTransition.gestureDirection - : DefaultTransition.gestureDirection, - } = (descriptor?.options || {}) as StackNavigationOptions; + : DefaultTransition.gestureDirection; + + const gestureDirection = descriptor?.options.animation + ? NAMED_TRANSITIONS_PRESETS[descriptor?.options.animation]?.gestureDirection + : defaultGestureDirection; return getDistanceForDirection(layout, gestureDirection, isRTL); }; @@ -236,13 +269,12 @@ export class CardStack extends React.Component { ].reduce((acc, curr) => { const descriptor = props.descriptors[curr.key] || props.preloadedDescriptors[curr.key]; - const { animationEnabled } = descriptor?.options || {}; + const { animation } = descriptor?.options || {}; acc[curr.key] = state.gestures[curr.key] || new Animated.Value( - (props.openingRouteKeys.includes(curr.key) && - animationEnabled !== false) || + (props.openingRouteKeys.includes(curr.key) && animation !== 'none') || props.state.preloadedRoutes.includes(curr) ? getDistanceFromOptions( state.layout, @@ -300,24 +332,34 @@ export class CardStack extends React.Component { ? nextDescriptor.options : descriptor.options; - const defaultTransitionPreset = - optionsForTransitionConfig.presentation === 'modal' - ? ModalTransition - : optionsForTransitionConfig.presentation === 'transparentModal' - ? ModalFadeTransition - : DefaultTransition; + // Disable screen transition animation by default on web, windows and macos to match the native behavior + const excludedPlatforms = + Platform.OS !== 'web' && + Platform.OS !== 'windows' && + Platform.OS !== 'macos'; + + const animation = + optionsForTransitionConfig.animation ?? + (excludedPlatforms ? 'default' : 'none'); + const isAnimationEnabled = animation !== 'none'; + + const transitionPreset = + animation !== 'default' + ? NAMED_TRANSITIONS_PRESETS[animation] + : optionsForTransitionConfig.presentation === 'modal' + ? ModalTransition + : optionsForTransitionConfig.presentation === 'transparentModal' + ? ModalFadeTransition + : DefaultTransition; const { - animationEnabled = Platform.OS !== 'web' && - Platform.OS !== 'windows' && - Platform.OS !== 'macos', - gestureEnabled = Platform.OS === 'ios' && animationEnabled, - gestureDirection = defaultTransitionPreset.gestureDirection, - transitionSpec = defaultTransitionPreset.transitionSpec, - cardStyleInterpolator = animationEnabled === false - ? forNoAnimationCard - : defaultTransitionPreset.cardStyleInterpolator, - headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator, + gestureEnabled = Platform.OS === 'ios' && isAnimationEnabled, + gestureDirection = transitionPreset.gestureDirection, + transitionSpec = transitionPreset.transitionSpec, + cardStyleInterpolator = isAnimationEnabled + ? transitionPreset.cardStyleInterpolator + : forNoAnimationCard, + headerStyleInterpolator = transitionPreset.headerStyleInterpolator, cardOverlayEnabled = (Platform.OS !== 'ios' && optionsForTransitionConfig.presentation !== 'transparentModal') || getIsModalPresentation(cardStyleInterpolator), @@ -345,7 +387,7 @@ export class CardStack extends React.Component { ...descriptor, options: { ...descriptor.options, - animationEnabled, + animation, cardOverlayEnabled, cardStyleInterpolator, gestureDirection, diff --git a/packages/stack/src/views/Stack/StackView.tsx b/packages/stack/src/views/Stack/StackView.tsx index ed118c63a2..72c800b161 100644 --- a/packages/stack/src/views/Stack/StackView.tsx +++ b/packages/stack/src/views/Stack/StackView.tsx @@ -142,7 +142,7 @@ export class StackView extends React.Component { const isAnimationEnabled = (key: string) => { const descriptor = props.descriptors[key] || state.descriptors[key]; - return descriptor ? descriptor.options.animationEnabled !== false : true; + return descriptor ? descriptor.options.animation !== 'none' : true; }; const getAnimationTypeForReplace = (key: string) => {