-
Notifications
You must be signed in to change notification settings - Fork 24.4k
Commit
Summary: - Fork NavigationAnimatedView to NavigationTransitioner - NavigationAnimatedView will soon be deprecated and we'd ask people to use NavigationTransitioner instead. Difference between NavigationTransitioner and NavigationAnimatedView - prop `applyAnimation` is removed. - new prop `configureTransition`, `onTransitionStart`, and `onTransitionEnd` are added. tl;dr; In NavigationAnimatedView, we `position` (an Animated.Value object) as a proxy of the transtion which happens whenever the index of navigation state changes. Because `position` does not change unless navigation index changes, it won't be possible to build animations for actions that changes the navigation state without changing the index. Also, we believe that the name `Transitioner` is a better name for this core component that focuses on transitioning. Note that the actual animation work is done via `<Animated.View />` returnd from the `renderScene` prop. Reviewed By: ericvicenti Differential Revision: D3302688 fbshipit-source-id: 720c3a4d3ccf97eb05b038baa44c9e780aad120b
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
/** | ||
* 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 { | ||
NavigationActionCaller, | ||
NavigationAnimatedValue, | ||
NavigationLayout, | ||
NavigationParentState, | ||
NavigationScene, | ||
NavigationSceneRenderer, | ||
NavigationTransitionConfigurator, | ||
} from 'NavigationTypeDefinition'; | ||
|
||
type Props = { | ||
configureTransition: NavigationTransitionConfigurator, | ||
navigationState: NavigationParentState, | ||
onNavigate: NavigationActionCaller, | ||
onTransitionEnd: () => void, | ||
onTransitionStart: () => void, | ||
renderOverlay: ?NavigationSceneRenderer, | ||
renderScene: NavigationSceneRenderer, | ||
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), | ||
}; | ||
|
||
function isSceneNotStale(scene: NavigationScene): boolean { | ||
return !scene.isStale; | ||
} | ||
|
||
class NavigationTransitioner extends React.Component<any, Props, State> { | ||
|
||
_onLayout: (event: any) => void; | ||
_onTransitionEnd: () => void; | ||
|
||
props: Props; | ||
state: State; | ||
|
||
static propTypes = { | ||
configureTransition: PropTypes.func, | ||
navigationState: NavigationPropTypes.navigationState.isRequired, | ||
onNavigate: PropTypes.func.isRequired, | ||
onTransitionEnd: PropTypes.func, | ||
onTransitionStart: PropTypes.func, | ||
renderOverlay: PropTypes.func, | ||
renderScene: 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), | ||
}; | ||
} | ||
|
||
componentWillMount(): void { | ||
this._onLayout = this._onLayout.bind(this); | ||
this._onTransitionEnd = this._onTransitionEnd.bind(this); | ||
} | ||
|
||
componentWillReceiveProps(nextProps: Props): void { | ||
const nextScenes = NavigationScenesReducer( | ||
this.state.scenes, | ||
nextProps.navigationState, | ||
this.props.navigationState | ||
); | ||
|
||
if (nextScenes === this.state.scenes) { | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
jmurzy
Contributor
|
||
return; | ||
} | ||
|
||
const { | ||
position, | ||
progress, | ||
} = this.state; | ||
|
||
// update scenes. | ||
this.setState({ | ||
scenes: nextScenes, | ||
}); | ||
|
||
// get the transition spec. | ||
const transitionUserSpec = nextProps.configureTransition ? | ||
nextProps.configureTransition() : | ||
null; | ||
|
||
const transtionSpec = { | ||
...DefaultTransitionSpec, | ||
...transitionUserSpec, | ||
}; | ||
|
||
progress.setValue(0); | ||
|
||
const animations = [ | ||
Animated.timing( | ||
progress, | ||
{ | ||
...transtionSpec, | ||
toValue: 1, | ||
}, | ||
), | ||
]; | ||
|
||
if (nextProps.navigationState.index !== this.props.navigationState.index) { | ||
animations.push( | ||
Animated.timing( | ||
position, | ||
{ | ||
...transtionSpec, | ||
toValue: nextProps.navigationState.index, | ||
}, | ||
), | ||
); | ||
} | ||
|
||
// play the transition. | ||
nextProps.onTransitionStart && nextProps.onTransitionStart(); | ||
Animated.parallel(animations).start(this._onTransitionEnd); | ||
This comment has been minimized.
Sorry, something went wrong.
jmurzy
Contributor
|
||
} | ||
|
||
render(): ReactElement { | ||
const overlay = this._renderOverlay(); | ||
const scenes = this._renderScenes(); | ||
return ( | ||
<View | ||
onLayout={this._onLayout} | ||
style={this.props.style}> | ||
<View style={styles.scenes} key="scenes"> | ||
{scenes} | ||
</View> | ||
{overlay} | ||
</View> | ||
); | ||
} | ||
|
||
_renderScenes(): Array<?ReactElement> { | ||
return this.state.scenes.map(this._renderScene, this); | ||
} | ||
|
||
_renderScene(scene: NavigationScene): ?ReactElement { | ||
const { | ||
navigationState, | ||
onNavigate, | ||
renderScene, | ||
} = this.props; | ||
|
||
const { | ||
position, | ||
progress, | ||
scenes, | ||
} = this.state; | ||
|
||
return renderScene({ | ||
layout: this.state.layout, | ||
navigationState, | ||
onNavigate, | ||
position, | ||
progress, | ||
scene, | ||
scenes, | ||
}); | ||
} | ||
|
||
_renderOverlay(): ?ReactElement { | ||
if (this.props.renderOverlay) { | ||
const { | ||
navigationState, | ||
onNavigate, | ||
renderOverlay, | ||
} = this.props; | ||
|
||
const { | ||
position, | ||
progress, | ||
scenes, | ||
} = this.state; | ||
|
||
return renderOverlay({ | ||
layout: this.state.layout, | ||
navigationState, | ||
onNavigate, | ||
position, | ||
progress, | ||
scene: scenes[navigationState.index], | ||
scenes, | ||
}); | ||
} | ||
return null; | ||
} | ||
|
||
_onLayout(event: any): void { | ||
const {height, width} = event.nativeEvent.layout; | ||
|
||
const layout = { | ||
...this.state.layout, | ||
initHeight: height, | ||
initWidth: width, | ||
isMeasured: true, | ||
}; | ||
|
||
layout.height.setValue(height); | ||
layout.width.setValue(width); | ||
|
||
this.setState({ layout }); | ||
} | ||
|
||
_onTransitionEnd(): void { | ||
const scenes = this.state.scenes.filter(isSceneNotStale); | ||
if (scenes.length !== this.state.scenes.length) { | ||
this.setState({ scenes }); | ||
This comment has been minimized.
Sorry, something went wrong.
jmurzy
Contributor
|
||
} | ||
this.props.onTransitionEnd && this.props.onTransitionEnd(); | ||
} | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
scenes: { | ||
flex: 1, | ||
}, | ||
}); | ||
|
||
module.exports = NavigationTransitioner; |
@hedgerwang Possible regression here. Given 2 scenes, namely scene A and scene B, where scene B is marked stale and if
nextNavigationState
tries to recover scene B from stale, thescenes
andnavigationState
go out of sync whenrender
is called.In that
renderScene
andrenderOverlay
are both called withscene = undefined
.i.e.
scenes[navigationState.index]
will beundefined
becausenavigationState.index = 1
wherescenes
only contains the first scene.Verified that AnimatedView doesn't have the same issue. Cannot determine exactly why but commenting out the following lines fixes the issue:
Update—
I think there is a race condition between when
render
is invoked and_onTransitionEnd
is called after animations finish. You are callingsetState
in_onTransitionEnd
after removing stale scenes that fires a rerender but there is no guarantees that happens after the initialrender
./cc @ericvicenti
🍺