Reputation: 1249
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>
);
}
}
Upvotes: 12
Views: 7605
Reputation: 23263
Or you can try using my util react-native-scroll-locky and get it over with..!
import RNLocky from "react-native-scroll-locky";
class WhateverClass extends Component {
constructor(props) {
super(props);
this.directionLockPanHandler = new RNLocky(
RNLocky.Direction.HORIZONTAL, // or RNLocky.Direction.VERTICAL
);
}
render() {
return (
<ScrollView
{...this.directionLockPanHandler.getPanHandlers()}
>
<ScrollView>
...
</ScrollView>
</ScrollView>
);
}
}
Now the scroll view you applied this to, will only respond to the direction you want it to, and the two of them won't get confused again about which one should respond to the pan/gesture.
Upvotes: 0
Reputation: 2183
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.
Upvotes: 18
Reputation: 2149
In your view controller which is using your code block(scrollView,pan gesture) ,
write this function:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Hope it helps.:)
Upvotes: -4