mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: add 'transparentModal' presentation to JS stack
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { View, StyleSheet, ScrollView, Platform } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Platform,
|
||||
Pressable,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { Button, Paragraph } from 'react-native-paper';
|
||||
import { ParamListBase, useTheme } from '@react-navigation/native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
StackScreenProps,
|
||||
useCardAnimation,
|
||||
} from '@react-navigation/stack';
|
||||
import Article from '../Shared/Article';
|
||||
|
||||
@@ -49,10 +57,28 @@ const DialogScreen = ({
|
||||
navigation,
|
||||
}: StackScreenProps<TransparentStackParams>) => {
|
||||
const { colors } = useTheme();
|
||||
const { current } = useCardAnimation();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.dialog, { backgroundColor: colors.card }]}>
|
||||
<Pressable style={styles.backdrop} onPress={() => navigation.goBack()} />
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.dialog,
|
||||
{
|
||||
backgroundColor: colors.card,
|
||||
transform: [
|
||||
{
|
||||
scale: current.progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.9, 1],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Paragraph>
|
||||
Mise en place is a French term that literally means “put in place.” It
|
||||
also refers to a way cooks in professional kitchens and restaurants
|
||||
@@ -67,7 +93,7 @@ const DialogScreen = ({
|
||||
<Button style={styles.close} compact onPress={navigation.goBack}>
|
||||
Okay
|
||||
</Button>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -84,7 +110,7 @@ export default function TransparentStackScreen({ navigation }: Props) {
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<TransparentStack.Navigator screenOptions={{ presentation: 'modal' }}>
|
||||
<TransparentStack.Navigator>
|
||||
<TransparentStack.Screen
|
||||
name="Article"
|
||||
component={ArticleScreen}
|
||||
@@ -95,32 +121,7 @@ export default function TransparentStackScreen({ navigation }: Props) {
|
||||
component={DialogScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
cardStyle: { backgroundColor: 'transparent' },
|
||||
cardOverlayEnabled: true,
|
||||
cardStyleInterpolator: ({ current: { progress } }) => ({
|
||||
cardStyle: {
|
||||
opacity: progress.interpolate({
|
||||
inputRange: [0, 0.5, 0.9, 1],
|
||||
outputRange: [0, 0.25, 0.7, 1],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
scale: progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.9, 1],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
overlayStyle: {
|
||||
opacity: progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 0.5],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
presentation: 'transparentModal',
|
||||
}}
|
||||
/>
|
||||
</TransparentStack.Navigator>
|
||||
@@ -146,6 +147,10 @@ const styles = StyleSheet.create({
|
||||
maxWidth: 400,
|
||||
borderRadius: 3,
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
},
|
||||
close: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
|
||||
@@ -361,6 +361,29 @@ export function forBottomSheetAndroid({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple fade animation for dialogs
|
||||
*/
|
||||
export function forFadeFromCenter({
|
||||
current: { progress },
|
||||
}: StackCardInterpolationProps): StackCardInterpolatedStyle {
|
||||
return {
|
||||
cardStyle: {
|
||||
opacity: progress.interpolate({
|
||||
inputRange: [0, 0.5, 0.9, 1],
|
||||
outputRange: [0, 0.25, 0.7, 1],
|
||||
}),
|
||||
},
|
||||
overlayStyle: {
|
||||
opacity: progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0, 0.5],
|
||||
extrapolate: 'clamp',
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function forNoAnimation(): StackCardInterpolatedStyle {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
forRevealFromBottomAndroid,
|
||||
forFadeFromBottomAndroid,
|
||||
forBottomSheetAndroid,
|
||||
forFadeFromCenter as forFadeCard,
|
||||
} from './CardStyleInterpolators';
|
||||
import { forFade } from './HeaderStyleInterpolators';
|
||||
import {
|
||||
@@ -114,6 +115,19 @@ export const BottomSheetAndroid: TransitionPreset = {
|
||||
headerStyleInterpolator: forFade,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fade transition for transparent modals.
|
||||
*/
|
||||
export const ModalFadeTransition: TransitionPreset = {
|
||||
gestureDirection: 'vertical',
|
||||
transitionSpec: {
|
||||
open: BottomSheetSlideInSpec,
|
||||
close: BottomSheetSlideOutSpec,
|
||||
},
|
||||
cardStyleInterpolator: forFadeCard,
|
||||
headerStyleInterpolator: forFade,
|
||||
};
|
||||
|
||||
/**
|
||||
* Default navigation transition for the current platform.
|
||||
*/
|
||||
|
||||
@@ -256,11 +256,16 @@ export type StackNavigationOptions = StackHeaderOptions &
|
||||
* - `modal`: Use Modal animations. This changes a few things:
|
||||
* - Sets `headerMode` to `screen` for the screen unless specified otherwise.
|
||||
* - Changes the screen animation to match the platform behavior for modals.
|
||||
* - `transparentModal`: Similar to `modal`. This changes following things:
|
||||
* - Sets `headerMode` to `screen` for the screen unless specified otherwise.
|
||||
* - Sets background color of the screen to transparent, so previous screen is visible
|
||||
* - Adjusts the `detachPreviousScreen` option so that the previous screen stays rendered.
|
||||
* - Prevents the previous screen from animating from its last position.
|
||||
* - Changes the screen animation to a vertical slide animation.
|
||||
*
|
||||
* Defaults to 'card'.
|
||||
*/
|
||||
presentation?: 'card' | 'modal';
|
||||
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.
|
||||
|
||||
@@ -46,10 +46,10 @@ type Props = ViewProps & {
|
||||
gestureDirection: GestureDirection;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
onTransition?: (props: { closing: boolean; gesture: boolean }) => void;
|
||||
onGestureBegin?: () => void;
|
||||
onGestureCanceled?: () => void;
|
||||
onGestureEnd?: () => void;
|
||||
onTransition: (props: { closing: boolean; gesture: boolean }) => void;
|
||||
onGestureBegin: () => void;
|
||||
onGestureCanceled: () => void;
|
||||
onGestureEnd: () => void;
|
||||
children: React.ReactNode;
|
||||
overlay: (props: {
|
||||
style: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
|
||||
@@ -34,14 +34,14 @@ type Props = {
|
||||
renderScene: (props: { route: Route<string> }) => React.ReactNode;
|
||||
onOpenRoute: (props: { route: Route<string> }) => void;
|
||||
onCloseRoute: (props: { route: Route<string> }) => void;
|
||||
onTransitionStart?: (
|
||||
onTransitionStart: (
|
||||
props: { route: Route<string> },
|
||||
closing: boolean
|
||||
) => void;
|
||||
onTransitionEnd?: (props: { route: Route<string> }, closing: boolean) => void;
|
||||
onGestureStart?: (props: { route: Route<string> }) => void;
|
||||
onGestureEnd?: (props: { route: Route<string> }) => void;
|
||||
onGestureCancel?: (props: { route: Route<string> }) => void;
|
||||
onTransitionEnd: (props: { route: Route<string> }, closing: boolean) => void;
|
||||
onGestureStart: (props: { route: Route<string> }) => void;
|
||||
onGestureEnd: (props: { route: Route<string> }) => void;
|
||||
onGestureCancel: (props: { route: Route<string> }) => void;
|
||||
hasAbsoluteFloatHeader: boolean;
|
||||
headerHeight: number;
|
||||
onHeaderHeightChange: (props: {
|
||||
@@ -49,6 +49,8 @@ type Props = {
|
||||
height: number;
|
||||
}) => void;
|
||||
isParentHeaderShown: boolean;
|
||||
isNextScreenTransparent: boolean;
|
||||
detachCurrentScreen: boolean;
|
||||
};
|
||||
|
||||
const EPSILON = 0.1;
|
||||
@@ -66,6 +68,8 @@ function CardContainer({
|
||||
headerHeight,
|
||||
onHeaderHeightChange,
|
||||
isParentHeaderShown,
|
||||
isNextScreenTransparent,
|
||||
detachCurrentScreen,
|
||||
interpolationIndex,
|
||||
layout,
|
||||
onCloseRoute,
|
||||
@@ -102,35 +106,35 @@ function CardContainer({
|
||||
const handleOpen = () => {
|
||||
const { route } = scene.descriptor;
|
||||
|
||||
onTransitionEnd?.({ route }, false);
|
||||
onTransitionEnd({ route }, false);
|
||||
onOpenRoute({ route });
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const { route } = scene.descriptor;
|
||||
|
||||
onTransitionEnd?.({ route }, true);
|
||||
onTransitionEnd({ route }, true);
|
||||
onCloseRoute({ route });
|
||||
};
|
||||
|
||||
const handleGestureBegin = () => {
|
||||
const { route } = scene.descriptor;
|
||||
|
||||
onPageChangeStart?.();
|
||||
onGestureStart?.({ route });
|
||||
onPageChangeStart();
|
||||
onGestureStart({ route });
|
||||
};
|
||||
|
||||
const handleGestureCanceled = () => {
|
||||
const { route } = scene.descriptor;
|
||||
|
||||
onPageChangeCancel?.();
|
||||
onGestureCancel?.({ route });
|
||||
onPageChangeCancel();
|
||||
onGestureCancel({ route });
|
||||
};
|
||||
|
||||
const handleGestureEnd = () => {
|
||||
const { route } = scene.descriptor;
|
||||
|
||||
onGestureEnd?.({ route });
|
||||
onGestureEnd({ route });
|
||||
};
|
||||
|
||||
const handleTransition = ({
|
||||
@@ -182,7 +186,6 @@ function CardContainer({
|
||||
|
||||
const {
|
||||
presentation,
|
||||
detachPreviousScreen,
|
||||
animationEnabled,
|
||||
cardOverlay,
|
||||
cardOverlayEnabled,
|
||||
@@ -222,7 +225,7 @@ function CardContainer({
|
||||
insets={insets}
|
||||
gesture={gesture}
|
||||
current={scene.progress.current}
|
||||
next={scene.progress.next}
|
||||
next={isNextScreenTransparent ? undefined : scene.progress.next}
|
||||
closing={closing}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
@@ -248,7 +251,15 @@ function CardContainer({
|
||||
? { marginTop: headerHeight }
|
||||
: null
|
||||
}
|
||||
contentStyle={[{ backgroundColor: colors.background }, cardStyle]}
|
||||
contentStyle={[
|
||||
{
|
||||
backgroundColor:
|
||||
presentation === 'transparentModal'
|
||||
? 'transparent'
|
||||
: colors.background,
|
||||
},
|
||||
cardStyle,
|
||||
]}
|
||||
style={[
|
||||
{
|
||||
// This is necessary to avoid unfocused larger pages increasing scroll area
|
||||
@@ -258,8 +269,8 @@ function CardContainer({
|
||||
// Hide unfocused screens when animation isn't enabled
|
||||
// This is also necessary for a11y on web
|
||||
animationEnabled === false &&
|
||||
detachPreviousScreen !== false &&
|
||||
presentation !== 'modal' &&
|
||||
isNextScreenTransparent === false &&
|
||||
detachCurrentScreen !== false &&
|
||||
!focused
|
||||
? 'none'
|
||||
: 'flex',
|
||||
|
||||
@@ -24,6 +24,7 @@ import CardContainer from './CardContainer';
|
||||
import {
|
||||
DefaultTransition,
|
||||
ModalTransition,
|
||||
ModalFadeTransition,
|
||||
} from '../../TransitionConfigs/TransitionPresets';
|
||||
import {
|
||||
forModalPresentationIOS,
|
||||
@@ -63,9 +64,9 @@ type Props = {
|
||||
closing: boolean
|
||||
) => void;
|
||||
onTransitionEnd: (props: { route: Route<string> }, closing: boolean) => void;
|
||||
onGestureStart?: (props: { route: Route<string> }) => void;
|
||||
onGestureEnd?: (props: { route: Route<string> }) => void;
|
||||
onGestureCancel?: (props: { route: Route<string> }) => void;
|
||||
onGestureStart: (props: { route: Route<string> }) => void;
|
||||
onGestureEnd: (props: { route: Route<string> }) => void;
|
||||
onGestureCancel: (props: { route: Route<string> }) => void;
|
||||
detachInactiveScreens?: boolean;
|
||||
};
|
||||
|
||||
@@ -225,6 +226,8 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
let defaultTransitionPreset =
|
||||
optionsForTransitionConfig.presentation === 'modal'
|
||||
? ModalTransition
|
||||
: optionsForTransitionConfig.presentation === 'transparentModal'
|
||||
? ModalFadeTransition
|
||||
: DefaultTransition;
|
||||
|
||||
const {
|
||||
@@ -238,7 +241,8 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
? forNoAnimationCard
|
||||
: defaultTransitionPreset.cardStyleInterpolator,
|
||||
headerStyleInterpolator = defaultTransitionPreset.headerStyleInterpolator,
|
||||
cardOverlayEnabled = Platform.OS !== 'ios' ||
|
||||
cardOverlayEnabled = (Platform.OS !== 'ios' &&
|
||||
optionsForTransitionConfig.presentation !== 'transparentModal') ||
|
||||
cardStyleInterpolator === forModalPresentationIOS,
|
||||
} = optionsForTransitionConfig;
|
||||
|
||||
@@ -246,6 +250,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
descriptor.options.headerMode ??
|
||||
(!(
|
||||
optionsForTransitionConfig.presentation === 'modal' ||
|
||||
optionsForTransitionConfig.presentation === 'transparentModal' ||
|
||||
cardStyleInterpolator === forModalPresentationIOS
|
||||
) &&
|
||||
Platform.OS === 'ios' &&
|
||||
@@ -461,7 +466,7 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
const { options } = scenes[i].descriptor;
|
||||
const {
|
||||
// By default, we don't want to detach the previous screen of the active one for modals
|
||||
detachPreviousScreen = options.presentation === 'modal' ||
|
||||
detachPreviousScreen = options.presentation === 'transparentModal' ||
|
||||
options.cardStyleInterpolator === forModalPresentationIOS
|
||||
? i !== scenes.length - 1
|
||||
: true,
|
||||
@@ -577,6 +582,14 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
interpolationIndex++;
|
||||
}
|
||||
|
||||
const isNextScreenTransparent =
|
||||
scenes[index + 1]?.descriptor.options.presentation ===
|
||||
'transparentModal';
|
||||
|
||||
const detachCurrentScreen =
|
||||
scenes[index + 1]?.descriptor.options.detachPreviousScreen !==
|
||||
false;
|
||||
|
||||
return (
|
||||
<MaybeScreen
|
||||
key={route.key}
|
||||
@@ -616,6 +629,8 @@ export default class CardStack extends React.Component<Props, State> {
|
||||
onCloseRoute={onCloseRoute}
|
||||
onTransitionStart={onTransitionStart}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
isNextScreenTransparent={isNextScreenTransparent}
|
||||
detachCurrentScreen={detachCurrentScreen}
|
||||
/>
|
||||
</MaybeScreen>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user