Compare commits

..

10 Commits

Author SHA1 Message Date
Brent Vatne
c0c5c86f63 Bump to patch version 1.0.2 2018-02-08 12:49:04 -08:00
Brent Vatne
4388b6403c Remove console logs from published version of navigation playground 2018-02-08 12:48:28 -08:00
Brent Vatne
2cb740fbf6 Only initialize the CardStack PanResponder if gestures are enabled 2018-02-08 12:36:26 -08:00
Brent Vatne
ac741a703b Remove extra scene in floating header if it hasn't been evicted due to transition yet 2018-02-08 12:32:09 -08:00
Brent Vatne
5641b42975 Revert "StackRouter block actions while transitioning (#3469)"
This reverts commit 858a0d7a53.
2018-02-08 11:49:25 -08:00
Brent Vatne
ea19ceaa6a Bump to minor version 1.0.1 2018-02-08 10:47:48 -08:00
Brent Vatne
57ae6e4736 Make TabRouter handle COMPLETE_TRANSITION in a child router without switching active index (#3473) 2018-02-08 10:46:12 -08:00
Eric Vicenti
858a0d7a53 StackRouter block actions while transitioning (#3469)
The most straightforward fix for two issues is to block all navigation actions while mid-transition of a stack navigator. This will fix:

The double-navigate on double tap issue, because the first navigation will start the transition and the second action will be ignored.

Will fix the buggy header experience that you can see when going back and forward to a different route quickly. This happens because the next navigate action happens before the completion action. After the fix, the navigate action will be ignored, the user will tap again, and will see a good transition
2018-02-08 09:02:47 -08:00
Dave Pack
cf36f22e68 Sync and switch SafeAreaView with standalone (#3452)
* add react-native-safe-area-view npm package

* remove local SafeAreaView, import from package in views

* update to latest react-native-safe-area-view

* update snapshots
2018-02-07 17:32:06 -08:00
Brent Vatne
7385c244b7 Add custom back button example 2018-02-07 10:42:06 -08:00
21 changed files with 686 additions and 717 deletions

View File

@@ -6,6 +6,11 @@
"development": {
"plugins": [
"transform-react-jsx-source"
],
},
"production": {
"plugins": [
"transform-remove-console"
]
}
}

View File

@@ -9,7 +9,7 @@ import type {
import * as React from 'react';
import { Button, ScrollView, StatusBar } from 'react-native';
import { StackNavigator, SafeAreaView } from 'react-navigation';
import { StackNavigator, SafeAreaView, withNavigation } from 'react-navigation';
import SampleText from './SampleText';
type MyNavScreenProps = {
@@ -17,6 +17,20 @@ type MyNavScreenProps = {
banner: React.Node,
};
class MyBackButton extends React.Component<any, any> {
render() {
return (
<Button onPress={this._navigateBack} title="Custom Back" />
);
}
_navigateBack = () => {
this.props.navigation.goBack(null);
}
}
const MyBackButtonWithNavigation = withNavigation(MyBackButton);
class MyNavScreen extends React.Component<MyNavScreenProps> {
render() {
const { navigation, banner } = this.props;
@@ -94,6 +108,7 @@ type MyPhotosScreenProps = {
class MyPhotosScreen extends React.Component<MyPhotosScreenProps> {
static navigationOptions = {
title: 'Photos',
headerLeft: <MyBackButtonWithNavigation />
};
_s0: NavigationEventSubscription;
_s1: NavigationEventSubscription;

View File

@@ -17,6 +17,7 @@
"react-navigation": "link:../.."
},
"devDependencies": {
"babel-plugin-transform-remove-console": "^6.9.0",
"babel-jest": "^21.0.0",
"flow-bin": "^0.61.0",
"jest": "^21.0.1",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "react-navigation",
"version": "1.0.0",
"version": "1.0.2",
"description": "Routing and navigation for your React Native apps",
"main": "src/react-navigation.js",
"repository": {
@@ -33,6 +33,7 @@
"path-to-regexp": "^1.7.0",
"prop-types": "^15.5.10",
"react-native-drawer-layout-polyfill": "^1.3.2",
"react-native-safe-area-view": "^0.6.0",
"react-native-tab-view": "^0.0.74"
},
"devDependencies": {

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Dimensions, Platform, ScrollView } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import createNavigator from './createNavigator';
import createNavigationContainer from '../createNavigationContainer';
@@ -7,7 +8,6 @@ import TabRouter from '../routers/TabRouter';
import DrawerScreen from '../views/Drawer/DrawerScreen';
import DrawerView from '../views/Drawer/DrawerView';
import DrawerItems from '../views/Drawer/DrawerNavigatorItems';
import SafeAreaView from '../views/SafeAreaView';
// A stack navigators props are the intersection between
// the base navigator props (navgiation, screenProps, etc)

View File

@@ -140,6 +140,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"paddingBottom": 0,
@@ -187,6 +188,7 @@ exports[`DrawerNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "rgba(0, 0, 0, .04)",

View File

@@ -133,6 +133,7 @@ exports[`StackNavigator applies correct values when headerRight is present 1`] =
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",
@@ -371,6 +372,7 @@ exports[`StackNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",

View File

@@ -71,6 +71,7 @@ exports[`TabNavigator renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",

View File

@@ -51,7 +51,7 @@ module.exports = {
return require('./views/CardStack/Card').default;
},
get SafeAreaView() {
return require('./views/SafeAreaView').default;
return require('react-native-safe-area-view').default;
},
// Header

View File

@@ -6,6 +6,13 @@ import NavigationActions from '../NavigationActions';
import validateRouteConfigMap from './validateRouteConfigMap';
import getScreenConfigDeprecated from './getScreenConfigDeprecated';
function childrenUpdateWithoutSwitchingIndex(actionType) {
return [
NavigationActions.SET_PARAMS,
NavigationActions.COMPLETE_TRANSITION,
].includes(actionType);
}
export default (routeConfigs, config = {}) => {
// Fail fast on invalid route definitions
validateRouteConfigMap(routeConfigs);
@@ -213,9 +220,13 @@ export default (routeConfigs, config = {}) => {
});
// console.log(`${order.join('-')}: Processed other tabs:`, {lastIndex: state.index, index});
// keep active tab index if action type is SET_PARAMS
index =
action.type === NavigationActions.SET_PARAMS ? state.index : index;
// Nested routers can be updated after switching tabs with actions such as SET_PARAMS
// and COMPLETE_TRANSITION.
// NOTE: This may be problematic with custom routers because we whitelist the actions
// that can be handled by child routers without automatically changing index.
if (childrenUpdateWithoutSwitchingIndex(action.type)) {
index = state.index;
}
if (index !== state.index || routes !== state.routes) {
return {

View File

@@ -8,6 +8,12 @@ import TabRouter from '../TabRouter';
import NavigationActions from '../../NavigationActions';
import addNavigationHelpers from '../../addNavigationHelpers';
import { _TESTING_ONLY_normalize_keys } from '../KeyGenerator';
beforeEach(() => {
_TESTING_ONLY_normalize_keys();
});
const ROUTERS = {
TabRouter,
StackRouter,
@@ -105,8 +111,8 @@ test('Handles no-op actions with tabs within stack router', () => {
type: NavigationActions.NAVIGATE,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0-0');
expect(state2.routes[0].key).toEqual('Init-id-0-1');
expect(state1.routes[0].key).toEqual('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -134,7 +140,7 @@ test('Handles deep action', () => {
key: 'StackRouterRoot',
routes: [
{
key: 'Init-id-0-2',
key: 'Init-id-0',
routeName: 'Bar',
},
],
@@ -174,8 +180,8 @@ test('Supports lazily-evaluated getScreen', () => {
immediate: true,
routeName: 'Qux',
});
expect(state1.routes[0].key).toEqual('Init-id-0-4');
expect(state2.routes[0].key).toEqual('Init-id-0-5');
expect(state1.routes[0].key).toEqual('Init-id-0');
expect(state2.routes[0].key).toEqual('Init-id-1');
state1.routes[0].key = state2.routes[0].key;
expect(state1).toEqual(state2);
const state3 = TestRouter.getStateForAction(
@@ -188,3 +194,60 @@ test('Supports lazily-evaluated getScreen', () => {
);
expect(state2).toEqual(state3);
});
test('Does not switch tab index when TabRouter child handles COMPLETE_NAVIGATION or SET_PARAMS', () => {
const FooStackNavigator = () => <div />;
const BarView = () => <div />;
FooStackNavigator.router = StackRouter({
Foo: {
screen: BarView,
},
Bar: {
screen: BarView,
},
});
const TestRouter = TabRouter({
Zap: { screen: FooStackNavigator },
Zoo: { screen: FooStackNavigator },
});
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
// Navigate to the second screen in the first tab
const state2 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Bar',
},
state1
);
// Switch tabs
const state3 = TestRouter.getStateForAction(
{
type: NavigationActions.NAVIGATE,
routeName: 'Zoo',
},
state2
);
const stateAfterCompleteTransition = TestRouter.getStateForAction(
{
type: NavigationActions.COMPLETE_TRANSITION,
key: state2.routes[0].key,
},
state3
);
const stateAfterSetParams = TestRouter.getStateForAction(
{
type: NavigationActions.SET_PARAMS,
key: state1.routes[0].routes[0].key,
params: { key: 'value' },
},
state3
);
expect(stateAfterCompleteTransition.index).toEqual(1);
expect(stateAfterSetParams.index).toEqual(1);
});

View File

@@ -206,132 +206,137 @@ class CardStack extends React.Component {
const { options } = this._getScreenDetails(scene);
const gestureDirectionInverted = options.gestureDirection === 'inverted';
const responder = PanResponder.create({
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
onMoveShouldSetPanResponder: (event, gesture) => {
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 = {},
} = this._getScreenDetails(scene).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) => {
// 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 =
(I18nManager.isRTL && 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) => {
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._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
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._goBack(immediateIndex, goBackDuration);
} else {
this._reset(immediateIndex, resetDuration);
}
});
},
});
const gesturesEnabled =
typeof options.gesturesEnabled === 'boolean'
? options.gesturesEnabled
: Platform.OS === 'ios';
const responder = !gesturesEnabled
? null
: PanResponder.create({
onPanResponderTerminate: () => {
this._isResponding = false;
this._reset(index, 0);
},
onPanResponderGrant: () => {
position.stopAnimation(value => {
this._isResponding = true;
this._gestureStartValue = value;
});
},
onMoveShouldSetPanResponder: (event, gesture) => {
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 = {},
} = this._getScreenDetails(scene).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) => {
// 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 =
(I18nManager.isRTL && 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) => {
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._reset(immediateIndex, resetDuration);
return;
}
if (gestureVelocity > 0.5) {
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._goBack(immediateIndex, goBackDuration);
} else {
this._reset(immediateIndex, resetDuration);
}
});
},
});
const handlers = gesturesEnabled ? responder.panHandlers : {};
const containerStyle = [
styles.container,

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { View, Text, Platform, StyleSheet } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import SafeAreaView from '../SafeAreaView';
import TouchableItem from '../TouchableItem';
/**

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import withCachedChildNavigation from '../../withCachedChildNavigation';
import NavigationActions from '../../NavigationActions';
import invariant from '../../utils/invariant';
import SafeAreaView from '../SafeAreaView';
/**
* Component that renders the sidebar screen of the drawer.
*/

View File

@@ -8,11 +8,11 @@ import {
View,
ViewPropTypes,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import HeaderTitle from './HeaderTitle';
import HeaderBackButton from './HeaderBackButton';
import HeaderStyleInterpolator from './HeaderStyleInterpolator';
import SafeAreaView from '../SafeAreaView';
import withOrientation from '../withOrientation';
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
@@ -262,7 +262,11 @@ class Header extends React.PureComponent {
let appBar;
if (this.props.mode === 'float') {
const scenesProps = this.props.scenes.map(scene => ({
const scenesByIndex = {};
this.props.scenes.forEach(scene => {
scenesByIndex[scene.index] = scene;
});
const scenesProps = Object.values(scenesByIndex).map(scene => ({
position: this.props.position,
progress: this.props.progress,
scene,

View File

@@ -1,316 +0,0 @@
import React, { Component } from 'react';
import {
DeviceInfo,
Dimensions,
InteractionManager,
NativeModules,
Platform,
StyleSheet,
Animated,
} from 'react-native';
import withOrientation from './withOrientation';
// See https://mydevice.io/devices/ for device dimensions
const X_WIDTH = 375;
const X_HEIGHT = 812;
const PAD_WIDTH = 768;
const PAD_HEIGHT = 1024;
const { height: D_HEIGHT, width: D_WIDTH } = Dimensions.get('window');
const { PlatformConstants = {} } = NativeModules;
const { minor = 0 } = PlatformConstants.reactNativeVersion || {};
const isIPhoneX = (() => {
if (Platform.OS === 'web') return false;
if (minor >= 50) {
return DeviceInfo.isIPhoneX_deprecated;
}
return (
Platform.OS === 'ios' &&
((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) ||
(D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT))
);
})();
const isIPad = (() => {
if (Platform.OS !== 'ios' || isIPhoneX) return false;
// if portrait and width is smaller than iPad width
if (D_HEIGHT > D_WIDTH && D_WIDTH < PAD_WIDTH) {
return false;
}
// if landscape and height is smaller that iPad height
if (D_WIDTH > D_HEIGHT && D_HEIGHT < PAD_WIDTH) {
return false;
}
return true;
})();
let _customStatusBarHeight = null;
const statusBarHeight = isLandscape => {
if (_customStatusBarHeight !== null) {
return _customStatusBarHeight;
}
/**
* This is a temporary workaround because we don't have a way to detect
* if the status bar is translucent or opaque. If opaque, we don't need to
* factor in the height here; if translucent (content renders under it) then
* we do.
*/
if (Platform.OS === 'android') {
if (global.Expo) {
return global.Expo.Constants.statusBarHeight;
} else {
return 0;
}
}
if (isIPhoneX) {
return isLandscape ? 0 : 44;
}
if (isIPad) {
return 20;
}
return isLandscape ? 0 : 20;
};
const doubleFromPercentString = percent => {
if (!percent.includes('%')) {
return 0;
}
const dbl = parseFloat(percent) / 100;
if (isNaN(dbl)) return 0;
return dbl;
};
class SafeView extends Component {
static setStatusBarHeight = height => {
_customStatusBarHeight = height;
};
state = {
touchesTop: true,
touchesBottom: true,
touchesLeft: true,
touchesRight: true,
orientation: null,
viewWidth: 0,
viewHeight: 0,
};
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this._onLayout();
});
}
componentWillReceiveProps() {
this._onLayout();
}
render() {
const { forceInset = false, isLandscape, children, style } = this.props;
const safeAreaStyle = this._getSafeAreaStyle();
return (
<Animated.View
ref={c => (this.view = c)}
onLayout={this._onLayout}
style={safeAreaStyle}
>
{this.props.children}
</Animated.View>
);
}
_onLayout = () => {
if (!this.view) return;
const { isLandscape } = this.props;
const { orientation } = this.state;
const newOrientation = isLandscape ? 'landscape' : 'portrait';
if (orientation && orientation === newOrientation) {
return;
}
const WIDTH = isLandscape ? X_HEIGHT : X_WIDTH;
const HEIGHT = isLandscape ? X_WIDTH : X_HEIGHT;
this.view._component.measureInWindow((winX, winY, winWidth, winHeight) => {
let realY = winY;
let realX = winX;
if (realY >= HEIGHT) {
realY = realY % HEIGHT;
} else if (realY < 0) {
realY = realY % HEIGHT + HEIGHT;
}
if (realX >= WIDTH) {
realX = realX % WIDTH;
} else if (realX < 0) {
realX = realX % WIDTH + WIDTH;
}
const touchesTop = realY === 0;
const touchesBottom = realY + winHeight >= HEIGHT;
const touchesLeft = realX === 0;
const touchesRight = realX + winWidth >= WIDTH;
this.setState({
touchesTop,
touchesBottom,
touchesLeft,
touchesRight,
orientation: newOrientation,
viewWidth: winWidth,
viewHeight: winHeight,
});
});
};
_getSafeAreaStyle = () => {
const { touchesTop, touchesBottom, touchesLeft, touchesRight } = this.state;
const { forceInset, isLandscape } = this.props;
const {
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
viewStyle,
} = this._getViewStyles();
const style = {
...viewStyle,
paddingTop: touchesTop ? this._getInset('top') : 0,
paddingBottom: touchesBottom ? this._getInset('bottom') : 0,
paddingLeft: touchesLeft ? this._getInset('left') : 0,
paddingRight: touchesRight ? this._getInset('right') : 0,
};
if (forceInset) {
Object.keys(forceInset).forEach(key => {
let inset = forceInset[key];
if (inset === 'always') {
inset = this._getInset(key);
}
if (inset === 'never') {
inset = 0;
}
switch (key) {
case 'horizontal': {
style.paddingLeft = inset;
style.paddingRight = inset;
break;
}
case 'vertical': {
style.paddingTop = inset;
style.paddingBottom = inset;
break;
}
case 'left':
case 'right':
case 'top':
case 'bottom': {
const padding = `padding${key[0].toUpperCase()}${key.slice(1)}`;
style[padding] = inset;
break;
}
}
});
}
// new height/width should only include padding from insets
// height/width should not be affected by padding from style obj
if (style.height && typeof style.height === 'number') {
style.height += style.paddingTop + style.paddingBottom;
}
if (style.width && typeof style.width === 'number') {
style.width += style.paddingLeft + style.paddingRight;
}
style.paddingTop = Math.max(style.paddingTop, paddingTop);
style.paddingBottom = Math.max(style.paddingBottom, paddingBottom);
style.paddingLeft = Math.max(style.paddingLeft, paddingLeft);
style.paddingRight = Math.max(style.paddingRight, paddingRight);
return style;
};
_getViewStyles = () => {
const { viewWidth } = this.state;
// get padding values from style to add back in after insets are determined
// default precedence: padding[Side] -> vertical | horizontal -> padding -> 0
let {
padding = 0,
paddingVertical = padding,
paddingHorizontal = padding,
paddingTop = paddingVertical,
paddingBottom = paddingVertical,
paddingLeft = paddingHorizontal,
paddingRight = paddingHorizontal,
...viewStyle
} = StyleSheet.flatten(this.props.style || {});
if (typeof paddingTop !== 'number') {
paddingTop = doubleFromPercentString(paddingTop) * viewWidth;
}
if (typeof paddingBottom !== 'number') {
paddingBottom = doubleFromPercentString(paddingBottom) * viewWidth;
}
if (typeof paddingLeft !== 'number') {
paddingLeft = doubleFromPercentString(paddingLeft) * viewWidth;
}
if (typeof paddingRight !== 'number') {
paddingRight = doubleFromPercentString(paddingRight) * viewWidth;
}
return {
paddingTop,
paddingBottom,
paddingLeft,
paddingRight,
viewStyle,
};
};
_getInset = key => {
const { isLandscape } = this.props;
switch (key) {
case 'horizontal':
case 'right':
case 'left': {
return isLandscape ? (isIPhoneX ? 44 : 0) : 0;
}
case 'vertical':
case 'top': {
return statusBarHeight(isLandscape);
}
case 'bottom': {
return isIPhoneX ? (isLandscape ? 24 : 34) : 0;
}
}
};
}
export default withOrientation(SafeView);

View File

@@ -7,8 +7,9 @@ import {
Platform,
Keyboard,
} from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import TabBarIcon from './TabBarIcon';
import SafeAreaView from '../SafeAreaView';
import withOrientation from '../withOrientation';
const majorVersion = parseInt(Platform.Version, 10);

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { View, StyleSheet, Platform } from 'react-native';
import { TabViewAnimated, TabViewPagerPan } from 'react-native-tab-view';
import SafeAreaView from 'react-native-safe-area-view';
import SceneView from '../SceneView';
import withCachedChildNavigation from '../../withCachedChildNavigation';
import SafeAreaView from '../SafeAreaView';
class TabView extends React.PureComponent {
static defaultProps = {

View File

@@ -27,6 +27,7 @@ exports[`TabBarBottom renders successfully 1`] = `
<View
collapsable={undefined}
onLayout={[Function]}
pointerEvents="box-none"
style={
Object {
"backgroundColor": "#F7F7F7",

View File

@@ -2512,7 +2512,7 @@ hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
hoist-non-react-statics@^2.2.0:
hoist-non-react-statics@^2.2.0, hoist-non-react-statics@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
@@ -4416,6 +4416,12 @@ react-native-drawer-layout@1.3.2:
dependencies:
react-native-dismiss-keyboard "1.0.0"
react-native-safe-area-view@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.6.0.tgz#ce01eb27905a77780219537e0f53fe9c783a8b3d"
dependencies:
hoist-non-react-statics "^2.3.1"
react-native-tab-view@^0.0.74:
version "0.0.74"
resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-0.0.74.tgz#62c0c882d9232b461ce181d440d683b4f99d1bd8"