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
6.12k stars 980 forks source link

waitFor and simultaneousHandlers do not work with LongPressGestureHandler and PanGestureHandler #1292

Closed Svarto closed 3 years ago

Svarto commented 3 years ago

Description

Adding the option that PanGestureHandler waitFor a LongPressGestureHandler does not work, the PanGestureHandler activates directly. And once LongPressGestureHandler activates, PanGestureHandler deactivates even with simultanousHandler setup according to docs.

Let me know if I have done anything incorrectly, but I have only followed the docs to the best of my ability combining waitFor and simultaneousHandlers...

Screenshots

Steps To Reproduce

  1. Create a new blank project with expo (expo init)
  2. Install Reanimated 2 and Gesture handler (expo install react-native-gesture-handler && npm install react-native-reanimated@2.0.0-rc.0)
  3. Copy and paste in the babel plugin for expo reanimated (plugins: ['react-native-reanimated/plugin'],)
  4. Download any image to project-dir/assets/example.jpg
  5. Copy and paste the below code snippet into the App.js and start the expo dev client (expo start -c)
  6. Try and drag the image and longpress - you will see it is reversed from what is expected.

Expected behavior

The PanGestureHandler should waitFor the LongPressGestureHandler to exit the "BEGIN" state before activating, and stay activated once the LongPressGestureHandler activates according to simultaneousHandlers.

Actual behavior

The PanGestureHandler ignores waitFor and simultaneousHandler and activates before LongPressGestureHandler, and deactives once LongPressGestureHandler activates.

Snack or minimal code example

import React from "react";
import { View, Image } from "react-native";
import {
  PanGestureHandler,
  LongPressGestureHandler,
  ScrollView,
} from "react-native-gesture-handler";

import Animated, {
  useAnimatedStyle,
  useAnimatedGestureHandler,
  useSharedValue,
} from "react-native-reanimated";

