ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
51.02k stars 13.52k forks source link

feat: expose direction property on gesture event #21430

Open uncvrd opened 4 years ago

uncvrd commented 4 years ago

Feature Request

Hi! I have a specific use case where I have an instagram stories-esque image carousel in my app. I am using Ionic Gestures to listen for pan events when swiping to the next image. The issue I have is that I am unable to disable a swipe gesture in a specific direction on the x axis.

For example, when the user opens their stories, they should not be able to swipe backwards because they are watching their first story. I figured I could use the canStart event for this. However, the GestureDetail's deltaX value is 0 since it is triggered right at the beginning of the gesture event (e.g. no delta has been registered).

I was wondering if we could have some sort of API that would allow us to prevent gestures in a negative of positive direction on an axis? Or is there a better way to do what I'm explaining?

Swipe Gesture

Notice the black screen when I try to swipe back? I am at index 0 and should not be allowed to swipe back like that.

For example here's my React Gesture:

const swipeGesture = createGesture({
            el: cubeRef.current!,
            gestureName: 'swipe-gesture',
            direction: 'x',
            gesturePriority: 50,
            onMove: (ev) => onMove(ev),
            onEnd: (ev) => onEnd(ev)
        });
        swipeGesture.enable(true);

I use react-spring for my transition animations:

const onMove = (ev: GestureDetail) => {

            const currentRotate = index * 90 * -1;
            const convert = linearConversion([0, width], [currentRotate, currentRotate + 90]);
            let v = convert(ev.deltaX);

            // prevent current drag event from transitioning more than 90 degrees
            if (v > currentRotate + 90) {
                v = currentRotate + 90;
            } else if (v < currentRotate - 90) {
                v = currentRotate - 90;
            }

            // this is where the transition animation occurs, I am setting my css `translateY` to the value of `rotateY` defined here
            set({
                rotateY: v,
                immediate: true,
            });
        };

In my code I can check to see if the user is at index === 0, if so, I would like to prevent pan gestures that result in a positive deltaX event.

Describe Preferred Solution

A very rough idea is to have an API like:

axis: 'positive' | 'negative' | 'both'

If it was possible to use canStart for this, I would do something like:

const canStart = (ev: GestureDetail) => {
    // if we are on the first item and the user tries to swipe backwards
    if (index === 0 && ev.deltaX > 0) {
        return false
    }
    // else allow start
    return true;
}

Describe Alternatives

I've tried to swipeGesture.enable(false) within my onMove() method with hopes that an onEnd() event would be called, so that I could re-enable the gesture once they let go but that doesn't work either.

Any thoughts on this would be super helpful, thank you!

Ionic version:

[x] 5.1.0

liamdebeasi commented 4 years ago

Thanks for the issue. Can you clarify the issue you are running into? You should be able to check index === 0 && ev.deltaX > 0 in onMove, and then do nothing if that case returns true.

uncvrd commented 4 years ago

Hey @liamdebeasi ! So the issue I have with short circuiting within the onMove event is that this will still result in an onEnd event firing if index === 0 && ev.deltaX > 0. This causes an issue because, I have the following onEnd method which disables the gesture until an animation completes:

const onEnd = (ev: GestureDetail) => {

    swipeGesture.enable(false);

    // if velocity is large enough, automatically translate to next story
    if (Math.abs(ev.velocityX) > 0.12) {
        onChange(ev.deltaX < 0 ? index + 1 : index - 1);
        return;
    }

    // next
    if (ev.deltaX < -(width / 2)) {
        onChange(index + 1);

        // prev
    } else if (ev.deltaX > width / 2) {
        onChange(index - 1);

        // current
    } else {
        set({
            rotateY: index * 90 * -1,
            onRest: () => {
                // re-enable gesture
                swipeGesture.enable(true);
                setAnimating(false);
            },
            immediate: false,
        });
    }
};

As you can see, i have a swipeGesture.enable(false) at the top of my onEnd method so that I can allow the animation to finish before allowing the swipeGesture to listen for more gesture events again. Since there is no animation that is completing (because we are short circuiting with no movement), the swipeGesture can never be re-enabled. I took this concept from the example in the docs here: https://ionicframework.com/docs/utilities/animations#gesture-animations

