mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-30 21:32:14 +08:00
feat: add iOS modal presentation style
This commit is contained in:
@@ -18,6 +18,7 @@ import WipeStack from './src/WipeStack';
|
||||
import ImageStack from './src/ImageStack';
|
||||
import TransparentStack from './src/TransparentStack';
|
||||
import ModalStack from './src/ModalStack';
|
||||
import ModalPresentation from './src/ModalPresentation';
|
||||
import LifecycleInteraction from './src/LifecycleInteraction';
|
||||
import GestureInteraction from './src/GestureInteraction';
|
||||
import SwitchWithStacks from './src/SwitchWithStacks';
|
||||
@@ -43,6 +44,11 @@ const data = [
|
||||
{ component: WipeStack, title: 'Wipe Preset', routeName: 'Wipe' },
|
||||
{ component: ImageStack, title: 'Image', routeName: 'ImageStack' },
|
||||
{ component: ModalStack, title: 'Modal', routeName: 'ModalStack' },
|
||||
{
|
||||
component: ModalPresentation,
|
||||
title: 'Modal (iOS style)',
|
||||
routeName: 'ModalPresentation',
|
||||
},
|
||||
{ component: FullScreen, title: 'Full Screen', routeName: 'FullScreen' },
|
||||
{
|
||||
component: LifecycleInteraction,
|
||||
|
||||
74
packages/stack/example/src/ModalPresentation.js
Normal file
74
packages/stack/example/src/ModalPresentation.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react';
|
||||
import { Button, View, Text } from 'react-native';
|
||||
import {
|
||||
createStackNavigator,
|
||||
TransitionPresets,
|
||||
} from 'react-navigation-stack';
|
||||
|
||||
class ListScreen extends React.Component {
|
||||
static navigationOptions = {
|
||||
title: 'My Modal',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text>List Screen</Text>
|
||||
<Text>A list may go here</Text>
|
||||
<Button
|
||||
title="Go to Details"
|
||||
onPress={() => this.props.navigation.navigate('Details')}
|
||||
/>
|
||||
<Button
|
||||
title="Go back to all examples"
|
||||
onPress={() => this.props.navigation.navigate('Home')}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailsScreen extends React.Component {
|
||||
static navigationOptions = {
|
||||
header: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text>Details Screen</Text>
|
||||
<Button
|
||||
title="Go to Details... again"
|
||||
onPress={() => this.props.navigation.push('Details')}
|
||||
/>
|
||||
<Button
|
||||
title="Go to List"
|
||||
onPress={() => this.props.navigation.navigate('List')}
|
||||
/>
|
||||
<Button
|
||||
title="Go back"
|
||||
onPress={() => this.props.navigation.goBack()}
|
||||
/>
|
||||
<Button
|
||||
title="Go back to all examples"
|
||||
onPress={() => this.props.navigation.navigate('Home')}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default createStackNavigator(
|
||||
{
|
||||
List: ListScreen,
|
||||
Details: DetailsScreen,
|
||||
},
|
||||
{
|
||||
...TransitionPresets.ModalPresentationIOS,
|
||||
mode: 'modal',
|
||||
defaultNavigationOptions: {
|
||||
cardOverlayEnabled: true,
|
||||
gesturesEnabled: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -100,11 +100,8 @@ Array [
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
},
|
||||
Object {
|
||||
"transform": Array [
|
||||
@@ -256,11 +253,8 @@ Array [
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
},
|
||||
Object {
|
||||
"transform": Array [
|
||||
|
||||
@@ -100,11 +100,8 @@ Array [
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
},
|
||||
Object {
|
||||
"transform": Array [
|
||||
@@ -419,11 +416,8 @@ Array [
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"bottom": 0,
|
||||
"left": 0,
|
||||
"position": "absolute",
|
||||
"right": 0,
|
||||
"top": 0,
|
||||
"flex": 1,
|
||||
"overflow": "hidden",
|
||||
},
|
||||
Object {
|
||||
"transform": Array [
|
||||
|
||||
@@ -162,6 +162,7 @@ export type TransitionSpec =
|
||||
| { timing: 'timing'; config: TimingConfig };
|
||||
|
||||
export type CardInterpolationProps = {
|
||||
index: number;
|
||||
progress: {
|
||||
current: Animated.Node<number>;
|
||||
next?: Animated.Node<number>;
|
||||
|
||||
@@ -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<Props> {
|
||||
private getInterpolatedStyle = memoize(
|
||||
(
|
||||
styleInterpolator: CardStyleInterpolator,
|
||||
index: number,
|
||||
current: Animated.Node<number>,
|
||||
next: Animated.Node<number> | undefined,
|
||||
layout: Layout
|
||||
) =>
|
||||
styleInterpolator({
|
||||
index,
|
||||
progress: {
|
||||
current,
|
||||
next,
|
||||
@@ -460,6 +463,7 @@ export default class Card extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
index,
|
||||
active,
|
||||
transparent,
|
||||
layout,
|
||||
@@ -481,7 +485,13 @@ export default class Card extends React.Component<Props> {
|
||||
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<Props> {
|
||||
onHandlerStateChange={handleGestureEvent}
|
||||
{...this.gestureActivationCriteria()}
|
||||
>
|
||||
<Animated.View style={[StyleSheet.absoluteFill, cardStyle]}>
|
||||
<Animated.View style={[styles.container, cardStyle]}>
|
||||
{shadowEnabled && !transparent ? (
|
||||
<Animated.View
|
||||
style={[styles.shadow, shadowStyle]}
|
||||
|
||||
@@ -78,6 +78,7 @@ export default class StackItem extends React.PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
index,
|
||||
layout,
|
||||
active,
|
||||
focused,
|
||||
@@ -110,6 +111,7 @@ export default class StackItem extends React.PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<Card
|
||||
index={index}
|
||||
active={active}
|
||||
transparent={cardTransparent}
|
||||
direction={direction}
|
||||
|
||||
Reference in New Issue
Block a user