software-mansion / react-native-gesture-handler

Declarative API exposing platform native touch and gesture system to React Native.
https://docs.swmansion.com/react-native-gesture-handler/
MIT License
5.85k stars 954 forks source link

PanGestureHandler doesn't work on Android #1478

Closed dpyeates closed 2 years ago

dpyeates commented 2 years ago

Description

I am using a PanGestureHandler in my React Native app. This works perfectly on iOS. When I run on Android, I get nothing at all, no triggers. I have installed as per the guidelines and have made the change to MainActivity.java to add the ReactActivityDelegate. I am using React Navigation. I have also attempted to use the gestureHandlerRootHOC method without any luck. Is PanGestureHandler compatible with React Navigation?

Expected behavior

Pan gesture should trigger and work as per iOS

Actual behavior

No trigger at all on Android

Package versions

jakub-gonet commented 2 years ago

Did you clear caches after installation? Try running ./gradlew clean in android/ directory and running metro with --reset-cache flag (i.e. yarn start --reset-cache).

dpyeates commented 2 years ago

Just tried cleaning and resetting the metro cache but still the same. TapGestureHandler works fine on both iOS and Android.

ammarahm-ed commented 2 years ago

I am facing the same problem. Tried the suggested steps but does not help.

Hardik500 commented 2 years ago

PanGestureHandler is working fine for me on Android. Are you seeing any error or something, and also have you performed the required steps for linking on Android? If you want I can also share the snippe of how I have used it.

rahmanharoon commented 2 years ago

PanGestureHandler is working fine for me on Android. Are you seeing any error or something, and also have you performed the required steps for linking on Android? If you want I can also share the snippe of how I have used it.

Can you share the snippet. It will be useful

Hardik500 commented 2 years ago

@rahmanharoon Here is the snippet for the gesture handler,

const translation = {
        x: useSharedValue(0),
};

const SWIPE_VELOCITY = 500;
const { width: screenWidth } = useWindowDimensions();
const hiddenTranslateX = 2 * screenWidth;

const gestureHandler = useAnimatedGestureHandler({
        onStart: (_, ctx) => {
            ctx.startX = translation.x.value;
        },
        onActive: (event, ctx) => {
            if (flipped) {
                translation.x.value = ctx.startX + event.translationX;
            }
        },
        onEnd: (event) => {
            if (flipped) {
                if (Math.abs(event.velocityX) < SWIPE_VELOCITY) {
                    translation.x.value = withSpring(0);

                    return;
                }

                //perform actions with swipe
                translation.x.value = withSpring(Math.sign(event.velocityX * 10000) * hiddenTranslateX);
            }
        },
    });

return (
    <PanGestureHandler onGestureEvent={gestureHandler}>
        <Cards/>
   </PanGestureHandler>
)
ajesamann commented 2 years ago

Also having this issue. Installed everything as per the docs, cleared cache and cleaned gradle. PanGestureHandler works fine on iOS but no triggers at all on Android.

What makes it even more strange is I have another PanGestureHandler in another component setup almost the exact same way that works fine on both iOS and Android. Any fixes for this? Here's my code in case it helps.

import React from 'react';
import {View, Text} from 'react-native';
import {PanGestureHandler} from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';
//redux
import {connect} from 'react-redux';
//styles
import {gbl} from '../styles/global_styles';
import {alert} from '../styles/components/alert';

let timer;

