Skip to content

Commit

Permalink
feat: add animation option for js stack (#11854)
Browse files Browse the repository at this point in the history
**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 <satyajit.happy@gmail.com>
  • Loading branch information
groot007 and satya164 committed Mar 7, 2024
1 parent 7991ce8 commit 1843675
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 37 deletions.
5 changes: 2 additions & 3 deletions example/src/Screens/MixedHeaderMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -112,7 +111,7 @@ export function MixedHeaderMode({
>
<SimpleStack.Group
screenOptions={{
...TransitionPresets.SlideFromRightIOS,
animation: 'slide_from_right',
headerMode: 'float',
}}
>
Expand All @@ -134,7 +133,7 @@ export function MixedHeaderMode({
name="Albums"
component={AlbumsScreen}
options={{
...TransitionPresets.ModalSlideFromBottomIOS,
animation: 'slide_from_bottom',
headerMode: 'screen',
title: 'Albums',
}}
Expand Down
13 changes: 13 additions & 0 deletions packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand Down
9 changes: 9 additions & 0 deletions packages/stack/src/TransitionConfigs/TransitionPresets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
forFadeFromBottomAndroid,
forFadeFromCenter as forFadeCard,
forHorizontalIOS,
forHorizontalIOSInverted,
forModalPresentationIOS,
forRevealFromBottomAndroid,
forScaleFromCenterAndroid,
Expand Down Expand Up @@ -150,3 +151,11 @@ export const ModalTransition = Platform.select({
ios: ModalPresentationIOS,
default: BottomSheetAndroid,
});

/**
* Slide from left transition.
*/
export const SlideFromLeftIOS: TransitionPreset = {
...SlideFromRightIOS,
cardStyleInterpolator: forHorizontalIOSInverted,
};
1 change: 1 addition & 0 deletions packages/stack/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export { useGestureHandlerRef } from './utils/useGestureHandlerRef';
* Types
*/
export type {
StackAnimationName,
StackCardInterpolatedStyle,
StackCardInterpolationProps,
StackCardStyleInterpolator,
Expand Down
30 changes: 25 additions & 5 deletions packages/stack/src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions packages/stack/src/views/Stack/CardContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function CardContainerInner({

const {
presentation,
animationEnabled,
animation,
cardOverlay,
cardOverlayEnabled,
cardShadowEnabled,
Expand Down Expand Up @@ -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
Expand Down
94 changes: 68 additions & 26 deletions packages/stack/src/views/Stack/CardStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -86,6 +94,21 @@ type State = {
headerHeights: Record<string, number>;
};

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<StackAnimationName, TransitionPreset>;

const EPSILON = 1e-5;

const STATE_INACTIVE = 0;
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -236,13 +269,12 @@ export class CardStack extends React.Component<Props, State> {
].reduce<GestureValues>((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,
Expand Down Expand Up @@ -300,24 +332,34 @@ export class CardStack extends React.Component<Props, State> {
? 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),
Expand Down Expand Up @@ -345,7 +387,7 @@ export class CardStack extends React.Component<Props, State> {
...descriptor,
options: {
...descriptor.options,
animationEnabled,
animation,
cardOverlayEnabled,
cardStyleInterpolator,
gestureDirection,
Expand Down
2 changes: 1 addition & 1 deletion packages/stack/src/views/Stack/StackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export class StackView extends React.Component<Props, State> {
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) => {
Expand Down

0 comments on commit 1843675

Please sign in to comment.