mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
transitioner2
Introducing a wild and hacky prototype of Transitioner 2! - "transitionProps" concept is now identical to transitioner.state - Far simpler state management than before - Descriptors only, no "scenes" - No more "position" Also, StackView is seeing improvements: - Easier to understand when "transition props" are just this.props - StackView only renders the current sceen and the one before it Currently broken: - Interpolation configuration, beyond the first push - Attempting to move away from getSceneIndicesForInterpolationInputRange because it works with position and it is confusing as hell, but haven't worked around it yet - Gestures, although some "backProgress" code is in the works - Interpolation is super buggy - onTransitionStart/End - Header, although a lot of code is moved over
This commit is contained in:
@@ -305,7 +305,8 @@ const AppNavigator = StackNavigator(
|
||||
}
|
||||
);
|
||||
|
||||
export default () => <AppNavigator />;
|
||||
// export default () => <AppNavigator />;
|
||||
export default SimpleStack;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -8,7 +8,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.get(state, 'a')).toEqual({
|
||||
key: 'a',
|
||||
@@ -21,7 +20,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.indexOf(state, 'a')).toBe(0);
|
||||
expect(NavigationStateUtils.indexOf(state, 'b')).toBe(1);
|
||||
@@ -32,7 +30,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.has(state, 'b')).toBe(true);
|
||||
expect(NavigationStateUtils.has(state, 'c')).toBe(false);
|
||||
@@ -43,11 +40,9 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
};
|
||||
expect(NavigationStateUtils.push(state, { key: 'b', routeName })).toEqual(
|
||||
@@ -59,7 +54,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() =>
|
||||
NavigationStateUtils.push(state, { key: 'a', routeName })
|
||||
@@ -71,12 +65,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.pop(state)).toEqual(newState);
|
||||
});
|
||||
@@ -85,7 +77,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.pop(state)).toBe(state);
|
||||
});
|
||||
@@ -95,12 +86,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.jumpToIndex(state, 0)).toBe(state);
|
||||
expect(NavigationStateUtils.jumpToIndex(state, 1)).toEqual(newState);
|
||||
@@ -110,7 +99,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() => NavigationStateUtils.jumpToIndex(state, 2)).toThrow();
|
||||
});
|
||||
@@ -119,12 +107,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.jumpTo(state, 'a')).toBe(state);
|
||||
expect(NavigationStateUtils.jumpTo(state, 'b')).toEqual(newState);
|
||||
@@ -134,7 +120,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(() => NavigationStateUtils.jumpTo(state, 'c')).toThrow();
|
||||
});
|
||||
@@ -143,12 +128,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.back(state)).toEqual(newState);
|
||||
expect(NavigationStateUtils.back(newState)).toBe(newState);
|
||||
@@ -158,12 +141,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(NavigationStateUtils.forward(state)).toEqual(newState);
|
||||
expect(NavigationStateUtils.forward(newState)).toBe(newState);
|
||||
@@ -174,12 +155,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAt(state, 'b', { key: 'c', routeName })
|
||||
@@ -190,12 +169,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'a', routeName }, { key: 'c', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(state, 1, { key: 'c', routeName })
|
||||
@@ -206,7 +183,6 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.replaceAtIndex(state, 1, state.routes[1])
|
||||
@@ -218,12 +194,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 1,
|
||||
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.reset(state, [
|
||||
@@ -241,12 +215,10 @@ describe('StateUtils', () => {
|
||||
const state = {
|
||||
index: 0,
|
||||
routes: [{ key: 'a', routeName }, { key: 'b', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
const newState = {
|
||||
index: 0,
|
||||
routes: [{ key: 'x', routeName }, { key: 'y', routeName }],
|
||||
isTransitioning: false,
|
||||
};
|
||||
expect(
|
||||
NavigationStateUtils.reset(
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function getChildEventSubscriber(addListener, key) {
|
||||
action,
|
||||
type: eventName,
|
||||
};
|
||||
const isTransitioning = !!state && state.isTransitioning;
|
||||
const isTransitioning = !!state && !!state.transitioningFromKey;
|
||||
|
||||
const previouslyLastEmittedEvent = lastEmittedEvent;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import createNavigationContainer from '../createNavigationContainer';
|
||||
import createNavigator from './createNavigator';
|
||||
import StackView from '../views/StackView/StackView';
|
||||
import StackView from '../views/StackView/StackView2';
|
||||
import StackRouter from '../routers/StackRouter';
|
||||
|
||||
function createStackNavigator(routeConfigMap, stackConfig = {}) {
|
||||
|
||||
@@ -65,7 +65,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
@@ -100,7 +100,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
};
|
||||
return {
|
||||
key: 'StackRouterRoot',
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: false,
|
||||
index: 0,
|
||||
routes: [route],
|
||||
};
|
||||
@@ -157,6 +157,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (!state) {
|
||||
return getInitialState(action);
|
||||
}
|
||||
const lastRouteKey = state.routes[state.index].key;
|
||||
|
||||
// Check if the focused child scene wants to handle the action, as long as
|
||||
// it is not a reset to the root stack
|
||||
@@ -221,13 +222,13 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
},
|
||||
};
|
||||
}
|
||||
// Return state with new index. Change isTransitioning only if index has changed
|
||||
// Return state with new index. Change transitioningFromKey only if index has changed
|
||||
return {
|
||||
...state,
|
||||
isTransitioning:
|
||||
transitioningFromKey:
|
||||
state.index !== lastRouteIndex
|
||||
? action.immediate !== true
|
||||
: undefined,
|
||||
? action.immediate !== true ? lastRouteKey : null
|
||||
: null,
|
||||
index: lastRouteIndex,
|
||||
routes,
|
||||
};
|
||||
@@ -253,7 +254,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
}
|
||||
return {
|
||||
...StateUtils.push(state, route),
|
||||
isTransitioning: action.immediate !== true,
|
||||
transitioningFromKey: action.immediate !== true ? lastRouteKey : null,
|
||||
};
|
||||
} else if (
|
||||
action.type === NavigationActions.PUSH &&
|
||||
@@ -320,7 +321,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
isTransitioning: action.immediate !== true,
|
||||
lastRouteKey: action.immediate !== true ? lastRouteKey : null,
|
||||
index: 0,
|
||||
routes: [state.routes[0]],
|
||||
};
|
||||
@@ -357,11 +358,11 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
if (
|
||||
action.type === NavigationActions.COMPLETE_TRANSITION &&
|
||||
(action.key == null || action.key === state.key) &&
|
||||
state.isTransitioning
|
||||
state.transitioningFromKey
|
||||
) {
|
||||
return {
|
||||
...state,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -440,7 +441,7 @@ export default (routeConfigs, stackConfig = {}) => {
|
||||
...state,
|
||||
routes: state.routes.slice(0, backRouteIndex),
|
||||
index: backRouteIndex - 1,
|
||||
isTransitioning: immediate !== true,
|
||||
transitioningFromKey: immediate !== true ? lastRouteKey : null,
|
||||
};
|
||||
} else if (
|
||||
backRouteIndex === 0 &&
|
||||
|
||||
@@ -67,7 +67,7 @@ export default (routeConfigs, config = {}) => {
|
||||
state = {
|
||||
routes,
|
||||
index: initialRouteIndex,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
};
|
||||
// console.log(`${order.join('-')}: Initial state`, {state});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('DrawerRouter', () => {
|
||||
const state = router.getStateForAction(INIT_ACTION);
|
||||
const expectedState = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
@@ -32,7 +32,7 @@ describe('DrawerRouter', () => {
|
||||
);
|
||||
const expectedState2 = {
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{ key: 'Foo', routeName: 'Foo', params: undefined },
|
||||
{ key: 'Bar', routeName: 'Bar', params: undefined },
|
||||
|
||||
@@ -135,7 +135,7 @@ test('Handles deep action', () => {
|
||||
const state1 = TestRouter.getStateForAction({ type: NavigationActions.INIT });
|
||||
const expectedState = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: false,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -103,7 +103,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -127,7 +127,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -138,7 +138,7 @@ describe('StackRouter', () => {
|
||||
expect(
|
||||
router.getComponentForState({
|
||||
index: 1,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{ key: 'a', routeName: 'foo' },
|
||||
{ key: 'b', routeName: 'bar' },
|
||||
@@ -353,7 +353,7 @@ describe('StackRouter', () => {
|
||||
const initState = TestRouter.getStateForAction(NavigationActions.init());
|
||||
expect(initState).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [{ key: 'id-0', routeName: 'foo' }],
|
||||
});
|
||||
@@ -494,7 +494,7 @@ describe('StackRouter', () => {
|
||||
|
||||
const state = {
|
||||
index: 2,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{ key: 'A', routeName: 'foo' },
|
||||
{ key: 'B', routeName: 'bar', params: { bazId: '321' } },
|
||||
@@ -507,7 +507,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(poppedState.routes.length).toBe(1);
|
||||
expect(poppedState.index).toBe(0);
|
||||
expect(poppedState.isTransitioning).toBe(true);
|
||||
expect(poppedState.transitioningFromKey).toBe('C');
|
||||
const poppedState2 = TestRouter.getStateForAction(
|
||||
NavigationActions.popToTop(),
|
||||
poppedState
|
||||
@@ -519,7 +519,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(poppedImmediatelyState.routes.length).toBe(1);
|
||||
expect(poppedImmediatelyState.index).toBe(0);
|
||||
expect(poppedImmediatelyState.isTransitioning).toBe(false);
|
||||
expect(poppedImmediatelyState.transitioningFromKey).toBe(null);
|
||||
});
|
||||
|
||||
test('Navigate Pushes duplicate routeName', () => {
|
||||
@@ -678,7 +678,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -706,7 +706,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -773,7 +773,7 @@ describe('StackRouter', () => {
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(1);
|
||||
expect(state2 && state2.isTransitioning).toEqual(true);
|
||||
expect(state2 && state2.transitioningFromKey).toEqual(state.routes[0].key);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
type: NavigationActions.COMPLETE_TRANSITION,
|
||||
@@ -781,7 +781,7 @@ describe('StackRouter', () => {
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(1);
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
expect(state3 && state3.transitioningFromKey).toEqual(null);
|
||||
});
|
||||
|
||||
test('Handle basic stack logic for components with router', () => {
|
||||
@@ -803,7 +803,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -831,7 +831,7 @@ describe('StackRouter', () => {
|
||||
);
|
||||
expect(state3).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -905,7 +905,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -929,7 +929,7 @@ describe('StackRouter', () => {
|
||||
const state = router.getStateForAction({ type: NavigationActions.INIT });
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
@@ -1274,19 +1274,19 @@ describe('StackRouter', () => {
|
||||
|
||||
expect(state).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'id-2',
|
||||
params: { code: 'test', foo: 'bar' },
|
||||
routeName: 'main',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'id-1',
|
||||
params: { code: 'test', foo: 'bar', id: '4' },
|
||||
routeName: 'profile',
|
||||
@@ -1333,19 +1333,19 @@ describe('StackRouter', () => {
|
||||
|
||||
expect(state2).toEqual({
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'StackRouterRoot',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'id-5',
|
||||
params: { code: '', foo: 'bar' },
|
||||
routeName: 'main',
|
||||
routes: [
|
||||
{
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
key: 'id-4',
|
||||
params: { code: '', foo: 'bar', id: '4' },
|
||||
routeName: 'profile',
|
||||
@@ -1448,7 +1448,7 @@ describe('StackRouter', () => {
|
||||
|
||||
const state = {
|
||||
index: 0,
|
||||
isTransitioning: false,
|
||||
transitioningFromKey: null,
|
||||
routes: [
|
||||
{
|
||||
index: 1,
|
||||
@@ -1664,10 +1664,12 @@ test('Handles deep navigate completion action', () => {
|
||||
},
|
||||
state
|
||||
);
|
||||
expect(state2 && state2.index).toEqual(0);
|
||||
expect(state2 && state2.isTransitioning).toEqual(false);
|
||||
expect(state2 && state2.routes[0].index).toEqual(1);
|
||||
expect(state2 && state2.routes[0].isTransitioning).toEqual(true);
|
||||
expect(state2.index).toEqual(0);
|
||||
expect(state2.transitioningFromKey).toEqual(null);
|
||||
expect(state2.routes[0].index).toEqual(1);
|
||||
expect(state2.routes[0].transitioningFromKey).toEqual(
|
||||
state.routes[0].routes[state.routes[0].index].key
|
||||
);
|
||||
expect(!!key).toEqual(true);
|
||||
const state3 = router.getStateForAction(
|
||||
{
|
||||
@@ -1675,8 +1677,8 @@ test('Handles deep navigate completion action', () => {
|
||||
},
|
||||
state2
|
||||
);
|
||||
expect(state3 && state3.index).toEqual(0);
|
||||
expect(state3 && state3.isTransitioning).toEqual(false);
|
||||
expect(state3 && state3.routes[0].index).toEqual(1);
|
||||
expect(state3 && state3.routes[0].isTransitioning).toEqual(false);
|
||||
expect(state3.index).toEqual(0);
|
||||
expect(state3.transitioningFromKey).toEqual(null);
|
||||
expect(state3.routes[0].index).toEqual(1);
|
||||
expect(state3.routes[0].transitioningFromKey).toEqual(null);
|
||||
});
|
||||
|
||||
594
src/views/Header/Header2.js
Normal file
594
src/views/Header/Header2.js
Normal file
@@ -0,0 +1,594 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Image,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewPropTypes,
|
||||
} from 'react-native';
|
||||
import { MaskedViewIOS } from '../../PlatformHelpers';
|
||||
import SafeAreaView from 'react-native-safe-area-view';
|
||||
|
||||
import HeaderTitle from './HeaderTitle';
|
||||
import HeaderBackButton from './HeaderBackButton';
|
||||
import ModularHeaderBackButton from './ModularHeaderBackButton';
|
||||
import HeaderStyleInterpolator from './HeaderStyleInterpolator2';
|
||||
import withOrientation from '../withOrientation';
|
||||
import { last } from 'rxjs/operators';
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
|
||||
const TITLE_OFFSET = Platform.OS === 'ios' ? 70 : 56;
|
||||
|
||||
const getAppBarHeight = isLandscape => {
|
||||
return Platform.OS === 'ios'
|
||||
? isLandscape && !Platform.isPad ? 32 : 44
|
||||
: 56;
|
||||
};
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
leftInterpolator: HeaderStyleInterpolator.forLeft,
|
||||
leftButtonInterpolator: HeaderStyleInterpolator.forLeftButton,
|
||||
leftLabelInterpolator: HeaderStyleInterpolator.forLeftLabel,
|
||||
titleFromLeftInterpolator: HeaderStyleInterpolator.forCenterFromLeft,
|
||||
titleInterpolator: HeaderStyleInterpolator.forCenter,
|
||||
rightInterpolator: HeaderStyleInterpolator.forRight,
|
||||
};
|
||||
|
||||
static get HEIGHT() {
|
||||
return APPBAR_HEIGHT + STATUSBAR_HEIGHT;
|
||||
}
|
||||
|
||||
state = {
|
||||
widths: {},
|
||||
};
|
||||
|
||||
_getHeaderTitleString({ options }) {
|
||||
if (typeof options.headerTitle === 'string') {
|
||||
return options.headerTitle;
|
||||
}
|
||||
return options.title;
|
||||
}
|
||||
|
||||
_getLastSceneDescriptor(descriptor) {
|
||||
const { state } = this.props.navigation;
|
||||
const index = state.routes.findIndex(r => r.key === descriptor.key);
|
||||
if (index < 1) {
|
||||
return null;
|
||||
}
|
||||
const lastKey = state.routes[index - 1].key;
|
||||
return this.props.descriptors[lastKey];
|
||||
}
|
||||
|
||||
_getBackButtonTitleString(descriptor) {
|
||||
const lastSceneDescriptor = this._getLastSceneDescriptor(descriptor);
|
||||
if (!lastSceneDescriptor) {
|
||||
return null;
|
||||
}
|
||||
const { headerBackTitle } = lastSceneDescriptor.options;
|
||||
if (headerBackTitle || headerBackTitle === null) {
|
||||
return headerBackTitle;
|
||||
}
|
||||
return this._getHeaderTitleString(lastSceneDescriptor);
|
||||
}
|
||||
|
||||
_getTruncatedBackButtonTitle(descriptor) {
|
||||
const lastSceneDescriptor = this._getLastSceneDescriptor(descriptor);
|
||||
if (!lastSceneDescriptor) {
|
||||
return null;
|
||||
}
|
||||
return lastSceneDescriptor.options.headerTruncatedBackTitle;
|
||||
}
|
||||
|
||||
_renderTitleComponent = props => {
|
||||
const { options } = props.descriptor;
|
||||
const headerTitle = options.headerTitle;
|
||||
if (React.isValidElement(headerTitle)) {
|
||||
return headerTitle;
|
||||
}
|
||||
const titleString = this._getHeaderTitleString(props.descriptor);
|
||||
|
||||
const titleStyle = options.headerTitleStyle;
|
||||
const color = options.headerTintColor;
|
||||
const allowFontScaling = options.headerTitleAllowFontScaling;
|
||||
|
||||
// On iOS, width of left/right components depends on the calculated
|
||||
// size of the title.
|
||||
const onLayoutIOS =
|
||||
Platform.OS === 'ios'
|
||||
? e => {
|
||||
this.setState({
|
||||
widths: {
|
||||
...this.state.widths,
|
||||
[props.descriptor.key]: e.nativeEvent.layout.width,
|
||||
},
|
||||
});
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const RenderedHeaderTitle =
|
||||
headerTitle && typeof headerTitle !== 'string'
|
||||
? headerTitle
|
||||
: HeaderTitle;
|
||||
return (
|
||||
<RenderedHeaderTitle
|
||||
onLayout={onLayoutIOS}
|
||||
allowFontScaling={allowFontScaling == null ? true : allowFontScaling}
|
||||
style={[color ? { color } : null, titleStyle]}
|
||||
>
|
||||
{titleString}
|
||||
</RenderedHeaderTitle>
|
||||
);
|
||||
};
|
||||
|
||||
_renderLeftComponent = props => {
|
||||
const { options } = props.descriptor;
|
||||
if (
|
||||
React.isValidElement(options.headerLeft) ||
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return options.headerLeft;
|
||||
}
|
||||
|
||||
if (props.index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.descriptor);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.descriptor
|
||||
);
|
||||
const width = this.state.widths[props.descriptor.key]
|
||||
? (this.props.layout.initWidth -
|
||||
this.state.widths[props.descriptor.key]) /
|
||||
2
|
||||
: undefined;
|
||||
const RenderedLeftComponent = options.headerLeft || HeaderBackButton;
|
||||
const goBack = () => {
|
||||
// Go back on next tick because button ripple effect needs to happen on Android
|
||||
requestAnimationFrame(() => {
|
||||
this.props.navigation.goBack(props.descriptor.key);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<RenderedLeftComponent
|
||||
onPress={goBack}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderModularLeftComponent = (
|
||||
props,
|
||||
ButtonContainerComponent,
|
||||
LabelContainerComponent
|
||||
) => {
|
||||
const { options } = props.descriptor;
|
||||
const backButtonTitle = this._getBackButtonTitleString(props.descriptor);
|
||||
const truncatedBackButtonTitle = this._getTruncatedBackButtonTitle(
|
||||
props.descriptor
|
||||
);
|
||||
const width = this.state.widths[props.descriptor.key]
|
||||
? (this.props.layout.initWidth -
|
||||
this.state.widths[props.descriptor.key]) /
|
||||
2
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ModularHeaderBackButton
|
||||
onPress={this._navigateBack}
|
||||
ButtonContainerComponent={ButtonContainerComponent}
|
||||
LabelContainerComponent={LabelContainerComponent}
|
||||
pressColorAndroid={options.headerPressColorAndroid}
|
||||
tintColor={options.headerTintColor}
|
||||
buttonImage={options.headerBackImage}
|
||||
title={backButtonTitle}
|
||||
truncatedTitle={truncatedBackButtonTitle}
|
||||
titleStyle={options.headerBackTitleStyle}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderRightComponent = props => {
|
||||
const { headerRight } = props.descriptor.options;
|
||||
return headerRight || null;
|
||||
};
|
||||
|
||||
_renderLeft(props) {
|
||||
const { options } = props.descriptor;
|
||||
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
// On Android, or if we have a custom header left, or if we have a custom back image, we
|
||||
// do not use the modular header (which is the one that imitates UINavigationController)
|
||||
if (
|
||||
transitionPreset !== 'uikit' ||
|
||||
options.headerBackImage ||
|
||||
options.headerLeft ||
|
||||
options.headerLeft === null
|
||||
) {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
'left',
|
||||
this._renderLeftComponent,
|
||||
this.props.leftInterpolator
|
||||
);
|
||||
} else {
|
||||
return this._renderModularSubView(
|
||||
props,
|
||||
'left',
|
||||
this._renderModularLeftComponent,
|
||||
this.props.leftLabelInterpolator,
|
||||
this.props.leftButtonInterpolator
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_renderTitle(props, options) {
|
||||
const style = {};
|
||||
const { transitionPreset } = this.props;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
if (!options.hasLeftComponent) {
|
||||
style.left = 0;
|
||||
}
|
||||
if (!options.hasRightComponent) {
|
||||
style.right = 0;
|
||||
}
|
||||
} else if (
|
||||
Platform.OS === 'ios' &&
|
||||
!options.hasLeftComponent &&
|
||||
!options.hasRightComponent
|
||||
) {
|
||||
style.left = 0;
|
||||
style.right = 0;
|
||||
}
|
||||
|
||||
return this._renderSubView(
|
||||
{ ...props, style },
|
||||
'title',
|
||||
this._renderTitleComponent,
|
||||
transitionPreset === 'uikit'
|
||||
? this.props.titleFromLeftInterpolator
|
||||
: this.props.titleInterpolator
|
||||
);
|
||||
}
|
||||
|
||||
_renderRight(props) {
|
||||
return this._renderSubView(
|
||||
props,
|
||||
'right',
|
||||
this._renderRightComponent,
|
||||
this.props.rightInterpolator
|
||||
);
|
||||
}
|
||||
|
||||
_renderModularSubView(
|
||||
props,
|
||||
name,
|
||||
renderer,
|
||||
labelStyleInterpolator,
|
||||
buttonStyleInterpolator
|
||||
) {
|
||||
const { descriptor, index, navigation } = props;
|
||||
const { key } = descriptor;
|
||||
|
||||
// Never render a modular back button on the first screen in a stack.
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = navigation.state.index - index;
|
||||
|
||||
if (Math.abs(offset) > 2) {
|
||||
// Scene is far away from the active scene. Hides it to avoid unnecessary
|
||||
// rendering.
|
||||
return null;
|
||||
}
|
||||
|
||||
const ButtonContainer = ({ children }) => (
|
||||
<Animated.View style={[buttonStyleInterpolator(props)]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const LabelContainer = ({ children }) => (
|
||||
<Animated.View style={[labelStyleInterpolator(props)]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const subView = renderer(props, ButtonContainer, LabelContainer);
|
||||
|
||||
if (subView === null) {
|
||||
return subView;
|
||||
}
|
||||
const isTransitioning = !!navigation.state.transitioningFromKey;
|
||||
|
||||
const pointerEvents = offset !== 0 || isTransitioning ? 'none' : 'box-none';
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`${name}_${key}`}
|
||||
pointerEvents={pointerEvents}
|
||||
style={[styles.item, styles[name], props.style]}
|
||||
>
|
||||
{subView}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderSubView(props, name, renderer, styleInterpolator) {
|
||||
const { descriptor, index, navigation } = props;
|
||||
const { key } = descriptor;
|
||||
|
||||
const offset = navigation.state.index - index;
|
||||
|
||||
if (Math.abs(offset) > 2) {
|
||||
// Scene is far away from the active scene. Hides it to avoid unnecessary
|
||||
// rendering.
|
||||
return null;
|
||||
}
|
||||
|
||||
const subView = renderer(props);
|
||||
|
||||
if (subView == null) {
|
||||
return null;
|
||||
}
|
||||
const isTransitioning = !!navigation.state.transitioningFromKey;
|
||||
|
||||
const pointerEvents = offset !== 0 || isTransitioning ? 'none' : 'box-none';
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents={pointerEvents}
|
||||
key={`${name}_${key}`}
|
||||
style={[
|
||||
styles.item,
|
||||
styles[name],
|
||||
props.style,
|
||||
styleInterpolator({
|
||||
// todo: determine if we really need to splat all this.props
|
||||
...this.props,
|
||||
...props,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{subView}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
_renderHeader(props) {
|
||||
const left = this._renderLeft(props);
|
||||
const right = this._renderRight(props);
|
||||
const title = this._renderTitle(props, {
|
||||
hasLeftComponent: !!left,
|
||||
hasRightComponent: !!right,
|
||||
});
|
||||
|
||||
const { isLandscape, transitionPreset } = this.props;
|
||||
const { options } = props.descriptor;
|
||||
|
||||
const wrapperProps = {
|
||||
style: styles.header,
|
||||
key: `header_${props.descriptor.key}`,
|
||||
};
|
||||
|
||||
if (
|
||||
options.headerLeft ||
|
||||
options.headerBackImage ||
|
||||
Platform.OS !== 'ios' ||
|
||||
transitionPreset !== 'uikit'
|
||||
) {
|
||||
return (
|
||||
<View {...wrapperProps}>
|
||||
{title}
|
||||
{left}
|
||||
{right}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<MaskedViewIOS
|
||||
{...wrapperProps}
|
||||
maskElement={
|
||||
<View style={styles.iconMaskContainer}>
|
||||
<Image
|
||||
source={require('../assets/back-icon-mask.png')}
|
||||
style={styles.iconMask}
|
||||
/>
|
||||
<View style={styles.iconMaskFillerRect} />
|
||||
</View>
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{left}
|
||||
{right}
|
||||
</MaskedViewIOS>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let appBar;
|
||||
const {
|
||||
mode,
|
||||
isLandscape,
|
||||
navigation,
|
||||
descriptors,
|
||||
descriptor,
|
||||
transition,
|
||||
} = this.props;
|
||||
const { index } = navigation.state;
|
||||
if (mode === 'float') {
|
||||
const scenesDescriptorsByIndex = [];
|
||||
const { state } = navigation;
|
||||
state.routes.forEach((route, routeIndex) => {
|
||||
scenesDescriptorsByIndex[routeIndex] = descriptors[route.key];
|
||||
});
|
||||
const scenesProps = scenesDescriptorsByIndex.map(
|
||||
(descriptor, descriptorIndex) => ({
|
||||
...this.props,
|
||||
descriptor,
|
||||
index: descriptorIndex,
|
||||
})
|
||||
);
|
||||
appBar = scenesProps.map(this._renderHeader, this);
|
||||
} else {
|
||||
appBar = this._renderHeader({ ...this.props, index });
|
||||
}
|
||||
|
||||
const { options } = descriptor;
|
||||
const { headerStyle = {} } = options;
|
||||
const headerStyleObj = StyleSheet.flatten(headerStyle);
|
||||
const appBarHeight = getAppBarHeight(isLandscape);
|
||||
|
||||
const {
|
||||
alignItems,
|
||||
justifyContent,
|
||||
flex,
|
||||
flexDirection,
|
||||
flexGrow,
|
||||
flexShrink,
|
||||
flexBasis,
|
||||
flexWrap,
|
||||
...safeHeaderStyle
|
||||
} = headerStyleObj;
|
||||
|
||||
if (__DEV__) {
|
||||
warnIfHeaderStyleDefined(alignItems, 'alignItems');
|
||||
warnIfHeaderStyleDefined(justifyContent, 'justifyContent');
|
||||
warnIfHeaderStyleDefined(flex, 'flex');
|
||||
warnIfHeaderStyleDefined(flexDirection, 'flexDirection');
|
||||
warnIfHeaderStyleDefined(flexGrow, 'flexGrow');
|
||||
warnIfHeaderStyleDefined(flexShrink, 'flexShrink');
|
||||
warnIfHeaderStyleDefined(flexBasis, 'flexBasis');
|
||||
warnIfHeaderStyleDefined(flexWrap, 'flexWrap');
|
||||
}
|
||||
|
||||
// TODO: warn if any unsafe styles are provided
|
||||
const containerStyles = [
|
||||
options.headerTransparent
|
||||
? styles.transparentContainer
|
||||
: styles.container,
|
||||
{ height: appBarHeight },
|
||||
safeHeaderStyle,
|
||||
];
|
||||
|
||||
const { headerForceInset } = options;
|
||||
const forceInset = headerForceInset || { top: 'always', bottom: 'never' };
|
||||
|
||||
return (
|
||||
<SafeAreaView forceInset={forceInset} style={containerStyles}>
|
||||
<View style={StyleSheet.absoluteFill}>{options.headerBackground}</View>
|
||||
<View style={{ flex: 1 }}>{appBar}</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function warnIfHeaderStyleDefined(value, styleProp) {
|
||||
if (value !== undefined) {
|
||||
console.warn(
|
||||
`${styleProp} was given a value of ${value}, this has no effect on headerStyle.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let platformContainerStyles;
|
||||
if (Platform.OS === 'ios') {
|
||||
platformContainerStyles = {
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#A7A7AA',
|
||||
};
|
||||
} else {
|
||||
platformContainerStyles = {
|
||||
shadowColor: 'black',
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: StyleSheet.hairlineWidth,
|
||||
shadowOffset: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
},
|
||||
elevation: 4,
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: Platform.OS === 'ios' ? '#F7F7F7' : '#FFF',
|
||||
...platformContainerStyles,
|
||||
},
|
||||
transparentContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
...platformContainerStyles,
|
||||
},
|
||||
header: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
item: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
iconMaskContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
iconMaskFillerRect: {
|
||||
flex: 1,
|
||||
backgroundColor: '#d8d8d8',
|
||||
marginLeft: -3,
|
||||
},
|
||||
iconMask: {
|
||||
// These are mostly the same as the icon in ModularHeaderBackButton
|
||||
height: 21,
|
||||
width: 12,
|
||||
marginLeft: 9,
|
||||
marginTop: -0.5, // resizes down to 20.5
|
||||
alignSelf: 'center',
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
title: {
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
left: TITLE_OFFSET,
|
||||
right: TITLE_OFFSET,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: Platform.OS === 'ios' ? 'center' : 'flex-start',
|
||||
},
|
||||
left: {
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
right: {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default withOrientation(Header);
|
||||
176
src/views/Header/HeaderStyleInterpolator2.js
Normal file
176
src/views/Header/HeaderStyleInterpolator2.js
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Dimensions, I18nManager } from 'react-native';
|
||||
|
||||
const crossFadeInterpolation = (first, index, last) => ({
|
||||
inputRange: [first, index - 0.9, index - 0.2, index, last],
|
||||
outputRange: [0, 0, 0.3, 1, 0],
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility that builds the style for the navigation header.
|
||||
*
|
||||
* +-------------+-------------+-------------+
|
||||
* | | | |
|
||||
* | Left | Title | Right |
|
||||
* | Component | Component | Component |
|
||||
* | | | |
|
||||
* +-------------+-------------+-------------+
|
||||
*/
|
||||
|
||||
function forLeft(props) {
|
||||
const { position, descriptor, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
};
|
||||
}
|
||||
|
||||
function forCenter(props) {
|
||||
const { position, scene } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
};
|
||||
}
|
||||
|
||||
function forRight(props) {
|
||||
const { position, scene } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate(crossFadeInterpolation(first, index, last)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* iOS UINavigationController style interpolators
|
||||
*/
|
||||
|
||||
function forLeftButton(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange: [
|
||||
first,
|
||||
first + Math.abs(index - first) / 2,
|
||||
index,
|
||||
last - Math.abs(last - index) / 2,
|
||||
last,
|
||||
],
|
||||
outputRange: [0, 0.5, 1, 0.5, 0],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: this offset calculation is a an approximation that gives us
|
||||
* decent results in many cases, but it is ultimately a poor substitute
|
||||
* for text measurement. See the comment on title for more information.
|
||||
*
|
||||
* - 70 is the width of the left button area.
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;
|
||||
function forLeftLabel(props) {
|
||||
const { position, scene, scenes } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
|
||||
const offset = LEFT_LABEL_OFFSET;
|
||||
|
||||
return {
|
||||
// For now we fade out the label before fading in the title, so the
|
||||
// differences between the label and title position can be hopefully not so
|
||||
// noticable to the user
|
||||
opacity: position.interpolate({
|
||||
inputRange: [first, index - 0.35, index, index + 0.5, last],
|
||||
outputRange: [0, 0, 1, 0.5, 0],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-offset, 0, offset]
|
||||
: [offset, 0, -offset * 1.5],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: this offset calculation is a an approximation that gives us
|
||||
* decent results in many cases, but it is ultimately a poor substitute
|
||||
* for text measurement. We want the back button label to transition
|
||||
* smoothly into the title text and to do this we need to understand
|
||||
* where the title is positioned within the title container (since it is
|
||||
* centered).
|
||||
*
|
||||
* - 70 is the width of the left button area.
|
||||
* - 25 is the width of the left button icon (to account for label offset)
|
||||
*/
|
||||
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;
|
||||
function forCenterFromLeft(props) {
|
||||
const { position, scene } = props;
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const inputRange = [first, index - 0.5, index, index + 0.5, last];
|
||||
const offset = TITLE_OFFSET_IOS;
|
||||
|
||||
return {
|
||||
opacity: position.interpolate({
|
||||
inputRange: [first, index - 0.5, index, index + 0.7, last],
|
||||
outputRange: [0, 0, 1, 0, 0],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
translateX: position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-offset, 0, offset]
|
||||
: [offset, 0, -offset],
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
forLeft,
|
||||
forLeftButton,
|
||||
forLeftLabel,
|
||||
forCenterFromLeft,
|
||||
forCenter,
|
||||
forRight,
|
||||
};
|
||||
546
src/views/StackView/StackView2.js
Normal file
546
src/views/StackView/StackView2.js
Normal file
@@ -0,0 +1,546 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import Transitioner from './Transitioner2';
|
||||
import NavigationActions from '../../NavigationActions';
|
||||
import Transitions from './StackViewTransitions';
|
||||
|
||||
const NativeAnimatedModule =
|
||||
NativeModules && NativeModules.NativeAnimatedModule;
|
||||
|
||||
import clamp from 'clamp';
|
||||
import {
|
||||
Animated,
|
||||
StyleSheet,
|
||||
PanResponder,
|
||||
Platform,
|
||||
View,
|
||||
I18nManager,
|
||||
Easing,
|
||||
NativeModules,
|
||||
} from 'react-native';
|
||||
|
||||
import Card from './StackViewCard';
|
||||
// import Header from '../Header/Header2'; // WIP.. interpolation reconfiguration, fun!
|
||||
import SceneView from '../SceneView';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
const emptyFunction = () => {};
|
||||
|
||||
const EaseInOut = Easing.inOut(Easing.ease);
|
||||
|
||||
/**
|
||||
* The max duration of the card animation in milliseconds after released gesture.
|
||||
* The actual duration should be always less then that because the rest distance
|
||||
* is always less then the full distance of the layout.
|
||||
*/
|
||||
const ANIMATION_DURATION = 500;
|
||||
|
||||
/**
|
||||
* The gesture distance threshold to trigger the back behavior. For instance,
|
||||
* `1/2` means that moving greater than 1/2 of the width of the screen will
|
||||
* trigger a back action
|
||||
*/
|
||||
const POSITION_THRESHOLD = 1 / 2;
|
||||
|
||||
/**
|
||||
* The threshold (in pixels) to start the gesture action.
|
||||
*/
|
||||
const RESPOND_THRESHOLD = 20;
|
||||
|
||||
/**
|
||||
* 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_VERTICAL = 135;
|
||||
|
||||
const animatedSubscribeValue = animatedValue => {
|
||||
if (!animatedValue.__isNative) {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(animatedValue._listeners).length === 0) {
|
||||
animatedValue.addListener(emptyFunction);
|
||||
}
|
||||
};
|
||||
|
||||
class StackViewLayout extends React.Component {
|
||||
/**
|
||||
* Used to identify the starting point of the position when the gesture starts, such that it can
|
||||
* be updated according to its relative position. This means that a card can effectively be
|
||||
* "caught"- If a gesture starts while a card is animating, the card does not jump into a
|
||||
* corresponding location for the touch.
|
||||
*/
|
||||
_gestureStartValue = 0;
|
||||
|
||||
// tracks if a touch is currently happening
|
||||
_isResponding = false;
|
||||
|
||||
/**
|
||||
* immediateIndex is used to represent the expected index that we will be on after a
|
||||
* transition. To achieve a smooth animation when swiping back, the action to go back
|
||||
* doesn't actually fire until the transition completes. The immediateIndex is used during
|
||||
* the transition so that gestures can be handled correctly. This is a work-around for
|
||||
* cases when the user quickly swipes back several times.
|
||||
*/
|
||||
_immediateIndex = null;
|
||||
|
||||
// _panResponder = PanResponder.create({
|
||||
// onPanResponderTerminate: () => {
|
||||
// this._isResponding = false;
|
||||
// this._reset(index, 0);
|
||||
// },
|
||||
// onPanResponderGrant: () => {
|
||||
// position.stopAnimation((value: number) => {
|
||||
// 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 { options } = scene.descriptor;
|
||||
|
||||
// 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) => {
|
||||
// // 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);
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
|
||||
_renderHeader(descriptor, headerMode) {
|
||||
const { options } = descriptor;
|
||||
const { header } = options;
|
||||
|
||||
if (typeof header !== 'undefined' && typeof header !== 'function') {
|
||||
return header;
|
||||
}
|
||||
|
||||
// const renderHeader = header || (props => <Header {...props} />);
|
||||
const renderHeader = header || (props => null);
|
||||
const {
|
||||
headerLeftInterpolator,
|
||||
headerTitleInterpolator,
|
||||
headerRightInterpolator,
|
||||
} = this._getTransitionConfig();
|
||||
|
||||
const {
|
||||
mode,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
...passProps
|
||||
} = this.props;
|
||||
|
||||
return renderHeader({
|
||||
...passProps,
|
||||
...transitionProps,
|
||||
descriptor,
|
||||
mode: headerMode,
|
||||
transitionPreset: this._getHeaderTransitionPreset(),
|
||||
leftInterpolator: headerLeftInterpolator,
|
||||
titleInterpolator: headerTitleInterpolator,
|
||||
rightInterpolator: headerRightInterpolator,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_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.layout.width);
|
||||
animatedSubscribeValue(props.layout.height);
|
||||
animatedSubscribeValue(props.position);
|
||||
}
|
||||
|
||||
// _reset(resetToIndex, duration) {
|
||||
// if (
|
||||
// Platform.OS === 'ios' &&
|
||||
// ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
// ) {
|
||||
// Animated.spring(this.props.transitionProps.position, {
|
||||
// toValue: resetToIndex,
|
||||
// stiffness: 5000,
|
||||
// damping: 600,
|
||||
// mass: 3,
|
||||
// useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
// }).start();
|
||||
// } else {
|
||||
// Animated.timing(this.props.transitionProps.position, {
|
||||
// toValue: resetToIndex,
|
||||
// duration,
|
||||
// easing: EaseInOut,
|
||||
// useNativeDriver: this.props.transitionProps.position.__isNative,
|
||||
// }).start();
|
||||
// }
|
||||
// }
|
||||
|
||||
// _goBack(backFromIndex, duration) {
|
||||
// const { navigation, position, scenes } = this.props.transitionProps;
|
||||
// const toValue = Math.max(backFromIndex - 1, 0);
|
||||
|
||||
// // set temporary index for gesture handler to respect until the action is
|
||||
// // dispatched at the end of the transition.
|
||||
// this._immediateIndex = toValue;
|
||||
|
||||
// const onCompleteAnimation = () => {
|
||||
// this._immediateIndex = null;
|
||||
// const backFromScene = scenes.find(s => s.index === toValue + 1);
|
||||
// if (!this._isResponding && backFromScene) {
|
||||
// navigation.dispatch(
|
||||
// NavigationActions.back({
|
||||
// key: backFromScene.route.key,
|
||||
// immediate: true,
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
// };
|
||||
|
||||
// if (
|
||||
// Platform.OS === 'ios' &&
|
||||
// ReactNativeFeatures.supportsImprovedSpringAnimation()
|
||||
// ) {
|
||||
// Animated.spring(position, {
|
||||
// toValue,
|
||||
// stiffness: 5000,
|
||||
// damping: 600,
|
||||
// mass: 3,
|
||||
// useNativeDriver: position.__isNative,
|
||||
// }).start(onCompleteAnimation);
|
||||
// } else {
|
||||
// Animated.timing(position, {
|
||||
// toValue,
|
||||
// duration,
|
||||
// easing: EaseInOut,
|
||||
// useNativeDriver: position.__isNative,
|
||||
// }).start(onCompleteAnimation);
|
||||
// }
|
||||
// }
|
||||
|
||||
render() {
|
||||
let floatingHeader = null;
|
||||
const headerMode = this._getHeaderMode();
|
||||
const {
|
||||
navigation,
|
||||
transition,
|
||||
descriptor,
|
||||
descriptors,
|
||||
layout,
|
||||
mode,
|
||||
} = this.props;
|
||||
if (headerMode === 'float') {
|
||||
floatingHeader = this._renderHeader(descriptor, headerMode);
|
||||
}
|
||||
const { index, routes } = navigation.state;
|
||||
const isVertical = mode === 'modal';
|
||||
const { options } = descriptor;
|
||||
|
||||
const gestureDirectionInverted = options.gestureDirection === 'inverted';
|
||||
|
||||
const gesturesEnabled =
|
||||
typeof options.gesturesEnabled === 'boolean'
|
||||
? options.gesturesEnabled
|
||||
: Platform.OS === 'ios';
|
||||
|
||||
// const handlers = gesturesEnabled ? this._panResponder.panHandlers : {};
|
||||
const handlers = {};
|
||||
|
||||
const containerStyle = [
|
||||
styles.container,
|
||||
this._getTransitionConfig().containerStyle,
|
||||
];
|
||||
|
||||
let forwardScene = null;
|
||||
let backwardScene = null;
|
||||
|
||||
if (transition) {
|
||||
const { fromDescriptor, toDescriptor } = transition;
|
||||
const fromKey = fromDescriptor.key;
|
||||
const toKey = toDescriptor.key;
|
||||
const toIndex = navigation.state.routes.findIndex(r => r.key === toKey);
|
||||
invariant(
|
||||
toIndex !== -1,
|
||||
`Could not find toIndex in navigation state for ${fromKey}`
|
||||
);
|
||||
const fromIndex = navigation.state.routes.findIndex(
|
||||
r => r.key === fromKey
|
||||
);
|
||||
if (fromIndex == -1) {
|
||||
// we are coming from a screen that is no longer in the stack
|
||||
backwardScene = fromDescriptor;
|
||||
} else if (toIndex > fromIndex) {
|
||||
// presumably we are going doing a push.
|
||||
backwardScene = fromDescriptor;
|
||||
} else {
|
||||
// we are navigating back, and the forward scene is on top
|
||||
forwardScene = fromDescriptor;
|
||||
}
|
||||
} else if (index > 0) {
|
||||
// when we aren't transitioning, render the previous screen in case we swipe back.
|
||||
const previousKey = routes[index - 1].key;
|
||||
const previousDescriptor = descriptors[previousKey];
|
||||
backwardScene = previousDescriptor;
|
||||
}
|
||||
|
||||
return (
|
||||
<View {...handlers} style={containerStyle}>
|
||||
<View style={styles.scenes}>
|
||||
{backwardScene && this._renderScene(backwardScene, index - 1)}
|
||||
{this._renderScene(descriptor, index)}
|
||||
{forwardScene && this._renderScene(forwardScene, index + 1)}
|
||||
</View>
|
||||
{floatingHeader}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
_getHeaderMode() {
|
||||
if (this.props.headerMode) {
|
||||
return this.props.headerMode;
|
||||
}
|
||||
if (Platform.OS === 'android' || this.props.mode === 'modal') {
|
||||
return 'screen';
|
||||
}
|
||||
return 'float';
|
||||
}
|
||||
|
||||
_getHeaderTransitionPreset() {
|
||||
// On Android or with header mode screen, we always just use in-place,
|
||||
// we ignore the option entirely (at least until we have other presets)
|
||||
if (Platform.OS === 'android' || this._getHeaderMode() === 'screen') {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
|
||||
// TODO: validations: 'fade-in-place' or 'uikit' are valid
|
||||
if (this.props.headerTransitionPreset) {
|
||||
return this.props.headerTransitionPreset;
|
||||
} else {
|
||||
return 'fade-in-place';
|
||||
}
|
||||
}
|
||||
|
||||
_renderInnerScene(descriptor) {
|
||||
const { options, navigation, getComponent } = descriptor;
|
||||
const SceneComponent = getComponent();
|
||||
|
||||
const { screenProps } = this.props;
|
||||
const headerMode = this._getHeaderMode();
|
||||
if (headerMode === 'screen') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
</View>
|
||||
{this._renderHeader(descriptor, headerMode)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SceneView
|
||||
screenProps={screenProps}
|
||||
navigation={navigation}
|
||||
component={SceneComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_getTransitionConfig = () => {
|
||||
const isModal = this.props.mode === 'modal';
|
||||
|
||||
return Transitions.getTransitionConfig(
|
||||
this.props.transitionConfig,
|
||||
this.props,
|
||||
isModal
|
||||
);
|
||||
};
|
||||
|
||||
_renderScene = (descriptor, index) => {
|
||||
const { screenInterpolator } = this._getTransitionConfig();
|
||||
const style =
|
||||
screenInterpolator &&
|
||||
screenInterpolator({ ...this.props, descriptor, index });
|
||||
|
||||
return (
|
||||
<Card
|
||||
{...this.props}
|
||||
// providing this descriptor will override this.props.descriptor, to tell the card exactly which scene to render, instead of this.props.descriptor, which defines what scene is active
|
||||
descriptor={descriptor}
|
||||
index={index}
|
||||
key={`card_${descriptor.key}`}
|
||||
style={[style, this.props.cardStyle]}
|
||||
>
|
||||
{this._renderInnerScene(descriptor)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
// Header is physically rendered after scenes so that Header won't be
|
||||
// covered by the shadows of the scenes.
|
||||
// That said, we'd have use `flexDirection: 'column-reverse'` to move
|
||||
// Header above the scenes.
|
||||
flexDirection: 'column-reverse',
|
||||
},
|
||||
scenes: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
class StackView extends React.Component {
|
||||
static defaultProps = {
|
||||
navigationConfig: {
|
||||
mode: 'card',
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Transitioner
|
||||
render={this._render}
|
||||
configureTransition={this._configureTransition}
|
||||
navigation={this.props.navigation}
|
||||
descriptors={this.props.descriptors}
|
||||
// onTransitionStart={this.props.onTransitionStart}
|
||||
// onTransitionEnd={(lastTransition, transition) => {
|
||||
// const { onTransitionEnd, navigation } = this.props;
|
||||
// navigation.dispatch(
|
||||
// NavigationActions.completeTransition({
|
||||
// key: navigation.state.key,
|
||||
// })
|
||||
// );
|
||||
// onTransitionEnd && onTransitionEnd(lastTransition, transition);
|
||||
// }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_configureTransition = transitionProps => {
|
||||
return {
|
||||
...Transitions.getTransitionConfig(
|
||||
this.props.navigationConfig.transitionConfig,
|
||||
transitionProps,
|
||||
this.props.navigationConfig.mode === 'modal'
|
||||
).transitionSpec,
|
||||
useNativeDriver: !!NativeAnimatedModule,
|
||||
};
|
||||
};
|
||||
|
||||
_render = transitionProps => {
|
||||
const { screenProps } = this.props;
|
||||
return <StackViewLayout {...transitionProps} screenProps={screenProps} />;
|
||||
};
|
||||
}
|
||||
|
||||
export default StackView;
|
||||
@@ -60,21 +60,19 @@ const FadeOutToBottomAndroid = {
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
function defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
function defaultTransitionConfig(transitionProps, isModal) {
|
||||
if (Platform.OS === 'android') {
|
||||
// Use the default Android animation no matter if the screen is a modal.
|
||||
// Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
if (
|
||||
prevTransitionProps &&
|
||||
transitionProps.index < prevTransitionProps.index
|
||||
) {
|
||||
// Navigating back to the previous screen
|
||||
return FadeOutToBottomAndroid;
|
||||
}
|
||||
// todo, uncomment and fix, stop using prevTransitionProps
|
||||
|
||||
// // Use the default Android animation no matter if the screen is a modal.
|
||||
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
// if (
|
||||
// prevTransitionProps &&
|
||||
// transitionProps.index < prevTransitionProps.index
|
||||
// ) {
|
||||
// // Navigating back to the previous screen
|
||||
// return FadeOutToBottomAndroid;
|
||||
// }
|
||||
return FadeInFromBottomAndroid;
|
||||
}
|
||||
// iOS and other platforms
|
||||
@@ -84,21 +82,12 @@ function defaultTransitionConfig(
|
||||
return SlideFromRightIOS;
|
||||
}
|
||||
|
||||
function getTransitionConfig(
|
||||
transitionConfigurer,
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
) {
|
||||
const defaultConfig = defaultTransitionConfig(
|
||||
transitionProps,
|
||||
prevTransitionProps,
|
||||
isModal
|
||||
);
|
||||
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
|
||||
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
|
||||
if (transitionConfigurer) {
|
||||
return {
|
||||
...defaultConfig,
|
||||
...transitionConfigurer(transitionProps, prevTransitionProps, isModal),
|
||||
...transitionConfigurer(transitionProps, isModal),
|
||||
};
|
||||
}
|
||||
return defaultConfig;
|
||||
|
||||
268
src/views/StackView/StackViewTransitions.js
Normal file
268
src/views/StackView/StackViewTransitions.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import { Animated, Easing, Platform } from 'react-native';
|
||||
import * as ReactNativeFeatures from '../../utils/ReactNativeFeatures';
|
||||
|
||||
import { I18nManager } from 'react-native';
|
||||
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
|
||||
|
||||
/**
|
||||
* Utility that builds the style for the card in the cards stack.
|
||||
*
|
||||
* +------------+
|
||||
* +-+ |
|
||||
* +-+ | |
|
||||
* | | | |
|
||||
* | | | Focused |
|
||||
* | | | Card |
|
||||
* | | | |
|
||||
* +-+ | |
|
||||
* +-+ |
|
||||
* +------------+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Render the initial style when the initial layout isn't measured yet.
|
||||
*/
|
||||
function forInitial(props) {
|
||||
const { navigation, descriptor } = props;
|
||||
const { state } = navigation;
|
||||
const activeKey = state.routes[state.index].key;
|
||||
|
||||
const focused = descriptor.key === activeKey;
|
||||
const opacity = focused ? 1 : 0;
|
||||
// If not focused, move the card far away.
|
||||
const translate = focused ? 0 : 1000000;
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX: translate }, { translateY: translate }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard iOS-style slide in from the right.
|
||||
*/
|
||||
function forHorizontal(props) {
|
||||
const { layout, transition, navigation, index } = props;
|
||||
const { state } = navigation;
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const first = index - 1;
|
||||
const last = index + 1;
|
||||
const opacity = transition
|
||||
? transition.progress.interpolate({
|
||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
||||
outputRange: [0, 1, 1, 0.85, 0],
|
||||
})
|
||||
: 1;
|
||||
|
||||
const width = layout.initWidth;
|
||||
const translateX = transition
|
||||
? transition.progress.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: I18nManager.isRTL
|
||||
? [-width, 0, width * 0.3]
|
||||
: [width, 0, width * -0.3],
|
||||
})
|
||||
: 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard iOS-style slide in from the bottom (used for modals).
|
||||
*/
|
||||
function forVertical(props) {
|
||||
const { layout, transition, descriptor } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const opacity = transition.progress.interpolate({
|
||||
inputRange: [first, first + 0.01, index, last - 0.01, last],
|
||||
outputRange: [0, 1, 1, 0.85, 0],
|
||||
});
|
||||
|
||||
const height = layout.initHeight;
|
||||
const translateY = transition.progress.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: [height, 0, 0],
|
||||
});
|
||||
const translateX = 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }, { translateY }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Android-style fade in from the bottom.
|
||||
*/
|
||||
function forFadeFromBottomAndroid(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const inputRange = [first, index, last - 0.01, last];
|
||||
|
||||
const opacity = position.interpolate({
|
||||
inputRange,
|
||||
outputRange: [0, 1, 1, 0],
|
||||
});
|
||||
|
||||
const translateY = position.interpolate({
|
||||
inputRange,
|
||||
outputRange: [50, 0, 0, 0],
|
||||
});
|
||||
const translateX = 0;
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{ translateX }, { translateY }],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* fadeIn and fadeOut
|
||||
*/
|
||||
function forFade(props) {
|
||||
const { layout, position, scene } = props;
|
||||
|
||||
if (!layout.isMeasured) {
|
||||
return forInitial(props);
|
||||
}
|
||||
const interpolate = getSceneIndicesForInterpolationInputRange(props);
|
||||
|
||||
if (!interpolate) return { opacity: 0 };
|
||||
|
||||
const { first, last } = interpolate;
|
||||
const index = scene.index;
|
||||
const opacity = position.interpolate({
|
||||
inputRange: [first, index, last],
|
||||
outputRange: [0, 1, 1],
|
||||
});
|
||||
|
||||
return {
|
||||
opacity,
|
||||
};
|
||||
}
|
||||
|
||||
const StyleInterpolator = {
|
||||
forHorizontal,
|
||||
forVertical,
|
||||
forFadeFromBottomAndroid,
|
||||
forFade,
|
||||
};
|
||||
|
||||
let IOSTransitionSpec;
|
||||
if (ReactNativeFeatures.supportsImprovedSpringAnimation()) {
|
||||
// These are the exact values from UINavigationController's animation configuration
|
||||
IOSTransitionSpec = {
|
||||
timing: Animated.spring,
|
||||
stiffness: 1000,
|
||||
damping: 500,
|
||||
mass: 3,
|
||||
};
|
||||
} else {
|
||||
// This is an approximation of the IOS spring animation using a derived bezier curve
|
||||
IOSTransitionSpec = {
|
||||
duration: 500,
|
||||
easing: Easing.bezier(0.2833, 0.99, 0.31833, 0.99),
|
||||
timing: Animated.timing,
|
||||
};
|
||||
}
|
||||
|
||||
// Standard iOS navigation transition
|
||||
const SlideFromRightIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forHorizontal,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
};
|
||||
|
||||
// Standard iOS navigation transition for modals
|
||||
const ModalSlideFromBottomIOS = {
|
||||
transitionSpec: IOSTransitionSpec,
|
||||
screenInterpolator: StyleInterpolator.forVertical,
|
||||
containerStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
};
|
||||
|
||||
// Standard Android navigation transition when opening an Activity
|
||||
const FadeInFromBottomAndroid = {
|
||||
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_open_enter.xml
|
||||
transitionSpec: {
|
||||
duration: 350,
|
||||
easing: Easing.out(Easing.poly(5)), // decelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
// Standard Android navigation transition when closing an Activity
|
||||
const FadeOutToBottomAndroid = {
|
||||
// See http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/res/res/anim/activity_close_exit.xml
|
||||
transitionSpec: {
|
||||
duration: 230,
|
||||
easing: Easing.in(Easing.poly(4)), // accelerate
|
||||
timing: Animated.timing,
|
||||
},
|
||||
screenInterpolator: StyleInterpolator.forFadeFromBottomAndroid,
|
||||
};
|
||||
|
||||
function defaultTransitionConfig(transitionProps, isModal) {
|
||||
if (Platform.OS === 'android') {
|
||||
// todo, uncomment and fix, stop using prevTransitionProps
|
||||
|
||||
// // Use the default Android animation no matter if the screen is a modal.
|
||||
// // Android doesn't have full-screen modals like iOS does, it has dialogs.
|
||||
// if (
|
||||
// prevTransitionProps &&
|
||||
// transitionProps.index < prevTransitionProps.index
|
||||
// ) {
|
||||
// // Navigating back to the previous screen
|
||||
// return FadeOutToBottomAndroid;
|
||||
// }
|
||||
return FadeInFromBottomAndroid;
|
||||
}
|
||||
// iOS and other platforms
|
||||
if (isModal) {
|
||||
return ModalSlideFromBottomIOS;
|
||||
}
|
||||
return SlideFromRightIOS;
|
||||
}
|
||||
|
||||
function getTransitionConfig(transitionConfigurer, transitionProps, isModal) {
|
||||
const defaultConfig = defaultTransitionConfig(transitionProps, isModal);
|
||||
if (transitionConfigurer) {
|
||||
return {
|
||||
...defaultConfig,
|
||||
...transitionConfigurer(transitionProps, isModal),
|
||||
};
|
||||
}
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
export default {
|
||||
defaultTransitionConfig,
|
||||
getTransitionConfig,
|
||||
StyleInterpolator,
|
||||
};
|
||||
225
src/views/StackView/Transitioner2.js
Normal file
225
src/views/StackView/Transitioner2.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import React from 'react';
|
||||
import { Animated, Easing, StyleSheet, View } from 'react-native';
|
||||
import invariant from '../../utils/invariant';
|
||||
|
||||
// Used for all animations unless overriden
|
||||
const DefaultTransitionSpec = {
|
||||
duration: 250,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
timing: Animated.timing,
|
||||
};
|
||||
|
||||
class Transitioner extends React.Component {
|
||||
_isMounted = false;
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps = (props, lastState) => {
|
||||
const { navigation, descriptors } = props;
|
||||
const { state } = navigation;
|
||||
const canGoBack = state.index > 0;
|
||||
|
||||
const activeKey = state.routes[state.index].key;
|
||||
const descriptor = descriptors[activeKey];
|
||||
|
||||
if (!lastState) {
|
||||
lastState = {
|
||||
backProgress: canGoBack ? new Animaged.Value(1) : null,
|
||||
descriptor,
|
||||
descriptors,
|
||||
navigation,
|
||||
transition: null,
|
||||
layout: {
|
||||
height: new Animated.Value(0),
|
||||
initHeight: 0,
|
||||
initWidth: 0,
|
||||
isMeasured: false,
|
||||
width: new Animated.Value(0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// const lastNavState = this.props.navigation.state;
|
||||
|
||||
const lastNavState = lastState.navigation.state;
|
||||
const lastActiveKey = lastNavState.routes[lastNavState.index].key;
|
||||
|
||||
// const transitionFromKey =
|
||||
// lastActiveKey !== activeKey ? lastActiveKey : null;
|
||||
const transitionFromKey = state.transitioningFromKey;
|
||||
const transitionFromDescriptor =
|
||||
transitionFromKey &&
|
||||
lastState.descriptor &&
|
||||
lastState.descriptor.key === transitionFromKey;
|
||||
|
||||
// We can only perform a transition if we have been told to via state.transitioningFromKey, and if our previous descriptor matches, indicating that the transitioningFromKey is currently being presented.
|
||||
if (transitionFromDescriptor) {
|
||||
if (lastState.transition) {
|
||||
// there is already a transition in progress.. Don't interrupt it!
|
||||
// At the end of the transition, we will compare props and start again
|
||||
return lastState;
|
||||
}
|
||||
|
||||
return {
|
||||
...lastState,
|
||||
navigation,
|
||||
descriptor,
|
||||
backProgress: null,
|
||||
transition: {
|
||||
fromDescriptor: lastState.descriptor,
|
||||
toDescriptor: descriptor,
|
||||
progress: new Animated.Value(0),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// No transition is being performed. If the key has changed, present it immediately without transition
|
||||
if (lastActiveKey !== activeKey) {
|
||||
return {
|
||||
...lastState,
|
||||
backProgress: canGoBack ? new Animaged.Value(1) : null,
|
||||
descriptor,
|
||||
transition: null,
|
||||
};
|
||||
}
|
||||
|
||||
return lastState;
|
||||
};
|
||||
|
||||
// React doesn't handle getDerivedStateFromProps yet, but the polyfill is simple..
|
||||
state = Transitioner.getDerivedStateFromProps(this.props);
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const nextState = Transitioner.getDerivedStateFromProps(
|
||||
nextProps,
|
||||
this.state
|
||||
);
|
||||
if (this.state !== nextState) {
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
_startTransition(transition) {
|
||||
const { configureTransition } = this.props;
|
||||
const { descriptors } = this.state;
|
||||
const { progress, fromDescriptor, toDescriptor } = transition;
|
||||
progress.setValue(0);
|
||||
|
||||
// get the transition spec.
|
||||
// passing the new transitionProps format (this.state) into configureTransition is a breaking change that I haven't documented yet!
|
||||
const transitionUserSpec =
|
||||
(configureTransition && configureTransition(this.state)) || null;
|
||||
|
||||
const transitionSpec = {
|
||||
...DefaultTransitionSpec,
|
||||
...transitionUserSpec,
|
||||
};
|
||||
|
||||
const { timing } = transitionSpec;
|
||||
|
||||
// mutating a prop, this is terrible!
|
||||
// it was in the previous transitioner implementation, so I'm leaving it as-is for now:
|
||||
delete transitionSpec.timing;
|
||||
|
||||
timing(progress, {
|
||||
...transitionSpec,
|
||||
toValue: 1,
|
||||
}).start(didComplete => {
|
||||
this._completeTransition(transition, didComplete);
|
||||
});
|
||||
}
|
||||
|
||||
_completeTransition(transition, didComplete) {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
const { progress, fromDescriptor, toDescriptor } = transition;
|
||||
const { navigation, descriptors } = this.props;
|
||||
|
||||
const nextState = navigation.state;
|
||||
const activeKey = nextState.routes[nextState.index].key;
|
||||
const nextDescriptor =
|
||||
descriptors[activeKey] || this.state.descriptors[activeKey];
|
||||
|
||||
if (activeKey !== toDescriptor.key) {
|
||||
// The user has changed navigation states during the transition! This is known as a queued transition.
|
||||
// Now we set state for a new transition to the current navigation state
|
||||
this.setState({
|
||||
navigation,
|
||||
descriptors,
|
||||
descriptor: nextDescriptor,
|
||||
transition: {
|
||||
fromDescriptor: toDescriptor,
|
||||
toDescriptor: nextDescriptor,
|
||||
progress: new Animated.Value(0),
|
||||
},
|
||||
backProgress: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const canGoBack = navigation.state.index > 0;
|
||||
|
||||
// All transitions are complete. Reset to normal state:
|
||||
this.setState({
|
||||
navigation,
|
||||
descriptors,
|
||||
descriptor: nextDescriptor,
|
||||
transition: null,
|
||||
backProgress: canGoBack ? new Animated.Value(1) : null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('Rendering Transitioner', this.state);
|
||||
return (
|
||||
<View onLayout={this._onLayout} style={[styles.main]}>
|
||||
{this.props.render(this.state)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(lastProps, lastState) {
|
||||
// start transition if it needs it
|
||||
if (
|
||||
this.state.transition &&
|
||||
(!lastState.transition ||
|
||||
lastState.transition.toDescriptor !==
|
||||
this.state.transition.toDescriptor)
|
||||
) {
|
||||
this._startTransition(this.state.transition);
|
||||
}
|
||||
}
|
||||
|
||||
_onLayout = event => {
|
||||
const lastLayout = this.state.layout;
|
||||
const { height, width } = event.nativeEvent.layout;
|
||||
if (lastLayout.initWidth === width && lastLayout.initHeight === height) {
|
||||
return;
|
||||
}
|
||||
const layout = {
|
||||
...lastLayout,
|
||||
initHeight: height,
|
||||
initWidth: width,
|
||||
isMeasured: true,
|
||||
};
|
||||
|
||||
layout.height.setValue(height);
|
||||
layout.width.setValue(width);
|
||||
|
||||
this.setState({ layout });
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
main: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default Transitioner;
|
||||
@@ -11,77 +11,24 @@ const MIN_POSITION_OFFSET = 0.01;
|
||||
*/
|
||||
export default function createPointerEventsContainer(Component) {
|
||||
class Container extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._pointerEvents = this._computePointerEvents();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._onPositionChange = this._onPositionChange.bind(this);
|
||||
this._onComponentRef = this._onComponentRef.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._bindPosition(this.props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._positionListener && this._positionListener.remove();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this._bindPosition(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
this._pointerEvents = this._computePointerEvents();
|
||||
return (
|
||||
<Component
|
||||
{...this.props}
|
||||
pointerEvents={this._pointerEvents}
|
||||
onComponentRef={this._onComponentRef}
|
||||
/>
|
||||
<Component {...this.props} pointerEvents={this._getPointerEvents()} />
|
||||
);
|
||||
}
|
||||
|
||||
_onComponentRef(component) {
|
||||
this._component = component;
|
||||
if (component) {
|
||||
invariant(
|
||||
typeof component.setNativeProps === 'function',
|
||||
'component must implement method `setNativeProps`'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_bindPosition(props) {
|
||||
this._positionListener && this._positionListener.remove();
|
||||
this._positionListener = new AnimatedValueSubscription(
|
||||
props.position,
|
||||
this._onPositionChange
|
||||
_getPointerEvents() {
|
||||
const { navigation, descriptor, transition } = this.props;
|
||||
const { state } = navigation;
|
||||
const descriptorIndex = navigation.state.routes.findIndex(
|
||||
r => r.key === descriptor.key
|
||||
);
|
||||
}
|
||||
|
||||
_onPositionChange() {
|
||||
if (this._component) {
|
||||
const pointerEvents = this._computePointerEvents();
|
||||
if (this._pointerEvents !== pointerEvents) {
|
||||
this._pointerEvents = pointerEvents;
|
||||
this._component.setNativeProps({ pointerEvents });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_computePointerEvents() {
|
||||
const { navigation, position, scene } = this.props;
|
||||
|
||||
if (scene.isStale || navigation.state.index !== scene.index) {
|
||||
if (descriptorIndex !== state.index) {
|
||||
// The scene isn't focused.
|
||||
return scene.index > navigation.state.index ? 'box-only' : 'none';
|
||||
return descriptorIndex > state.index ? 'box-only' : 'none';
|
||||
}
|
||||
|
||||
const offset = position.__getAnimatedValue() - navigation.state.index;
|
||||
if (Math.abs(offset) > MIN_POSITION_OFFSET) {
|
||||
if (transition) {
|
||||
// The positon is still away from scene's index.
|
||||
// Scene's children should not receive touches until the position
|
||||
// is close enough to scene's index.
|
||||
|
||||
Reference in New Issue
Block a user