From 206f8465072c8592c48c9e8dab680635903f6d05 Mon Sep 17 00:00:00 2001 From: Hedger Wang Date: Mon, 21 Mar 2016 11:13:59 -0700 Subject: [PATCH] Move scenes reducer logic into a separate module. Reviewed By: ericvicenti Differential Revision: D3068237 fb-gh-sync-id: 0146f38be6024c080ed4e00ac6b7c1e2f3d8e56f shipit-source-id: 0146f38be6024c080ed4e00ac6b7c1e2f3d8e56f --- .../NavigationCardStack.js | 2 +- .../NavigationLegacyNavigator.js | 3 +- .../NavigationAnimatedView.js | 72 +------ .../NavigationTypeDefinition.js | 1 + .../Reducer/NavigationScenesReducer.js | 149 +++++++++++++ .../__tests__/NavigationScenesReducer-test.js | 203 ++++++++++++++++++ 6 files changed, 359 insertions(+), 71 deletions(-) create mode 100644 Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js create mode 100644 Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js diff --git a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js index 319d8d721..503884937 100644 --- a/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js +++ b/Libraries/CustomComponents/NavigationExperimental/NavigationCardStack.js @@ -129,7 +129,7 @@ class NavigationCardStack extends React.Component { return ( { 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 { return ( 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, - nextState: NavigationParentState, - lastState: ?NavigationParentState - ): Array { - 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(); diff --git a/Libraries/NavigationExperimental/NavigationTypeDefinition.js b/Libraries/NavigationExperimental/NavigationTypeDefinition.js index 6ecd2774b..694991ee7 100644 --- a/Libraries/NavigationExperimental/NavigationTypeDefinition.js +++ b/Libraries/NavigationExperimental/NavigationTypeDefinition.js @@ -45,6 +45,7 @@ export type NavigationPosition = NavigationAnimatedValue; export type NavigationScene = { index: number, isStale: boolean, + key: string, navigationState: NavigationState, }; diff --git a/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js new file mode 100644 index 000000000..9fe5781bb --- /dev/null +++ b/Libraries/NavigationExperimental/Reducer/NavigationScenesReducer.js @@ -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, + nextState: NavigationParentState, + prevState: ?NavigationParentState, +): Array { + + 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; diff --git a/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js new file mode 100644 index 000000000..affd15837 --- /dev/null +++ b/Libraries/NavigationExperimental/Reducer/__tests__/NavigationScenesReducer-test.js @@ -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' + }, + }, + ]); + }); +});