Jamgreen
Jamgreen

Reputation: 11039

Detect swipe left in React Native

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

Answers (9)

Kuza Grave
Kuza Grave

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

  • Import the previous useSwipe hook.
  • Add 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

Gabriel Morin
Gabriel Morin

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

highway__61
highway__61

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

Fotios Tsakiris
Fotios Tsakiris

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

king
king

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

Daniel Garmoshka
Daniel Garmoshka

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

Joe
Joe

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

Bhavan Patel
Bhavan Patel

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

Nikhil Gogineni
Nikhil Gogineni

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

Related Questions