const Alert = props => {
  const translateY = useSharedValue(-290);

  const slideIn = () => {
    translateY.value = withTiming(0);
  };

  const slideOut = () => {
    translateY.value = withTiming(-290);
  };

  const clearTimer = () => {
    clearTimeout(timer);
  };

  const setTimer = () => {
    timer = setTimeout(() => {
      props.alert.show ? (translateY.value = withTiming(-290)) : null;
    }, 2000);
  };

  const animate = () => {
    slideIn();
    timer = setTimeout(() => {
      props.alert.show ? slideOut() : null;
    }, 3000);
  };

  animate();

  const panGestureEvent = useAnimatedGestureHandler({
    onStart: (e, context) => {
      context.translateY = translateY.value;
    },
    onActive: (e, context) => {
      runOnJS(clearTimer)();
      if (!(e.translationY >= 20)) {
        translateY.value = context.translateY + e.translationY;
      }
    },
    onEnd: (e, context) => {
      if (e.translationY <= -10) {
        translateY.value = withTiming(-290);
      } else {
        translateY.value = withTiming(0);
        runOnJS(setTimer)();
      }
    },
  });

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          translateY: translateY.value,
        },
      ],
    };
  });

  return (
    <PanGestureHandler onGestureEvent={panGestureEvent}>
      <Animated.View
        style={[
          gbl.centerColumn,
          gbl.w100,
          {
            position: 'absolute',
            top: '12.5%',
            zIndex: 10000000,
          },
          animatedStyle,
        ]}>
        <View
          style={[
            gbl.w90,
            gbl.py15,
            gbl.pl15,
            gbl.pr15,
            alert.alert,
            gbl.leftRow,
          ]}>
          {/* circle shape on alert */}
          <View
            style={[
              alert.alert_shape,
              gbl.mr10,
              {backgroundColor: props.alert.color},
            ]}></View>
          <View style={[{flex: 1}, gbl.centerColumn]}>
            <Text style={alert.alert_msg}>{props.alert.msg}</Text>
          </View>
        </View>
      </Animated.View>
    </PanGestureHandler>
  );
};

const mapStateToProps = state => {
  const {alert} = state;
  return {alert};
};

export default connect(mapStateToProps)(Alert);
dpyeates commented 2 years ago

For me at least, this is now fixed.

The problem for me - which took wayyyy longer to uncover than it should - was that i had quite a complex view which I was using pointerEvents="none" on Image components. Now, on iOS this worked, but on Android the Image component doesn't seem to support pointerEvents="none", so the image was consuming the touch events and this is why my handler, wasn't seeing them. I fixed this by wrapping the Image component in a View and setting pointerEvents="none" on that instead. Ping! it all started working.

ajesamann commented 2 years ago

Hmm, interesting. I'm not using any images, and what seems to happen is my click event goes right through my view as if it's transparent. For example, if a TextInput is behind it, it will click the TextInput and not the view with the PanGestureHandler. I've tried removing all styles just to see if I could get any type of event to fire, thus to no avail. Again it makes no sense as I have another component working fine with PanGestureHandler lol. It's just Android as well, I genuinely have no clue.

amwebexpert commented 2 years ago

