diff --git a/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx b/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx index 89c6564f..70dafbb3 100644 --- a/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx +++ b/packages/stack/src/TransitionConfigs/CardStyleInterpolators.tsx @@ -1,8 +1,9 @@ import { I18nManager } from 'react-native'; import Animated from 'react-native-reanimated'; import { CardInterpolationProps, CardInterpolatedStyle } from '../types'; +import { getStatusBarHeight } from 'react-native-safe-area-view'; -const { cond, multiply, interpolate } = Animated; +const { cond, add, multiply, interpolate } = Animated; /** * Standard iOS-style slide in from the right. @@ -71,6 +72,58 @@ export function forVerticalIOS({ }; } +/** + * Standard iOS-style modal animation in iOS 13. + */ +export function forModalPresentationIOS({ + index, + progress: { current, next }, + layouts: { screen }, +}: CardInterpolationProps): CardInterpolatedStyle { + const topOffset = 10; + const statusBarHeight = getStatusBarHeight(screen.width > screen.height); + const aspectRatio = screen.height / screen.width; + + const progress = add(current, next ? next : 0); + + const translateY = interpolate(progress, { + inputRange: [0, 1, 2], + outputRange: [ + screen.height, + index === 0 ? 0 : topOffset, + (index === 0 ? statusBarHeight : 0) - topOffset * aspectRatio, + ], + }); + + const overlayOpacity = interpolate(progress, { + inputRange: [0, 1, 2], + outputRange: [0, 0.3, 1], + }); + + const scale = interpolate(progress, { + inputRange: [0, 1, 2], + outputRange: [1, 1, screen.width ? 1 - (topOffset * 2) / screen.width : 1], + }); + + const borderRadius = + index === 0 + ? interpolate(progress, { + inputRange: [0, 1, 2], + outputRange: [0, 0, 10], + }) + : 10; + + return { + cardStyle: { + borderTopLeftRadius: borderRadius, + borderTopRightRadius: borderRadius, + marginTop: index === 0 ? 0 : statusBarHeight, + transform: [{ translateY }, { scale }], + }, + overlayStyle: { opacity: overlayOpacity }, + }; +} + /** * Standard Android-style fade in from the bottom for Android Oreo. */ diff --git a/packages/stack/src/TransitionConfigs/TransitionPresets.tsx b/packages/stack/src/TransitionConfigs/TransitionPresets.tsx index dc78e120..f1066a1c 100644 --- a/packages/stack/src/TransitionConfigs/TransitionPresets.tsx +++ b/packages/stack/src/TransitionConfigs/TransitionPresets.tsx @@ -3,6 +3,7 @@ import { forVerticalIOS, forWipeFromBottomAndroid, forFadeFromBottomAndroid, + forModalPresentationIOS, } from './CardStyleInterpolators'; import { forNoAnimation, forFade } from './HeaderStyleInterpolators'; import { @@ -38,6 +39,17 @@ export const ModalSlideFromBottomIOS: TransitionPreset = { headerStyleInterpolator: forNoAnimation, }; +// Standard iOS modal presentation style (introduced in iOS 13) +export const ModalPresentationIOS: TransitionPreset = { + direction: 'vertical', + transitionSpec: { + open: TransitionIOSSpec, + close: TransitionIOSSpec, + }, + cardStyleInterpolator: forModalPresentationIOS, + headerStyleInterpolator: forNoAnimation, +}; + // Standard Android navigation transition when opening or closing an Activity on Android < 9 export const FadeFromBottomAndroid: TransitionPreset = { direction: 'vertical', diff --git a/packages/stack/src/types.tsx b/packages/stack/src/types.tsx index b980fbf1..256cc7e9 100644 --- a/packages/stack/src/types.tsx +++ b/packages/stack/src/types.tsx @@ -162,6 +162,7 @@ export type TransitionSpec = | { timing: 'timing'; config: TimingConfig }; export type CardInterpolationProps = { + index: number; progress: { current: Animated.Node; next?: Animated.Node; diff --git a/packages/stack/src/views/Stack/Card.tsx b/packages/stack/src/views/Stack/Card.tsx index df63e11f..0000cba2 100755 --- a/packages/stack/src/views/Stack/Card.tsx +++ b/packages/stack/src/views/Stack/Card.tsx @@ -19,6 +19,7 @@ import StackGestureContext from '../../utils/StackGestureContext'; import PointerEventsView from './PointerEventsView'; type Props = ViewProps & { + index: number; active: boolean; closing?: boolean; transparent?: boolean; @@ -404,11 +405,13 @@ export default class Card extends React.Component { private getInterpolatedStyle = memoize( ( styleInterpolator: CardStyleInterpolator, + index: number, current: Animated.Node, next: Animated.Node | undefined, layout: Layout ) => styleInterpolator({ + index, progress: { current, next, @@ -460,6 +463,7 @@ export default class Card extends React.Component { render() { const { + index, active, transparent, layout, @@ -481,7 +485,13 @@ export default class Card extends React.Component { cardStyle, overlayStyle, shadowStyle, - } = this.getInterpolatedStyle(styleInterpolator, current, next, layout); + } = this.getInterpolatedStyle( + styleInterpolator, + index, + current, + next, + layout + ); const handleGestureEvent = direction === 'vertical' @@ -509,7 +519,7 @@ export default class Card extends React.Component { onHandlerStateChange={handleGestureEvent} {...this.gestureActivationCriteria()} > - + {shadowEnabled && !transparent ? ( { render() { const { + index, layout, active, focused, @@ -110,6 +111,7 @@ export default class StackItem extends React.PureComponent { return (