How to differentiate between double tap and single tap on react native? - react-native

I need to perform different action on single and double tap on a view. On double tap I need to like the image just like Instagram double tap experience. On single tap I need to open a modal.
For double tap I have used TapGestureHandler which works perfect
<TapGestureHandler
ref={this.doubleTapRef}
maxDelayMs={200}
onHandlerStateChange={this.onProductImageDoubleTap}
numberOfTaps={2}
>
<SomeChildComponent ...
But when I add any Touchable to detect single tap in the
<TouchableWithoutFeedback onPress={this.imageTapped}>
on double tapping the this.imageTapped function is called twice along with this.onProductImageDoubleTap. Is there any way to cancel tap on touchable when two taps are done is quick succession

The best solution is not using state, since setting state is asynchronous.Works like a charm for me on android !
let lastPress = 0;
const functionalComp = () => {
const onDoublePress = () => {
const time = new Date().getTime();
const delta = time - lastPress;
const DOUBLE_PRESS_DELAY = 400;
if (delta < DOUBLE_PRESS_DELAY) {
// Success double press
console.log('double press');
}
lastPress = time;
};
return <View
onStartShouldSetResponder =
{(evt) => onDoublePress()}>
</View>
}

2022 update
This is a performant native solution without any JS thread blocking calculation!
Many more tips here
const tap = Gesture.Tap()
.numberOfTaps(2)
.onStart(() => {
console.log('Yay, double tap!');
});
return (
<GestureDetector gesture={tap}>
{children}
</GestureDetector>
);

The best solution is use react-native-gesture-handler
https://github.com/software-mansion/react-native-gesture-handler
Here is my solution -
import {State, TapGestureHandler} from 'react-native-gesture-handler';
export const DoubleTap = ({children}: any) => {
const doubleTapRef = useRef(null);
const onSingleTapEvent = (event: any) => {
if (event.nativeEvent.state === State.ACTIVE) {
console.log("single tap 1");
}
};
const onDoubleTapEvent = (event: any) => {
if (event.nativeEvent.state === State.ACTIVE) {
console.log("double tap 1");
}
};
return (
<TapGestureHandler
onHandlerStateChange={onSingleTapEvent}
waitFor={doubleTapRef}>
<TapGestureHandler
ref={doubleTapRef}
onHandlerStateChange={onDoubleTapEvent}
numberOfTaps={2}>
{children}
</TapGestureHandler>
</TapGestureHandler>
);
};
Now we will wrap the component where we need to detect the double and single tap : -
<DoubleTap>
<View>
...some view and text
</View>
</DoubleTap>

The package react-native-double-tap seems to be what you are looking for.