export default function App() {
  const imageSize = 50;

  const longPressRef = React.createRef();
  const panRef = React.createRef();

  const y = useSharedValue(0);
  const x = useSharedValue(0);
  const onGestureEvent = useAnimatedGestureHandler({
    onStart: (event, ctx) => {
      ctx.offsetX = x.value;
    },
    onActive: (event, ctx) => {
      y.value = event.translationY;
      x.value = event.translationX + ctx.offsetX;
    },
  });

  const style = useAnimatedStyle(() => ({
    position: "absolute",
    top: 0,
    left: 0,
    width: imageSize,
    height: imageSize,
    transform: [{ translateX: x.value }, { translateY: y.value }],
  }));

  return (
    <View
      style={{
        flex: 1,
        paddingTop: 100,
        backgroundColor: "#fff",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <ScrollView
        horizontal={true}
        showsHorizontalScrollIndicator={false}
        style={{ positon: "relative", height: imageSize, borderWidth: 1 }}
        contentContainerStyle={{ width: imageSize * 3 }}
      >
        <PanGestureHandler
          ref={panRef}
          simultaneousHandlers={longPressRef}
          waitFor={longPressRef}
          onHandlerStateChange={({ nativeEvent }) =>
            console.log("PANGESTURE ", nativeEvent.state)
          }
          {...{ onGestureEvent }}
        >
          <Animated.View>
            <LongPressGestureHandler
              ref={longPressRef}
              minDurationMs={1000}
              simultaneousHandlers={panRef}
              onHandlerStateChange={({ nativeEvent }) => {
                console.log("LONG PRESS", nativeEvent.state);
                console.log("LONG PRESS, PAN GESTURE SHOULD NOW ACTIVATE");
              }}
            >
              <Animated.View
                style={[
                  {
                    position: "absolute",
                    top: 0,
                    left: 0,
                  },
                  style,
                ]}
              >
                <Image
                  source={require("./assets/example.jpg")}
                  style={{ width: imageSize, height: imageSize }}
                />
              </Animated.View>
            </LongPressGestureHandler>
          </Animated.View>
        </PanGestureHandler>
      </ScrollView>
    </View>
  );
}

Package versions

"expo": "~40.0.0",
"react-native-reanimated": "^2.0.0-rc.0",
"react-native-gesture-handler": "~1.8.0"
MoOx commented 3 years ago

First thing I see: you need to use useRef (hook, for function) instead of createRef (imperative, for class).

Svarto commented 3 years ago

@MoOx thanks for the reply and having a look! Did you get it to work?

I tried with your edits in my local test environment (as mentioned in the bug report, steps to reproduce), and it still does not work with useRef instead of createRef.

MoOx commented 3 years ago

I can't say I have a thing totally working because my setup is a bit more complex but I have a working PanGestureHandler (a list) with multiple nested LongPressGestureHandler (manual handles to star dragging) - but in my situation, I can't have the ScrollView to be usable when you are trying to scroll from the PanGestureHandler are. I tried various waitFor/simultaneousHandlers but I don't have a fully working example yet.

Svarto commented 3 years ago

That's exactly what I'm trying to do, I don't need the Scrollview to be active while dragging. Only when they have not activated / are in use...

I'm confused what I'm doing wrong, I tried nesting them in the reverse order too and all sort of combination of waitFor but can't get it to work.

Sent from ProtonMail mobile

-------- Original Message -------- On 13 Jan 2021, 17:45, Max Thirouin wrote:

I can't say I have a thing totally working because my setup is a bit more complex but I have a working PanGestureHandler (a list) with multiple nested LongPressGestureHandler (manual handles to star dragging) - but in my situation, I can't have the ScrollView to be usable when you are trying to scroll from the PanGestureHandler are. I tried various waitFor/simultaneousHandlers but I don't have a fully working example yet.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub, or unsubscribe.

MoOx commented 3 years ago

Try this patch https://github.com/software-mansion/react-native-gesture-handler/commit/0b0fabc96da1d585a740da6476123ecd74a22fec It hasn't been released yet.

Svarto commented 3 years ago

Try this patch 0b0fabc It hasn't been released yet.

hmm, no difference for me. i applied it and restarted with expo start -c to clear cache...

MoOx commented 3 years ago

Ok if I am correct here is what I got working:

Svarto commented 3 years ago

@MoOx hmm, damn it just doesn't work for me. Once LongPressGestureHandler activates, then the PanGestureHandler deactivates and stops working. Somehow the PanGestureHandler also activates before LongPressGestureHandler (i.e. ignores waitFor). It is so weird...

This is the code I am using now, replicating what you just detailed out.

import React from "react";
import { View, Image } from "react-native";
import {
  PanGestureHandler,
  LongPressGestureHandler,
  ScrollView,
} from "react-native-gesture-handler";

import Animated, {
  useAnimatedStyle,
  useAnimatedGestureHandler,
  useSharedValue,
} from "react-native-reanimated";

export default function App() {
  const imageSize = 50;

  const scrollRef = React.useRef(null);
  const longPressRef = React.useRef(null);
  const panRef = React.useRef(null);

  const y = useSharedValue(0);
  const x = useSharedValue(0);
  const onGestureEvent = useAnimatedGestureHandler({
    onStart: (event, ctx) => {
      ctx.offsetX = x.value;
    },
    onActive: (event, ctx) => {
      y.value = event.translationY;
      x.value = event.translationX + ctx.offsetX;
    },
  });

  const style = useAnimatedStyle(() => ({
    position: "absolute",
    top: 0,
    left: 0,
    width: imageSize,
    height: imageSize,
    transform: [{ translateX: x.value }, { translateY: y.value }],
  }));

  return (
    <View
      style={{
        flex: 1,
        paddingTop: 100,
        height: 300,
        backgroundColor: "#fff",
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <ScrollView
        ref={scrollRef}
        horizontal={true}
        showsHorizontalScrollIndicator={false}
        style={{ positon: "relative", height: imageSize, borderWidth: 1 }}
        contentContainerStyle={{ width: imageSize * 3 }}
      >
        <PanGestureHandler
          ref={panRef}
          simultaneousHandlers={scrollRef}
          waitFor={longPressRef}
          onHandlerStateChange={({ nativeEvent }) =>
            console.log("PANGESTURE ", nativeEvent.state)
          }
          {...{ onGestureEvent }}
        >
          <Animated.View>
            <LongPressGestureHandler
              ref={longPressRef}
              minDurationMs={0}
              simultaneousHandlers={panRef}
              onHandlerStateChange={({ nativeEvent }) => {
                console.log("LONG PRESS", nativeEvent.state);
                console.log("LONG PRESS, PAN GESTURE SHOULD NOW ACTIVATE");
              }}
            >
              <Animated.View
                style={[
                  {
                    position: "absolute",
                    top: 0,
                    left: 0,
                  },
                  style,
                ]}
              >
                <Image
                  source={require("./assets/example.jpg")}
                  style={{ width: imageSize, height: imageSize }}
                />
              </Animated.View>
            </LongPressGestureHandler>
          </Animated.View>
        </PanGestureHandler>
      </ScrollView>
    </View>
  );
}
MoOx commented 3 years ago

Actually I said something wrong: in my case I don't use waitFor on the Pan, because I just cannot (I have multiple LongPress and I don't know the one I should for until you press it... so I use a state to say "go for it" that I use in my onGestureHandler before doing my stuff.

Svarto commented 3 years ago

@MoOx I see, how d you set the state and have the PanGestureHandler activate without the user "lifting" the finger? I tried with the below, the problem is that the state correctly flips to true and activates the PanGestureHandler but only after I "lift" my finger and basically stop the LongPressGestureHandler.

Snippet (note the enabled on PanGestureHandler and the onGestureEvent state flipping active to true):

        <PanGestureHandler
          ref={panRef}
          enabled={active}
          waitFor={longPressRef}
          onHandlerStateChange={({ nativeEvent }) =>
            console.log("PANGESTURE ", nativeEvent.state)
          }
          {...{ onGestureEvent }}
        >
          <Animated.View>
            <LongPressGestureHandler
              ref={longPressRef}
              minDurationMs={1000}
              maxDist={10}
              onGestureEvent={() => setActive(true)}
              onHandlerStateChange={({ nativeEvent }) => {
                console.log("LONG PRESS", nativeEvent.state);
                console.log("LONG PRESS, PAN GESTURE SHOULD NOW ACTIVATE");
              }}
            >
MoOx commented 3 years ago

Don't use enabled, instead edit your onGesture/onHandlerStateChange to do (or not) your logic. And put back simultaneousHandlers={scrollRef} so the scrollview can work too. I have some minor perf issue with my solution that involve a state but I will try to publish something when this is sorted out. I was shocked to not be able to find such a trivial problem: sortable list inside a scrollview that can be sorted with just tiny handle (very common thing on sortable list)...

debianw commented 3 years ago

@Svarto did you solved this ?. I'm having the same issue

jakub-gonet commented 3 years ago

This issue was not active for some time so I'm closing it. If you need further help please reply.

aLemonFox commented 3 years ago

For anyone coming across something like this in the future, this is how I solved it:

  1. Make a ScrollView component and set its scrollEnabled to your trigger value:

    const scrollViewRef = useRef(null);
    const [allowedDragId, setAllowedDragId] = useState<string | null>(null);
    
    const handleDragStart = (id: string) => {
        setAllowedDragId(id);
    }
    const handleDragEnd = () => {
        setAllowedDragId(null);
    }
    
    return (
        <ScrollView
            ref={scrollViewRef}
            scrollEnabled={allowedDragId === null}
        >
            {LOCATIONS.map((location, index) => (
                <DraggableView
                    scrollViewRef={scrollViewRef}
                    onDragStart={handleDragStart}
                    onDragEnd={handleDragEnd}
                    allowedDragId={allowedDragId}
                />
            ))}
        </ScrollView>
    );
  2. In your draggable component, call onDragStart() when you want to allow a component to be draggable. In the gestureHandler, filter on the trigger value from the previous step:

    const [isDragging, setDragging] = useState(false);
    
    const eventHandler = useAnimatedGestureHandler({
        onStart: (event, ctx) => {
            if (props.allowedDragId === props.location.id) {
                runOnJS(setDragging)(true);
            }
        },
        onActive: (event, ctx) => {
            if (isDragging) {
            // dragging code
        },
        onEnd: (event, ctx) => {
            if (isDragging) {
                // drag end code
                runOnJS(setDragging)(false);
                runOnJS(props.onDragEnd)();
            }
        },
    });
    
    return (
        <Animated.View style={animatedStyle}>
            <PanGestureHandler
                onGestureEvent={eventHandler}
                simultaneousHandlers={props.scrollViewRef}
            >
                <Animated.View>
                    <CustomView onLongPress={() => props.onDragStart(props.location.id)} />
                </Animated.View>
            </PanGestureHandler>
        </Animated.View>
    );
  3. Pass scrollViewRef to the PanGestureHandler simultaneousHandlers prop.

You can now enable and disable dragging your draggable component without having to lift your finger off the screen.