mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-29 12:55:21 +08:00
Use react-native-gesture-handler's PanGestureHandler instead of PanResponder
This commit is contained in:
@@ -1,12 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Expo from 'expo';
|
import Expo from 'expo';
|
||||||
import { FlatList } from 'react-native';
|
import { FlatList, I18nManager } from 'react-native';
|
||||||
import { createSwitchNavigator } from 'react-navigation';
|
import { createSwitchNavigator } from 'react-navigation';
|
||||||
import { createStackNavigator } from 'react-navigation-stack';
|
import { createStackNavigator } from 'react-navigation-stack';
|
||||||
import { ListSection, Divider } from 'react-native-paper';
|
import { ListSection, Divider } from 'react-native-paper';
|
||||||
|
|
||||||
import SimpleStack from './src/SimpleStack';
|
import SimpleStack from './src/SimpleStack';
|
||||||
|
import ImageStack from './src/ImageStack';
|
||||||
import TransparentStack from './src/TransparentStack';
|
import TransparentStack from './src/TransparentStack';
|
||||||
|
import ModalStack from './src/ModalStack';
|
||||||
|
import LifecycleInteraction from './src/LifecycleInteraction';
|
||||||
|
import GestureInteraction from './src/GestureInteraction';
|
||||||
|
|
||||||
|
// Uncomment the following line to force RTL. Requires closing and re-opening
|
||||||
|
// your app after you first load it with this option enabled.
|
||||||
|
// I18nManager.forceRTL(true);
|
||||||
|
|
||||||
// Comment the following two lines to stop using react-native-screens
|
// Comment the following two lines to stop using react-native-screens
|
||||||
import { useScreens } from 'react-native-screens';
|
import { useScreens } from 'react-native-screens';
|
||||||
@@ -14,18 +22,26 @@ useScreens();
|
|||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{ component: SimpleStack, title: 'Simple', routeName: 'SimpleStack' },
|
{ component: SimpleStack, title: 'Simple', routeName: 'SimpleStack' },
|
||||||
|
{ component: ImageStack, title: 'Image', routeName: 'ImageStack' },
|
||||||
|
{ component: ModalStack, title: 'Modal', routeName: 'ModalStack' },
|
||||||
|
{ component: LifecycleInteraction, title: 'Lifecycle', routeName: 'LifecycleStack' },
|
||||||
{
|
{
|
||||||
component: TransparentStack,
|
component: TransparentStack,
|
||||||
title: 'Transparent',
|
title: 'Transparent',
|
||||||
routeName: 'TransparentStack',
|
routeName: 'TransparentStack',
|
||||||
},
|
},
|
||||||
|
{ component: GestureInteraction, title: 'Gesture Interaction', routeName: 'GestureInteraction' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Expo.Asset.loadAsync(require('react-navigation/src/views/assets/back-icon.png'));
|
||||||
|
Expo.Asset.loadAsync(require('react-navigation/src/views/assets/back-icon-mask.png'));
|
||||||
|
|
||||||
class Home extends React.Component {
|
class Home extends React.Component {
|
||||||
static navigationOptions = {
|
static navigationOptions = {
|
||||||
title: 'Examples',
|
title: 'Examples',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
_renderItem = ({ item }) => (
|
_renderItem = ({ item }) => (
|
||||||
<ListSection.Item
|
<ListSection.Item
|
||||||
title={item.title}
|
title={item.title}
|
||||||
@@ -42,6 +58,7 @@ class Home extends React.Component {
|
|||||||
renderItem={this._renderItem}
|
renderItem={this._renderItem}
|
||||||
keyExtractor={this._keyExtractor}
|
keyExtractor={this._keyExtractor}
|
||||||
data={data}
|
data={data}
|
||||||
|
style={{ backgroundColor: '#fff' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ const path = require('path');
|
|||||||
const glob = require('glob-to-regexp');
|
const glob = require('glob-to-regexp');
|
||||||
const blacklist = require('metro/src/blacklist');
|
const blacklist = require('metro/src/blacklist');
|
||||||
const pak = require('../package.json');
|
const pak = require('../package.json');
|
||||||
|
const pak2 = require('./package.json');
|
||||||
|
|
||||||
const dependencies = Object.keys(pak.dependencies);
|
const dependencies = Object.keys(pak.dependencies);
|
||||||
|
const localDependencies = Object.keys(pak2.dependencies);
|
||||||
const peerDependencies = Object.keys(pak.peerDependencies);
|
const peerDependencies = Object.keys(pak.peerDependencies);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -13,7 +15,7 @@ module.exports = {
|
|||||||
return [__dirname, path.resolve(__dirname, '..')];
|
return [__dirname, path.resolve(__dirname, '..')];
|
||||||
},
|
},
|
||||||
getProvidesModuleNodeModules() {
|
getProvidesModuleNodeModules() {
|
||||||
return [...dependencies, ...peerDependencies];
|
return [...dependencies, ...localDependencies, ...peerDependencies];
|
||||||
},
|
},
|
||||||
getBlacklistRE() {
|
getBlacklistRE() {
|
||||||
return blacklist([glob(`${path.resolve(__dirname, '..')}/node_modules/*`)]);
|
return blacklist([glob(`${path.resolve(__dirname, '..')}/node_modules/*`)]);
|
||||||
|
|||||||
93
packages/stack/example/src/GestureInteraction.js
Normal file
93
packages/stack/example/src/GestureInteraction.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Button,
|
||||||
|
InteractionManager,
|
||||||
|
WebView,
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
} from 'react-native';
|
||||||
|
import { MapView } from 'expo';
|
||||||
|
import { createStackNavigator, withNavigationFocus } from 'react-navigation';
|
||||||
|
import { StackGestureContext } from 'react-navigation-stack';
|
||||||
|
import {
|
||||||
|
PanGestureHandler,
|
||||||
|
NativeViewGestureHandler,
|
||||||
|
} from 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
const IndexScreen = ({ navigation }) => (
|
||||||
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Button title="Go to MapView" onPress={() => navigation.navigate('Map')} />
|
||||||
|
<Button title="Go to WebView" onPress={() => navigation.navigate('Web')} />
|
||||||
|
<Button
|
||||||
|
title="Return to other examples"
|
||||||
|
onPress={() => navigation.navigate('Home')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
IndexScreen.navigationOptions = {
|
||||||
|
title: 'Gesture Interactions',
|
||||||
|
};
|
||||||
|
|
||||||
|
class MapScreen extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
InteractionManager.runAfterInteractions(() => {
|
||||||
|
this.setState({ interactionComplete: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
interactionComplete: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.state.interactionComplete) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
<ActivityIndicator />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackGestureContext.Consumer>
|
||||||
|
{ref => (
|
||||||
|
<NativeViewGestureHandler waitFor={ref}>
|
||||||
|
<MapView style={{ flex: 1 }} />
|
||||||
|
</NativeViewGestureHandler>
|
||||||
|
)}
|
||||||
|
</StackGestureContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MapScreen.navigationOptions = {
|
||||||
|
title: 'MapView',
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebViewScreen = () => (
|
||||||
|
<StackGestureContext.Consumer>
|
||||||
|
{ref => (
|
||||||
|
<NativeViewGestureHandler waitFor={ref}>
|
||||||
|
<WebView
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
source={{ uri: 'https://news.google.com' }}
|
||||||
|
/>
|
||||||
|
</NativeViewGestureHandler>
|
||||||
|
)}
|
||||||
|
</StackGestureContext.Consumer>
|
||||||
|
);
|
||||||
|
|
||||||
|
WebViewScreen.navigationOptions = {
|
||||||
|
title: 'WebView',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createStackNavigator({
|
||||||
|
Index: IndexScreen,
|
||||||
|
Map: MapScreen,
|
||||||
|
Web: WebViewScreen,
|
||||||
|
});
|
||||||
89
packages/stack/example/src/ImageStack.js
Normal file
89
packages/stack/example/src/ImageStack.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dimensions, Button, Image, View, Text } from 'react-native';
|
||||||
|
import { createStackNavigator } from 'react-navigation-stack';
|
||||||
|
import { FlatList, BorderlessButton } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
class ListScreen extends React.Component {
|
||||||
|
static navigationOptions = ({ navigation }) => ({
|
||||||
|
title: 'Image list',
|
||||||
|
headerBackTitle: 'Back',
|
||||||
|
headerLeft: (
|
||||||
|
<Button title="Back" onPress={() => navigation.navigate('Home')} />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
state = {
|
||||||
|
items: Array.apply(null, Array(60)).map((v, i) => {
|
||||||
|
return {
|
||||||
|
id: i,
|
||||||
|
src: `https://source.unsplash.com/random/400x${400 + i}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={this.state.items}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View style={{ flex: 1, flexDirection: 'column', margin: 1 }}>
|
||||||
|
<BorderlessButton
|
||||||
|
onPress={() =>
|
||||||
|
this.props.navigation.navigate('Details', {
|
||||||
|
id: item.id,
|
||||||
|
src: item.src,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image style={{ height: 100 }} source={{ uri: item.src }} />
|
||||||
|
</BorderlessButton>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
numColumns={3}
|
||||||
|
keyExtractor={(item, index) => index}
|
||||||
|
style={{ flex: 1, backgroundColor: '#fff' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DetailsScreen extends React.Component {
|
||||||
|
static navigationOptions = {
|
||||||
|
title: 'Random image from Unsplash',
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let id = this.props.navigation.getParam('id', 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: `https://source.unsplash.com/random/1080x${1920 + id}`,
|
||||||
|
}}
|
||||||
|
style={{ width: Dimensions.get('window').width, height: 400 }}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title="Go back"
|
||||||
|
onPress={() => this.props.navigation.goBack()}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createStackNavigator(
|
||||||
|
{
|
||||||
|
List: ListScreen,
|
||||||
|
Details: DetailsScreen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialRouteName: 'List',
|
||||||
|
}
|
||||||
|
);
|
||||||
69
packages/stack/example/src/LifecycleInteraction.js
Normal file
69
packages/stack/example/src/LifecycleInteraction.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ActivityIndicator, Button, Text, View, StyleSheet } from 'react-native';
|
||||||
|
import { BarCodeScanner } from 'expo';
|
||||||
|
import { createStackNavigator, withNavigationFocus } from 'react-navigation';
|
||||||
|
|
||||||
|
const IndexScreen = ({ navigation }) => (
|
||||||
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
title="Go to BarCodeScanner"
|
||||||
|
onPress={() => navigation.navigate('BarCode')}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title="Return to other examples"
|
||||||
|
onPress={() => navigation.navigate('Home')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
IndexScreen.navigationOptions = {
|
||||||
|
title: 'Lifecycle Interactions',
|
||||||
|
};
|
||||||
|
|
||||||
|
@withNavigationFocus
|
||||||
|
class BarCodeScreen extends React.Component {
|
||||||
|
handleBarCodeScanned = data => {
|
||||||
|
console.log('scanned...');
|
||||||
|
this.props.navigation.navigate('Info', { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<BarCodeScanner
|
||||||
|
onBarCodeScanned={this.props.isFocused ? this.handleBarCodeScanned : null}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BarCodeScreen.navigationOptions = {
|
||||||
|
title: 'BarCodeView',
|
||||||
|
};
|
||||||
|
|
||||||
|
class InfoScreen extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text>{JSON.stringify(this.props.navigation.getParam('data'))}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoScreen.navigationOptions = {
|
||||||
|
title: 'Info',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createStackNavigator(
|
||||||
|
{
|
||||||
|
Index: IndexScreen,
|
||||||
|
BarCode: BarCodeScreen,
|
||||||
|
Info: InfoScreen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialRouteName: 'Index',
|
||||||
|
}
|
||||||
|
);
|
||||||
59
packages/stack/example/src/ModalStack.js
Normal file
59
packages/stack/example/src/ModalStack.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, View, Text } from 'react-native';
|
||||||
|
import { createStackNavigator } from 'react-navigation-stack';
|
||||||
|
|
||||||
|
class ListScreen extends React.Component {
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initialRouteName: 'List',
|
||||||
|
mode: 'modal',
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -5,7 +5,14 @@ import { createStackNavigator } from 'react-navigation-stack';
|
|||||||
class ListScreen extends React.Component {
|
class ListScreen extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text>List Screen</Text>
|
<Text>List Screen</Text>
|
||||||
<Text>A list may go here</Text>
|
<Text>A list may go here</Text>
|
||||||
<Button
|
<Button
|
||||||
@@ -24,7 +31,14 @@ class ListScreen extends React.Component {
|
|||||||
class DetailsScreen extends React.Component {
|
class DetailsScreen extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Text>Details Screen</Text>
|
<Text>Details Screen</Text>
|
||||||
<Button
|
<Button
|
||||||
title="Go to Details... again"
|
title="Go to Details... again"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "react-navigation-stack",
|
"name": "react-navigation-stack",
|
||||||
"version": "0.6.0",
|
"version": "1.0.0-alpha.13",
|
||||||
"description": "Stack navigator component for React Navigation",
|
"description": "Stack navigator component for React Navigation",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "eslint . --fix",
|
"format": "eslint . --fix",
|
||||||
"precommit": "yarn lint && yarn test",
|
|
||||||
"build": "babel --no-babelrc --plugins=syntax-jsx,syntax-class-properties,syntax-object-rest-spread,transform-flow-strip-types src --copy-files --out-dir dist --ignore '**/__tests__/**'",
|
"build": "babel --no-babelrc --plugins=syntax-jsx,syntax-class-properties,syntax-object-rest-spread,transform-flow-strip-types src --copy-files --out-dir dist --ignore '**/__tests__/**'",
|
||||||
"prepare": "yarn build"
|
"prepare": "yarn build"
|
||||||
},
|
},
|
||||||
@@ -62,6 +61,8 @@
|
|||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*",
|
"react-native": "*",
|
||||||
"react-native-screens": "^1.0.0 || ^1.0.0-alpha",
|
"react-native-screens": "^1.0.0 || ^1.0.0-alpha",
|
||||||
|
"react-native-gesture-handler": "^1.0.0",
|
||||||
|
"react-native-reanimated": "^1.0.0 || ^1.0.0-alpha",
|
||||||
"react-navigation": ">=2.0 || ^2.0.0-beta"
|
"react-navigation": ">=2.0 || ^2.0.0-beta"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|||||||
@@ -52,4 +52,7 @@ module.exports = {
|
|||||||
get ScenesReducer() {
|
get ScenesReducer() {
|
||||||
return require('./views/ScenesReducer').default;
|
return require('./views/ScenesReducer').default;
|
||||||
},
|
},
|
||||||
|
get StackGestureContext() {
|
||||||
|
return require('./utils/StackGestureContext').default;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
3
packages/stack/src/utils/StackGestureContext.js
Normal file
3
packages/stack/src/utils/StackGestureContext.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default React.createContext(null);
|
||||||
@@ -523,7 +523,6 @@ class Header extends React.PureComponent {
|
|||||||
});
|
});
|
||||||
const scenesProps = Object.values(scenesByIndex).map(scene => ({
|
const scenesProps = Object.values(scenesByIndex).map(scene => ({
|
||||||
position: this.props.position,
|
position: this.props.position,
|
||||||
progress: this.props.progress,
|
|
||||||
scene,
|
scene,
|
||||||
}));
|
}));
|
||||||
appBar = scenesProps.map(this._renderHeader, this);
|
appBar = scenesProps.map(this._renderHeader, this);
|
||||||
@@ -531,7 +530,6 @@ class Header extends React.PureComponent {
|
|||||||
} else {
|
} else {
|
||||||
const headerProps = {
|
const headerProps = {
|
||||||
position: new Animated.Value(this.props.scene.index),
|
position: new Animated.Value(this.props.scene.index),
|
||||||
progress: new Animated.Value(0),
|
|
||||||
scene: this.props.scene,
|
scene: this.props.scene,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ class HeaderBackButton extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_renderBackImage() {
|
_renderBackImage() {
|
||||||
const { backImage, title, tintColor } = this.props;
|
const { backImage, backTitleVisible, tintColor } = this.props;
|
||||||
|
let title = this._getTitleText();
|
||||||
|
|
||||||
let BackImage;
|
let BackImage;
|
||||||
let props;
|
let props;
|
||||||
@@ -51,7 +52,7 @@ class HeaderBackButton extends React.PureComponent {
|
|||||||
props = {
|
props = {
|
||||||
style: [
|
style: [
|
||||||
styles.icon,
|
styles.icon,
|
||||||
!!title && styles.iconWithTitle,
|
!!backTitleVisible && styles.iconWithTitle,
|
||||||
!!tintColor && { tintColor },
|
!!tintColor && { tintColor },
|
||||||
],
|
],
|
||||||
source: defaultBackImage,
|
source: defaultBackImage,
|
||||||
@@ -61,33 +62,33 @@ class HeaderBackButton extends React.PureComponent {
|
|||||||
return <BackImage {...props} />;
|
return <BackImage {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getTitleText = () => {
|
||||||
|
const { width, title, truncatedTitle } = this.props;
|
||||||
|
|
||||||
|
let { initialTextWidth } = this.state;
|
||||||
|
|
||||||
|
if (title === null) {
|
||||||
|
return null;
|
||||||
|
} else if (!title) {
|
||||||
|
return truncatedTitle;
|
||||||
|
} else if (initialTextWidth && width && initialTextWidth > width) {
|
||||||
|
return truncatedTitle;
|
||||||
|
} else {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_maybeRenderTitle() {
|
_maybeRenderTitle() {
|
||||||
const {
|
const {
|
||||||
layoutPreset,
|
|
||||||
backTitleVisible,
|
backTitleVisible,
|
||||||
width,
|
|
||||||
title,
|
|
||||||
titleStyle,
|
titleStyle,
|
||||||
tintColor,
|
tintColor,
|
||||||
truncatedTitle,
|
truncatedTitle,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const renderTruncated =
|
let backTitleText = this._getTitleText();
|
||||||
this.state.initialTextWidth && width
|
|
||||||
? this.state.initialTextWidth > width
|
|
||||||
: false;
|
|
||||||
|
|
||||||
const backButtonTitle = renderTruncated ? truncatedTitle : title;
|
if (!backTitleVisible || backTitleText === null) {
|
||||||
|
|
||||||
// If the left preset is used and we aren't on Android, then we
|
|
||||||
// default to disabling the label
|
|
||||||
const titleDefaultsToDisabled =
|
|
||||||
layoutPreset === 'left' ||
|
|
||||||
Platform.OS === 'android' ||
|
|
||||||
typeof backButtonTitle !== 'string';
|
|
||||||
|
|
||||||
// If the title is explicitly enabled then we respect that
|
|
||||||
if (titleDefaultsToDisabled && !backTitleVisible) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ class HeaderBackButton extends React.PureComponent {
|
|||||||
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
|
style={[styles.title, !!tintColor && { color: tintColor }, titleStyle]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{backButtonTitle}
|
{this._getTitleText()}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet, Platform } from 'react-native';
|
import { Animated, StyleSheet, Platform } from 'react-native';
|
||||||
import { Screen } from 'react-native-screens';
|
import { Screen } from 'react-native-screens';
|
||||||
import createPointerEventsContainer from './createPointerEventsContainer';
|
import createPointerEventsContainer from './createPointerEventsContainer';
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ function getAccessibilityProps(isActive) {
|
|||||||
class Card extends React.Component {
|
class Card extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
animatedStyle,
|
||||||
children,
|
children,
|
||||||
pointerEvents,
|
pointerEvents,
|
||||||
style,
|
style,
|
||||||
@@ -42,28 +43,61 @@ class Card extends React.Component {
|
|||||||
extrapolate: 'clamp',
|
extrapolate: 'clamp',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
shadowOpacity,
|
||||||
|
overlayOpacity,
|
||||||
|
...containerAnimatedStyle
|
||||||
|
} = animatedStyle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Screen
|
<Screen
|
||||||
pointerEvents={pointerEvents}
|
pointerEvents={pointerEvents}
|
||||||
onComponentRef={this.props.onComponentRef}
|
onComponentRef={this.props.onComponentRef}
|
||||||
style={[transparent ? styles.transparent : styles.main, style]}
|
style={[StyleSheet.absoluteFill, containerAnimatedStyle, style]}
|
||||||
active={active}
|
active={active}
|
||||||
{...getAccessibilityProps(isActive)}
|
|
||||||
>
|
>
|
||||||
{children}
|
{shadowOpacity ? (
|
||||||
|
<Animated.View
|
||||||
|
style={[styles.shadow, { shadowOpacity }]}
|
||||||
|
pointerEvents="none"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<Animated.View
|
||||||
|
{...getAccessibilityProps(isActive)}
|
||||||
|
style={[transparent ? styles.transparent : styles.card]}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
{overlayOpacity ? (
|
||||||
|
<Animated.View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[styles.overlay, { opacity: overlayOpacity }]}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Screen>
|
</Screen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
main: {
|
card: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
backgroundColor: '#E9E9EF',
|
backgroundColor: '#000',
|
||||||
shadowColor: 'black',
|
},
|
||||||
shadowOffset: { width: 0, height: 0 },
|
shadow: {
|
||||||
shadowOpacity: 0.2,
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 3,
|
||||||
|
position: 'absolute',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
shadowOffset: { width: -1, height: 1 },
|
||||||
shadowRadius: 5,
|
shadowRadius: 5,
|
||||||
|
shadowColor: '#000',
|
||||||
},
|
},
|
||||||
transparent: {
|
transparent: {
|
||||||
...StyleSheet.absoluteFillObject,
|
...StyleSheet.absoluteFillObject,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clamp from '../../utils/clamp';
|
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -19,11 +18,13 @@ import {
|
|||||||
NavigationProvider,
|
NavigationProvider,
|
||||||
} from 'react-navigation';
|
} from 'react-navigation';
|
||||||
import { ScreenContainer } from 'react-native-screens';
|
import { ScreenContainer } from 'react-native-screens';
|
||||||
|
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
||||||
|
|
||||||
import Card from './StackViewCard';
|
import Card from './StackViewCard';
|
||||||
import Header from '../Header/Header';
|
import Header from '../Header/Header';
|
||||||
|
|
||||||
import TransitionConfigs from './StackViewTransitionConfigs';
|
import TransitionConfigs from './StackViewTransitionConfigs';
|
||||||
|
import StackGestureContext from '../../utils/StackGestureContext';
|
||||||
|
import clamp from '../../utils/clamp';
|
||||||
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
import { supportsImprovedSpringAnimation } from '../../utils/ReactNativeFeatures';
|
||||||
|
|
||||||
const emptyFunction = () => {};
|
const emptyFunction = () => {};
|
||||||
@@ -70,17 +71,10 @@ const RESPOND_THRESHOLD = 20;
|
|||||||
/**
|
/**
|
||||||
* The distance of touch start from the edge of the screen where the gesture will be recognized
|
* The distance of touch start from the edge of the screen where the gesture will be recognized
|
||||||
*/
|
*/
|
||||||
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 25;
|
const GESTURE_RESPONSE_DISTANCE_HORIZONTAL = 50;
|
||||||
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
const GESTURE_RESPONSE_DISTANCE_VERTICAL = 135;
|
||||||
|
|
||||||
const animatedSubscribeValue = animatedValue => {
|
const USE_NATIVE_DRIVER = true;
|
||||||
if (!animatedValue.__isNative) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Object.keys(animatedValue._listeners).length === 0) {
|
|
||||||
animatedValue.addListener(emptyFunction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDefaultHeaderHeight = isLandscape => {
|
const getDefaultHeaderHeight = isLandscape => {
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
@@ -119,6 +113,9 @@ class StackViewLayout extends React.Component {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.panGestureRef = React.createRef();
|
||||||
|
this.gestureX = new Animated.Value(0);
|
||||||
|
this.gestureY = new Animated.Value(0);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// Used when card's header is null and mode is float to make transition
|
// Used when card's header is null and mode is float to make transition
|
||||||
@@ -127,6 +124,7 @@ class StackViewLayout extends React.Component {
|
|||||||
// on mount what the header height is so we have just used the most
|
// on mount what the header height is so we have just used the most
|
||||||
// common cases here.
|
// common cases here.
|
||||||
floatingHeaderHeight: getDefaultHeaderHeight(props.isLandscape),
|
floatingHeaderHeight: getDefaultHeaderHeight(props.isLandscape),
|
||||||
|
gesturePosition: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +164,12 @@ class StackViewLayout extends React.Component {
|
|||||||
{renderHeader({
|
{renderHeader({
|
||||||
...passProps,
|
...passProps,
|
||||||
...transitionProps,
|
...transitionProps,
|
||||||
|
position: this._getPosition(),
|
||||||
scene,
|
scene,
|
||||||
mode: headerMode,
|
mode: headerMode,
|
||||||
transitionPreset: this._getHeaderTransitionPreset(),
|
transitionPreset: this._getHeaderTransitionPreset(),
|
||||||
layoutPreset: this._getHeaderLayoutPreset(),
|
layoutPreset: this._getHeaderLayoutPreset(),
|
||||||
backTitleVisible: this._getheaderBackTitleVisible(),
|
backTitleVisible: this._getHeaderBackTitleVisible(),
|
||||||
leftInterpolator: headerLeftInterpolator,
|
leftInterpolator: headerLeftInterpolator,
|
||||||
titleInterpolator: headerTitleInterpolator,
|
titleInterpolator: headerTitleInterpolator,
|
||||||
rightInterpolator: headerRightInterpolator,
|
rightInterpolator: headerRightInterpolator,
|
||||||
@@ -180,19 +179,6 @@ class StackViewLayout extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_animatedSubscribe(props) {
|
|
||||||
// Hack to make this work with native driven animations. We add a single listener
|
|
||||||
// so the JS value of the following animated values gets updated. We rely on
|
|
||||||
// some Animated private APIs and not doing so would require using a bunch of
|
|
||||||
// value listeners but we'd have to remove them to not leak and I'm not sure
|
|
||||||
// when we'd do that with the current structure we have. `stopAnimation` callback
|
|
||||||
// is also broken with native animated values that have no listeners so if we
|
|
||||||
// want to remove this we have to fix this too.
|
|
||||||
animatedSubscribeValue(props.transitionProps.layout.width);
|
|
||||||
animatedSubscribeValue(props.transitionProps.layout.height);
|
|
||||||
animatedSubscribeValue(props.transitionProps.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
_reset(resetToIndex, duration) {
|
_reset(resetToIndex, duration) {
|
||||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||||
Animated.spring(this.props.transitionProps.position, {
|
Animated.spring(this.props.transitionProps.position, {
|
||||||
@@ -200,14 +186,14 @@ class StackViewLayout extends React.Component {
|
|||||||
stiffness: 5000,
|
stiffness: 5000,
|
||||||
damping: 600,
|
damping: 600,
|
||||||
mass: 3,
|
mass: 3,
|
||||||
useNativeDriver: this.props.transitionProps.position.__isNative,
|
useNativeDriver: USE_NATIVE_DRIVER,
|
||||||
}).start();
|
}).start();
|
||||||
} else {
|
} else {
|
||||||
Animated.timing(this.props.transitionProps.position, {
|
Animated.timing(this.props.transitionProps.position, {
|
||||||
toValue: resetToIndex,
|
toValue: resetToIndex,
|
||||||
duration,
|
duration,
|
||||||
easing: EaseInOut,
|
easing: EaseInOut,
|
||||||
useNativeDriver: this.props.transitionProps.position.__isNative,
|
useNativeDriver: USE_NATIVE_DRIVER,
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,203 +223,21 @@ class StackViewLayout extends React.Component {
|
|||||||
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
if (Platform.OS === 'ios' && supportsImprovedSpringAnimation()) {
|
||||||
Animated.spring(position, {
|
Animated.spring(position, {
|
||||||
toValue,
|
toValue,
|
||||||
stiffness: 5000,
|
stiffness: 7000,
|
||||||
damping: 600,
|
damping: 600,
|
||||||
mass: 3,
|
mass: 3,
|
||||||
useNativeDriver: position.__isNative,
|
useNativeDriver: USE_NATIVE_DRIVER,
|
||||||
}).start(onCompleteAnimation);
|
}).start(onCompleteAnimation);
|
||||||
} else {
|
} else {
|
||||||
Animated.timing(position, {
|
Animated.timing(position, {
|
||||||
toValue,
|
toValue,
|
||||||
duration,
|
duration,
|
||||||
easing: EaseInOut,
|
easing: EaseInOut,
|
||||||
useNativeDriver: position.__isNative,
|
useNativeDriver: USE_NATIVE_DRIVER,
|
||||||
}).start(onCompleteAnimation);
|
}).start(onCompleteAnimation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_panResponder = PanResponder.create({
|
|
||||||
onPanResponderTerminate: () => {
|
|
||||||
const { navigation } = this.props.transitionProps;
|
|
||||||
const { index } = navigation.state;
|
|
||||||
this._isResponding = false;
|
|
||||||
this._reset(index, 0);
|
|
||||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
|
||||||
},
|
|
||||||
onPanResponderGrant: () => {
|
|
||||||
const {
|
|
||||||
transitionProps: { navigation, position, scene },
|
|
||||||
} = this.props;
|
|
||||||
const { index } = navigation.state;
|
|
||||||
|
|
||||||
if (index !== scene.index) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
position.stopAnimation(value => {
|
|
||||||
this._isResponding = true;
|
|
||||||
this._gestureStartValue = value;
|
|
||||||
});
|
|
||||||
this.props.onGestureBegin && this.props.onGestureBegin();
|
|
||||||
},
|
|
||||||
onMoveShouldSetPanResponder: (event, gesture) => {
|
|
||||||
const {
|
|
||||||
transitionProps: { navigation, layout, scene },
|
|
||||||
mode,
|
|
||||||
} = this.props;
|
|
||||||
const { index } = navigation.state;
|
|
||||||
const isVertical = mode === 'modal';
|
|
||||||
const { options } = scene.descriptor;
|
|
||||||
const gestureDirection = options.gestureDirection;
|
|
||||||
|
|
||||||
const gestureDirectionInverted =
|
|
||||||
typeof gestureDirection === 'string'
|
|
||||||
? gestureDirection === 'inverted'
|
|
||||||
: I18nManager.isRTL;
|
|
||||||
|
|
||||||
if (index !== scene.index) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const immediateIndex =
|
|
||||||
this._immediateIndex == null ? index : this._immediateIndex;
|
|
||||||
const currentDragDistance = gesture[isVertical ? 'dy' : 'dx'];
|
|
||||||
const currentDragPosition =
|
|
||||||
event.nativeEvent[isVertical ? 'pageY' : 'pageX'];
|
|
||||||
const axisLength = isVertical
|
|
||||||
? layout.height.__getValue()
|
|
||||||
: layout.width.__getValue();
|
|
||||||
const axisHasBeenMeasured = !!axisLength;
|
|
||||||
|
|
||||||
// Measure the distance from the touch to the edge of the screen
|
|
||||||
const screenEdgeDistance = gestureDirectionInverted
|
|
||||||
? axisLength - (currentDragPosition - currentDragDistance)
|
|
||||||
: currentDragPosition - currentDragDistance;
|
|
||||||
// Compare to the gesture distance relavant to card or modal
|
|
||||||
|
|
||||||
const {
|
|
||||||
gestureResponseDistance: userGestureResponseDistance = {},
|
|
||||||
} = options;
|
|
||||||
const gestureResponseDistance = isVertical
|
|
||||||
? userGestureResponseDistance.vertical ||
|
|
||||||
GESTURE_RESPONSE_DISTANCE_VERTICAL
|
|
||||||
: userGestureResponseDistance.horizontal ||
|
|
||||||
GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
|
||||||
// GESTURE_RESPONSE_DISTANCE is about 25 or 30. Or 135 for modals
|
|
||||||
if (screenEdgeDistance > gestureResponseDistance) {
|
|
||||||
// Reject touches that started in the middle of the screen
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasDraggedEnough =
|
|
||||||
Math.abs(currentDragDistance) > RESPOND_THRESHOLD;
|
|
||||||
|
|
||||||
const isOnFirstCard = immediateIndex === 0;
|
|
||||||
const shouldSetResponder =
|
|
||||||
hasDraggedEnough && axisHasBeenMeasured && !isOnFirstCard;
|
|
||||||
return shouldSetResponder;
|
|
||||||
},
|
|
||||||
onPanResponderMove: (event, gesture) => {
|
|
||||||
const {
|
|
||||||
transitionProps: { navigation, position, layout, scene },
|
|
||||||
mode,
|
|
||||||
} = this.props;
|
|
||||||
const { index } = navigation.state;
|
|
||||||
const isVertical = mode === 'modal';
|
|
||||||
const { options } = scene.descriptor;
|
|
||||||
const gestureDirection = options.gestureDirection;
|
|
||||||
|
|
||||||
const gestureDirectionInverted =
|
|
||||||
typeof gestureDirection === 'string'
|
|
||||||
? gestureDirection === 'inverted'
|
|
||||||
: I18nManager.isRTL;
|
|
||||||
|
|
||||||
// Handle the moving touches for our granted responder
|
|
||||||
const startValue = this._gestureStartValue;
|
|
||||||
const axis = isVertical ? 'dy' : 'dx';
|
|
||||||
const axisDistance = isVertical
|
|
||||||
? layout.height.__getValue()
|
|
||||||
: layout.width.__getValue();
|
|
||||||
const currentValue =
|
|
||||||
axis === 'dx' && gestureDirectionInverted
|
|
||||||
? startValue + gesture[axis] / axisDistance
|
|
||||||
: startValue - gesture[axis] / axisDistance;
|
|
||||||
const value = clamp(index - 1, currentValue, index);
|
|
||||||
position.setValue(value);
|
|
||||||
},
|
|
||||||
onPanResponderTerminationRequest: () =>
|
|
||||||
// Returning false will prevent other views from becoming responder while
|
|
||||||
// the navigation view is the responder (mid-gesture)
|
|
||||||
false,
|
|
||||||
onPanResponderRelease: (event, gesture) => {
|
|
||||||
const {
|
|
||||||
transitionProps: { navigation, position, layout, scene },
|
|
||||||
mode,
|
|
||||||
} = this.props;
|
|
||||||
const { index } = navigation.state;
|
|
||||||
const isVertical = mode === 'modal';
|
|
||||||
const { options } = scene.descriptor;
|
|
||||||
const gestureDirection = options.gestureDirection;
|
|
||||||
|
|
||||||
const gestureDirectionInverted =
|
|
||||||
typeof gestureDirection === 'string'
|
|
||||||
? gestureDirection === 'inverted'
|
|
||||||
: I18nManager.isRTL;
|
|
||||||
|
|
||||||
if (!this._isResponding) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._isResponding = false;
|
|
||||||
|
|
||||||
const immediateIndex =
|
|
||||||
this._immediateIndex == null ? index : this._immediateIndex;
|
|
||||||
|
|
||||||
// Calculate animate duration according to gesture speed and moved distance
|
|
||||||
const axisDistance = isVertical
|
|
||||||
? layout.height.__getValue()
|
|
||||||
: layout.width.__getValue();
|
|
||||||
const movementDirection = gestureDirectionInverted ? -1 : 1;
|
|
||||||
const movedDistance =
|
|
||||||
movementDirection * gesture[isVertical ? 'dy' : 'dx'];
|
|
||||||
const gestureVelocity =
|
|
||||||
movementDirection * gesture[isVertical ? 'vy' : 'vx'];
|
|
||||||
const defaultVelocity = axisDistance / ANIMATION_DURATION;
|
|
||||||
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
|
|
||||||
const resetDuration = gestureDirectionInverted
|
|
||||||
? (axisDistance - movedDistance) / velocity
|
|
||||||
: movedDistance / velocity;
|
|
||||||
const goBackDuration = gestureDirectionInverted
|
|
||||||
? movedDistance / velocity
|
|
||||||
: (axisDistance - movedDistance) / velocity;
|
|
||||||
|
|
||||||
// To asyncronously get the current animated value, we need to run stopAnimation:
|
|
||||||
position.stopAnimation(value => {
|
|
||||||
// If the speed of the gesture release is significant, use that as the indication
|
|
||||||
// of intent
|
|
||||||
if (gestureVelocity < -0.5) {
|
|
||||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
|
||||||
this._reset(immediateIndex, resetDuration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (gestureVelocity > 0.5) {
|
|
||||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
|
||||||
this._goBack(immediateIndex, goBackDuration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
|
|
||||||
// and the back will happen.
|
|
||||||
if (value <= index - POSITION_THRESHOLD) {
|
|
||||||
this.props.onGestureFinish && this.props.onGestureFinish();
|
|
||||||
this._goBack(immediateIndex, goBackDuration);
|
|
||||||
} else {
|
|
||||||
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
|
||||||
this._reset(immediateIndex, resetDuration);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
_onFloatingHeaderLayout = e => {
|
_onFloatingHeaderLayout = e => {
|
||||||
this.setState({ floatingHeaderHeight: e.nativeEvent.layout.height });
|
this.setState({ floatingHeaderHeight: e.nativeEvent.layout.height });
|
||||||
};
|
};
|
||||||
@@ -455,33 +259,316 @@ class StackViewLayout extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
transitionProps: { scene, scenes },
|
transitionProps: { navigation, scene, scenes },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { options } = scene.descriptor;
|
const { options } = scene.descriptor;
|
||||||
|
const { index } = navigation.state;
|
||||||
|
|
||||||
const gesturesEnabled =
|
const gesturesEnabled =
|
||||||
typeof options.gesturesEnabled === 'boolean'
|
typeof options.gesturesEnabled === 'boolean'
|
||||||
? options.gesturesEnabled
|
? options.gesturesEnabled
|
||||||
: Platform.OS === 'ios';
|
: Platform.OS === 'ios';
|
||||||
|
|
||||||
const responder = !gesturesEnabled ? null : this._panResponder;
|
|
||||||
|
|
||||||
const handlers = gesturesEnabled ? responder.panHandlers : {};
|
|
||||||
const containerStyle = [
|
const containerStyle = [
|
||||||
styles.container,
|
styles.container,
|
||||||
this._getTransitionConfig().containerStyle,
|
this._getTransitionConfig().containerStyle,
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View {...handlers} style={containerStyle}>
|
<PanGestureHandler
|
||||||
<ScreenContainer style={styles.scenes}>
|
{...this._gestureActivationCriteria()}
|
||||||
{scenes.map(s => this._renderCard(s))}
|
ref={this.panGestureRef}
|
||||||
</ScreenContainer>
|
onGestureEvent={Animated.event(
|
||||||
{floatingHeader}
|
[
|
||||||
</View>
|
{
|
||||||
|
nativeEvent: {
|
||||||
|
translationX: this.gestureX,
|
||||||
|
translationY: this.gestureY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
useNativeDriver: USE_NATIVE_DRIVER,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onHandlerStateChange={this._handlePanGestureStateChange}
|
||||||
|
enabled={index > 0 && gesturesEnabled}
|
||||||
|
>
|
||||||
|
<Animated.View style={containerStyle}>
|
||||||
|
<StackGestureContext.Provider value={this.panGestureRef}>
|
||||||
|
<ScreenContainer style={styles.scenes}>
|
||||||
|
{scenes.map(s => this._renderCard(s))}
|
||||||
|
</ScreenContainer>
|
||||||
|
{floatingHeader}
|
||||||
|
</StackGestureContext.Provider>
|
||||||
|
</Animated.View>
|
||||||
|
</PanGestureHandler>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gestureActivationCriteria = () => {
|
||||||
|
let { layout } = this.props.transitionProps;
|
||||||
|
|
||||||
|
if (this._isMotionVertical()) {
|
||||||
|
let height = layout.height.__getValue();
|
||||||
|
|
||||||
|
return {
|
||||||
|
maxDeltaX: 15,
|
||||||
|
minOffsetY: 5,
|
||||||
|
hitSlop: { bottom: -height + GESTURE_RESPONSE_DISTANCE_VERTICAL },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let width = layout.width.__getValue();
|
||||||
|
let hitSlop = -width + GESTURE_RESPONSE_DISTANCE_HORIZONTAL;
|
||||||
|
|
||||||
|
return {
|
||||||
|
minOffsetX: this._isMotionInverted() ? -5 : 5,
|
||||||
|
maxDeltaY: 20,
|
||||||
|
hitSlop: this._isMotionInverted()
|
||||||
|
? { left: hitSlop }
|
||||||
|
: { right: hitSlop },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Without using Reanimated it's not possible to do all of the following
|
||||||
|
// stuff with native driver.
|
||||||
|
_handlePanGestureEvent = ({ nativeEvent }) => {
|
||||||
|
if (this._isMotionVertical()) {
|
||||||
|
this._handleVerticalPan(nativeEvent);
|
||||||
|
} else {
|
||||||
|
this._handleHorizontalPan(nativeEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_isMotionVertical = () => {
|
||||||
|
return this.props.mode === 'modal';
|
||||||
|
};
|
||||||
|
|
||||||
|
// This only currently applies to the horizontal gesture!
|
||||||
|
_isMotionInverted = () => {
|
||||||
|
const {
|
||||||
|
transitionProps: { scene },
|
||||||
|
} = this.props;
|
||||||
|
const { options } = scene.descriptor;
|
||||||
|
const { gestureDirection } = options;
|
||||||
|
|
||||||
|
return typeof gestureDirection === 'string'
|
||||||
|
? gestureDirection === 'inverted'
|
||||||
|
: I18nManager.isRTL;
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleHorizontalPan = nativeEvent => {
|
||||||
|
let value = this._computeHorizontalGestureValue(nativeEvent);
|
||||||
|
this.props.transitionProps.position.setValue(Math.max(0, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
_computeHorizontalGestureValue = nativeEvent => {
|
||||||
|
let {
|
||||||
|
transitionProps: { navigation, layout },
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let { index } = navigation.state;
|
||||||
|
|
||||||
|
// TODO: remove this __getValue!
|
||||||
|
let distance = layout.width.__getValue();
|
||||||
|
|
||||||
|
let translationX = this._isMotionInverted()
|
||||||
|
? -1 * nativeEvent.translationX
|
||||||
|
: nativeEvent.translationX;
|
||||||
|
|
||||||
|
let value = index - translationX / distance;
|
||||||
|
return clamp(index - 1, value, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
_computeVerticalGestureValue = nativeEvent => {
|
||||||
|
let {
|
||||||
|
transitionProps: { navigation, layout },
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
let { index } = navigation.state;
|
||||||
|
|
||||||
|
// TODO: remove this __getValue!
|
||||||
|
let distance = layout.height.__getValue();
|
||||||
|
|
||||||
|
let translationY = nativeEvent.translationY;
|
||||||
|
let value = index - nativeEvent.translationY / distance;
|
||||||
|
return clamp(index - 1, value, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
_handlePanGestureStateChange = ({ nativeEvent }) => {
|
||||||
|
if (nativeEvent.oldState === State.ACTIVE) {
|
||||||
|
if (this._isMotionVertical()) {
|
||||||
|
this._handleReleaseVertical(nativeEvent);
|
||||||
|
} else {
|
||||||
|
this._handleReleaseHorizontal(nativeEvent);
|
||||||
|
}
|
||||||
|
} else if (nativeEvent.state === State.ACTIVE) {
|
||||||
|
if (this._isMotionVertical()) {
|
||||||
|
this._handleActivateGestureVertical(nativeEvent);
|
||||||
|
} else {
|
||||||
|
this._handleActivateGestureHorizontal(nativeEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleActivateGestureHorizontal = nativeEvent => {
|
||||||
|
let { index } = this.props.transitionProps.navigation.state;
|
||||||
|
|
||||||
|
if (this._isMotionInverted()) {
|
||||||
|
this.setState({
|
||||||
|
gesturePosition: Animated.add(
|
||||||
|
index,
|
||||||
|
Animated.divide(
|
||||||
|
this.gestureX,
|
||||||
|
this.props.transitionProps.layout.width
|
||||||
|
)
|
||||||
|
).interpolate({
|
||||||
|
inputRange: [index - 1, index],
|
||||||
|
outputRange: [index - 1, index],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
gesturePosition: Animated.add(
|
||||||
|
index,
|
||||||
|
Animated.multiply(
|
||||||
|
-1,
|
||||||
|
Animated.divide(
|
||||||
|
this.gestureX,
|
||||||
|
this.props.transitionProps.layout.width
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).interpolate({
|
||||||
|
inputRange: [index - 1, index],
|
||||||
|
outputRange: [index - 1, index],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleActivateGestureVertical = nativeEvent => {
|
||||||
|
let { index } = this.props.transitionProps.navigation.state;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
gesturePosition: Animated.add(
|
||||||
|
index,
|
||||||
|
Animated.multiply(
|
||||||
|
-1,
|
||||||
|
Animated.divide(
|
||||||
|
this.gestureY,
|
||||||
|
this.props.transitionProps.layout.height
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).interpolate({
|
||||||
|
inputRange: [index - 1, index],
|
||||||
|
outputRange: [index - 1, index],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleReleaseHorizontal = nativeEvent => {
|
||||||
|
const {
|
||||||
|
transitionProps: { navigation, position, layout, scene },
|
||||||
|
mode,
|
||||||
|
} = this.props;
|
||||||
|
const { index } = navigation.state;
|
||||||
|
const immediateIndex =
|
||||||
|
this._immediateIndex == null ? index : this._immediateIndex;
|
||||||
|
|
||||||
|
// Calculate animate duration according to gesture speed and moved distance
|
||||||
|
const distance = layout.width.__getValue();
|
||||||
|
const movementDirection = this._isMotionInverted() ? -1 : 1;
|
||||||
|
const movedDistance = movementDirection * nativeEvent.translationX;
|
||||||
|
const gestureVelocity = movementDirection * nativeEvent.velocityX;
|
||||||
|
const defaultVelocity = distance / ANIMATION_DURATION;
|
||||||
|
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
|
||||||
|
const resetDuration = this._isMotionInverted()
|
||||||
|
? (distance - movedDistance) / velocity
|
||||||
|
: movedDistance / velocity;
|
||||||
|
const goBackDuration = this._isMotionInverted()
|
||||||
|
? movedDistance / velocity
|
||||||
|
: (distance - movedDistance) / velocity;
|
||||||
|
|
||||||
|
// Get the current position value and reset to using the statically driven
|
||||||
|
// (rather than gesture driven) position.
|
||||||
|
let value = this._computeHorizontalGestureValue(nativeEvent);
|
||||||
|
position.setValue(value);
|
||||||
|
this.setState({ gesturePosition: null }, () => {
|
||||||
|
// If the speed of the gesture release is significant, use that as the indication
|
||||||
|
// of intent
|
||||||
|
if (gestureVelocity < -50) {
|
||||||
|
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||||
|
this._reset(immediateIndex, resetDuration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gestureVelocity > 50) {
|
||||||
|
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||||
|
this._goBack(immediateIndex, goBackDuration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
|
||||||
|
// and the back will happen.
|
||||||
|
if (value <= index - POSITION_THRESHOLD) {
|
||||||
|
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||||
|
this._goBack(immediateIndex, goBackDuration);
|
||||||
|
} else {
|
||||||
|
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||||
|
this._reset(immediateIndex, resetDuration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_handleReleaseVertical = nativeEvent => {
|
||||||
|
const {
|
||||||
|
transitionProps: { navigation, position, layout, scene },
|
||||||
|
mode,
|
||||||
|
} = this.props;
|
||||||
|
const { index } = navigation.state;
|
||||||
|
const immediateIndex =
|
||||||
|
this._immediateIndex == null ? index : this._immediateIndex;
|
||||||
|
|
||||||
|
// Calculate animate duration according to gesture speed and moved distance
|
||||||
|
const distance = layout.height.__getValue();
|
||||||
|
const movedDistance = nativeEvent.translationY;
|
||||||
|
const gestureVelocity = nativeEvent.velocityY;
|
||||||
|
const defaultVelocity = distance / ANIMATION_DURATION;
|
||||||
|
const velocity = Math.max(Math.abs(gestureVelocity), defaultVelocity);
|
||||||
|
const resetDuration = movedDistance / velocity;
|
||||||
|
const goBackDuration = (distance - movedDistance) / velocity;
|
||||||
|
|
||||||
|
let value = this._computeVerticalGestureValue(nativeEvent);
|
||||||
|
position.setValue(value);
|
||||||
|
this.setState({ gesturePosition: null }, () => {
|
||||||
|
// If the speed of the gesture release is significant, use that as the indication
|
||||||
|
// of intent
|
||||||
|
if (gestureVelocity < -50) {
|
||||||
|
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||||
|
this._reset(immediateIndex, resetDuration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gestureVelocity > 50) {
|
||||||
|
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||||
|
this._goBack(immediateIndex, goBackDuration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then filter based on the distance the screen was moved. Over a third of the way swiped,
|
||||||
|
// and the back will happen.
|
||||||
|
if (value <= index - POSITION_THRESHOLD) {
|
||||||
|
this.props.onGestureFinish && this.props.onGestureFinish();
|
||||||
|
this._goBack(immediateIndex, goBackDuration);
|
||||||
|
} else {
|
||||||
|
this.props.onGestureCanceled && this.props.onGestureCanceled();
|
||||||
|
this._reset(immediateIndex, resetDuration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
_getHeaderMode() {
|
_getHeaderMode() {
|
||||||
if (this.props.headerMode) {
|
if (this.props.headerMode) {
|
||||||
return this.props.headerMode;
|
return this.props.headerMode;
|
||||||
@@ -551,10 +638,17 @@ class StackViewLayout extends React.Component {
|
|||||||
return 'fade-in-place';
|
return 'fade-in-place';
|
||||||
}
|
}
|
||||||
|
|
||||||
_getheaderBackTitleVisible() {
|
_getHeaderBackTitleVisible() {
|
||||||
const { headerBackTitleVisible } = this.props;
|
const { headerBackTitleVisible } = this.props;
|
||||||
|
const layoutPreset = this._getHeaderLayoutPreset();
|
||||||
|
|
||||||
return headerBackTitleVisible;
|
// Even when we align to center on Android, people should need to opt-in to
|
||||||
|
// showing the back title
|
||||||
|
const enabledByDefault = !(layoutPreset === 'left' || Platform.OS === 'android');
|
||||||
|
|
||||||
|
return typeof headerBackTitleVisible === 'boolean'
|
||||||
|
? headerBackTitleVisible
|
||||||
|
: enabledByDefault;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderInnerScene(scene) {
|
_renderInnerScene(scene) {
|
||||||
@@ -591,18 +685,38 @@ class StackViewLayout extends React.Component {
|
|||||||
|
|
||||||
return TransitionConfigs.getTransitionConfig(
|
return TransitionConfigs.getTransitionConfig(
|
||||||
this.props.transitionConfig,
|
this.props.transitionConfig,
|
||||||
this.props.transitionProps,
|
{
|
||||||
|
...this.props.transitionProps,
|
||||||
|
position: this._getPosition(),
|
||||||
|
},
|
||||||
this.props.lastTransitionProps,
|
this.props.lastTransitionProps,
|
||||||
isModal
|
isModal
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_getPosition = () => {
|
||||||
|
if (!this.state.gesturePosition) {
|
||||||
|
return this.props.transitionProps.position;
|
||||||
|
} else {
|
||||||
|
let { gesturePosition } = this.state;
|
||||||
|
let staticPosition = Animated.add(
|
||||||
|
this.props.transitionProps.position,
|
||||||
|
Animated.multiply(-1, this.props.transitionProps.position)
|
||||||
|
);
|
||||||
|
return Animated.add(gesturePosition, staticPosition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_renderCard = scene => {
|
_renderCard = scene => {
|
||||||
const { screenInterpolator } = this._getTransitionConfig();
|
const { screenInterpolator } = this._getTransitionConfig();
|
||||||
|
|
||||||
const style =
|
const style =
|
||||||
screenInterpolator &&
|
screenInterpolator &&
|
||||||
screenInterpolator({ ...this.props.transitionProps, scene });
|
screenInterpolator({
|
||||||
|
...this.props.transitionProps,
|
||||||
|
position: this._getPosition(),
|
||||||
|
scene,
|
||||||
|
});
|
||||||
|
|
||||||
// When using a floating header, we need to add some top
|
// When using a floating header, we need to add some top
|
||||||
// padding on the scene.
|
// padding on the scene.
|
||||||
@@ -618,8 +732,11 @@ class StackViewLayout extends React.Component {
|
|||||||
<Card
|
<Card
|
||||||
{...this.props.transitionProps}
|
{...this.props.transitionProps}
|
||||||
key={`card_${scene.key}`}
|
key={`card_${scene.key}`}
|
||||||
|
position={this._getPosition()}
|
||||||
|
realPosition={this.props.transitionProps.position}
|
||||||
|
animatedStyle={style}
|
||||||
transparent={this.props.transparentCard}
|
transparent={this.props.transparentCard}
|
||||||
style={[style, { paddingTop }, this.props.cardStyle]}
|
style={[{ paddingTop }, this.props.cardStyle]}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
>
|
>
|
||||||
{this._renderInnerScene(scene)}
|
{this._renderInnerScene(scene)}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { I18nManager } from 'react-native';
|
import { I18nManager } from 'react-native';
|
||||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||||
|
|
||||||
|
const EPS = 1e-5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility that builds the style for the card in the cards stack.
|
* Utility that builds the style for the card in the cards stack.
|
||||||
*
|
*
|
||||||
@@ -47,11 +49,6 @@ function forHorizontal(props) {
|
|||||||
|
|
||||||
const { first, last } = interpolate;
|
const { first, last } = interpolate;
|
||||||
const index = scene.index;
|
const index = scene.index;
|
||||||
const opacity = position.interpolate({
|
|
||||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
|
||||||
outputRange: [0, 1, 1, 0.85, 0],
|
|
||||||
extrapolate: 'clamp',
|
|
||||||
});
|
|
||||||
|
|
||||||
const width = layout.initWidth;
|
const width = layout.initWidth;
|
||||||
const translateX = position.interpolate({
|
const translateX = position.interpolate({
|
||||||
@@ -61,11 +58,25 @@ function forHorizontal(props) {
|
|||||||
: [width, 0, width * -0.3],
|
: [width, 0, width * -0.3],
|
||||||
extrapolate: 'clamp',
|
extrapolate: 'clamp',
|
||||||
});
|
});
|
||||||
const translateY = 0;
|
|
||||||
|
// TODO: add flag to disable shadow
|
||||||
|
const shadowOpacity = position.interpolate({
|
||||||
|
inputRange: [first, index, last],
|
||||||
|
outputRange: [0, 0.7, 0],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: disable overlay by default, add flag to enable
|
||||||
|
let overlayOpacity = position.interpolate({
|
||||||
|
inputRange: [index, last - 0.5, last, last + EPS],
|
||||||
|
outputRange: [0, 0.05, 0.05, 0],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
opacity,
|
transform: [{ translateX }],
|
||||||
transform: [{ translateX }, { translateY }],
|
overlayOpacity,
|
||||||
|
shadowOpacity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,23 +95,15 @@ function forVertical(props) {
|
|||||||
|
|
||||||
const { first, last } = interpolate;
|
const { first, last } = interpolate;
|
||||||
const index = scene.index;
|
const index = scene.index;
|
||||||
const opacity = position.interpolate({
|
|
||||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
|
||||||
outputRange: [0, 1, 1, 0.85, 0],
|
|
||||||
extrapolate: 'clamp',
|
|
||||||
});
|
|
||||||
|
|
||||||
const height = layout.initHeight;
|
const height = layout.initHeight;
|
||||||
const translateY = position.interpolate({
|
const translateY = position.interpolate({
|
||||||
inputRange: [first, index, last],
|
inputRange: [first, index, last],
|
||||||
outputRange: [height, 0, 0],
|
outputRange: [height, 0, 0],
|
||||||
extrapolate: 'clamp',
|
extrapolate: 'clamp',
|
||||||
});
|
});
|
||||||
const translateX = 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
opacity,
|
transform: [{ translateY }],
|
||||||
transform: [{ translateX }, { translateY }],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const SlideFromRightIOS = {
|
|||||||
transitionSpec: IOSTransitionSpec,
|
transitionSpec: IOSTransitionSpec,
|
||||||
screenInterpolator: StyleInterpolator.forHorizontal,
|
screenInterpolator: StyleInterpolator.forHorizontal,
|
||||||
containerStyle: {
|
containerStyle: {
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#eee',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const ModalSlideFromBottomIOS = {
|
|||||||
transitionSpec: IOSTransitionSpec,
|
transitionSpec: IOSTransitionSpec,
|
||||||
screenInterpolator: StyleInterpolator.forVertical,
|
screenInterpolator: StyleInterpolator.forVertical,
|
||||||
containerStyle: {
|
containerStyle: {
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#eee',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export default function createPointerEventsContainer(Component) {
|
|||||||
this._bindPosition();
|
this._bindPosition();
|
||||||
this._pointerEvents = this._computePointerEvents();
|
this._pointerEvents = this._computePointerEvents();
|
||||||
|
|
||||||
|
const { navigation, scene } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...this.props}
|
{...this.props}
|
||||||
@@ -45,12 +47,17 @@ export default function createPointerEventsContainer(Component) {
|
|||||||
_bindPosition() {
|
_bindPosition() {
|
||||||
this._positionListener && this._positionListener.remove();
|
this._positionListener && this._positionListener.remove();
|
||||||
this._positionListener = new AnimatedValueSubscription(
|
this._positionListener = new AnimatedValueSubscription(
|
||||||
this.props.position,
|
this.props.realPosition,
|
||||||
this._onPositionChange
|
this._onPositionChange
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPositionChange = () => {
|
_onPositionChange = ({ value }) => {
|
||||||
|
// This should log each frame when releasing the gesture or when pressing
|
||||||
|
// the back button! If not, something has gone wrong with the animated
|
||||||
|
// value subscription
|
||||||
|
// console.log(value);
|
||||||
|
|
||||||
if (this._component) {
|
if (this._component) {
|
||||||
const pointerEvents = this._computePointerEvents();
|
const pointerEvents = this._computePointerEvents();
|
||||||
if (this._pointerEvents !== pointerEvents) {
|
if (this._pointerEvents !== pointerEvents) {
|
||||||
@@ -61,14 +68,14 @@ export default function createPointerEventsContainer(Component) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
_computePointerEvents() {
|
_computePointerEvents() {
|
||||||
const { navigation, position, scene } = this.props;
|
const { navigation, realPosition, scene } = this.props;
|
||||||
|
|
||||||
if (scene.isStale || navigation.state.index !== scene.index) {
|
if (scene.isStale || navigation.state.index !== scene.index) {
|
||||||
// The scene isn't focused.
|
// The scene isn't focused.
|
||||||
return scene.index > navigation.state.index ? 'box-only' : 'none';
|
return scene.index > navigation.state.index ? 'box-only' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const offset = position.__getAnimatedValue() - navigation.state.index;
|
const offset = realPosition.__getAnimatedValue() - navigation.state.index;
|
||||||
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
|
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
|
||||||
// The positon is still away from scene's index.
|
// The positon is still away from scene's index.
|
||||||
// Scene's children should not receive touches until the position
|
// Scene's children should not receive touches until the position
|
||||||
|
|||||||
@@ -25,10 +25,20 @@ class Transitioner extends React.Component {
|
|||||||
width: new Animated.Value(0),
|
width: new Animated.Value(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const position = new Animated.Value(this.props.navigation.state.index);
|
||||||
|
this._positionListener = position.addListener(({ value }) => {
|
||||||
|
// This should work until we detach position from a view! so we have to be
|
||||||
|
// careful to not ever detach it, thus the gymnastics in _getPosition in
|
||||||
|
// StackViewLayout
|
||||||
|
// This should log each frame when releasing the gesture or when pressing
|
||||||
|
// the back button! If not, something has gone wrong with the animated
|
||||||
|
// value subscription
|
||||||
|
// console.log(value);
|
||||||
|
});
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
layout,
|
layout,
|
||||||
position: new Animated.Value(this.props.navigation.state.index),
|
position,
|
||||||
progress: new Animated.Value(1),
|
|
||||||
scenes: NavigationScenesReducer(
|
scenes: NavigationScenesReducer(
|
||||||
[],
|
[],
|
||||||
this.props.navigation.state,
|
this.props.navigation.state,
|
||||||
@@ -50,6 +60,7 @@ class Transitioner extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
|
this._positionListener && this._positionListener.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/no-deprecated
|
// eslint-disable-next-line react/no-deprecated
|
||||||
@@ -90,15 +101,12 @@ class Transitioner extends React.Component {
|
|||||||
scenes: nextScenes,
|
scenes: nextScenes,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { position, progress } = nextState;
|
const { position } = nextState;
|
||||||
|
const toValue = nextProps.navigation.state.index;
|
||||||
progress.setValue(0);
|
|
||||||
|
|
||||||
this._prevTransitionProps = this._transitionProps;
|
this._prevTransitionProps = this._transitionProps;
|
||||||
this._transitionProps = buildTransitionProps(nextProps, nextState);
|
this._transitionProps = buildTransitionProps(nextProps, nextState);
|
||||||
|
|
||||||
const toValue = nextProps.navigation.state.index;
|
|
||||||
|
|
||||||
if (!this._transitionProps.navigation.state.isTransitioning) {
|
if (!this._transitionProps.navigation.state.isTransitioning) {
|
||||||
this.setState(nextState, async () => {
|
this.setState(nextState, async () => {
|
||||||
if (nextProps.onTransitionStart) {
|
if (nextProps.onTransitionStart) {
|
||||||
@@ -110,13 +118,27 @@ class Transitioner extends React.Component {
|
|||||||
await result;
|
await result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
progress.setValue(1);
|
|
||||||
position.setValue(toValue);
|
position.setValue(toValue);
|
||||||
this._onTransitionEnd();
|
this._onTransitionEnd();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update scenes and play the transition
|
||||||
|
this._isTransitionRunning = true;
|
||||||
|
this.setState(nextState, async () => {
|
||||||
|
if (nextProps.onTransitionStart) {
|
||||||
|
const result = nextProps.onTransitionStart(
|
||||||
|
this._transitionProps,
|
||||||
|
this._prevTransitionProps
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
await result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// get the transition spec.
|
// get the transition spec.
|
||||||
const transitionUserSpec = nextProps.configureTransition
|
const transitionUserSpec = nextProps.configureTransition
|
||||||
? nextProps.configureTransition(
|
? nextProps.configureTransition(
|
||||||
@@ -133,38 +155,14 @@ class Transitioner extends React.Component {
|
|||||||
const { timing } = transitionSpec;
|
const { timing } = transitionSpec;
|
||||||
delete transitionSpec.timing;
|
delete transitionSpec.timing;
|
||||||
|
|
||||||
const positionHasChanged = position.__getValue() !== toValue;
|
|
||||||
|
|
||||||
// if swiped back, indexHasChanged == true && positionHasChanged == false
|
// if swiped back, indexHasChanged == true && positionHasChanged == false
|
||||||
const animations =
|
const positionHasChanged = position.__getValue() !== toValue;
|
||||||
indexHasChanged && positionHasChanged
|
if (indexHasChanged && positionHasChanged) {
|
||||||
? [
|
timing(position, {
|
||||||
timing(progress, {
|
...transitionSpec,
|
||||||
...transitionSpec,
|
toValue: nextProps.navigation.state.index,
|
||||||
toValue: 1,
|
}).start(this._onTransitionEnd);
|
||||||
}),
|
}
|
||||||
timing(position, {
|
|
||||||
...transitionSpec,
|
|
||||||
toValue: nextProps.navigation.state.index,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// update scenes and play the transition
|
|
||||||
this._isTransitionRunning = true;
|
|
||||||
this.setState(nextState, async () => {
|
|
||||||
if (nextProps.onTransitionStart) {
|
|
||||||
const result = nextProps.onTransitionStart(
|
|
||||||
this._transitionProps,
|
|
||||||
this._prevTransitionProps
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
await result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Animated.parallel(animations).start(this._onTransitionEnd);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -247,7 +245,7 @@ class Transitioner extends React.Component {
|
|||||||
function buildTransitionProps(props, state) {
|
function buildTransitionProps(props, state) {
|
||||||
const { navigation } = props;
|
const { navigation } = props;
|
||||||
|
|
||||||
const { layout, position, progress, scenes } = state;
|
const { layout, position, scenes } = state;
|
||||||
|
|
||||||
const scene = scenes.find(isSceneActive);
|
const scene = scenes.find(isSceneActive);
|
||||||
|
|
||||||
@@ -257,7 +255,6 @@ function buildTransitionProps(props, state) {
|
|||||||
layout,
|
layout,
|
||||||
navigation,
|
navigation,
|
||||||
position,
|
position,
|
||||||
progress,
|
|
||||||
scenes,
|
scenes,
|
||||||
scene,
|
scene,
|
||||||
index: scene.index,
|
index: scene.index,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user