Same problem happens on our App (and we also have React Navigation 5.x if it's related). So I decided to try a simple example to see if it was some wrong code on our side. The following simple example does not work on Android but works as expected on iOS with our dependencies:

michaelknoch commented 2 years ago

running into the same issues, so far I have no idea whats wrong. I just see that my PanGestureHandler works perfectly on iOS and on android is does not receive any touch events.

alantoa commented 2 years ago

@jakub-gonet Hi, excuse me, I found an example that can be reproduced stably, if you use @react-navigation/native-stack, PanGestureHandler doesn't work on Android! Example code:

const Stack = createNativeStackNavigator();

export default function App() { return (

); }

const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#fff", alignItems: "center", justifyContent: "center", }, });


Example page:

import { PanGestureHandler } from "react-native-gesture-handler"; import { View, Alert } from "react-native"; import React from "react"; export const Example = () => { return ( <PanGestureHandler onActivated={() => { Alert.alert("onActivated"); }} onBegan={() => { Alert.alert("onBegan"); }} onEnded={() => { Alert.alert("onEnded"); }} onGestureEvent={() => { Alert.alert("onGestureEvent"); }} onHandlerStateChange={() => { Alert.alert("onHandlerStateChange"); }} onFailed={() => { Alert.alert("onFailed"); }}

<View style={{ height: 300, width: 500, backgroundColor: "red" }}> ); };

alantoa commented 2 years ago

@dpyeates @ammarahm-ed @michaelknoch @ajesamann I've solved the problem, I guess you use @react-navigation/native-stack and react-native-modal, because modals are not located under React Native Root view in native hierarchy.,you can see that. So you can use :

1.gestureHandlerRootHOC Example code:

import { gestureHandlerRootHOC } from "react-native-gesture-handler"
const ExampleWithHoc = gestureHandlerRootHOC(() => (
  <NavigationContainer>
    <Stack.Navigator>
      <Stack.Screen name="Example" component={Example} />
    </Stack.Navigator>
  </NavigationContainer>
));
OR 
  1. GestureHandlerRootView Example code:
    import { GestureHandlerRootView } from "react-native-gesture-handler"
    <GestureHandlerRootView style={{flex:1}}>
      <RootNavigator/>
    </GestureHandlerRootView>
Hyodori04 commented 2 years ago

it dosen't work for me. if i use pangesturehandler in my components not screen. for example i use pangestruehandler in childern component in mainscreen where i should wrap

jakub-gonet commented 2 years ago

RNGH should always work in modals since 2.2.0 given that views under modal are wrapped in RNGH root view.

loupmasneri commented 1 year ago

still does not work on android:

VictorPulzz commented 1 year ago

@jakub-gonet Not working PanGestureHandler on android inside default inside modal component.. How example you can see this problem in github.com/jeremybarbet/react-native-modalize/ lib. If I use parameter withReactModal click on overlay not working (only android).

Here is a simple example that does't work:

 <Modal
        supportedOrientations={['landscape', 'portrait', 'portrait-upside-down']}
        visible
        coverScreen
        style={{ backgroundColor: '#303030', width: WIDTH, height: HEIGHT }}
      >
        <PanGestureHandler onGestureEvent={() => console.log(12381239812312312387638)}>
          <AnimatedBox flex={1} backgroundColor="black" />
        </PanGestureHandler>
      </Modal>

"react-native": "0.70.6", "react-native-gesture-handler": "~2.7.1",

alielhusseini commented 1 year ago

Same issue not working on Android, with sdk 47:

import React, { useState } from "react";
import { StyleSheet, View } from "react-native";
import { PanGestureHandler, TapGestureHandler, } from "react-native-gesture-handler";
import Animated, { cancelAnimation, runOnJS, runOnUI, useAnimatedGestureHandler, useAnimatedProps, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring, } from "react-native-reanimated";
import Svg, { Path } from "react-native-svg";

const AnimatedPath = Animated.createAnimatedComponent(Path);

export function PatternLock(props) {
  const [isError, setIsError] = useState(false);
  const canTouch = useSharedValue(true);
  const patternPoints = useSharedValue();
  const selectedIndexes = useSharedValue([]);
  const endPoint = useSharedValue();
  const containerLayout = useSharedValue({ width: 0, height: 0, min: 0 });
  const R = useDerivedValue(
    () =>
      (containerLayout.value.min / props.rowCount - props.patternMargin * 2) / 2
  );
  const cvc = useAnimatedStyle(() => ({
    flexDirection: "row",
    flexWrap: "wrap",
    marginBottom: `${Math.max(
      0,
      containerLayout.value.height / containerLayout.value.width - 1.25
    ) * 50
      }%`,
    width: containerLayout.value.min,
    height: containerLayout.value.min,
  }));
  const msgX = useSharedValue(0);
  const msgColor = { color: isError ? props.errorColor : props.activeColor };
  const msgStyle = useAnimatedStyle(() => {
    return { transform: [{ translateX: msgX.value }] };
  });
  const onContainerLayout = ({
    nativeEvent: {
      layout: { x, y, width, height },
    },
  }) =>
  (containerLayout.value = {
    width,
    height,
    min: Math.min(width, height),
  });
  const onPatternLayout = ({ nativeEvent: { layout } }) => {
    const points = [];
    for (let i = 0; i < props.rowCount; i++) {
      for (let j = 0; j < props.columnCount; j++) {
        points.push({
          x: layout.x + (layout.width / props.columnCount) * (j + 0.5),
          y: layout.y + (layout.height / props.rowCount) * (i + 0.5),
        });
      }
    }
    patternPoints.value = points;
  };
  const onEndJS = (res) => {
    console.log('first')
    if (props.onCheck) {
      canTouch.value = false;
      if (!props.onCheck(res)) {
        setIsError(true);
        const closeError = () => setIsError(false);
        runOnUI(() => {
          cancelAnimation(msgX);
          //修复iOS上原地spring不动的问题。
          msgX.value = withSpring(
            msgX.value === 0 ? 0.1 : 0,
            {
              stiffness: 2000,
              damping: 10,
              mass: 1,
              velocity: 2000,
            },
            (finished) => {
              runOnJS(closeError)();
              canTouch.value = true;
              selectedIndexes.value = [];
            }
          );
        })();
      } else {
        setIsError(false);
        setTimeout(() => {
          selectedIndexes.value = [];
          canTouch.value = true;
        }, 1000);
      }
    }
  };
  const panHandler = useAnimatedGestureHandler({
    onStart: (evt) => {
      console.log('x')
      if (
        canTouch.value &&
        patternPoints.value &&
        selectedIndexes.value.length === 0
      ) {
        const selected = [];
        patternPoints.value.every((p, idx) => {
          if (
            (p.x - evt.x) * (p.x - evt.x) + (p.y - evt.y) * (p.y - evt.y) <
            R.value * R.value
          ) {
            selected.push(idx);
            return false;
          }
          return true;
        });
        selectedIndexes.value = selected;
      }
    },
    onActive: (evt) => {
      if (
        canTouch.value &&
        patternPoints.value &&
        selectedIndexes.value.length > 0
      ) {
        patternPoints.value.every((p, idx) => {
          if (
            (p.x - evt.x) * (p.x - evt.x) + (p.y - evt.y) * (p.y - evt.y) <
            R.value * R.value
          ) {
            if (selectedIndexes.value.indexOf(idx) < 0) {
              selectedIndexes.value = [...selectedIndexes.value, idx];
            }
            return false;
          }
          return true;
        });
        endPoint.value = { x: evt.x, y: evt.y };
      }
    },
    onEnd: (evt) => {
      if (!canTouch.value) return;
      endPoint.value = null;
      if (selectedIndexes.value.length > 0)
        runOnJS(onEndJS)(selectedIndexes.value.join(""));
    },
  });
  const animatedProps = useAnimatedProps(() => {
    let d = "";
    selectedIndexes.value.forEach((idx) => {
      d += !d ? " M" : " L";
      d += ` ${patternPoints.value[idx].x},${patternPoints.value[idx].y}`;
    });
    if (d && endPoint.value) d += ` L${endPoint.value.x},${endPoint.value.y}`;
    if (!d) d = "M-1,-1";
    return { d };
  });

  return (
    <PanGestureHandler onGestureEvent={panHandler}>
      <Animated.View style={styles.container} onLayout={onContainerLayout}>
        <TapGestureHandler onGestureEvent={panHandler}>
          <Animated.View style={styles.container}>
            <View style={styles.msgc}>
              <Animated.Text style={[msgColor, msgStyle]}>
                {props.message}
              </Animated.Text>
            </View>
            <Animated.View style={cvc} onLayout={onPatternLayout}>
              {Array(props.rowCount * props.columnCount)
                .fill(0)
                .map((_, idx) => {
                  const patternColor = useDerivedValue(() => {
                    if (selectedIndexes.value.findIndex((v) => v === idx) < 0) {
                      return props.inactiveColor;
                    } else if (isError) {
                      return props.errorColor;
                    } else {
                      return props.activeColor;
                    }
                  });
                  const outer = useAnimatedStyle(() => {
                    return {
                      borderWidth: 2,
                      width: 2 * R.value,
                      height: 2 * R.value,
                      alignItems: "center",
                      justifyContent: "center",
                      borderColor: patternColor.value,
                      borderRadius: 2 * R.value,
                      margin: props.patternMargin,
                    };
                  });
                  const inner = useAnimatedStyle(() => {
                    return {
                      width: R.value * 0.8,
                      height: R.value * 0.8,
                      borderRadius: R.value * 0.8,
                      backgroundColor: patternColor.value,
                    };
                  });
                  return (
                    <Animated.View key={idx} style={outer}>
                      <Animated.View style={inner} />
                    </Animated.View>
                  );
                })}
            </Animated.View>
            <Svg style={styles.svg} width="100%" height="100%">
              <AnimatedPath
                fill="none"
                strokeWidth={3}
                animatedProps={animatedProps}
                stroke={isError ? props.errorColor : props.activeColor}
              />
            </Svg>
          </Animated.View>
        </TapGestureHandler>
      </Animated.View>
    </PanGestureHandler>
  );
}

PatternLock.defaultProps = {
  message: "",
  rowCount: 3,
  columnCount: 3,
  patternMargin: 25,
  inactiveColor: "#8E91A8",
  activeColor: "#5FA8FC",
  errorColor: "#D93609",
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignSelf: "stretch",
    alignItems: "center",
  },
  msgc: {
    flex: 1,
    justifyContent: "center",
    alignSelf: "center",
  },
  svg: {
    position: "absolute",
    left: 0,
    top: 0,
  },
});
0xVaibhav11 commented 1 year ago

