Expose a new prop transition for scene renderer.

Summary:
= Breaking Change (for experimental features) =

Major API changes in  NavigationAnimatedView

= New prop `transition` for scene renderer

In NavigationAnimatedView, we should not use `position` as a proxy of the
transtion which happens whenever navigation state changes.

Because `position` does not change unless navigation index changes, it won't
be possible to build animations for actions that replace navigation state
without changing the index.

This diff introduces an abstract prop `transition` that is exposed to the scene
renderers.

= Replace `applyAnimation` with `configureTransition`.

Expose a new optional prop  `configureTransition` that allows people to configure
transitions easily.

For instance, to configure the transition, do this:

```
function configureTransition() {
  return {
    dutation: 123,
    easing: Easing.easeInOut,
  };
}

```
<NavigationAnimatedView configureTransition={configureTransition) />

```

Reviewed By: ericvicenti

Differential Revision: D3278698

fbshipit-source-id: 25ebad286d8b41f46c35c0f32d6023ebd01f19e7
This commit is contained in:
Hedger Wang
2016-05-11 18:44:48 -07:00
committed by Facebook Github Bot 0
parent 763e9cce27
commit 55c308615a
4 changed files with 91 additions and 42 deletions

View File

@@ -12,6 +12,7 @@
'use strict';
const Animated = require('Animated');
const Easing = require('Easing');
const NavigationPropTypes = require('NavigationPropTypes');
const NavigationScenesReducer = require('NavigationScenesReducer');
const React = require('React');
@@ -26,6 +27,7 @@ import type {
NavigationParentState,
NavigationScene,
NavigationSceneRenderer,
NavigationTransitionConfigurator,
} from 'NavigationTypeDefinition';
type Props = {
@@ -34,6 +36,7 @@ type Props = {
onNavigate: NavigationActionCaller,
renderOverlay: ?NavigationSceneRenderer,
renderScene: NavigationSceneRenderer,
configureTransition: NavigationTransitionConfigurator,
style: any,
};
@@ -41,10 +44,20 @@ type State = {
layout: NavigationLayout,
position: NavigationAnimatedValue,
scenes: Array<NavigationScene>,
transition: NavigationAnimatedValue,
};
const {PropTypes} = React;
const DefaultTransitionSpec = {
duration: 250,
easing: Easing.inOut(Easing.ease),
};
function isSceneNotStale(scene: NavigationScene): boolean {
return !scene.isStale;
}
function applyDefaultAnimation(
position: NavigationAnimatedValue,
navigationState: NavigationParentState,
@@ -62,14 +75,14 @@ class NavigationAnimatedView
extends React.Component<any, Props, State> {
_onLayout: (event: any) => void;
_onProgressChange: (data: {value: number}) => void;
_positionListener: any;
_onTransitionEnd: () => void;
props: Props;
state: State;
static propTypes = {
applyAnimation: PropTypes.func,
configureTransition: PropTypes.func,
navigationState: NavigationPropTypes.navigationState.isRequired,
onNavigate: PropTypes.func.isRequired,
renderOverlay: PropTypes.func,
@@ -78,6 +91,7 @@ class NavigationAnimatedView
static defaultProps = {
applyAnimation: applyDefaultAnimation,
configureTransition: () => DefaultTransitionSpec,
};
constructor(props: Props, context: any) {
@@ -97,58 +111,64 @@ class NavigationAnimatedView
layout,
position: new Animated.Value(this.props.navigationState.index),
scenes: NavigationScenesReducer([], this.props.navigationState),
transition: new Animated.Value(1),
};
}
componentWillMount(): void {
this._onLayout = this._onLayout.bind(this);
this._onProgressChange = this._onProgressChange.bind(this);
}
componentDidMount(): void {
this._positionListener =
this.state.position.addListener(this._onProgressChange);
this._onTransitionEnd = this._onTransitionEnd.bind(this);
}
componentWillReceiveProps(nextProps: Props): void {
if (nextProps.navigationState !== this.props.navigationState) {
this.setState({
scenes: NavigationScenesReducer(
this.state.scenes,
nextProps.navigationState,
this.props.navigationState
),
});
}
}
const nextScenes = NavigationScenesReducer(
this.state.scenes,
nextProps.navigationState,
this.props.navigationState
);
componentDidUpdate(lastProps: Props): void {
if (lastProps.navigationState.index !== this.props.navigationState.index) {
this.props.applyAnimation(
this.state.position,
this.props.navigationState,
lastProps.navigationState
);
}
}
componentWillUnmount(): void {
this.state.position.removeListener(this._positionListener);
}
_onProgressChange(data: Object): void {
const delta = Math.abs(data.value - this.props.navigationState.index);
if (delta > Number.EPSILON) {
if (nextScenes === this.state.scenes) {
return;
}
const scenes = this.state.scenes.filter(scene => {
return !scene.isStale;
const {
position,
transition,
} = this.state;
// update scenes.
this.setState({
scenes: nextScenes,
});
if (scenes.length !== this.state.scenes.length) {
this.setState({ scenes });
// get the transition spec.
const transtionSpec = nextProps.configureTransition();
transition.setValue(0);
const animations = [
Animated.timing(
transition,
{
...transtionSpec,
toValue: 1,
},
),
];
if (nextProps.navigationState.index !== this.props.navigationState.index) {
animations.push(
Animated.timing(
position,
{
...transtionSpec,
toValue: nextProps.navigationState.index,
},
),
);
}
// play the transition.
Animated.parallel(animations).start(this._onTransitionEnd);
}
render(): ReactElement {
@@ -180,6 +200,7 @@ class NavigationAnimatedView
const {
position,
scenes,
transition,
} = this.state;
return renderScene({
@@ -189,6 +210,7 @@ class NavigationAnimatedView
position,
scene,
scenes,
transition,
});
}
@@ -203,6 +225,7 @@ class NavigationAnimatedView
const {
position,
scenes,
transition,
} = this.state;
return renderOverlay({
@@ -212,6 +235,7 @@ class NavigationAnimatedView
position,
scene: scenes[navigationState.index],
scenes,
transition,
});
}
return null;
@@ -232,6 +256,13 @@ class NavigationAnimatedView
this.setState({ layout });
}
_onTransitionEnd(): void {
const scenes = this.state.scenes.filter(isSceneNotStale);
if (scenes.length !== this.state.scenes.length) {
this.setState({ scenes });
}
}
}
const styles = StyleSheet.create({

View File

@@ -72,6 +72,7 @@ const SceneRenderer = {
position: animatedValue.isRequired,
scene: scene.isRequired,
scenes: PropTypes.arrayOf(scene).isRequired,
transition: animatedValue.isRequired,
};
/* NavigationPanPanHandlers */
@@ -103,6 +104,7 @@ function extractSceneRendererProps(
position: props.position,
scene: props.scene,
scenes: props.scenes,
transition: props.transition,
};
}

View File

@@ -41,8 +41,6 @@ export type NavigationLayout = {
width: NavigationAnimatedValue,
};
export type NavigationPosition = NavigationAnimatedValue;
export type NavigationScene = {
index: number,
isStale: boolean,
@@ -61,13 +59,20 @@ export type NavigationSceneRendererProps = {
onNavigate: NavigationActionCaller,
// The progressive index of the containing view's navigation state.
position: NavigationPosition,
position: NavigationAnimatedValue,
// The scene to render.
scene: NavigationScene,
// All the scenes of the containing view's.
scenes: Array<NavigationScene>,
// The value that represents the progress of the transition when navigation
// state changes from one to another. Its numberic value will range from 0
// to 1.
// transition.__getAnimatedValue() < 1 : transtion is happening.
// transition.__getAnimatedValue() == 1 : transtion completes.
transition: NavigationAnimatedValue,
};
export type NavigationPanPanHandlers = {
@@ -85,6 +90,12 @@ export type NavigationPanPanHandlers = {
onStartShouldSetResponderCapture: Function,
};
export type NavigationTransitionSpec = {
duration: number,
// An easing function from `Easing`.
easing: () => any,
};
// Functions.
export type NavigationActionCaller = Function;
@@ -112,3 +123,5 @@ export type NavigationSceneRenderer = (
export type NavigationStyleInterpolator = (
props: NavigationSceneRendererProps,
) => Object;
export type NavigationTransitionConfigurator = () => NavigationTransitionSpec;

View File

@@ -72,6 +72,9 @@ function NavigationScenesReducer(
nextState: NavigationParentState,
prevState: ?NavigationParentState,
): Array<NavigationScene> {
if (prevState === nextState) {
return scenes;
}
const prevScenes = new Map();
const freshScenes = new Map();