Closed viettranme closed 7 years ago
Tab transitions use a spring animations which takes some time to settle. The navigation state is not updated until the animation finishes.
If you want to style the selected tab differently, you should use the animated position
prop which is passed into the callback. This way the text color/background color will change from one to the other while swiping.
Here's a simple example which changes the text color on tab change - https://github.com/react-native-community/react-native-tab-view/blob/master/example/src/ScrollViewsExample.js#L83
@satya164: I know the navigation state is not updated until the animation finishes. The problem here is that there's quite long delay since the animation finishes til the tab header is updated (for example: it's style in this case; noticable delay changing selected tab icon from outline to solid style). When you run the example you can see the issue there.
@viettranme The problem is you're relying on navigation state to update the tabs. When swiping, you don't know what's the next tab that'll be selected. We cannot update the navigation state. The tab header is not re-rendered until navigation state changes. We need to wait for the swipe to finish.
Now, we can just update everything when the swipe finishes, but this will cause a frame drop between the end of swipe and start of animation. That's why we wait for the animation to finish. And spring animations take long time to settle down.
The position
prop is an animated value which represents the animation position. You can use it in your style like in the example I linked to. Please check the example.
OK, I see. This is because of "spring animations take long time to settle down" issue.
In my app, there is another bar above the tabbar to show title + custom things related to the current tab. Is it possible to guess the target tab by using the position prop? Thanks
PS: I have tried with this before but encounter max stack calls limit issue as I try to setState inside that amination callback function.
@viettranme You cannot directly use the position
prop, but you can do this,
const tabPosition = new Animated.Value(0)
, and pass it downonChangePosition
callback, _handleChangePosition = value => this.props.tabPosition.setValue(value)
Let me try with your suggestion. Thanks @satya164.
@viettranme Lemme know.
@satya164 I have created a wrapper with a state attribute tabPosition and a callback to guess tabPosition based on the animated postion props of the tab view (using Math.round).
const initialLayout = {
height: 0,
width: Dimensions.get('window').width,
};
class ExampleOfLongDelayWrapper extends React.Component {
state = {
tabPosition: 0
};
_handleChangePosition = (value) => {
let next = Math.round(value);
if (next != this.state.tabPosition) {
this.setState({tabPosition: next});
}
}
_renderTopBar = () => {
const routes = [
{ key: '1', title: 'First', icon: 'ios-book' },
{ key: '2', title: 'Second', icon: 'ios-chatboxes' },
{ key: '3', title: 'Third', icon: 'ios-paper' },
];
const title = routes[this.state.tabPosition].title;
return (
<View style={styles.headerBarContainer}>
<Text style={styles.headerText}>{title}</Text>
</View>
);
}
render() {
return (
<View style={styles.container}>
{this._renderTopBar()}
<ExampleOfLongDelay
onChangePosition={this._handleChangePosition}
/>
</View>
);
}
}
class ExampleOfLongDelay extends React.Component {
state = {
index: 0,
routes: [
{ key: '1', title: 'First', icon: 'ios-book' },
{ key: '2', title: 'Second', icon: 'ios-chatboxes' },
{ key: '3', title: 'Third', icon: 'ios-paper' },
],
};
shouldComponentUpdate(nextProps, nextState) {
return this.state != nextState;
}
_onChangeTabIndex = (index) => {
this.setState({
index,
});
};
_renderLabel = ({ navigationState }) => ({ route, index }) => {
const selected = navigationState.index === index;
return (
<Text style={[ styles.label, selected ? styles.selected : styles.idle ]}>
{route.title}
</Text>
);
};
_renderIcon = ({ navigationState }) => ({ route, index }: any) => {
const selected = navigationState.index === index;
return (
<Icon
name={selected ? route.icon : route.icon + '-outline'}
size={24}
style={[ selected ? styles.selected : styles.idle ]}
/>
);
};
_renderPager = (props) => {
return <TabViewPagerPan {...props} swipeEnabled={true} />;
};
_renderHeader = (props) => {
return (
<TabBarTop
{...props}
renderIcon={this._renderIcon(props)}
renderLabel={this._renderLabel(props)}
jumpToIndex={this._onChangeTabIndex}
style={styles.tabbar}
activeOpacity={1}
/>
)
};
_renderScene = ({route}) => {
switch (route.key) {
case '1':
return <View style={[ styles.page, { backgroundColor: '#ff4081' } ]} />;
case '2':
return <View style={[ styles.page, { backgroundColor: '#673ab7' } ]} />;
case '3':
return <View style={[ styles.page, { backgroundColor: '#4caf50' } ]} />;
default:
return null;
}
};
render() {
return (
<TabViewAnimated
style={{ flex: 1 }}
navigationState={this.state}
renderScene={this._renderScene}
renderHeader={this._renderHeader}
renderPager={this._renderPager}
onRequestChangeTab={this._onChangeTabIndex}
initialLayout={initialLayout}
onChangePosition={this.props.onChangePosition}
/>
);
}
}
The result Now, my custom top bar can be updated very quickly on tab view swiping (as it is re-rendered based on the guessed tab position), so this is OK.
The issue is still with the original tabbar of the component as it still has a lag time. Could I have any option to intervene with the renderHeader callback so that I can use the guessed tab position value to update the tabbar quickly in advance?
The issue is still with the original tabbar of the component as it still has a lag time
You can use the position
prop there to change the styles. For icons you can have one on top of another and cross-fade. Check the example I linked to.
Thanks @satya164, now I can change the styles of selected tab item. For the icons cross-fade animation, do you have an example somewhere? (I am quite new with react-native animation.) Thanks in advance
@viettranme Don't have an example, right now, but you can just place 2 icons absolutely positioned above a=one another, and then give them opacity,
const inputRange = props.navigationState.routes.map((x, i) => i);
const opacity1 = props.position.interpolate({
inputRange,
outputRange: inputRange.map(inputIndex => inputIndex === index ? 1 : 0),
});
const opacity2 = props.position.interpolate({
inputRange,
outputRange: inputRange.map(inputIndex => inputIndex === index ? 0 : 1),
});
Thanks @satya164. It works now.
Just my 2 cents: While it's OK for now with this workaround fix, I am thinking of moving the renderHeader() and renderFooter() codes from the function TabViewAnimated._renderItems() to the function TabViewAnimated.render() so that we can update the tab header/footer before hand during tab view transition based on guessed target tab index.
Moving the code to render won't matter because a render isn't triggered until state is updated. We avoid updating state between the transition because it makes the animation feel janky, especially on Android.
The proper way is to use position
prop if we want good performance. That's what's used internally everywhere.
Just to clarify my idea as it may be helpful for other readers:
I mean:
_renderItems = (props: SceneRendererProps) => {
const { renderPager, renderHeader, renderFooter } = this.props;
return (
<View style={styles.container}>
{renderPager({
....
})}
</View>
);
};
render() {
return (
<View style={styles.container}>
<RENDER_HEADER_CODE>
<TabViewTransitioner
{...this.props}
loaded={this.state.loaded}
onChangePosition={this._handleChangePosition}
render={this._renderItems}
/>
<RENDER_FOOTER_CODE>
</View>
);
}
The handler _handleChangePosition will use the position prop to guess the target tab position (by using Math.round) and update a state attribute tabIndex
Due to this state change, the
That's details of my idea. Not sure if there's something wrong with it.
Closing the issue since this is the expected behaviour. We should document this though to avoid confusion.
Hello,
In the example below, I use two methods for tab navigation:
Method 1: by clicking on tab headers ( using jumpToIndex)
Method 2: by swiping tab views (using onRequestChangeTab)
These two methods show different user experiences.
In case of method 1, the tab top bar is updated instantly to reflect tab change.
In case of method 2, there's a quite noticable and somewhat annoying delay between tab transition end and tab header update.
Note: Tested on a physical Android device via 'react-native run-android'
Due to this issue, now I have to disable tab view swiping.
Please advice how I can fix it.
Thanks