Handling multiple click events in React Native
Joel Raju
Posted on February 13, 2021
Originally published on my blog.
Introduction to click events in React Native
React Native provides 3 main primitives to handle click events.
- TouchableHighlight
- TouchableOpacity
- TouchableWithoutFeedback
Using any of these is a pretty standard affair. Just wrap our component that needs to respond to click events.
// imports...
<TouchableHighlight onPress={() => console.log('Clicked')}>
<Text>Click me</Text>
</TouchableHighlight>
Problem with Touchables
Though they work well for most of the use cases, there are some tricky situations which they cannot handle. Consider the case of handling a single click, double click and a long press event, all on the same element.
PanResponder to the rescue
PanResponder provides a predicatable wrapper to the lower level Gesture Responder System API. It provides much granular control over the touch events and also gives access to useful meta info like touch start position, touch end position, velocity of the gesture etc.
Let's learn how to make a View
component respond to touch events using PanResponder.
import { View, PanResponder, Text } from 'react-native';
const MyComponent = () => {
const responder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderStart: (event) => {
console.log('Touch has started !');
},
onPanResponderRelease: (event, gestureState) => {
console.log('Touch has ended !');
},
onPanResponderTerminate: () => {},
});
return (
<View {...responder.panHandlers}>
<Text>Click Me </Text>
</View>
);
};
onStartShouldSetPanResponder
should return true
to allow the view to become the responder at the start of a touch event.
onMoveShouldSetPanResponder
should return true
to allow the view to become the responder at the start of a drag event.
onPanResponderStart
callback is fired when the PanResponder
registers touch events.
onPanResponderRelease
callback is fired when the touch has been released.
onPanResponderTerminate
callback is fired when the responder has been taken from the View. This can happen when other views makes a call to onPanResponderTerminationRequest
or it can be taken by
the OS without asking (happens with control center/ notification center on iOS).
To make the double click work, we need to use a counter and set a maximum time duration between the click so as to treat it as a double click. 400ms of delay between the clicks is a good place to start. We'll use the handleTap
to determine the type of click event based on the timer.
const MyComponent = () => {
const [isTerminated, setTerminated] = useState(false);
const [touchStartTime, setTouchStartTime] = useState(0);
const [lastTap, setLastTap] = useState(0);
const DOUBLE_PRESS_DELAY = 400;
const handleTap = (event, gestureState) => {
const timeNow = Date.now();
if (lastTap && timeNow - lastTap < DOUBLE_PRESS_DELAY) {
console.log('Handle double press');
} else {
setLastTap(timeNow);
const timeout = setTimeout(() => {
setLastTap(0);
console.log('Handle single press');
}, DOUBLE_PRESS_DELAY);
}
};
const responder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderStart: () => {
const timeout = setTimeout(() => {
if (!isTerminated) {
setTouchStartTime(Date.now());
}
});
},
onPanResponderRelease: (event, gestureState) => {
handleTap(event, gestureState);
},
onPanResponderTerminate: () => {
setTerminated(true);
},
});
return (
<View {...responder.panHandlers}>
<Text>Click Me </Text>
</View>
);
};
And now to make the long press work we need another counter with a delay of 700ms. We'll first check if it is a long press before checking it was a single press or a double press. We'll use handlePressOut
to determine the type of click and deligate the action for it.
const MyComponent = () => {
const [isTerminated, setTerminated] = useState(false);
const [touchStartTime, setTouchStartTime] = useState(0);
const [lastTap, setLastTap] = useState(0);
const [longPressTimer, setLongPressTimer] = useState(0);
const [singlePressTimer, setSinglePressTimer] = useState(0);
const DOUBLE_PRESS_DELAY = 400;
const LONG_PRESS_DELAY = 700;
const cancelLongPressTimer = () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
setLongPressTimer(0);
}
};
const cancelSinglePressTimer = () => {
if (singlePressTimer) {
clearTimeout(singlePressTimer);
setSinglePressTimer(0);
}
};
const handleTap = (event, gestureState) => {
cancelSinglePressTimer();
const timeNow = Date.now();
if (lastTap && timeNow - lastTap < DOUBLE_PRESS_DELAY) {
console.log('Handle double press');
} else {
setLastTap(timeNow);
const timeout = setTimeout(() => {
setLastTap(0);
console.log('Handle single press');
}, DOUBLE_PRESS_DELAY);
setSinglePressTimer(timeout);
}
};
const handlePressOut = (event, gestureState) => {
const elapsedTime = Date.now() - touchStartTime;
if (elapsedTime > LONG_PRESS_DELAY) {
console.log('Handle long press');
} else {
handleTap(event, gestureState); // handles the single or double click
}
setTouchStartTime(0);
};
const responder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderStart: () => {
cancelLongPressTimer();
const timeout = setTimeout(() => {
if (!isTerminated) {
setTouchStartTime(Date.now());
}
});
setLongPressTimer(timeout);
},
onPanResponderRelease: (event, gestureState) => {
handlePressOut(event, gestureState);
},
onPanResponderTerminate: () => {
setTerminated(true);
},
});
return (
<View {...responder.panHandlers}>
<Text>Click Me </Text>
</View>
);
};
Conclusion
I've made react-native-gifted-touch which exactly does this so you can handle multiple clicks on the same element effortlessly. The default time delays in the library can be configured using props
to better suit your requirements. Feel free to check it out.
Posted on February 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 4, 2021