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

Long press is getting cancelled in Expo #3034

Closed boclar closed 1 month ago

boclar commented 3 months ago

Description

For some reason long press with one finger is working like a charm, but it does not work when screen is pressed with more than 1 fingers on iOS specifically.

const longPress = Gesture.LongPress()
        .runOnJS(true)
        .minDuration(1000)
        .onStart(() => {
            Alert.alert('Long press started');
        })
        .onTouchesCancelled((event: GestureTouchEvent) => {
            console.warn('Touches cancelled', event?.numberOfTouches);
        });

    if (!fontsLoaded || fontError) {
        return null;
    }

    return (
        <GestureHandlerRootView style={rootViewStyle}>
            <GestureDetector gesture={longPress}>
                <GlobalContext>
                    <PortalHost name="portal" />

                    <Slot />
                </GlobalContext>
            </GestureDetector>
        </GestureHandlerRootView>
    );

Steps to reproduce

  1. Run app with expo
  2. Use long press handler
  3. Do long press with more than 2 fingers and log the events for touches cancelled. You will notice start event is not triggered.

Snack or a link to a repository

N/A

Gesture Handler version

~2.16.1

React Native version

0.74.3

Platforms

iOS

JavaScript runtime

None

Workflow

None

Architecture

None

Build type

None

Device

None

Device model

iPhone 14 Pro Max

Acknowledgements

Yes

m-bert commented 3 months ago

Hi @boclar! Thanks for reporting this problem! The thing is, it works on other platforms because we do not perform any action when you add another pointer. iOS implementation of Gesture Handler uses native recognizers. As you can see in the docs, UILongPressGestureRecognizer has property called numberOfTouchesRequired, which defaults to 1.

I think the best solution would be to add numberOfPointers property to LongPress gesture (like in Fling).

I'll get back to you with a PR!

boclar commented 3 months ago

That makes sense. I think biggest issue is that the option for defining the number of touches does not exist for LongPress

m-bert commented 2 months ago

Hi @boclar! Could you please check if this PR meets your requirements? 😅

boclar commented 2 months ago

Hello @m-bert, from what I can read from the PR description, it seems to fit my requirements. Is there anything else I can do for you?

Thank you!

m-bert commented 2 months ago

I'd be glad if you could test these changes and confirm that they work

boclar commented 2 months ago

Sure, no problem.

I currently have the plugin installed, but wonder if there is a way to pull these PR updates to my branch instead of the production ones?

boclar commented 2 months ago

@m-bert I managed to have it installed in my project, plugin is in a separate folder within my root project, and I changed all the imports from node_modules to this other folder instead, which includes your PR changes.

I tested it on 2 devices and I am getting the same issue you see in the terminal logs, which stop my app from loading, so it stays on splash screen.

Error: WARN Tried to call timer with ID 9 but no such timer exists. WARN Tried to call timer with ID 9 but no such timer exists.

Here is my component code:

import { CustomGestureDetectorProps } from './custom-gesture-detector.types';
import { customGestureDetectorStyles } from './custom-gesture-detector.styles';
import { router, usePathname } from 'expo-router';
import {
    Gesture,
    GestureDetector,
    GestureHandlerRootView,
    GestureTouchEvent,
} from '../../../react-native-gesture-handler';
import { useEffect, useState } from 'react';
import { Alert, AlertProps } from '@boclar/booking-app-components';
import { useTranslation } from 'react-i18next';
import { getFromAsyncStorage } from '@/utils/storage/storage.utils';
import { View, Alert as AlertRN } from 'react-native';
import { useAuth } from '@/hooks/use-auth/use-auth.hooks';

/**
 * Detects double tap gesture and navigates to report issue screen
 */