Here's the code snippet I'm referring to, notice how gesture is disabled until onFinish? That's what I'm doing with my onEnd as well:

const onEnd = (ev): {
  if (!started) { return; }

  gesture.enable(false);

  const step = getStep(ev);
  const shouldComplete = step > 0.5;

  animation
    .progressEnd((shouldComplete) ? 1 : 0, step)
    .onFinish((): { gesture.enable(true); });

  initialStep = (shouldComplete) ? MAX_TRANSLATE : 0;
  started = false;
}

So to clarify, short circuiting in the onMove still causes an onEnd event to fire even though there is no animation translation happening. In the onEnd, the swipeGesture is disabled until the animation complete, but since we are hitting the onEnd event with no translation, the swipeGesture will no be re-enabled

Does that help a bit? Thanks!

liamdebeasi commented 4 years ago

Hmm can you make a CodePen/GitHub repo with what you are trying to do? It's hard to get a good idea of what is going on without being able to interact with the gesture.

edit: If you put the code into an Ionic starter app and push it to GitHub that might be fastest.

uncvrd commented 4 years ago

@liamdebeasi here's a sample repo!

Link: https://github.com/uncvrd/ionic-gesture-direction

The Home.tsx has the demo.

Cube.tsx is where most of the inner workings are. I initialize and use Ionic Gestures on line 137 of Cube.tsx.

In the demo, if you try swiping back on the first slide it will not change since we are shortcircuiting the animation in onMove (great!), however the swipeGesture will be disabled due to the fact that once you let go of the gesture, onEnd is fired. Normally, this gesture is re-enabled on the completion of an animation, but since we are shortcircuiting in onMove and preventing any animation, the animation finish callback never fires since there is no animation to "complete" (which reenables the gesture as you can see on line 185).

Let me know if you have more questions

liamdebeasi commented 4 years ago

Thanks for the repo. So I was able to get this gesture to work by adding the following to the top of onEnd:

if (index === 0 && ev.deltaX > 0) {
  return;
}

Can you try this and let me know if it resolves the issue?

uncvrd commented 4 years ago

@liamdebeasi yes this will suffice as a work around, thanks! What are your thoughts about keeping this open as a feature request? My reasoning being:

  1. onMove to prevent panning back if index == 0
  2. onMove to prevent panning forward if index == images.length (reached the end)
  3. onEnd to prevent events from firing if index == 0
  4. onEnd to prevent events from firing if index == images.length

I think it would be a nice extension to the gesture controller to be able to control which specific magnitude of direction a gesture can handle. Just a thought!

liamdebeasi commented 4 years ago

HammerJS exposes a direction property on each event object -- I think we could do something like that. In your app, you would just check event.direction === 'left' or something similar.

uncvrd commented 4 years ago

@liamdebeasi I think that would be useful! Or an event exposed that we can use to be able to check and prevent the animation from even starting. For example react-gesture-responder allows you to listen for a gesture event, check direction, and prevent movement (sort of like canStart in Ionic Gestures, but this one you can actually detect direction). For example:

onMoveShouldSet: ({ direction }) => {
      if (!enableGestures) {
        return false;
      }

      // beginning of stack and swiping left
      if (index === 0 && direction[0] > 0) {
        return false;
      }

      // end of stack and swiping right
      if (direction[0] < 0 && !hasNext(index)) {
        return false;
      }

      // swiping horizontally
      return Math.abs(direction[0]) > Math.abs(direction[1]);
    },

Maybe this can be fixed with adding direction to the GestureDetail for canStart, but it seems like it triggers RIGHT on click/press so I don't know if it can detect direction.

Link to repo: https://github.com/bmcmahen/react-gesture-responder

Note: this is what I was using before switching to Ionic Gestures. I switched because I was having conflicting gesture priorities with Ionics SwipeToClose Modal gesture. Switched so I can manage gesture priorities in a single gesture library :)

andi23rosca commented 10 months ago

I'm guessing this has been forgotten about but in case someone from Ionic sees this, I'm running into similar issues as the other posters.

I have multiple swipes on the screen that can potentially block each other.

If the canStart was triggered after the threshold got crossed then it would be possible to return false when a swipe is left to right. Then it wouldn't block a right to left swipe.