react-native/Libraries/NavigationExperimental/NavigationTransitioner.js
Timur Gibadullin df43cc7f6b Fix animation in fast navigation between scenes
Summary:
There is a bug in navigation animation:
![-05-2016 16-04-23](https://cloud.githubusercontent.com/assets/3778452/20030228/8d93bc3e-a371-11e6-87d6-2c5c994733b5.gif)
navigation animation from route-2 was awful, route-3 scene appeared without any animation, I pushed above example to 46dd8c93b4.

This bug can be reproduced when user navigates to the next scene immediately after navigation to the current one or when navigating between scenes programmatically (e.g. between loading scene and whatever next scene). I made some investigation and It looks like that `progress` should be reseted before building the new _transitionProps.

The above example after applying this patch:
![-05-2016 16-12-16](https://cloud.githubusercontent.com/assets/3778452/20030259/a800681e-a372-11e6-847a-991d355a5940.gif)
Closes https://github.com/facebook/react-native/pull/10761

Differential Revision: D4226864

Pulled By: mkonicek

fbshipit-source-id: 31dceb6c8e497b2cbd891bbda4cb3add01cbcca0
2016-11-23 09:58:42 -08:00

288 lines
6.6 KiB
JavaScript

/**
* 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 NavigationTransitioner
* @flow
*/
'use strict';
const Animated = require('Animated');
const Easing = require('Easing');
const NavigationPropTypes = require('NavigationPropTypes');
const NavigationScenesReducer = require('NavigationScenesReducer');
const React = require('React');
const StyleSheet = require('StyleSheet');
const View = require('View');
import type {
NavigationAnimatedValue,
NavigationLayout,
NavigationScene,
NavigationState,
NavigationTransitionProps,
NavigationTransitionSpec,
} from 'NavigationTypeDefinition';
type Props = {
configureTransition: (
a: NavigationTransitionProps,
b: ?NavigationTransitionProps,
) => NavigationTransitionSpec,
navigationState: NavigationState,
onTransitionEnd: () => void,
onTransitionStart: () => void,
render: (a: NavigationTransitionProps, b: ?NavigationTransitionProps) => any,
style: any,
};
type State = {
layout: NavigationLayout,
position: NavigationAnimatedValue,
progress: NavigationAnimatedValue,
scenes: Array<NavigationScene>,
};
const {PropTypes} = React;
const DefaultTransitionSpec = {
duration: 250,
easing: Easing.inOut(Easing.ease),
timing: Animated.timing,
};
class NavigationTransitioner extends React.Component<any, Props, State> {
_onLayout: (event: any) => void;
_onTransitionEnd: () => void;
_prevTransitionProps: ?NavigationTransitionProps;
_transitionProps: NavigationTransitionProps;
_isMounted: boolean;
props: Props;
state: State;
static propTypes = {
configureTransition: PropTypes.func,
navigationState: NavigationPropTypes.navigationState.isRequired,
onTransitionEnd: PropTypes.func,
onTransitionStart: PropTypes.func,
render: PropTypes.func.isRequired,
};
constructor(props: Props, context: any) {
super(props, context);
// The initial layout isn't measured. Measured layout will be only available
// when the component is mounted.
const layout = {
height: new Animated.Value(0),
initHeight: 0,
initWidth: 0,
isMeasured: false,
width: new Animated.Value(0),
};
this.state = {
layout,
position: new Animated.Value(this.props.navigationState.index),
progress: new Animated.Value(1),
scenes: NavigationScenesReducer([], this.props.navigationState),
};
this._prevTransitionProps = null;
this._transitionProps = buildTransitionProps(props, this.state);
this._isMounted = false;
}
componentWillMount(): void {
this._onLayout = this._onLayout.bind(this);
this._onTransitionEnd = this._onTransitionEnd.bind(this);
}
componentDidMount(): void {
this._isMounted = true;
}
componentWillUnmount(): void {
this._isMounted = false;
}
componentWillReceiveProps(nextProps: Props): void {
const nextScenes = NavigationScenesReducer(
this.state.scenes,
nextProps.navigationState,
this.props.navigationState
);
if (nextScenes === this.state.scenes) {
return;
}
const nextState = {
...this.state,
scenes: nextScenes,
};
const {
position,
progress,
} = nextState;
progress.setValue(0);
this._prevTransitionProps = this._transitionProps;
this._transitionProps = buildTransitionProps(nextProps, nextState);
// get the transition spec.
const transitionUserSpec = nextProps.configureTransition ?
nextProps.configureTransition(
this._transitionProps,
this._prevTransitionProps,
) :
null;
const transitionSpec = {
...DefaultTransitionSpec,
...transitionUserSpec,
};
const {timing} = transitionSpec;
delete transitionSpec.timing;
const animations = [
timing(
progress,
{
...transitionSpec,
toValue: 1,
},
),
];
if (nextProps.navigationState.index !== this.props.navigationState.index) {
animations.push(
timing(
position,
{
...transitionSpec,
toValue: nextProps.navigationState.index,
},
),
);
}
// update scenes and play the transition
this.setState(nextState, () => {
nextProps.onTransitionStart && nextProps.onTransitionStart(
this._transitionProps,
this._prevTransitionProps,
);
Animated.parallel(animations).start(this._onTransitionEnd);
});
}
render(): React.Element<any> {
return (
<View
onLayout={this._onLayout}
style={[styles.main, this.props.style]}>
{this.props.render(this._transitionProps, this._prevTransitionProps)}
</View>
);
}
_onLayout(event: any): void {
const {height, width} = event.nativeEvent.layout;
if (this.state.layout.initWidth === width &&
this.state.layout.initHeight === height) {
return;
}
const layout = {
...this.state.layout,
initHeight: height,
initWidth: width,
isMeasured: true,
};
layout.height.setValue(height);
layout.width.setValue(width);
const nextState = {
...this.state,
layout,
};
this._transitionProps = buildTransitionProps(this.props, nextState);
this.setState(nextState);
}
_onTransitionEnd(): void {
if (!this._isMounted) {
return;
}
const prevTransitionProps = this._prevTransitionProps;
this._prevTransitionProps = null;
const nextState = {
...this.state,
scenes: this.state.scenes.filter(isSceneNotStale),
};
this._transitionProps = buildTransitionProps(this.props, nextState);
this.setState(nextState, () => {
this.props.onTransitionEnd && this.props.onTransitionEnd(
this._transitionProps,
prevTransitionProps,
);
});
}
}
function buildTransitionProps(
props: Props,
state: State,
): NavigationTransitionProps {
const {
navigationState,
} = props;
const {
layout,
position,
progress,
scenes,
} = state;
return {
layout,
navigationState,
position,
progress,
scenes,
// $FlowFixMe(>=0.32.0) - find can return undefined
scene: scenes.find(isSceneActive),
};
}
function isSceneNotStale(scene: NavigationScene): boolean {
return !scene.isStale;
}
function isSceneActive(scene: NavigationScene): boolean {
return scene.isActive;
}
const styles = StyleSheet.create({
main: {
flex: 1,
},
});
module.exports = NavigationTransitioner;