since you are asking on handling one tap and double tap, here's a simple code i think should covered your issue
Untested
first defined clickCount:0 in state:
state={clickCount:0, //another state}
then create a function with setTimeout to handling if user tapping once or two times:
handlingTap(){
this.state.clickCount==1 ?
//user tap twice so run this.onProductImageDoubleTap()
this.onProductImageDoubleTap :
//user tap once so run this.imageTapped with setTimeout and setState
//i set 1 sec for double tap, so if user tap twice more than 1 sec, it's count as one tap
this.setState({clickCount:1}, ()=>{
setTimeout(()=>{
this.setState({clickCount:0})
this.imageTapped()
}, 1000)
})
}
<TouchableWithoutFeedback onPress={this.handlingTap()}/>
just used TouchableWithoutFeedback instead of TapGestureHandler

With hooks:
const [lastPressed, setLastPressed] = useState(0);
const handlePress = useCallback(() => {
const time = new Date().getTime();
const delta = time - lastPressed;
setLastPressed(time);
if (lastPressed) {
if (delta < DOUBLE_PRESS_DELAY) {
console.log('double press');
} else {
console.log('single press');
}
}
}, [lastPressed]);

I have modified flix's answer into this. By this way, you can catch one and double click separately. I also changed debounce into 300ms which is fairly well for one and double click.
state={clickCount:0, //another state}
Binding context into the handlingTap method
constructor(props){
super(props)
this.handlingTap = this.handlingTap.bind(this)
}
With this function you can catch them separately
handlingTap() {
this.state.clickCount === 1
? this.doubleClick() // This catches double click
: this.setState(state => ({ clickCount: state.clickCount + 1 }), () => {
setTimeout(() => {
if (this.state.clickCount !== 2) {
this.oneClick() // this catches one click
}
this.setState({ clickCount: 0 })
}, 300)
})
}
In the button you can use this way
<TouchableWithoutFeedback onPress={this.handlingTap}></TouchableWithoutFeedback>

Related

Reset the initial state value in Recat Native

Requirement: I have to blink a View for 2 sec with 2 different color (for ex red and white).
I can do this by using this code -
const [state, setState] = React.useState(false)
const [initialState, setInitialState] = React.useState(0)
React.useEffect(() => {
if (initialState < 2){
let interval = setInterval(() => {
setState(true)
setInitialState(initialState + 1)
setTimeout(() => {
setState(false)
}, 80);
}, 300);
setTimeout(() => {
clearInterval(interval)
}, 600);
}
}, [initialState])
and called it like -
<View style={{...styles.mainContainer, backgroundColor: state ? Colors.GRO7 : Colors.GRC9}}>
Another Requirement: I have another screen from it i an change the address, on successful address change i have to blink this view again for 2 sec. I'm not sure where i can reset the initial value to 0 again.
I am new In react native, could some one guide me how to achieve this functionality
Can‘t completely understand what‘s your target.
Here is a blinking text sample
you can pass in the initial value to this component instead of defining 0 in this line:
const [initialState, setInitialState] = React.useState(0)
you could have a param to pass in and put it instead of 0 so that every time that param changes this component will re render. and so you get a new initial state.
for example:
const [initialState, setInitialState] = React.useState(initialValue)

React navigation prevent double push()

I'm building an app with react-navigation-4.2.1. The app has multiple stack navigators. So there are a lots of navigation.push('Routename') calls.
Trouble is when the control surface (i.e. TouchableOpacity) is tapped rapidly multiple times (first one, and the rest during screen transition) I end up pushing multiple screens into the stack. Is there a way to restrict the surface to the first tap/call of push()?
The component below is what i use to make things touchable. it handle multiple touches in small period of time.
Use component below instead of TouchableOpacity. wrap any thing you want with this component and it will be touchable.
<SafeTouch
onPress={...}
>
<Text> hey! im a touchable text now</Text>
</SafeTouch>
The component below is written used TypeScirpt.
every touch within 300ms after first touch will be ignored(thats where help you with your problem).
import * as React from 'react'
import { TouchableOpacity } from 'react-native'
interface ISafeTouchProps {
onPress: () => void
onLongPress?: () => void
onPressIn?: () => void
onPressOut?: () => void,
activeOpacity?: number,
disabled?: boolean,
style: any
}
export class SafeTouch extends React.PureComponent<ISafeTouchProps> {
public static defaultProps: ISafeTouchProps = {
onPress: () => { },
onLongPress: () => { },
onPressIn: () => { },
onPressOut: () => { },
disabled: false,
style: null
}
private isTouchValid: boolean = true
private touchTimeout: any = null
public constructor(props: ISafeTouchProps) {
super(props)
{// Binding methods
this.onPressEvent = this.onPressEvent.bind(this)
}
}
public render(): JSX.Element {
return (
<TouchableOpacity
onPress={this.onPressEvent}
onLongPress={this.props.onLongPress}
onPressIn={this.props.onPressIn}
onPressOut={this.props.onPressOut}
activeOpacity={this.props.activeOpacity}
disabled={this.props.disabled}
style={[{minWidth: 24, minHeight: 24}, this.props.style]}
>
{
this.props.children
}
</TouchableOpacity>
)
}
public componentWillUnmount() {
this.clearTimeoutIfExists()
}
private onPressEvent(): void {
requestAnimationFrame(() => {
if (this.isTouchValid === false) {
return
}
this.isTouchValid = false
this.clearTimeoutIfExists()
this.touchTimeout = setTimeout(() => {
this.isTouchValid = true
}, 300)
if (typeof this.props.onPress === 'function') {
this.props.onPress()
}
})
}
private clearTimeoutIfExists(): void {
if (this.touchTimeout != null) {
clearTimeout(this.touchTimeout)
this.touchTimeout = null
}
}
}
This is the proper behavior for Push and it is not a bug if you want
to avoid the duplicate screen on double tab you can just use navigation.navigate.
To avoid pushing the screen more than once when clicking in the same button in a short span of time, I created a generic hook to avoid running a function more than once (accepting an interval to allow run again):
export const useCallOnce = <T extends unknown[], K>(
fn: (...args: T) => K,
allowAfter?: number,
) => {
const ref = React.useRef<number | undefined>();
const resultFn = (...args: T) => {
const now = new Date().getTime();
if (!ref.current || (allowAfter && ref.current + allowAfter < now)) {
ref.current = now;
return fn(...args);
}
};
return resultFn;
};
Then, you can just call it as in the following example:
const navigation = useNavigation<NativeStackNavigationProp<{ ExampleScreen: undefined }>>();
const push = useCallOnce(() => navigation.push('ExampleScreen'), 500);
// just call on the button click event as: onSomeEvent={() => push()}
You can create a generic button component that accept the push parameters with the hook above, similar to the example, and use this button whenever you want a button to navigate between pages.

Need assistance fixing maximum update depth exceeded issue

I'm trying to write a hacky fix to ScrollableTabView since it isn't playing nice with the function that triggers when there's a tab switch. When I replace the setState with console.log I see that it only triggers once with every tab switch so it's not looping infinitely like the error is complaining.
Parent container
state = {
headerName: 'Loading',
}
setHeader = (header) => {
this.setState({'headerName': header})
}
render () {
return (
<ScrollableTabView
renderTabBar={() => <BottomTabBar setHeader={this.setHeader} headerNames={['A','B','C']} />}
>
)
}
BottomTabBar
render() {
this.props.setHeader(this.props.headerNames[this.props.activeTab])
...
}

React Native : How can I get gestures onTouchStart, onTouchEnd and onTouchMove event positions ( like X and Y Coordinations) in Android

How would I get the coordinates of my onTouchEnd event. So I touch or move figure anywhere within the display and I can retrieve the X, Y positioning of where it happened. onResponderRelease is not triggered in a onTouchEnd on Android,
I've included example implementations for all of the gesture response event handlers, but have commented out most of them on my View to just provide the basic functionality: subscribing to all touch and move events
pseudo code would look like this :
<View
style={this.props.style}
ref={this._saveRef}
onStartShouldSetResponder={(event) => {
return this.handleDoubleTap({nativeEvent:
event.nativeEvent});
}}
onResponderGrant={this._onTouchStart}
onResponderMove={this._onTouchMove}
onResponderRelease={this._onTouchEnd}
onResponderTerminate={this._onTouchEnd} // When
onResponderRelease can't call by some reason
>
{this.props.children}
</View>
Responder Event Handler Methods :
if (this.isDoubleTap) {
return false;
}
this.context.setScroll && this.context.setScroll(false);
const currentTouchTimeStamp = Date.now();
this._prevTouchInfo = {
prevTouchX: nativeEvent.pageX,
prevTouchY: nativeEvent.pageY,
prevTouchTimeStamp: currentTouchTimeStamp
};
this.props.onStart(nativeEvent);
};
_onTouchMove = ({nativeEvent}) => {
if (nativeEvent.touches.length <= 1) {
if (this.isDoubleTap) {
return false;
}
const self = this;
const gesture = {
x0: this._prevTouchInfo.prevTouchX,
x1: nativeEvent.pageX,
y0: this._prevTouchInfo.prevTouchY,
y1: nativeEvent.pageY,
dx: nativeEvent.pageX - this._prevTouchInfo.prevTouchX,
dy: nativeEvent.pageY - this._prevTouchInfo.prevTouchY
};
InteractionManager.runAfterInteractions(function () {
self.props.onMove(nativeEvent, gesture);
});
}
};
_onTouchEnd = ({nativeEvent}) => {
nativeEvent.touches.length === 0 && this.context.setScroll && this.context.setScroll(true);
this.props.onEnd(nativeEvent);
};
}
I am Developing an Application in React native.Actually when i am touch on Particular Position On view, getting the Corresponding x and y co-ordinates. App UI would look like this:
If this is still not enough functionality for android (eg. if you need multi-touch info), refer to PanResponde

