React Native Zoom Toolkit

repository·main·Indexed 19 days ago

https://github.com/glazzes/react-native-zoom-toolkit

A high-performance React Native library for pinch-to-zoom interactions, built with react-native-reanimated and react-native-gesture-handler. It provides component-agnostic zooming utilities and specialized components including CropZoom for image/video cropping, Gallery for Telegram-style zoomable lists, SnapbackZoom for previews, and ResumableZoom for detail screens. The toolkit is TypeScript-based and compatible with Expo.

Tokens
23.4K
Snippets
72
Records
88
Agent score
62%

What's inside react-native-zoom-toolkit

  1. Overview of library usage examples

    main

    The example app showcases five distinct implementation patterns using the toolkit's components:

    • SnapbackZoom: Used for zoomable chat message previews, integrated with a Flatlist for realistic scrolling behavior.
    • ResumableZoom: Implemented as a basic full-screen image detail screen.
    • ResumableZoom Gallery: A complex gallery implementation utilizing onSwipeRight, onSwipeLeft, and onHorizontalBoundsExceeded properties to navigate between images.
    • CropZoom (Managed): A profile picture selection/cropping screen used in conjunction with the expo-image-manipulator library.
    • CropZoom (Skia): A high-performance cropping implementation using react-native-skia to apply color matrix filters during the cropping process.
  2. Overview of React Native Zoom Toolkit

    main

    React Native Zoom Toolkit is a high-performance library providing components and utilities for pinch-to-zoom interactions in React Native. It is built using react-native-reanimated and react-native-gesture-handler to ensure smooth gesture interactions.

    Key capabilities include:

    • Component Agnostic: You can zoom any component, not just images.
    • High Performance: Powered by Reanimated and Gesture Handler.
    • Expo Compatible: Written in TypeScript and uses only Expo-supported modules.
    • Advanced Use Cases: Includes specialized components like SnapbackZoom for previews, ResumableZoom for gallery-style detail screens, CropZoom for cropping, and Gallery for list-based zooming. It also provides a Mirror utility to sync transformations between components.
  3. Overview of React Native Zoom Toolkit components

    main

    React Native Zoom Toolkit provides a set of components and utilities designed to handle common pinch-to-zoom requirements in React Native applications. Key components include:

    • SnapbackZoom: Optimized for zoomable preview handling (e.g., tapping a thumbnail to expand).
    • ResumableZoom: Designed for detail screens where users need to pick up zooming exactly where they left off.
    • CropZoom: An unopinionated component specifically for image and video cropping needs.
    • Gallery: A practical gallery component that mimics the behavior of Telegram's gallery.
  4. Implement the CropZoom component with an Overlay

    main

    To build a full-screen cropper, use the CropZoom component. You can pass an OverlayComponent prop which accepts a function that returns a component (e.g., an SVG with a hole) to visually indicate the crop area. Use useImageResolution to ensure the image is loaded and its resolution is available before rendering the cropper.

    Key props:

    • ref: A ref of type CropZoomType to access imperative methods like crop(), rotate(), flipHorizontal(), and flipVertical().
    • cropSize: A SizeVector defining the dimensions of the crop area.
    • resolution: The actual resolution of the image.
    • OverlayComponent: A function returning a component to be rendered as an overlay.
    import { CropZoom, useImageResolution, type CropZoomType, type SizeVector } from 'react-native-zoom-toolkit';
    
    // ... inside component
    const cropRef = useRef<CropZoomType>(null);
    const { isFetching, resolution } = useImageResolution({ uri: IMAGE });
    const cropSize: SizeVector<number> = { width: 300, height: 300 };
    
    const renderOverlay = () => <SVGOverlay cropSize={cropSize} />;
    
    if (isFetching || resolution === undefined) return null;
    
    return (
      <CropZoom
        ref={cropRef}
        cropSize={cropSize}
        resolution={resolution}
        OverlayComponent={renderOverlay}
      >
        <Image source={{ uri: IMAGE }} style={{ width: '100%', height: '100%' }} />
      </CropZoom>
    );
  5. Install react-native-zoom-toolkit

    main

    To use react-native-zoom-toolkit, you must also have react-native-gesture-handler and react-native-reanimated installed in your project.

    For Expo 54 and above

    Requires react-native-worklets as well.

    For Expo 53 and below

    Does not require react-native-worklets.

    Compatibility Matrix

    React Native VersionToolkit VersionGesture Handler version
    <= 0.76>= 3.0.02.16.0 and above.
    >= 0.76>= 4.0.02.19.0 and above.
    >= 0.81>= 6.0.02.19.0 and above.
    npm install react-native-zoom-toolkit react-native-gesture-handler react-native-reanimated react-native-worklets
  6. Access CropZoom methods via ref

    main

    To interact with the CropZoom component programmatically (e.g., to trigger a crop, reset transformations, or rotate), you must use a ref object with the CropZoomRefType.

    import { useRef } from 'react';
    import { CropZoom, type CropZoomRefType } from 'react-native-zoom-toolkit';
    
    const ref = useRef<CropZoomRefType>(null);
    
    // Example: Triggering a crop operation
    const handleCrop = () => {
      ref.current?.crop(200);
    };
    
    <CropZoom ref={ref} />
  7. Use the Gallery component

    main

    The Gallery component is an unopinionated component designed to build zoomable galleries with behavior similar to Telegram. It uses a Flatlist-like API and supports pinch gestures, double-tap to zoom, tap-to-item navigation, and custom scroll transitions.

    Key Implementation Details

    • Performance: Like Flatlist, you should memoize your renderItem and keyExtractor callbacks using useCallback to maintain high performance.
    • Sizing: Each cell is sized to match the dimensions of the Gallery component itself.
    • Image Fitting: When rendering images, use the fitContainer utility to calculate the appropriate size for the image based on its aspect ratio and the container dimensions.
    import React, { useCallback, useRef } from 'react';
    import {
      stackTransition,
      Gallery,
      type GalleryType,
      fitContainer
    } from 'react-native-zoom-toolkit';
    import { Image, useWindowDimensions } from 'react-native';
    
    const images = ['url1', 'url2'];
    
    const GalleryExample = () => {
      const ref = useRef<GalleryType>(null);
    
      const renderItem = useCallback((item: string, index: number) => {
        // Use fitContainer to handle image sizing
        return <GalleryImage uri={item} index={index} />;
      }, []);
    
      const keyExtractor = useCallback((item: string, index: number) => {
        return `${item}-${index}`;
      }, []);
    
      const transition = useCallback(stackTransition, []);
    
      return (
        <Gallery
          ref={ref}
          data={images}
          keyExtractor={keyExtractor}
          renderItem={renderItem}
          customTransition={transition}
        />
      );
    };
    
    // Helper component example
    const GalleryImage = ({ uri, index }: { uri: string; index: number }) => {
      const { width, height } = useWindowDimensions();
      const [resolution, setResolution] = React.useState({ width: 1, height: 1 });
    
      const size = fitContainer(resolution.width / resolution.height, { width, height });
    
      return (
        <Image
          source={{ uri }}
          style={size}
          onLoad={(e) => {
            setResolution({
              width: e.nativeEvent.source.width,
              height: e.nativeEvent.source.height,
            });
          }}
        />
      );
    };
  8. How to use the CropZoom component

    main

    The CropZoom component is used for image or video cropping. It requires a cropSize (the dimensions of the output crop) and the resolution (the actual dimensions of the source media).

    Important Implementation Details:

    • The CropZoom component uses flex: 1 and will attempt to fill all available space. Its minimum dimensions are determined by the cropSize property.
    • The child component (the image or video being cropped) must use the styles { width: '100%', height: '100%' } to ensure it fills the container correctly.
    • It is highly recommended to use the useImageResolution hook to retrieve the source media's resolution before rendering the component to avoid undefined values.
    import { Image, View, StyleSheet } from 'react-native';
    import { CropZoom, useImageResolution, type CropZoomType } from 'react-native-zoom-toolkit';
    import React, { useRef } from 'react';
    
    const imageUrl = 'url to some image';
    const cropSize = { width: 200, height: 200 };
    
    const App = () => {
      // A reference so you can access all methods
      const ref = useRef<CropZoomType>(null);
    
      // Utility hook to get the resolution of a network image
      const { resolution } = useImageResolution({ uri: imageUrl });
    
      // A function that renders an svg with a hole in it.
      const renderOverlay = () => <SomeComponent />
    
      if (resolution === undefined) {
        return null;
      }
    
      return (
        <CropZoom
          ref={ref}
          cropSize={cropSize}
          resolution={resolution}
          OverlayComponent={renderOverlay}
        >
          <Image
            source={{ uri: imageUrl }}
            style={{ width: '100%', height: '100%' }}
          />
        </CropZoom>
      );
    }
    
    export default App;
  9. Downscale nested components to preserve their scale

    main

    When using a zoom component, all nested elements are scaled equally by default. To preserve the visual scale of a nested component (e.g., keeping a map marker at its original size while zooming), you must apply an inverse scale transformation to that component. This is achieved by applying a scale of 1 / currentScale to the nested element.

    To implement this, create a Downscale component that accepts a scale prop (a SharedValue<number>) and applies the reciprocal scale via useAnimatedStyle from react-native-reanimated.

    import React from 'react';
    import type { ViewStyle } from 'react-native';
    import Animated, {
      useAnimatedStyle,
      type SharedValue,
    } from 'react-native-reanimated';
    
    type DownscaleProps = React.PropsWithChildren<{
      scale: SharedValue<number>;
      style?: ViewStyle;
    }>;
    
    const Downscale = ({ scale, style, children }: DownscaleProps) => {
      const animatedStyle = useAnimatedStyle(() => ({
        transform: [{ scale: 1 / scale.value }],
      }));
    
      return (
        <Animated.View style={[animatedStyle, style]}>
          {children}
        </Animated.View>
      );
    };
  10. Obtain the current zoom scale for downscaling

    main

    To drive the Downscale component, you need a SharedValue<number> representing the current zoom scale and a way to update it. You can do this in two ways:

    Use the useTransformationState hook with the 'resumable' argument. This provides a state object containing the scale and an onUpdate worklet.

    2. Manual implementation

    Manually manage a useSharedValue and update it within the onUpdate callback of your zoom component.

    Note: If you are not trying to mirror the current zoom component's state, use the manual approach.

    const scale = useSharedValue<number>(1);
    const onUpdate = (state) => {
      'worklet';
      scale.value = state.scale;
    };
    const { onUpdate, state } = useTransformationState('resumable');
    state.scale; // Holds the shared value describing the current scale
    onUpdate; // Update worklet function
  11. Setup CropZoom in an Expo project

    main

    To use CropZoom within an Expo managed project, create a new project using the blank TypeScript template and install the required dependencies including react-native-zoom-toolkit and its peer dependencies (react-native-reanimated, react-native-gesture-handler, and @shopify/react-native-skia).

    npx create-expo-app "crop-example" --template "blank-typescript"
    cd crop-example
    npx expo install react-native-reanimated react-native-gesture-handler @shopify/react-native-skia react-native-zoom-toolkit
  12. Use the SnapbackZoom component

    main

    The SnapbackZoom component is designed for preview handling (similar to Telegram or Instagram), where the content returns to its original position after a pinch gesture ends.

    Child Component Guidelines

    To ensure the component can measure its children correctly, follow these rules:

    • Use absolute sizes: Avoid relative units like {width: '100%'}. Use explicit values like {width: 200, height: 200}.
    • Avoid absolute positioning: Do not use {position: 'absolute'} on the child. If you need an absolute positioned view, wrap the SnapbackZoom component itself in an absolute positioned view.

    Limitations

    SnapbackZoom only makes its child zoomable. It cannot bypass zIndex or overflow style restrictions. You must structure your layout to ensure the zoomed content is visible over other elements.

    import { SnapbackZoom } from "react-native-zoom-toolkit"
    
    // Simple use case
    <SnapbackZoom>
      <Image
        source={{ uri: IMAGE }}
        style={{ width: 200, height: 200 }}
        resizeMethod={"scale"}
        resizeMode={"cover"}
      />
    </SnapbackZoom>