Android: Fix "long press handler broke gesture recognition" (#44)
* Long press for Android * Clean up
This commit is contained in:
parent
f5508f72a1
commit
80eb318c2c
|
@ -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 (
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -36,7 +36,6 @@ const useImageDimensions = (image: ImageSource): Dimensions | null => {
|
|||
},
|
||||
// @ts-ignore
|
||||
(error) => {
|
||||
console.warn(error);
|
||||
resolve({ width: 0, height: 0 });
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue