Glazzes / react-native-zoom-toolkit

Smoothly zoom any image, video or component you want!
https://glazzes.github.io/react-native-zoom-toolkit/
MIT License
158 stars 9 forks source link

[Question]: Use react-native-snap-carousel + react-native-orientation-locker + zoom toolkit #65

Open lucianomlima opened 1 day ago

lucianomlima commented 1 day ago

Summary

Hello! I'm working on a photo gallery with react-native-snap-carousel that allows changing orientation with react-native-orientation-locker.

Gallery component ```ts import React, { useCallback, useEffect, useState, useRef, memo, startTransition, } from 'react'; import { useWindowDimensions } from 'react-native'; import { Box } from '@gympass/yoga'; import { useNavigation, type ParamListBase } from '@react-navigation/native'; import type { StackNavigationProp } from '@react-navigation/stack'; import { useConfig } from 'hooks/remoteConfig'; import tracker from 'services/tracker'; import Dots from './components/Dots'; import { Photo, type PhotoItem } from './components/Photo'; import { Loading, Carousel } from './styles'; export type GalleryProps = { index: number; photos: Array; }; type RenderItemParams = { item: PhotoItem; index: number; }; function Gallery({ index, photos }: GalleryProps) { const [currentIndex, setCurrentIndex] = useState(index); const [isScreenReady, setIsScreenReady] = useState(false); const [isZoomEnabled, setZoomEnabled] = useState(false); const timeoutID = useRef(); const navigation = useNavigation>(); const enablePartnerGalleryZoom = Boolean( useConfig('enablePartnerGalleryZoom'), ); const { width } = useWindowDimensions(); function onResizeForIndex(itemIndex: number) { return function onResize(scale: number) { if (itemIndex !== currentIndex) return; if (timeoutID.current) { clearTimeout(timeoutID.current); timeoutID.current = undefined; } timeoutID.current = setTimeout(() => { setZoomEnabled(scale !== 1); }, 500); }; } const onPanEnd = useCallback((item: PhotoItem) => { tracker.trackEvent('partner_gallery_photo_scrolled', { photo_position: index, photo_id: item.id, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onPinchEnd = useCallback((scale: number, item: PhotoItem) => { const event = scale > 1 ? 'zoom_in' : 'zoom_out'; tracker.trackEvent(`partner_gallery_photo_${event}`, { photo_position: index, photo_id: item.id, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function renderItem({ item, index: itemIndex }: RenderItemParams) { return ( onPanEnd(item)} onPinchEnd={e => onPinchEnd(e.scale, item)} onZoomUpdate={onResizeForIndex(itemIndex)} /> ); } useEffect(() => { const unsubscribe = navigation.addListener('transitionEnd', e => { if (!e.data.closing && !isScreenReady) { startTransition(() => { setIsScreenReady(true); }); } }); return unsubscribe; }, [navigation, isScreenReady]); if (!isScreenReady) { return ( ); } return ( item.id} renderItem={renderItem} onBeforeSnapToItem={setCurrentIndex} onSnapToItem={setCurrentIndex} /> {photos.length > 1 && ( )} ); } export default memo(Gallery); ``` A PhotoItem have the following type: ```ts type PhotoItem = { id: string; uri: string; } ```

When the orientation changes from portrait to landscape, I must update the image dimensions to fit on the screen.

