Move scenes reducer logic into a separate module.
Reviewed By: ericvicenti Differential Revision: D3068237 fb-gh-sync-id: 0146f38be6024c080ed4e00ac6b7c1e2f3d8e56f shipit-source-id: 0146f38be6024c080ed4e00ac6b7c1e2f3d8e56f
This commit is contained in:
parent
b7ae7d0b4e
commit
206f846507
|
@ -129,7 +129,7 @@ class NavigationCardStack extends React.Component {
|
||||||
return (
|
return (
|
||||||
<NavigationCard
|
<NavigationCard
|
||||||
{...props}
|
{...props}
|
||||||
key={'card_' + props.scene.navigationState.key}
|
key={'card_' + props.scene.key}
|
||||||
panHandlers={panHandlers}
|
panHandlers={panHandlers}
|
||||||
renderScene={this.props.renderScene}
|
renderScene={this.props.renderScene}
|
||||||
style={style}
|
style={style}
|
||||||
|
|
|
@ -273,6 +273,7 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
|
||||||
return React.cloneElement(
|
return React.cloneElement(
|
||||||
navigationBar,
|
navigationBar,
|
||||||
{
|
{
|
||||||
|
key: 'header_' + props.scene.key,
|
||||||
ref: this._onNavigationBarRef,
|
ref: this._onNavigationBarRef,
|
||||||
navigator: navigationBarNavigator || this,
|
navigator: navigationBarNavigator || this,
|
||||||
navState: {...this.state},
|
navState: {...this.state},
|
||||||
|
@ -319,7 +320,7 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
|
||||||
return (
|
return (
|
||||||
<NavigationCard
|
<NavigationCard
|
||||||
{...props}
|
{...props}
|
||||||
key={'card_' + props.scene.navigationState.key}
|
key={'card_' + props.scene.key}
|
||||||
panHandlers={panHandlers}
|
panHandlers={panHandlers}
|
||||||
renderScene={this._renderScene}
|
renderScene={this._renderScene}
|
||||||
style={style}
|
style={style}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
const Animated = require('Animated');
|
const Animated = require('Animated');
|
||||||
const NavigationContainer = require('NavigationContainer');
|
const NavigationContainer = require('NavigationContainer');
|
||||||
const NavigationPropTypes = require('NavigationPropTypes');
|
const NavigationPropTypes = require('NavigationPropTypes');
|
||||||
const NavigationStateUtils = require('NavigationStateUtils');
|
const NavigationScenesReducer = require('NavigationScenesReducer');
|
||||||
const React = require('react-native');
|
const React = require('react-native');
|
||||||
const StyleSheet = require('StyleSheet');
|
const StyleSheet = require('StyleSheet');
|
||||||
const View = require('View');
|
const View = require('View');
|
||||||
|
@ -26,43 +26,8 @@ import type {
|
||||||
NavigationParentState,
|
NavigationParentState,
|
||||||
NavigationScene,
|
NavigationScene,
|
||||||
NavigationSceneRenderer,
|
NavigationSceneRenderer,
|
||||||
NavigationState,
|
|
||||||
} from 'NavigationTypeDefinition';
|
} from 'NavigationTypeDefinition';
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to compare route keys (e.g. "9", "11").
|
|
||||||
*/
|
|
||||||
function compareKey(one: string, two: string): number {
|
|
||||||
var delta = one.length - two.length;
|
|
||||||
if (delta > 0) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (delta < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return one > two ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to sort scenes based on their index and view key.
|
|
||||||
*/
|
|
||||||
function compareScenes(
|
|
||||||
one: NavigationScene,
|
|
||||||
two: NavigationScene,
|
|
||||||
): number {
|
|
||||||
if (one.index > two.index) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (one.index < two.index) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return compareKey(
|
|
||||||
one.navigationState.key,
|
|
||||||
two.navigationState.key,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
applyAnimation: NavigationAnimationSetter,
|
applyAnimation: NavigationAnimationSetter,
|
||||||
navigationState: NavigationParentState,
|
navigationState: NavigationParentState,
|
||||||
|
@ -125,7 +90,7 @@ class NavigationAnimatedView
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
position: new Animated.Value(this.props.navigationState.index),
|
position: new Animated.Value(this.props.navigationState.index),
|
||||||
scenes: this._reduceScenes([], this.props.navigationState),
|
scenes: NavigationScenesReducer([], this.props.navigationState),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +107,7 @@ class NavigationAnimatedView
|
||||||
componentWillReceiveProps(nextProps: Props): void {
|
componentWillReceiveProps(nextProps: Props): void {
|
||||||
if (nextProps.navigationState !== this.props.navigationState) {
|
if (nextProps.navigationState !== this.props.navigationState) {
|
||||||
this.setState({
|
this.setState({
|
||||||
scenes: this._reduceScenes(
|
scenes: NavigationScenesReducer(
|
||||||
this.state.scenes,
|
this.state.scenes,
|
||||||
nextProps.navigationState,
|
nextProps.navigationState,
|
||||||
this.props.navigationState
|
this.props.navigationState
|
||||||
|
@ -180,37 +145,6 @@ class NavigationAnimatedView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_reduceScenes(
|
|
||||||
scenes: Array<NavigationScene>,
|
|
||||||
nextState: NavigationParentState,
|
|
||||||
lastState: ?NavigationParentState
|
|
||||||
): Array<NavigationScene> {
|
|
||||||
const nextScenes = nextState.children.map((child, index) => {
|
|
||||||
return {
|
|
||||||
index,
|
|
||||||
isStale: false,
|
|
||||||
navigationState: child,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lastState) {
|
|
||||||
lastState.children.forEach((child: NavigationState, index: number) => {
|
|
||||||
if (
|
|
||||||
!NavigationStateUtils.get(nextState, child.key) &&
|
|
||||||
index !== nextState.index
|
|
||||||
) {
|
|
||||||
nextScenes.push({
|
|
||||||
index,
|
|
||||||
isStale: true,
|
|
||||||
navigationState: child,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextScenes.sort(compareScenes);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): ReactElement {
|
render(): ReactElement {
|
||||||
const overlay = this._renderOverlay();
|
const overlay = this._renderOverlay();
|
||||||
const scenes = this._renderScenes();
|
const scenes = this._renderScenes();
|
||||||
|
|
|
@ -45,6 +45,7 @@ export type NavigationPosition = NavigationAnimatedValue;
|
||||||
export type NavigationScene = {
|
export type NavigationScene = {
|
||||||
index: number,
|
index: number,
|
||||||
isStale: boolean,
|
isStale: boolean,
|
||||||
|
key: string,
|
||||||
navigationState: NavigationState,
|
navigationState: NavigationState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @providesModule NavigationScenesReducer
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const invariant = require('fbjs/lib/invariant');
|
||||||
|
|
||||||
|
import type {
|
||||||
|
NavigationParentState,
|
||||||
|
NavigationScene,
|
||||||
|
} from 'NavigationTypeDefinition';
|
||||||
|
|
||||||
|
const SCENE_KEY_PREFIX = 'scene_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to compare route keys (e.g. "9", "11").
|
||||||
|
*/
|
||||||
|
function compareKey(one: string, two: string): number {
|
||||||
|
var delta = one.length - two.length;
|
||||||
|
if (delta > 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (delta < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return one > two ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to sort scenes based on their index and view key.
|
||||||
|
*/
|
||||||
|
function compareScenes(
|
||||||
|
one: NavigationScene,
|
||||||
|
two: NavigationScene,
|
||||||
|
): number {
|
||||||
|
if (one.index > two.index) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (one.index < two.index) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return compareKey(
|
||||||
|
one.key,
|
||||||
|
two.key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function areScenesShallowEqual(
|
||||||
|
one: NavigationScene,
|
||||||
|
two: NavigationScene,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
one.key === two.key &&
|
||||||
|
one.index === two.index &&
|
||||||
|
one.isStale === two.isStale &&
|
||||||
|
one.navigationState === two.navigationState &&
|
||||||
|
one.navigationState.key === two.navigationState.key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationScenesReducer(
|
||||||
|
scenes: Array<NavigationScene>,
|
||||||
|
nextState: NavigationParentState,
|
||||||
|
prevState: ?NavigationParentState,
|
||||||
|
): Array<NavigationScene> {
|
||||||
|
|
||||||
|
const prevScenes = new Map();
|
||||||
|
const freshScenes = new Map();
|
||||||
|
const staleScenes = new Map();
|
||||||
|
|
||||||
|
// Populate stale scenes from previous scenes marked as stale.
|
||||||
|
scenes.forEach(scene => {
|
||||||
|
const {key} = scene;
|
||||||
|
if (scene.isStale) {
|
||||||
|
staleScenes.set(key, scene);
|
||||||
|
}
|
||||||
|
prevScenes.set(key, scene);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextKeys = new Set();
|
||||||
|
nextState.children.forEach((navigationState, index) => {
|
||||||
|
const key = SCENE_KEY_PREFIX + navigationState.key;
|
||||||
|
const scene = {
|
||||||
|
index,
|
||||||
|
isStale: false,
|
||||||
|
key,
|
||||||
|
navigationState,
|
||||||
|
};
|
||||||
|
invariant(
|
||||||
|
!nextKeys.has(key),
|
||||||
|
`navigationState.children[${index}].key "${key}" conflicts with` +
|
||||||
|
'another child!'
|
||||||
|
);
|
||||||
|
nextKeys.add(key);
|
||||||
|
|
||||||
|
if (staleScenes.has(key)) {
|
||||||
|
// A previously `stale` scene is now part of the nextState, so we
|
||||||
|
// revive it by removing it from the stale scene map.
|
||||||
|
staleScenes.delete(key);
|
||||||
|
}
|
||||||
|
freshScenes.set(key, scene);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prevState) {
|
||||||
|
// Look at the previous children and classify any removed scenes as `stale`.
|
||||||
|
prevState.children.forEach((navigationState, index) => {
|
||||||
|
const key = SCENE_KEY_PREFIX + navigationState.key;
|
||||||
|
if (freshScenes.has(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
staleScenes.set(key, {
|
||||||
|
index,
|
||||||
|
isStale: true,
|
||||||
|
key,
|
||||||
|
navigationState,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextScenes = [];
|
||||||
|
|
||||||
|
const mergeScene = (nextScene => {
|
||||||
|
const {key} = nextScene;
|
||||||
|
const prevScene = prevScenes.has(key) ? prevScenes.get(key) : null;
|
||||||
|
if (prevScene && areScenesShallowEqual(prevScene, nextScene)) {
|
||||||
|
// Reuse `prevScene` as `scene` so view can avoid unnecessary re-render.
|
||||||
|
// This assumes that the scene's navigation state is immutable.
|
||||||
|
nextScenes.push(prevScene);
|
||||||
|
} else {
|
||||||
|
nextScenes.push(nextScene);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
staleScenes.forEach(mergeScene);
|
||||||
|
freshScenes.forEach(mergeScene);
|
||||||
|
|
||||||
|
return nextScenes.sort(compareScenes);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = NavigationScenesReducer;
|
|
@ -0,0 +1,203 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2015-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
jest.dontMock('NavigationScenesReducer');
|
||||||
|
|
||||||
|
const NavigationScenesReducer = require('NavigationScenesReducer');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulate scenes transtion with changes of navigation states.
|
||||||
|
*/
|
||||||
|
function testTransition(states) {
|
||||||
|
const navigationStates = states.map(keys => {
|
||||||
|
return {
|
||||||
|
children: keys.map(key => {
|
||||||
|
return { key };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let scenes = [];
|
||||||
|
let prevState = null;
|
||||||
|
navigationStates.forEach((nextState) => {
|
||||||
|
scenes = NavigationScenesReducer(scenes, nextState, prevState);
|
||||||
|
prevState = nextState;
|
||||||
|
});
|
||||||
|
|
||||||
|
return scenes;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('NavigationScenesReducer', () => {
|
||||||
|
|
||||||
|
it('gets initial scenes', () => {
|
||||||
|
const scenes = testTransition([
|
||||||
|
['1', '2'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(scenes).toEqual([
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_1',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '1'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 1,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_2',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '2'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pushes new scenes', () => {
|
||||||
|
// Transition from ['1', '2'] to ['1', '2', '3'].
|
||||||
|
const scenes = testTransition([
|
||||||
|
['1', '2'],
|
||||||
|
['1', '2', '3'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(scenes).toEqual([
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_1',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '1'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 1,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_2',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '2'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 2,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_3',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '3'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pops scenes', () => {
|
||||||
|
// Transition from ['1', '2', '3'] to ['1', '2'].
|
||||||
|
const scenes = testTransition([
|
||||||
|
['1', '2', '3'],
|
||||||
|
['1', '2'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(scenes).toEqual([
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_1',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '1'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 1,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_2',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '2'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 2,
|
||||||
|
'isStale': true,
|
||||||
|
'key': 'scene_3',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '3'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces scenes', () => {
|
||||||
|
const scenes = testTransition([
|
||||||
|
['1', '2'],
|
||||||
|
['3'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(scenes).toEqual([
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': true,
|
||||||
|
'key': 'scene_1',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '1'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_3',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '3'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 1,
|
||||||
|
'isStale': true,
|
||||||
|
'key': 'scene_2',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '2'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revives scenes', () => {
|
||||||
|
const scenes = testTransition([
|
||||||
|
['1', '2'],
|
||||||
|
['3'],
|
||||||
|
['2'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(scenes).toEqual([
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': true,
|
||||||
|
'key': 'scene_1',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '1'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': false,
|
||||||
|
'key': 'scene_2',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '2'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'index': 0,
|
||||||
|
'isStale': true,
|
||||||
|
'key': 'scene_3',
|
||||||
|
'navigationState': {
|
||||||
|
'key': '3'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue