React-native-reanimated: V2 alpha: Unable to run TapGestureHandler-based animation more than once

Created on 29 May 2020  路  17Comments  路  Source: software-mansion/react-native-reanimated

Description

Unsure if this is a bug, but when using <TapGestureHandler /> and reanimated v2 (which is amazing!), I'm unable to re-trigger my animation after it finishes. It's pretty simple - on tap start, set the scale from 1.0 to 0.9. On tap end, reset back to 1.0. It works once, then not again. Having migrated from v1 to v2, I suspect it may be an underlying issue with the clock? I didn't see anything documentation suggesting that anything other than what's below is required for triggering this kind of simple animation more than once. The basic random width example works fine.

Code

import React from 'react'
import { View } from 'react-native'
import Animated, {
  useSharedValue,
  withSpring,
  useAnimatedStyle,
  useAnimatedGestureHandler,
} from 'react-native-reanimated';
import { TapGestureHandler } from 'react-native-gesture-handler';

const styles = {
  container: {
    flex: 1,
    backgroundColor: 'rgba(200, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center'
  },
  box: {
    width: 140,
    height: 300,
    backgroundColor: 'rgba(200, 200, 200, 0.6)',
    borderRadius: 26
  }
}

export function ReanimatedExample() {
  const s = useSharedValue(1);

  const tapGestureHandler = useAnimatedGestureHandler({
    onStart: (event, ctx) => {
      s.value = withSpring(0.9);
    },
    onEnd: (event, ctx) => {
      s.value = withSpring(1);
    },
  });

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

  return (
    <View style={styles.container}>
      <TapGestureHandler onGestureEvent={tapGestureHandler}>
        <Animated.View style={[
          styles.box,
          animatedStyle
        ]} />
      </TapGestureHandler>
    </View>
  );
}

Package versions

  • React: v16.11.0
  • React Native: v0.62.2
  • React Native Reanimated: v2.0.0-alpha.1
  • React Native Gesture Handler: v1.6.0
鉂換uestion 馃彔 Reanimated2

Most helpful comment

After some testing, I can clearly say this is a bug in reanimated's useAnimatedGestureHandler.

The problem that it uses this condition for "onStart" check:

if (event.oldState === UNDETERMINED && handlers.onStart) {
  handlers.onStart(event, context);
}

But it should use this (at least for tap for sure) instead:

if (event.state === BEGAN && handlers.onStart) {
  handlers.onStart(event, context);
}

I will submit a fix later today.

All 17 comments

Hi. What is the version of the gesture handler you used?

Oops, edited to include my gesture-handler v1.6.0. By the way, I tried using onHandlerStateChange as suggested in your comment prior to edit - no luck.

I think this may be a v2 bug, though I can't change the label. Anybody have any suggestions? I've tried all sorts of combinations of properties for TapGestureHander/BaseGestureHander, but no luck. This component is the only component rendered in the app besides react-navigation containers (no drawer navigators or anything though). In other words, I don't think any other gesture handlers are interfering.

Could you try to upgrade the gesture handler to 1.6.1?

Ok, I ran this example and your problem is that tap gesture is getting canceled on long-press because it has some maxDurationMs. Try to use some duration here.
In the other case, it dispatches the onFail event instead.

Also, make sure to implement onFail to handle the case when the user moves the finger outside the button.

EDIT: Adding verbose logs below to demonstrate the issue of onStart not getting fired again for both onHandlerStateChange and onGestureEvent on subsequent taps. These logs were simply printed inside handler callbacks like so:

