Android: Fix "long press handler broke gesture recognition" (#44)

* Long press for Android

* Clean up
This commit is contained in:
Maxim Bolshakov 2020-05-17 13:55:11 +02:00 committed by GitHub
parent f5508f72a1
commit 80eb318c2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 67 additions and 39 deletions

View File

@ -44,8 +44,8 @@ export default function App() {
const getImageUrls = memoize((images) =>
images.map((image) => ({ uri: image.original as string }))
);
const onLongPress = (event, image) => {
Alert.alert('Long Pressed', image.uri);
const onLongPress = (image) => {
Alert.alert("Long Pressed", image.uri);
};
return (

View File

@ -14,7 +14,6 @@ import {
View,
VirtualizedList,
ModalProps,
GestureResponderEvent,
} from "react-native";
import Modal from "./components/Modal/Modal";
@ -31,7 +30,7 @@ type Props = {
imageIndex: number;
visible: boolean;
onRequestClose: () => void;
onLongPress?: (event: GestureResponderEvent, image: ImageSource) => void;
onLongPress?: (image: ImageSource) => void;
onImageIndexChange?: (imageIndex: number) => void;
presentationStyle?: ModalProps["presentationStyle"];
animationType?: ModalProps["animationType"];
@ -61,7 +60,7 @@ function ImageViewing({
presentationStyle,
swipeToCloseEnabled,
doubleTapToZoomEnabled,
delayLongPress= DEFAULT_DELAY_LONG_PRESS,
delayLongPress = DEFAULT_DELAY_LONG_PRESS,
HeaderComponent,
FooterComponent,
}: Props) {

View File

@ -14,12 +14,10 @@ import {
StyleSheet,
NativeScrollEvent,
NativeSyntheticEvent,
TouchableWithoutFeedback,
GestureResponderEvent,
} from "react-native";
import useImageDimensions from "../../hooks/useImageDimensions";
import useZoomPanResponder from "../../hooks/useZoomPanResponder";
import usePanResponder from "../../hooks/usePanResponder";
import { getImageStyles, getImageTransform } from "../../utils";
import { ImageSource } from "../../@types";
@ -35,7 +33,7 @@ type Props = {
imageSrc: ImageSource;
onRequestClose: () => void;
onZoom: (isZoomed: boolean) => void;
onLongPress: (event: GestureResponderEvent, image: ImageSource) => void;
onLongPress: (image: ImageSource) => void;
delayLongPress: number;
swipeToCloseEnabled?: boolean;
doubleTapToZoomEnabled?: boolean;
@ -67,11 +65,17 @@ const ImageItem = ({
}
};
const [panHandlers, scaleValue, translateValue] = useZoomPanResponder({
const onLongPressHandler = useCallback(() => {
onLongPress(imageSrc);
}, [imageSrc, onLongPress]);
const [panHandlers, scaleValue, translateValue] = usePanResponder({
initialScale: scale || 1,
initialTranslate: translate || { x: 0, y: 0 },
onZoom: onZoomPerformed,
doubleTapToZoomEnabled,
onLongPress: onLongPressHandler,
delayLongPress,
});
const imagesStyles = getImageStyles(
@ -107,13 +111,6 @@ const ImageItem = ({
scrollValueY.setValue(offsetY);
};
const onLongPressHandler = useCallback(
(event: GestureResponderEvent) => {
onLongPress(event, imageSrc);
},
[]
);
return (
<Animated.ScrollView
ref={imageContainer}
@ -129,17 +126,12 @@ const ImageItem = ({
onScrollEndDrag,
})}
>
<TouchableWithoutFeedback
onLongPress={onLongPressHandler}
delayLongPress={delayLongPress}
>
<Animated.Image
{...panHandlers}
source={imageSrc}
style={imageStylesWithOpacity}
onLoad={onLoaded}
/>
</TouchableWithoutFeedback>
<Animated.Image
{...panHandlers}
source={imageSrc}
style={imageStylesWithOpacity}
onLoad={onLoaded}
/>
{(!isLoaded || !imageDimensions) && <ImageLoading />}
</Animated.ScrollView>
);

View File

@ -14,7 +14,7 @@ declare type Props = {
imageSrc: ImageSource;
onRequestClose: () => void;
onZoom: (isZoomed: boolean) => void;
onLongPress: (event: GestureResponderEvent, image: ImageSource) => void;
onLongPress: (image: ImageSource) => void;
delayLongPress: number;
swipeToCloseEnabled?: boolean;
doubleTapToZoomEnabled?: boolean;
@ -26,7 +26,7 @@ declare const _default: React.MemoExoticComponent<({
onRequestClose,
onLongPress,
delayLongPress,
swipeToCloseEnabled
swipeToCloseEnabled,
}: Props) => JSX.Element>;
export default _default;

View File

@ -37,7 +37,7 @@ type Props = {
imageSrc: ImageSource;
onRequestClose: () => void;
onZoom: (scaled: boolean) => void;
onLongPress: (event: GestureResponderEvent, image: ImageSource) => void;
onLongPress: (image: ImageSource) => void;
delayLongPress: number;
swipeToCloseEnabled?: boolean;
doubleTapToZoomEnabled?: boolean;
@ -108,9 +108,9 @@ const ImageItem = ({
const onLongPressHandler = useCallback(
(event: GestureResponderEvent) => {
onLongPress(event, imageSrc);
onLongPress(imageSrc);
},
[]
[imageSrc, onLongPress]
);
return (

View File

@ -20,7 +20,7 @@ let lastTapTS: number | null = null;
/**
* This is iOS only.
* Same functionality for Android implemented inside useZoomPanResponder hook.
* Same functionality for Android implemented inside usePanResponder hook.
*/
function useDoubleTapToZoom(
scrollViewRef: React.RefObject<ScrollView>,

View File

@ -36,7 +36,6 @@ const useImageDimensions = (image: ImageSource): Dimensions | null => {
},
// @ts-ignore
(error) => {
console.warn(error);
resolve({ width: 0, height: 0 });
}
);

View File

@ -6,7 +6,7 @@
*
*/
import { useMemo, useEffect } from "react";
import { useMemo, useEffect, useRef } from "react";
import {
Animated,
Dimensions,
@ -27,6 +27,7 @@ import {
const SCREEN = Dimensions.get("window");
const SCREEN_WIDTH = SCREEN.width;
const SCREEN_HEIGHT = SCREEN.height;
const MIN_DIMENSION = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT);
const SCALE_MAX = 2;
const DOUBLE_TAP_DELAY = 300;
@ -37,13 +38,17 @@ type Props = {
initialTranslate: Position;
onZoom: (isZoomed: boolean) => void;
doubleTapToZoomEnabled: boolean;
onLongPress: () => void;
delayLongPress: number;
};
const useZoomPanResponder = ({
const usePanResponder = ({
initialScale,
initialTranslate,
onZoom,
doubleTapToZoomEnabled,
onLongPress,
delayLongPress,
}: Props): Readonly<
[GestureResponderHandlers, Animated.Value, Animated.ValueXY]
> => {
@ -55,7 +60,9 @@ const useZoomPanResponder = ({
let tmpTranslate: Position | null = null;
let isDoubleTapPerformed = false;
let lastTapTS: number | null = null;
let longPressHandlerRef: number | null = null;
const meaningfulShift = MIN_DIMENSION * 0.01;
const scaleValue = new Animated.Value(initialScale);
const translateValue = new Animated.ValueXY(initialTranslate);
@ -113,7 +120,21 @@ const useZoomPanResponder = ({
return () => scaleValue.removeAllListeners();
});
const cancelLongPressHandle = () => {
longPressHandlerRef && clearTimeout(longPressHandlerRef);
};
const handlers = {
onGrant: (
_: GestureResponderEvent,
gestureState: PanResponderGestureState
) => {
numberInitialTouches = gestureState.numberActiveTouches;
if (gestureState.numberActiveTouches > 1) return;
longPressHandlerRef = setTimeout(onLongPress, delayLongPress);
},
onStart: (
event: GestureResponderEvent,
gestureState: PanResponderGestureState
@ -125,6 +146,7 @@ const useZoomPanResponder = ({
const tapTS = Date.now();
// Handle double tap event by calculating diff between first and second taps timestamps
isDoubleTapPerformed = Boolean(
lastTapTS && tapTS - lastTapTS < DOUBLE_TAP_DELAY
);
@ -183,8 +205,17 @@ const useZoomPanResponder = ({
event: GestureResponderEvent,
gestureState: PanResponderGestureState
) => {
const { dx, dy } = gestureState;
if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) {
cancelLongPressHandle();
}
// Don't need to handle move because double tap in progress (was handled in onStart)
if (doubleTapToZoomEnabled && isDoubleTapPerformed) return;
if (doubleTapToZoomEnabled && isDoubleTapPerformed) {
cancelLongPressHandle();
return;
}
if (
numberInitialTouches === 1 &&
@ -200,6 +231,8 @@ const useZoomPanResponder = ({
numberInitialTouches === 2 && gestureState.numberActiveTouches === 2;
if (isPinchGesture) {
cancelLongPressHandle();
const initialDistance = getDistanceBetweenTouches(initialTouches);
const currentDistance = getDistanceBetweenTouches(
event.nativeEvent.touches
@ -292,6 +325,8 @@ const useZoomPanResponder = ({
}
},
onRelease: () => {
cancelLongPressHandle();
if (isDoubleTapPerformed) {
isDoubleTapPerformed = false;
}
@ -359,4 +394,4 @@ const useZoomPanResponder = ({
return [panResponder.panHandlers, scaleValue, translateValue];
};
export default useZoomPanResponder;
export default usePanResponder;

View File

@ -136,6 +136,7 @@ type HandlerType = (
) => void;
type PanResponderProps = {
onGrant: HandlerType;
onStart?: HandlerType;
onMove: HandlerType;
onRelease?: HandlerType;
@ -143,6 +144,7 @@ type PanResponderProps = {
};
export const createPanResponder = ({
onGrant,
onStart,
onMove,
onRelease,
@ -153,6 +155,7 @@ export const createPanResponder = ({
onStartShouldSetPanResponderCapture: () => true,
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: onGrant,
onPanResponderStart: onStart,
onPanResponderMove: onMove,
onPanResponderRelease: onRelease,