move the view up when keyboard as shown in react-native

hai i am trying to move the view up when keyboard as shown using react-native,I followed the #sherlock's comment in (How to auto-slide the window out from behind keyboard when TextInput has focus? i got an error like this
I don't know how to resolve this error, can any one help me how to resolve this, any help much appreciated.
There's a great discussion about this in the react-native github issues
https://github.com/facebook/react-native/issues/3195#issuecomment-147427391
I'd start there, but here are a couple more links you may find useful, one of which is mentioned already in the article you referenced...
[React Tips] Responding to the keyboard with React Native
Andr3wHur5t/react-native-keyboard-spacer
In my library "react-native-form-generator" (https://github.com/MichaelCereda/react-native-form-generator) i did the following.
I created a Keyboard Aware scroll view (partially modified from https://github.com/facebook/react-native/issues/3195#issuecomment-146518331)
the following it's just an excerpt
export class KeyboardAwareScrollView extends React.Component {
constructor (props) {
super(props)
this.state = {
keyboardSpace: 0,
}
this.updateKeyboardSpace = this.updateKeyboardSpace.bind(this)
this.resetKeyboardSpace = this.resetKeyboardSpace.bind(this)
}
updateKeyboardSpace (frames) {
let coordinatesHeight = frames.endCoordinates.height;
const keyboardSpace = (this.props.viewIsInsideTabBar) ? coordinatesHeight - 49 : coordinatesHeight
this.setState({
keyboardSpace: keyboardSpace,
})
}
resetKeyboardSpace () {
this.setState({
keyboardSpace: 0,
})
}
componentDidMount () {
// Keyboard events
DeviceEventEmitter.addListener('keyboardWillShow', this.updateKeyboardSpace)
DeviceEventEmitter.addListener('keyboardWillHide', this.resetKeyboardSpace)
}
componentWillUnmount () {
DeviceEventEmitter.removeAllListeners('keyboardWillShow')
DeviceEventEmitter.removeAllListeners('keyboardWillHide')
}
scrollToFocusedInput (event, reactNode, extraHeight = 69) {
const scrollView = this.refs.keyboardScrollView.getScrollResponder();
setTimeout(() => {
scrollView.scrollResponderScrollNativeHandleToKeyboard(
reactNode, extraHeight, true
)
}, 220)
}
render () {
return (
<ScrollView
ref='keyboardScrollView'
keyboardDismissMode='interactive'
contentInset={{bottom: this.state.keyboardSpace}}
showsVerticalScrollIndicator={true}
style={this.props.style}>
{this.props.children}
</ScrollView>
)
}
Then i use it like any other scrollview
import { KeyboardAwareScrollView } from 'react-native-form-generator'
...
handleFormFocus(event, reactNode){
this.refs.scroll.scrollToFocusedInput(event, reactNode)
}
...
<KeyboardAwareScrollView ref='scroll'>
<Form ref='registrationForm'
onFocus={this.handleFormFocus.bind(this)}
onChange={this.handleFormChange.bind(this)}
label="Personal Information">
........
</Form>
</KeyboardAwareScrollView>
on change my component (Form) will call scrollToFocusedInput in KeyboardAwareScrollView (using the ref).
i suggest to check the code of my library (see the link on top), or simply use it (everything it's already tested and working).
If you have further questions just comment