const tapGestureHandler = useAnimatedGestureHandler({
  onStart: (event, ctx) => {
    console.log('ON_GESTURE_EVENT onStart!')
    // s.value = withSpring(Math.random(6, 9));
  },
  ...
}
// Tap #1: tap screen for a reasonable length of time (~400ms)
ON_HANDLER_STATE_CHANGE onStart!
ON_GESTURE_EVENT onStart!
// Tap #1: release tap from screen.
// note that onStart is fired again...?
ON_HANDLER_STATE_CHANGE onStart!
ON_HANDLER_STATE_CHANGE onActive!
ON_GESTURE_EVENT onStart!
ON_GESTURE_EVENT onActive!
ON_HANDLER_STATE_CHANGE onEnd!
ON_HANDLER_STATE_CHANGE onFinish!
ON_GESTURE_EVENT onEnd!
ON_GESTURE_EVENT onFinish!

// ----------------------------------------------------------------- //

// Tap #2: again, tap screen for a reasonable length of time (~400ms)
/*
  // HERE is where both handlers' onStart callback is NOT fired
  ON_HANDLER_STATE_CHANGE onStart!
  ON_GESTURE_EVENT onStart!
*/

// Tap #1: release tap from screen.
// Below is the same as tap #1 -- all eight events below are fired
// again, note that onStart is fired again...?
ON_HANDLER_STATE_CHANGE onStart!
ON_HANDLER_STATE_CHANGE onActive!
ON_GESTURE_EVENT onStart!
ON_GESTURE_EVENT onActive!
ON_HANDLER_STATE_CHANGE onEnd!
ON_HANDLER_STATE_CHANGE onFinish!
ON_GESTURE_EVENT onEnd!
ON_GESTURE_EVENT onFinish!

My questions are:

  1. Why is onStart fired upon release of a gesture?
  2. Why is onStart _not_ fired in the same manner as it was during tap #1?

Original comment below:

I'm not sure I understand how my tap gesture is getting cancelled on long press. I'm not really holding down the gesture -- I just tap quickly. Either way, adding maxDurationMs doesn't seem to make a difference. It actually seems to make it worse as no other gesture event seems to get called now. The spring animation runs to a value of 0.9, but then becomes stuck whereas before adding maxDurationMs property, onEnd would get called and reset it back to 1.

I've changed my gesture handler config to just move to a random scale value onStart so that my issue is a little more clear. Like the original example, this only gets called once. Shouldn't I expect it to move to a new scale every time my tap gesture begins? Or is there something I don't understand about react-native-gesture-handler?

const tapGestureHandler = useAnimatedGestureHandler({
  onStart: (event, ctx) => {
    s.value = withSpring(Math.random(4, 9)); // only gets called once
  },
  // onEnd: (event, ctx) => {
  //   s.value = withSpring(1);
  // },
  // onFail: (event, ctx) => {
  //   s.value = withSpring(1);
  // },
});
...
<TapGestureHandler onHandlerStateChange={tapGestureHandler}>
  <Animated.View style={[
    styles.box,
    animatedStyle
  ]} />
</TapGestureHandler>

Could you provide reproducing repo then? That's the only way for me to debug it.

Sure thing - here you go. I cloned the latest reanimated-v2 playground, dropped in the example above in App.js, and confirmed the same behavior. Thanks for the speedy responses.

I already can see you use separate handlers for each event. You need to use the same one for both. But with latest version of gesture handler you should use only "onGestureEvent".

Will try to run it tomorrow.

In that example I only had both handlers registered to understand the order of events. Regardless of whether I have both, just onGestureEvent, or just onHandlerStateChange, onStart is not fired on the second tap. Let me know if you see anything... for now I have moved my animation to onActive which consistently fires on every tap event, but I wanted to add some scaling animations onStart.

For context, what I'm trying to replicate is the iOS control panel slider. onStart would trigger a scale decrease to 0.95 with spring, then onActive would animate the width and height to occupy the center of the screen.

Hi @drewandre,

I was face same issue and use some other method. Would you try with LongPressGesture Handler

 const {height} = Dimensions.get('screen');
 const animatedHeight = useSharedValue(height * 0.9);
 const gestureHandler = useAnimatedGestureHandler({
    onStart: (event) => {
      animatedHeight.value = withSpring(height / 2, springConfig);
    },
    onEnd: (event) => {
      animatedHeight.value = withSpring(height * 0.9, springConfig);
    },
  });
return(
        <LongPressGestureHandler onHandlerStateChange={gestureHandler} minDurationMs={0.5}>
          <Animated.View style={[{height: animatedHeight.value}]}> </Animated.View>
        </LongPressGestureHandler>
);

screencap-2020-06-01T200137 298Z

thanks @Xicy, this works! I'm confused why TapGestureHandler wasn't triggering the onStart event again, though. @terrysahaidak I'm curious if you find anything, or if I was just misunderstanding how the handler is supposed to use used?

After some testing, I can clearly say this is a bug in reanimated's useAnimatedGestureHandler.

The problem that it uses this condition for "onStart" check:

if (event.oldState === UNDETERMINED && handlers.onStart) {
  handlers.onStart(event, context);
}

But it should use this (at least for tap for sure) instead:

if (event.state === BEGAN && handlers.onStart) {
  handlers.onStart(event, context);
}

I will submit a fix later today.

I experienced this problem too, also I experienced a bug where the onFail is not being called when Tap gesture fails.

Hey @terrysahaidak,
I think I could fix all this buggy behaviour of useAnimatedGestureHandler in a single PR.
If you haven't started working on the fix yet, can I take this up and submit a PR?

Edit: I really love this library and enjoy using it. It would be so nice if I could contribute to it.

I got sick, so go ahead :)

Just make sure to test all the other handlers too. You can check out my reanimated-gallery. It uses almost all of them (but uses custom useAnimatedGestureHandler)

It would be great, if the folks here, who were having this issue too could test out this PR!

I've been trying to test but I'm unable to render any gesture handlers at the moment due to #867. Seemed to come out of no where after I cleaned my xcode project. I'll post any updates here -- still working on 2.0.0-alpha.1 branch at the moment.

@divykj and @terrysahaidak 2.0.0-alpha.3 release seemed to solve the issue I was experiencing above and these changes worked great against the newest alpha release. I'm now able to re-run TapGestureHandler-based animations more than once. Thanks for your help!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

robertgonzales picture robertgonzales  路  3Comments

zxccvvv picture zxccvvv  路  3Comments

alexfov picture alexfov  路  3Comments

ArsalanCsquare picture ArsalanCsquare  路  3Comments

hosseinmd picture hosseinmd  路  3Comments