Photo component ```ts import React, { useCallback, useEffect, useRef } from 'react'; import { Image, useWindowDimensions, type LayoutChangeEvent, } from 'react-native'; import { Box } from '@gympass/yoga'; import Orientation, { OrientationType } from 'react-native-orientation-locker'; import { runOnJS } from 'react-native-reanimated'; import { ResumableZoom, getAspectRatioSize, useImageResolution, type CommonZoomState, type ResumableZoomProps, } from 'react-native-zoom-toolkit'; import { Loading } from '../styles'; export type PhotoItem = { id: string; uri: string; }; export type PhotoProps = { item: PhotoItem; panEnabled: boolean; onZoomUpdate: (scale: number) => void; } & ResumableZoomProps; export function Photo({ item, panEnabled = false, onZoomUpdate, ...zoomProps }: Readonly) { const currentOrientation = useRef( Orientation.getInitialOrientation(), ); const height = useRef(); const { width } = useWindowDimensions(); const { resolution, isFetching } = useImageResolution({ uri: item.uri }); const onOrientationChange = useCallback((orientation: OrientationType) => { currentOrientation.current = orientation; }, []); useEffect(() => { Orientation.addDeviceOrientationListener(onOrientationChange); return () => { Orientation.removeDeviceOrientationListener(onOrientationChange); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onLayoutchange = useCallback(({ nativeEvent }: LayoutChangeEvent) => { if (height.current !== nativeEvent.layout.height) { height.current = nativeEvent.layout.height; } }, []); if (isFetching || resolution === undefined) { return ( ); } function onUpdateHandler({ scale }: CommonZoomState) { 'worklet'; runOnJS(onZoomUpdate)(scale); } function isPortrait() { return currentOrientation.current === OrientationType.PORTRAIT; } const imageSize = getAspectRatioSize({ aspectRatio: resolution.width / resolution.height, width: isPortrait() ? width : undefined, height: !isPortrait() ? height.current : undefined, }); return ( ); } ```

But when it changes to the landscape orientation (right or left), there is a quick image flicker that gets bigger and then go to its desired size.

image-flickering.webm

Do you know if there is any way I can animate this change in width/height so that this flicker is not visible? As the Zoom toolkit uses react-native-animated, I think it's possible to animate the transition, but I can't find a way.

Glazzes commented 1 day ago

Hi there, did you try this issue in isolation? Like the example described on ResumableZoom docs? What you mention here is something way too specific.

Making random guesses before I know your answer, from my perspective it looks like useWindowDimensions runs faster than the device orientation change callback, I say this because the image in the video clearly gets wider.

Did you try to request the new width as the orientation is set, like so:

const [width, setWidth] = useState<number>(Dimensions.get("window").width);

....
const onOrientationChange = useCallback((orientation: OrientationType) => {
  currentOrientation.current = orientation;
  setWidth(Dimensions.get("window").width);
}, []);
lucianomlima commented 16 hours ago

Thank you for the answer. Yeah, I'm construct this case in isolation. First I did mount the Gallery, then I put the images inside ResumableZoom with pinch and double tap gestures enabled and now I'm trying to integrate with orientation changes.

For the previous steps I did make work properly. But you give me more insights to try. I'll try them and answer back here later.

Glazzes commented 8 hours ago

Currently I can only think of a way to make this work but I can not put it to the test myself because the version on my computer of this library is really broken right now, however try it and let me know.

Pretty much is about preventing a rerender when the orientation changes, Pinchable views are heavy components, so I'd advise you to place your size changing logic within the only place it makes sense to me, on the onLayout callback:

Try out the following code ```jsx import React from 'react'; import { Dimensions, View, type ViewStyle } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; import { ResumableZoom } from 'react-native-zoom-toolkit'; const IMAGE = 'https://e0.pxfuel.com/wallpapers/322/848/desktop-wallpaper-fennec-fox-algeria-national-animal-desert-fox.jpg'; const { width } = Dimensions.get('window'); const Index = () => { const imageWidth = useSharedValue(width); const imageHeight = useSharedValue(width / (850 / 565)); const imageStyles = useAnimatedStyle( () => ({ width: imageWidth.value, height: imageHeight.value, }), [imageWidth, imageHeight] ); const onLayout = () => { const currentWidth = Dimensions.get('window').width; imageWidth.value = currentWidth; imageHeight.value = currentWidth / (850 / 565); }; const style: ViewStyle = { flex: 1, }; return ( ); }; export default Index; ```

Be sure to try out the gestures, as I said the version I have is quite broken right now because of Expo.

TDLR: I deleted the project and cloned it, it works like a charm for me and do not forget to reset transformations on onLayout changes.