I am making my own implementation of a tab navigator with swiping in React-Native. It works fine, but when I have a ScrollView inside one of my tabs it seems to break. Swiping left and right to change tabs works fine, and also scrolling down and up in scrollview. It breaks when I click to drag the scrollView and then move sideways without releasing to swipe. Then the tab system just resets to the first tab.
I made a hack where I disable swiping from inside a tab when the scrollview is scrolled. This works, but feels like a bad solution because the tab content has to be aware that it is inside a tab.
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { View, Animated, Dimensions, PanResponder } from 'react-native';
import Immutable from 'immutable';
import Tab1 from './Tab1';
import Tab2 from './Tab2';
import ScrollViewTab from './ScrollViewTab';
@connect(
state => ({
tabs: state.tabs
})
)
export default class Tabs extends Component {
static propTypes = {
tabs: PropTypes.instanceOf(Immutable.Map).isRequired,
dispatch: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.justLoaded = true;
this.state = {
left: new Animated.Value(0),
tabs: [{ // Tabs must be in order, despite index.
name: 'tab1',
component: <Tab1 setTab={this.setTab} />,
index: 0
}, {
name: 'tab2',
component: <Tab2 setTab={this.setTab} />,
index: 1
}, {
name: 'scrollViewTab',
component: <ScrollViewTab setTab={this.setTab} />,
index: 2
}]
};
this.getIndex = this.getIndex.bind(this);
}
componentWillMount() {
this.panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
if (Math.abs(gestureState.dx) > 10) {
return true;
}
return false;
},
onPanResponderGrant: () => {
this.state.left.setOffset(this.state.left._value);
this.state.left.setValue(0);
},
onPanResponderMove: (e, gestureState) => {
if (this.isSwipingOverLeftBorder(gestureState) ||
this.isSwipingOverRightBorder(gestureState)) return;
Animated.event([null, {
dx: this.state.left
}])(e, gestureState);
},
onPanResponderRelease: (e, gestureState) => {
this.state.left.flattenOffset();
if (this.isSwipingOverLeftBorder(gestureState) ||
this.isSwipingOverRightBorder(gestureState)) {
return;
}
Animated.timing(
this.state.left,
{ toValue: this.calcX(gestureState) }
).start();
}
});
}
componentDidMount() {
this.justLoaded = false;
}
getStyle() {
const oldLeft = this.state.left;
let left = 0;
const screenWidth = Dimensions.get('window').width;
// Set tab carouselle coordinate to match the selected tab.
this.state.tabs.forEach((tab) => {
if (tab.name === this.props.tabs.get('tab')) {
left = -tab.index * screenWidth;
}
});
if (this.justLoaded) {
Animated.timing(
this.state.left,
{ toValue: left,
duration: 0
}
).start();
return { transform: [{ translateX: oldLeft }], flexDirection: 'row', height: '100%' };
}
Animated.timing(
this.state.left,
{ toValue: left
}
).start();
return { transform: [{ translateX: oldLeft }], flexDirection: 'row', height: '100%' };
}
getIndex(tabN) {
let index = 0;
this.state.tabs.forEach((tab) => {
if (tab.name === tabN) {
index = tab.index;
}
return tab;
});
return index;
}
setTab(tab, props) {
this.navProps = props;
this.props.dispatch({ type: 'SET_TAB', tab });
}
isSwipingOverLeftBorder(gestureState) {
return (this.props.tabs.get('tab') === this.state.tabs[0].name &&
gestureState.dx > 0);
}
isSwipingOverRightBorder(gestureState) {
return (this.props.tabs.get('tab') === this.state.tabs[this.state.tabs.length - 1].name &&
gestureState.dx < 0);
}
calcX(gestureState) {
const screenWidth = Dimensions.get('window').width;
const activeTab = this.getIndex(this.props.tabs.get('tab'));
let coord = 0;
if (gestureState.dx > screenWidth * 0.2) {
coord = (activeTab * screenWidth) - screenWidth;
} else if (gestureState.dx < -(screenWidth * 0.2)) {
coord = (activeTab * screenWidth) + screenWidth;
} else {
coord = activeTab * screenWidth;
}
this.updateTab(-coord, screenWidth);
return -coord;
}
updateTab(coord, screenWidth) {
// Update current tab according to location and screenwidth
this.state.tabs.forEach((tab) => {
if (coord === -tab.index * screenWidth) {
this.props.dispatch({ type: 'SET_TAB', tab: tab.name });
}
});
}
render() {
return (
<View
style={{ flex: 1 }}
>
<Animated.View
style={this.getStyle()}
{...this.panResponder.panHandlers}
>
{this.state.tabs.map(tab => tab.component)}
</Animated.View>
</View>
);
}
}
Try using a function for onMoveShouldSetResponder so it only swipes horizontally when gestureState.dx is far greater than gestureState.dy like so:
onMoveShouldSetResponder: (evt, gestureState) => {
return Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 3);
},
You could also have a function in onPanResponderMove tracks the direction of the swipe gesture and then reset in onPanResponderRelease so you don't have problems when a vertical swipe changes into a horizontal swipe like so:
checkSwipeDirection(gestureState) {
if(
(Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 3) ) &&
(Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 3) )
) {
this._swipeDirection = "horizontal";
} else {
this._swipeDirection = "vertical";
}
}
canMove() {
if(this._swipeDirection === "horizontal") {
return true;
} else {
return false;
}
}
Then use it like so:
onMoveShouldSetPanResponder: this.canMove,
onPanResponderMove: (evt, gestureState) => {
if(!this._swipeDirection) this.checkSwipeDirection(gestureState);
// Your other code here
},
onPanResponderRelease: (evt, gestureState) => {
this._swipeDirection = null;
}
I found an awesome article online by Satyajit Sahoo on Medium How I built React Native Tab View.It shows more in-depth how to implement your own Tab View. I recommend taking a look at the blog post as it was really helpful for me.
Update: Check out the documentation here Gesture Responder Lifecycle if you want a parent component to prevent a child component from becoming the gesture responder or vice versa.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With