feat: add iOS modal presentation style

This commit is contained in:
satyajit.happy
2019-06-07 15:05:52 +02:00
parent 2da09926d5
commit c6ba6e8c1c
9 changed files with 169 additions and 23 deletions

View File

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

View 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,
},
}
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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]}

View File

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