const CustomGestureDetector = ({
    children,
    ...props
}: CustomGestureDetectorProps) => {
    const styles = customGestureDetectorStyles();
    const [alertMsg, setAlertMsg] = useState<{
        message: string;
        type: AlertProps['type'];
    }>();
    const { t } = useTranslation();
    const pathname = usePathname();
    const { isLogged } = useAuth();

    // Navigate to report issue screen on double tap
    // const doubleTap = Gesture.Tap()
    //     .runOnJS(true)
    //     .numberOfTaps(3)
    //     .onStart(() => {
    //         router.push('/report-issue');
    //     })
    //     .withTestId('double-tap');

    const longPress = Gesture.LongPress()
        .runOnJS(true)
        // .minDuration(2000)
        .numberOfPointers(2)
        .onStart(() => {
            AlertRN.alert('Long press started');
        })
        .onTouchesCancelled((event: GestureTouchEvent) => {
            console.warn('Touches cancelled', event?.numberOfTouches);
        });

    useEffect(() => {
        const showAlert = async () => {
            const hasOpenedReportIssue = await getFromAsyncStorage(
                'hasOpenedReportIssue'
            );
            !hasOpenedReportIssue &&
                setAlertMsg({
                    message: t('reportIssueScreen.alertMessage'),
                    type: 'info',
                });
        };

        showAlert();
    }, [t]);

    useEffect(() => {
        // Clear alert message when navigating to report issue screen
        pathname === '/report-issue' && setAlertMsg(undefined);
    }, [pathname]);

    return isLogged ? (
        <>
            {alertMsg && (
                <Alert
                    autoDismiss={10000}
                    cancelable={false}
                    message={alertMsg.message}
                    onAlertClose={setAlertMsg}
                    position="top"
                    presentationStyle="absolute"
                    style={styles.alert}
                    type={alertMsg?.type}
                />
            )}

            <GestureHandlerRootView
                style={styles.fitScreen}
                {...props}
            >
                <GestureDetector gesture={longPress}>
                    <View style={styles.fitScreen}>{children}</View>
                </GestureDetector>
            </GestureHandlerRootView>
        </>
    ) : (
        <>{children}</>
    );
};

export { CustomGestureDetector };

image

boclar commented 2 months ago

Tested on:

iPhone 14 Pro Max. Android Samsung S20 FE

m-bert commented 2 months ago

I currently have the plugin installed, but wonder if there is a way to pull these PR updates to my branch instead of the production ones?

You can install Gesture Handler from my branch, this way you don't have to do any gymnastics with imports.

I tested it on 2 devices and I am getting the same issue you see in the terminal logs, which stop my app from loading, so it >stays on splash screen.

Error: WARN Tried to call timer with ID 9 but no such timer exists.

That's strange. Do you have anything else, like stack trace for this error?

boclar commented 2 months ago

There is no stack error, that is the only thing that shows up on the terminal as an error. I am willing to jump into a meeting if that works for you.

m-bert commented 2 months ago

Unfortunately I don't have much time right now to schedule a meeting 😞

I'll try to look into it and I'll get back to you if I find something. In the meantime, would it be possible for you to create a repository with reproduction? Since it works ok on my end it is hard to determine what actually causes this issue.

m-bert commented 2 months ago

Okay, I've found out that on android code that is responsible for scheduling LongPress activation was called on every move event. I've already fixed it.

I'm not sure if that was the cause of the warning that you've got. Also, I can't see what could cause it on iOS. Could you check if this issue is gone?

boclar commented 2 months ago

Here is the stack error. Hopefully it helps you debug the issue.

It is still not working on my end, but if it works for you, I would think that is fine. I could try again after you publish the version to npm.

ERROR Error: [Reanimated] Another instance of Reanimated was detected. See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#another-instance-of-reanimated-was-detected for more details. Previous: 3.12.0, current: 3.10.1., js engine: hermes at ContextNavigator (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:231591:24) at ExpoRoot (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:231547:28) at App at ErrorToastContainer (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:723792:24) at ErrorOverlay at withDevTools(ErrorOverlay) (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:593260:27) at RCTView at View (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:41375:43) at RCTView at View (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:41375:43) at AppContainer (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:41218:25) at main(RootComponent) (http://192.168.1.95:8081/src/index.ts.bundle//&platform=android&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=true&transform.routerRoot=.%2Fsrc%2Fapp:120270:28) WARN Tried to call timer with ID 23 but no such timer exists.

boclar commented 2 months ago

I think the issue is because I cant install the compatible packages with expo install, because it is a branch, not a package, right now. Therefore it gives me the error about several instances

m-bert commented 2 months ago

Oh, you're right, I forgot that expo requires specific versions (at least for native code, and in my PR there are native changes).

I've removed closes directive from my PR - we will merge it and keep this issue open until you'd be able to confirm that it works (or not).

m-bert commented 1 month ago

HI @boclar! Have you been able to test it yet? Fixing PR has already been released and I'd like to know if it works 😅

boclar commented 1 month ago

I will test it tonight

boclar commented 1 month ago

I am currently testing version "2.19.0" which is the latest version published until now. I tried on expo GO and it is giving me errors, therefore I decided to build an ios app and will keep you posted of what results I get on iPhone 14 Pro Max.

boclar commented 1 month ago

Long press not working on Expo GO using SDK 51, neither on iPhone 15. You will find logs below.

Do you think this is Expo specific error?

