Reputation: 11039
How can I detect a left swipe on the entire screen in React Native?
Would it be necessary to use PanResponder or can it be done a little more easy?
Upvotes: 39
Views: 90920
Reputation: 1574
I made this simple solution using scrollviews and touch position.
It has a very clean implementation with no heavy components or external modules.
You can also use this with <View>
components instead of scrollviews.
So first, we will be creating a hook: useSwipe.tsx
import { Dimensions } from 'react-native';
const windowWidth = Dimensions.get('window').width;
export function useSwipe(onSwipeLeft?: any, onSwipeRight?: any, rangeOffset = 4) {
let firstTouch = 0
// set user touch start position
function onTouchStart(e: any) {
firstTouch = e.nativeEvent.pageX
}
// when touch ends check for swipe directions
function onTouchEnd(e: any){
// get touch position and screen size
const positionX = e.nativeEvent.pageX
const range = windowWidth / rangeOffset
// check if position is growing positively and has reached specified range
if(positionX - firstTouch > range){
onSwipeRight && onSwipeRight()
}
// check if position is growing negatively and has reached specified range
else if(firstTouch - positionX > range){
onSwipeLeft && onSwipeLeft()
}
}
return {onTouchStart, onTouchEnd};
}
then, in your component... in my case im going to use: exampleComponent.tsx
useSwipe
hook.onTouchStart
and onTouchEnd
events to your scrollView.ExampleComponent
import * as React from 'react';
import { ScrollView } from 'react-native';
import { useSwipe } from '../hooks/useSwipe'
export function ExampleComponent(props: any) {
const { onTouchStart, onTouchEnd } = useSwipe(onSwipeLeft, onSwipeRight, 6)
function onSwipeLeft(){
console.log('SWIPE_LEFT')
}
function onSwipeRight(){
console.log('SWIPE_RIGHT')
}
return (
<ScrollView onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
{props.children}
</ScrollView>
);
}
You can mess around with the offsetRange
property to handle precision.
And adapt the original code to be used with normal class components instead of hooks.
Upvotes: 47
Reputation: 2190
A quick example I built on top of Daniel's answer, using hooks, and fixing the issue I had on Android if used on a screen containing a Scroll or children with too many pointer events, eventually preventing the onTouchEnd
event to be fired.
import React, { useCallback, useRef } from 'react'
import { Platform, View } from 'react-native'
const SWIPE_DISTANCE = Platform.OS === 'android' ? 50 : 100 // -> higher = harder to swipe
const MAX_Y_SCROLL_DISTANCE_TO_ALLOW_SWIPE = 100 // -> lower = harder to swipe
// MAX_Y_SCROLL_DISTANCE_TO_ALLOW_SWIPE on most device does not make a big difference, better not filtering too much
// The main factor is the swipe distance
export const MyScreenComponent = () => {
const touchX = useRef(0)
const touchY = useRef(0)
const onSwipeTab = useCallback((direction: 'next' | 'previous') => {
switch (direction) {
case 'next':
// do something
break
case 'previous':
// do something
}
}, [])
const onTouchStart = useCallback((e: any) => {
const { pageX, pageY } = e.nativeEvent
touchX.current = pageX
touchY.current = pageY
}, [])
const onTouchEnd = useCallback(
(e: any) => {
const { pageX, pageY } = e.nativeEvent
const diffX = touchX.current - pageX
const absDiffY = Math.abs(touchY.current - pageY)
if (absDiffY < MAX_Y_SCROLL_DISTANCE_TO_ALLOW_SWIPE) {
if (diffX > SWIPE_DISTANCE) {
onSwipeTab('next')
} else if (diffX < -SWIPE_DISTANCE) {
onSwipeTab('previous')
}
}
},
[onSwipeTab],
)
return (
<View onTouchCancel={onTouchEnd} onTouchEnd={onTouchEnd} onTouchStart={onTouchStart}>
{/* Your screen */}
</View>
)
}
Using a library such as react-native-swipe-gestures is probably a good alternative, but in some cases, this is good enough and avoids an additional third-party dependency.
Upvotes: 0
Reputation: 81
I was using the solution provided by Kuza Grave but encountered a bug on a Samsung Galaxy phone where onTouchEnd
was not being fired as expected. I ended up creating another implementation using the PanResponder.
SwipeContainer.tsx
import React, { FC, ReactNode } from "react";
import { View, Animated, PanResponder } from "react-native";
type Props = {
children: ReactNode;
onSwipeRight: () => void;
onSwipeLeft: () => void;
};
const SWIPE_THRESHOLD = 200;
export const SwipeContainer: FC<Props> = ({
children,
onSwipeLeft,
onSwipeRight,
}) => {
const panResponder = PanResponder.create({
onStartShouldSetPanResponder: (_evt, _gestureState) => true,
onPanResponderRelease: (_evt, gestureState) => {
const { dx } = gestureState;
if (dx > SWIPE_THRESHOLD) {
onSwipeRight();
}
if (dx < -SWIPE_THRESHOLD) {
onSwipeLeft();
}
// If needed, could add up and down swipes here with `gestureState.dy`
},
});
return (
<Animated.View {...panResponder.panHandlers}>
<View>{children}</View>
</Animated.View>
);
Example.tsx
:
import React, { FC } from "react";
import { ChildComponent1, ChildComponent2, SwipeContainer } from "./components";
export const Example: FC = () => {
const left = () => console.log("left");
const right = () => console.log("right");
return (
<SwipeContainer onSwipeLeft={left} onSwipeRight={right}>
<ChildComponent1 />
<ChildComponent2 />
</SwipeContainer>
);
};
Upvotes: 1
Reputation: 1558
Credits to @Nikhil Gogineni! I modified his code to a functional component without a componentWillMount.
SwipeGesture.tsx
import React, { useEffect } from 'react';
import {
View,
Animated,
PanResponder
} from 'react-native';
/* Credits to: https://github.com/nikhil-gogineni/react-native-swipe-gesture */
const SwipeGesture = (props: any) => {
const panResponder = React.useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
const x = gestureState.dx;
const y = gestureState.dy;
if (Math.abs(x) > Math.abs(y)) {
if (x >= 0) {
props.onSwipePerformed('right')
}
else {
props.onSwipePerformed('left')
}
}
else {
if (y >= 0) {
props.onSwipePerformed('down')
}
else {
props.onSwipePerformed('up')
}
}
}
})).current;
return (
<Animated.View {...panResponder.panHandlers} style={props.gestureStyle}>
<View>{props.children}</View>
</Animated.View>
)
}
export default SwipeGesture;
And the usage is the "same" ... Thanks Nikhil!
Upvotes: 1
Reputation: 81
You can just user FlingGestureHandler from react-native-gesture-handler link to the docs. Wrap your view with it. Here's you do it.
import { Directions, Gesture, GestureDetector } from 'react-native-gesture-handler'
const MyComponentWithLeftSwipe = () => {
const flingGestureLeft = Gesture
.Fling()
.direction(Directions.LEFT)
.onEnd(() => console.log("I was swiped!")
return <GestureDetector gesture={flingGestureLeft}>
<View>
...
</View>
</GestureDetector>
}
Upvotes: 6
Reputation: 6352
I've found that react-native-swipe-gestures
isn't stable (swipes works randomly on android) and react-native-gesture-handler
is overcomplicated (too much efforts to just add to project).
Simplified solution based on Kuza Grave's answer, who's solution works perfect and very simple:
<View
onTouchStart={e=> this.touchY = e.nativeEvent.pageY}
onTouchEnd={e => {
if (this.touchY - e.nativeEvent.pageY > 20)
console.log('Swiped up')
}}
style={{height: 300, backgroundColor: '#ccc'}}
/>
Upvotes: 39
Reputation: 8272
If you are using a managed Expo project there is a documented gesture handler here: https://docs.expo.io/versions/latest/sdk/gesture-handler/
The source code docs can be found here: https://docs.swmansion.com/react-native-gesture-handler/docs/
For swiping I think you would need to use FlingGestureHandler
:
https://docs.swmansion.com/react-native-gesture-handler/docs/handler-fling
Upvotes: 5
Reputation: 1755
There is an existing component react-native-swipe-gestures
for handling swipe gestures in up, down, left and right direction, see https://github.com/glepur/react-native-swipe-gestures
Upvotes: 26
Reputation: 138
You can use react-native-swipe-gesture. You don't need to install any third party module using npm. Download the file into your project and follow the given steps
Upvotes: 12