@dpyeates @ammarahm-ed @michaelknoch @ajesamann I've solved the problem, I guess you use @react-navigation/native-stack and react-native-modal, because modals are not located under React Native Root view in native hierarchy.,you can see that. So you can use :

1.gestureHandlerRootHOC Example code:

import { gestureHandlerRootHOC } from "react-native-gesture-handler"
const ExampleWithHoc = gestureHandlerRootHOC(() => (
  <NavigationContainer>
    <Stack.Navigator>
      <Stack.Screen name="Example" component={Example} />
    </Stack.Navigator>
  </NavigationContainer>
));
OR 
  1. GestureHandlerRootView Example code:
import { GestureHandlerRootView } from "react-native-gesture-handler"
  <GestureHandlerRootView style={{flex:1}}>
      <RootNavigator/>
    </GestureHandlerRootView>

It worked!!

alielhusseini commented 1 year ago

@vanoob404 yeah thank you already solved it this way but forgot to post the answer. Appreciated still 🙌

ifeanyi-ugwu commented 9 months ago

@jakub-gonet Hi, excuse me, I found an example that can be reproduced stably, if you use @react-navigation/native-stack, PanGestureHandler doesn't work on Android! Example code:

  • navigation page
import "react-native-gesture-handler";
import { StyleSheet, Text, View } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack"; //  native-stack don't work
import { Example } from "./src/screens";

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Example" component={Example} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

Example page:

import { PanGestureHandler } from "react-native-gesture-handler";
import { View, Alert } from "react-native";
import React from "react";
export const Example = () => {
  return (
    <PanGestureHandler
      onActivated={() => {
        Alert.alert("onActivated");
      }}
      onBegan={() => {
        Alert.alert("onBegan");
      }}
      onEnded={() => {
        Alert.alert("onEnded");
      }}
      onGestureEvent={() => {
        Alert.alert("onGestureEvent");
      }}
      onHandlerStateChange={() => {
        Alert.alert("onHandlerStateChange");
      }}
      onFailed={() => {
        Alert.alert("onFailed");
      }}
    >
      <View style={{ height: 300, width: 500, backgroundColor: "red" }}></View>
    </PanGestureHandler>
  );
};

This works except that i only had to import and wrap the PanGestureHandler with the GestureHandlerRootView