`[GESTURE HANDLER] Initialize gesture handler for view <RCTRootContentView: 0x106e51870; frame = (0 0; 393 852); gestureRecognizers = <NSArray: 0x600000cdfea0>; layer = <CALayer: 0x600000294240>> reactTag: 11; frame = {{0, 0}, {393, 852}}; layer = <CALayer: 0x600000294240> Detected the current OS's ImageIO PNG Decoder is buggy on indexed color PNG. Perform workaround solution... Bridge call to: deviceContexts libc++abi: terminating due to uncaught exception of type facebook::jsi::JSError: Object is not a function

TypeError: Object is not a function at anonymous (JavaScript:1:67) at apply (native) at runWorklet (JavaScript:1:202) at anonymous (JavaScript:1:701) at handleAndFlushAnimationFrame (JavaScript:1:148)`

Here is my code for gesture handling:

const CustomGestureDetector = ({
    children,
    ...props
}: CustomGestureDetectorProps) => {
    const styles = customGestureDetectorStyles();
    const [alertMsg, setAlertMsg] = useState<{
        message: string;
        type: AlertProps['type'];
    }>();
    const { t } = useTranslation();
    const pathname = usePathname();

    // Navigate to report issue screen on double tap
    const tapGesture = Gesture.Tap()
        .runOnJS(true)
        .numberOfTaps(3)
        .onStart(() => {
            router.navigate('/report-issue');
        })
        .withTestId('double-tap');

    const longPress = Gesture.LongPress()

        .numberOfPointers(1)
        // .minDuration(1000)
        .onStart(() => {
            router.navigate('/report-issue');
            console.log('long press');
        })
        .onTouchesCancelled(() => {
            console.log('long press cancelled');
        });

    useEffect(() => {
        const showAlert = async () => {
            const hasOpenedReportIssue = await getFromAsyncStorage(
                'hasOpenedReportIssue'
            );
            !hasOpenedReportIssue &&
                setAlertMsg({
                    message: t('reportIssueScreen.alertMessage'),
                    type: 'info',
                });
        };

        showAlert();
    }, [t]);

    useEffect(() => {
        // Clear alert message when navigating to report issue screen
        pathname === '/report-issue' && setAlertMsg(undefined);
    }, [pathname]);

    return (
        <>
            {alertMsg && (
                <Alert
                    autoDismiss={5000}
                    cancelable={false}
                    message={alertMsg.message}
                    onAlertClose={setAlertMsg}
                    position="top"
                    presentationStyle="absolute"
                    style={styles.alert}
                    type={alertMsg?.type}
                />
            )}

            <GestureHandlerRootView
                style={styles.fitScreen}
                {...props}
            >
                <GestureDetector gesture={longPress}>
                    <View style={styles.fitScreen}>{children}</View>
                </GestureDetector>
            </GestureHandlerRootView>
        </>
    );
};

export { CustomGestureDetector }
boclar commented 1 month ago

I just noticed I did not have runOnJS(true) to be executed.

These were my results without runOnJS(true): When numberOfPointers is 1 app closes automatically, when greater than 1 touches still get cancelled and nothing happens.

Having runOnJS(true) executed on long press:

Press with one fingers seems to be working fine, however, numberOfPointers are ignored, regardless I define 2, 3, 4 fingers it will only detect long press with one finger.

iOS

boclar commented 1 month ago

Additionally, I have my expo app to work for web, android and ios. Now when long press is enabled as a gesture. It is not detected on the web either

m-bert commented 1 month ago

Expo GO won't work since it is compatible with exact version of Gesture Handler (I'm not sure which one is used in latest expo go, but I'm pretty sure it is below 2.19).

Now when long press is enabled as a gesture. It is not detected on the web either

I've just tested numberOfPointers on web and it works fine.

Could you prepare a reproduction that we can run? I'm not sure what's happening since all of the things you've mentioned should work. Also, code that you've posted is not copy-pastable as it contains some functions that are not defined within snippet.

boclar commented 1 month ago

@m-bert I'll have a demo for you in the next hours.

boclar commented 1 month ago

@m-bert Hey, this is the best I can get for you. Let me know if this works. You can run project with Expo Go.

[example]

m-bert commented 1 month ago

Hi @boclar!

I've looked into your package.json and here's what I've found:

"react-native-gesture-handler": "2.16.1",

so there's no way it will work since PR with numberOfPointers was released in 2.19.0. The other thing is I'm not sure which version is supported by Expo GO (even if you install newer version of package, only the JS side will be affected).

Also this is quite complex example, not MCVE.

Given number of errors that you get (for example the one with Reanimated) we believe that the problem lies in your complex setup, not in the Gesture Handler itself. Unfortunately at this point we don't have capacity to help you.