feat: add 'transparentModal' presentation to JS stack

This commit is contained in:
Satyajit Sahoo
2021-05-21 14:20:27 +02:00
parent 2cb44a5663
commit 3d147401e8
7 changed files with 130 additions and 57 deletions

View File

@@ -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',
},

View File

@@ -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 {};
}

View File

@@ -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.
*/

View File

@@ -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.

View File

@@ -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>>;

View File

@@ -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',

View File

@@ -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>
);