From a40902a8f12b0af1d71948a8965e4048c5a58157 Mon Sep 17 00:00:00 2001 From: MorganDavid Date: Thu, 3 Aug 2023 12:05:45 +0100 Subject: [PATCH 1/2] feat: onPress remove tap anywhere to call onPress because it breaks multi touch on ios add ios single press event android onPress event --- src/ImageViewing.tsx | 5 ++++ .../ImageItem/ImageItem.android.tsx | 7 +++++ src/components/ImageItem/ImageItem.d.ts | 21 ++++++++------ src/components/ImageItem/ImageItem.ios.tsx | 14 +++++++++- src/hooks/useDoubleTapToZoom.ts | 15 +++++++++- src/hooks/usePanResponder.ts | 28 +++++++++++++++---- 6 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/ImageViewing.tsx b/src/ImageViewing.tsx index 3782ee0e..12fab089 100644 --- a/src/ImageViewing.tsx +++ b/src/ImageViewing.tsx @@ -15,6 +15,7 @@ import { VirtualizedList, ModalProps, Modal, + TouchableWithoutFeedback, } from "react-native"; import ImageItem from "./components/ImageItem/ImageItem"; @@ -33,6 +34,7 @@ type Props = { visible: boolean; onRequestClose: () => void; onLongPress?: (image: ImageSource) => void; + onPress?: (image: ImageSource) => void; onImageIndexChange?: (imageIndex: number) => void; presentationStyle?: ModalProps["presentationStyle"]; animationType?: ModalProps["animationType"]; @@ -57,6 +59,7 @@ function ImageViewing({ visible, onRequestClose, onLongPress = () => {}, + onPress = () => {}, onImageIndexChange, animationType = DEFAULT_ANIMATION_TYPE, backgroundColor = DEFAULT_BG_COLOR, @@ -103,6 +106,7 @@ function ImageViewing({ hardwareAccelerated > + {typeof HeaderComponent !== "undefined" ? ( @@ -137,6 +141,7 @@ function ImageViewing({ imageSrc={imageSrc} onRequestClose={onRequestCloseEnhanced} onLongPress={onLongPress} + onPress={onPress} delayLongPress={delayLongPress} swipeToCloseEnabled={swipeToCloseEnabled} doubleTapToZoomEnabled={doubleTapToZoomEnabled} diff --git a/src/components/ImageItem/ImageItem.android.tsx b/src/components/ImageItem/ImageItem.android.tsx index f463b0b1..8b15f2c7 100644 --- a/src/components/ImageItem/ImageItem.android.tsx +++ b/src/components/ImageItem/ImageItem.android.tsx @@ -36,6 +36,7 @@ type Props = { onRequestClose: () => void; onZoom: (isZoomed: boolean) => void; onLongPress: (image: ImageSource) => void; + onPress: (image: ImageSource) => void; delayLongPress: number; swipeToCloseEnabled?: boolean; doubleTapToZoomEnabled?: boolean; @@ -46,6 +47,7 @@ const ImageItem = ({ onZoom, onRequestClose, onLongPress, + onPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, @@ -73,6 +75,10 @@ const ImageItem = ({ onLongPress(imageSrc); }, [imageSrc, onLongPress]); + const onPressHandler = useCallback(() => { + onPress(imageSrc); + }, [imageSrc, onLongPress]); + const [panHandlers, scaleValue, translateValue] = usePanResponder({ initialScale: scale || 1, initialTranslate: translate || { x: 0, y: 0 }, @@ -80,6 +86,7 @@ const ImageItem = ({ doubleTapToZoomEnabled, onLongPress: onLongPressHandler, delayLongPress, + onPress: onPressHandler, }); const imagesStyles = getImageStyles( diff --git a/src/components/ImageItem/ImageItem.d.ts b/src/components/ImageItem/ImageItem.d.ts index 57a902e9..83bb7da7 100644 --- a/src/components/ImageItem/ImageItem.d.ts +++ b/src/components/ImageItem/ImageItem.d.ts @@ -15,18 +15,23 @@ declare type Props = { onRequestClose: () => void; onZoom: (isZoomed: boolean) => void; onLongPress: (image: ImageSource) => void; + onPress: (image: ImageSource) => void; delayLongPress: number; swipeToCloseEnabled?: boolean; doubleTapToZoomEnabled?: boolean; }; -declare const _default: React.MemoExoticComponent<({ - imageSrc, - onZoom, - onRequestClose, - onLongPress, - delayLongPress, - swipeToCloseEnabled, -}: Props) => JSX.Element>; +declare const _default: React.MemoExoticComponent< + ({ + imageSrc, + onZoom, + onRequestClose, + onLongPress, + delayLongPress, + currentImageIndex, + onPress, + swipeToCloseEnabled, + }: Props) => JSX.Element +>; export default _default; diff --git a/src/components/ImageItem/ImageItem.ios.tsx b/src/components/ImageItem/ImageItem.ios.tsx index de85a146..d85b4953 100644 --- a/src/components/ImageItem/ImageItem.ios.tsx +++ b/src/components/ImageItem/ImageItem.ios.tsx @@ -38,6 +38,7 @@ type Props = { onRequestClose: () => void; onZoom: (scaled: boolean) => void; onLongPress: (image: ImageSource) => void; + onPress: (image: ImageSource) => void; delayLongPress: number; swipeToCloseEnabled?: boolean; doubleTapToZoomEnabled?: boolean; @@ -48,6 +49,7 @@ const ImageItem = ({ onZoom, onRequestClose, onLongPress, + onPress, delayLongPress, swipeToCloseEnabled = true, doubleTapToZoomEnabled = true, @@ -56,7 +58,17 @@ const ImageItem = ({ const [loaded, setLoaded] = useState(false); const [scaled, setScaled] = useState(false); const imageDimensions = useImageDimensions(imageSrc); - const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN); + + const onPressHandler = useCallback(() => { + onPress(imageSrc); + }, [imageSrc, onPress]); + + const handleDoubleTap = useDoubleTapToZoom( + scrollViewRef, + scaled, + SCREEN, + onPressHandler + ); const [translate, scale] = getImageTransform(imageDimensions, SCREEN); const scrollValueY = new Animated.Value(0); diff --git a/src/hooks/useDoubleTapToZoom.ts b/src/hooks/useDoubleTapToZoom.ts index 8fcb4a38..69e0589b 100644 --- a/src/hooks/useDoubleTapToZoom.ts +++ b/src/hooks/useDoubleTapToZoom.ts @@ -18,6 +18,8 @@ import { Dimensions } from "../@types"; const DOUBLE_TAP_DELAY = 300; let lastTapTS: number | null = null; +let isWaitingToSendSinglePress = false; + /** * This is iOS only. * Same functionality for Android implemented inside usePanResponder hook. @@ -25,7 +27,8 @@ let lastTapTS: number | null = null; function useDoubleTapToZoom( scrollViewRef: React.RefObject, scaled: boolean, - screen: Dimensions + screen: Dimensions, + onPress: () => void ) { const handleDoubleTap = useCallback( (event: NativeSyntheticEvent) => { @@ -33,6 +36,7 @@ function useDoubleTapToZoom( const scrollResponderRef = scrollViewRef?.current?.getScrollResponder(); if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) { + isWaitingToSendSinglePress = false; const { pageX, pageY } = event.nativeEvent; let targetX = 0; let targetY = 0; @@ -58,6 +62,15 @@ function useDoubleTapToZoom( }); } else { lastTapTS = nowTS; + + if (!isWaitingToSendSinglePress) { + isWaitingToSendSinglePress = true; + setTimeout(() => { + if (!isWaitingToSendSinglePress) return; + isWaitingToSendSinglePress = false; + onPress(); + }, DOUBLE_TAP_DELAY); + } } }, [scaled] diff --git a/src/hooks/usePanResponder.ts b/src/hooks/usePanResponder.ts index 202eca8e..bc58cb20 100644 --- a/src/hooks/usePanResponder.ts +++ b/src/hooks/usePanResponder.ts @@ -33,12 +33,15 @@ const SCALE_MAX = 2; const DOUBLE_TAP_DELAY = 300; const OUT_BOUND_MULTIPLIER = 0.75; +let isWaitingToSendSinglePress = false; + type Props = { initialScale: number; initialTranslate: Position; onZoom: (isZoomed: boolean) => void; doubleTapToZoomEnabled: boolean; onLongPress: () => void; + onPress: () => void; delayLongPress: number; }; @@ -48,6 +51,7 @@ const usePanResponder = ({ onZoom, doubleTapToZoomEnabled, onLongPress, + onPress, delayLongPress, }: Props): Readonly< [GestureResponderHandlers, Animated.Value, Animated.ValueXY] @@ -152,6 +156,7 @@ const usePanResponder = ({ ); if (doubleTapToZoomEnabled && isDoubleTapPerformed) { + isWaitingToSendSinglePress = false; const isScaled = currentTranslate.x !== initialTranslate.x; // currentScale !== initialScale; const { pageX: touchX, pageY: touchY } = event.nativeEvent.touches[0]; const targetScale = SCALE_MAX; @@ -199,6 +204,15 @@ const usePanResponder = ({ lastTapTS = null; } else { lastTapTS = Date.now(); + + if (!isWaitingToSendSinglePress) { + isWaitingToSendSinglePress = true; + setTimeout(() => { + if (!isWaitingToSendSinglePress) return; + isWaitingToSendSinglePress = false; + onPress(); + }, DOUBLE_TAP_DELAY); + } } }, onMove: ( @@ -208,11 +222,13 @@ const usePanResponder = ({ const { dx, dy } = gestureState; if (Math.abs(dx) >= meaningfulShift || Math.abs(dy) >= meaningfulShift) { + isWaitingToSendSinglePress = false; cancelLongPressHandle(); } // Don't need to handle move because double tap in progress (was handled in onStart) if (doubleTapToZoomEnabled && isDoubleTapPerformed) { + isWaitingToSendSinglePress = false; cancelLongPressHandle(); return; } @@ -232,6 +248,7 @@ const usePanResponder = ({ if (isPinchGesture) { cancelLongPressHandle(); + isWaitingToSendSinglePress = false; const initialDistance = getDistanceBetweenTouches(initialTouches); const currentDistance = getDistanceBetweenTouches( @@ -280,9 +297,8 @@ const usePanResponder = ({ if (isTapGesture && currentScale > initialScale) { const { x, y } = currentTranslate; const { dx, dy } = gestureState; - const [topBound, leftBound, bottomBound, rightBound] = getBounds( - currentScale - ); + const [topBound, leftBound, bottomBound, rightBound] = + getBounds(currentScale); let nextTranslateX = x + dx; let nextTranslateY = y + dy; @@ -324,6 +340,7 @@ const usePanResponder = ({ tmpTranslate = { x: nextTranslateX, y: nextTranslateY }; } }, + onRelease: () => { cancelLongPressHandle(); @@ -347,9 +364,8 @@ const usePanResponder = ({ if (tmpTranslate) { const { x, y } = tmpTranslate; - const [topBound, leftBound, bottomBound, rightBound] = getBounds( - currentScale - ); + const [topBound, leftBound, bottomBound, rightBound] = + getBounds(currentScale); let nextTranslateX = x; let nextTranslateY = y; From 6be023a5dbac170d69cabe00e46de159a5087e42 Mon Sep 17 00:00:00 2001 From: MorganDavid Date: Wed, 16 Aug 2023 13:43:57 +0100 Subject: [PATCH 2/2] feat: toggle header/footer visible on press --- src/ImageViewing.tsx | 11 ++++++-- src/hooks/useAnimatedComponents.ts | 44 +++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/ImageViewing.tsx b/src/ImageViewing.tsx index 12fab089..211718ae 100644 --- a/src/ImageViewing.tsx +++ b/src/ImageViewing.tsx @@ -73,7 +73,7 @@ function ImageViewing({ const imageList = useRef>(null); const [opacity, onRequestCloseEnhanced] = useRequestClose(onRequestClose); const [currentImageIndex, onScroll] = useImageIndexChange(imageIndex, SCREEN); - const [headerTransform, footerTransform, toggleBarsVisible] = + const [headerTransform, footerTransform, setBarsVisible, toggleBarsVisible] = useAnimatedComponents(); useEffect(() => { @@ -86,11 +86,16 @@ function ImageViewing({ (isScaled: boolean) => { // @ts-ignore imageList?.current?.setNativeProps({ scrollEnabled: !isScaled }); - toggleBarsVisible(!isScaled); + setBarsVisible(!isScaled); }, [imageList] ); + const onPressHandler = (src: ImageSource) => { + onPress(src); + toggleBarsVisible(); + }; + if (!visible) { return null; } @@ -141,7 +146,7 @@ function ImageViewing({ imageSrc={imageSrc} onRequestClose={onRequestCloseEnhanced} onLongPress={onLongPress} - onPress={onPress} + onPress={onPressHandler} delayLongPress={delayLongPress} swipeToCloseEnabled={swipeToCloseEnabled} doubleTapToZoomEnabled={doubleTapToZoomEnabled} diff --git a/src/hooks/useAnimatedComponents.ts b/src/hooks/useAnimatedComponents.ts index cc6e76d1..eb3acbc2 100644 --- a/src/hooks/useAnimatedComponents.ts +++ b/src/hooks/useAnimatedComponents.ts @@ -6,6 +6,7 @@ * */ +import { useRef } from "react"; import { Animated } from "react-native"; const INITIAL_POSITION = { x: 0, y: 0 }; @@ -15,33 +16,50 @@ const ANIMATION_CONFIG = { }; const useAnimatedComponents = () => { - const headerTranslate = new Animated.ValueXY(INITIAL_POSITION); - const footerTranslate = new Animated.ValueXY(INITIAL_POSITION); + const headerTranslate = useRef(new Animated.ValueXY(INITIAL_POSITION)); + const footerTranslate = useRef(new Animated.ValueXY(INITIAL_POSITION)); - const toggleVisible = (isVisible: boolean) => { - if (isVisible) { + const isVisible = useRef(true); + + const setIsVisible = (shouldMakeVisible: boolean) => { + if (shouldMakeVisible) { Animated.parallel([ - Animated.timing(headerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }), - Animated.timing(footerTranslate.y, { ...ANIMATION_CONFIG, toValue: 0 }), - ]).start(); + Animated.timing(headerTranslate.current.y, { + ...ANIMATION_CONFIG, + toValue: 0, + }), + Animated.timing(footerTranslate.current.y, { + ...ANIMATION_CONFIG, + toValue: 0, + }), + ]).start(() => (isVisible.current = true)); } else { Animated.parallel([ - Animated.timing(headerTranslate.y, { + Animated.timing(headerTranslate.current.y, { ...ANIMATION_CONFIG, toValue: -300, }), - Animated.timing(footerTranslate.y, { + Animated.timing(footerTranslate.current.y, { ...ANIMATION_CONFIG, toValue: 300, }), - ]).start(); + ]).start(() => (isVisible.current = false)); } }; - const headerTransform = headerTranslate.getTranslateTransform(); - const footerTransform = footerTranslate.getTranslateTransform(); + const toggleIsVisible = () => { + setIsVisible(!isVisible.current); + }; + + const headerTransform = headerTranslate.current.getTranslateTransform(); + const footerTransform = footerTranslate.current.getTranslateTransform(); - return [headerTransform, footerTransform, toggleVisible] as const; + return [ + headerTransform, + footerTransform, + setIsVisible, + toggleIsVisible, + ] as const; }; export default useAnimatedComponents;