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:
Hedger Wang 2016-03-21 11:13:59 -07:00 committed by Facebook Github Bot 2
parent b7ae7d0b4e
commit 206f846507
6 changed files with 359 additions and 71 deletions

View File

@ -129,7 +129,7 @@ class NavigationCardStack extends React.Component {
return (
<NavigationCard
{...props}
key={'card_' + props.scene.navigationState.key}
key={'card_' + props.scene.key}
panHandlers={panHandlers}
renderScene={this.props.renderScene}
style={style}

View File

@ -273,6 +273,7 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
return React.cloneElement(
navigationBar,
{
key: 'header_' + props.scene.key,
ref: this._onNavigationBarRef,
navigator: navigationBarNavigator || this,
navState: {...this.state},
@ -319,7 +320,7 @@ class NavigationLegacyNavigator extends React.Component<any, Props, State> {
return (
<NavigationCard
{...props}
key={'card_' + props.scene.navigationState.key}
key={'card_' + props.scene.key}
panHandlers={panHandlers}
renderScene={this._renderScene}
style={style}

View File

@ -14,7 +14,7 @@
const Animated = require('Animated');
const NavigationContainer = require('NavigationContainer');
const NavigationPropTypes = require('NavigationPropTypes');
const NavigationStateUtils = require('NavigationStateUtils');
const NavigationScenesReducer = require('NavigationScenesReducer');
const React = require('react-native');
const StyleSheet = require('StyleSheet');
const View = require('View');
@ -26,43 +26,8 @@ import type {
NavigationParentState,
NavigationScene,
NavigationSceneRenderer,
NavigationState,
} 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 = {
applyAnimation: NavigationAnimationSetter,
navigationState: NavigationParentState,
@ -125,7 +90,7 @@ class NavigationAnimatedView
this.state = {
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 {
if (nextProps.navigationState !== this.props.navigationState) {
this.setState({
scenes: this._reduceScenes(
scenes: NavigationScenesReducer(
this.state.scenes,
nextProps.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 {
const overlay = this._renderOverlay();
const scenes = this._renderScenes();

View File

@ -45,6 +45,7 @@ export type NavigationPosition = NavigationAnimatedValue;
export type NavigationScene = {
index: number,
isStale: boolean,
key: string,
navigationState: NavigationState,
};

View File

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

View File

@ -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'
},
},
]);
});
});