react-native-zoomable-view

repository·master·Indexed 42 days ago

https://github.com/openspacelabs/react-native-zoomable-view

A high-performance React Native component providing pinch-to-zoom, panning, and double-tap-to-zoom functionality for images, text, and complex nested views. Version 3.0.0-beta.6 supports simultaneous zooming and panning, static pin positioning for map-like behavior, and UI-thread worklet callbacks for optimized performance. Requires react-native-gesture-handler and react-native-reanimated.

Tokens
9.5K
Snippets
18
Records
36
Agent score
49%

What's inside react-native-zoomable-view

  1. Overview of @openspacelabs/react-native-zoomable-view

    master
    The @openspacelabs/react-native-zoomable-view library provides a React Native component that enables pinch-to-zoom, tap-to-move, and double-tap-to-zoom capabilities. Unlike many other zoom-pan libraries, it allows users to zoom and pan simultaneously and ensures the zoom center is correctly placed at the pinch center. It can be used to zoom images, text, or complex nested views.
  2. Implement UI-thread Worklet callbacks

    master

    The library provides several high-frequency callbacks that run on the UI thread for performance. These props must be defined with the 'worklet'; directive at the start of the function body. Without this, the Reanimated Babel plugin will not compile them correctly, and the UI-thread invocation will cause a crash.

    Supported Worklet Props:

    • onLayoutWorklet: Receives { x, y, width, height } when internal measurements change.
    • onTransformWorklet: Fires on every transform tick.
    • onStaticPinPositionMoveWorklet: Fires whenever the pin's content position changes.
    • onPanResponderMoveWorklet: Fires on every move tick. Returning true from this function will short-circuit the library's internal pan/pinch handling for that frame.
    <ReactNativeZoomableView
      onTransformWorklet={(() => {
        'worklet';
        console.log('Transforming...');
      })()}
    />
  3. Configure pinch and double-tap zoom behavior

    master

    Pinch Zoom

    • Zoom Centre: Calculated as the midpoint of the two touches, or the staticPinPosition if set (which keeps the pin stable during the pinch).
    • Sensitivity: Resistance scales linearly from 0% (sensitivity=0) to 90% (sensitivity=10). The formula is: deltaGrowth × (1 - sensitivity × 9 / 100).
    • Clamping: maxZoom and minZoom are applied per frame. Use Infinity or -Infinity for unbounded scaling.
    • Note: If zoomEnabled=false, pinch frames are short-circuited, but onZoomEnd will still fire when the gesture ends.

    Double-tap Zoom

    Double-tap zoom cycles through zoom levels based on zoomStep. If maxZoom is reached, it cycles back to initialZoom.

    Example Logic (with initialZoom=1, maxZoom=2, zoomStep=0.5): 1 → 1.5 → 2 → 1 → ...

  4. Use Static Pin Position for Map-like behavior

    master

    To keep a "static" pin in the center of the viewport while moving the content underneath (useful for maps), use the following props:

    nametypedescription
    staticPinPositionVec2DWhere in the viewport to put the pin
    staticPinIconElementThe pin icon itself
    onStaticPinPositionChange(position: Vec2D) => voidCallback when the pin is at rest
    onStaticPinPositionMoveWorklet(position: Vec2D) => voidWorklet callback live while pin is moving (UI thread)
    pinPropsViewPropsProps forwarded to the pin wrapper
  5. Understand coordinate systems: Content vs Viewport

    master

    The library uses two distinct coordinate systems:

    1. Content Coordinates: The logical pixel space of the content being zoomed (origin at top-left). Used by:

      • contentWidth and contentHeight
      • moveStaticPinTo's position argument
      • Payloads for onStaticPinPositionChange and onStaticPinPositionMoveWorklet
      • zoomTo() and double-tap zoomCenter (subject-relative)
    2. Viewport Coordinates: Component-relative pixels (the same space as CSS left/top). Used by:

      • staticPinPosition
      • Input for viewportPositionToImagePosition
      • The pinch zoom centre (when staticPinPosition is set)

    Important Note on Resize Modes: The utility viewportPositionToImagePosition assumes the content is rendered with contain resize-mode semantics (aspect-ratio-preserving, letterboxed). If using cover, fill, or stretch, coordinate conversions will be incorrect.

  6. Understand gesture classification and tap handling

    master

    The library uses a single Gesture.Manual() to handle touches. Gestures are classified based on finger count and movement:

    • 1 finger, moved >2 px: Classified as shift.
    • 2 fingers: Classified as pinch.
    • 3+ fingers: Forces a non-release gesture end. If the user drops back to $\le 2$ fingers, the library enters recovery mode. In this mode, onPanResponderGrant is not re-fired and the long-press timer is not re-armed; any in-progress long-press classification is preserved.
    • No movement on release: Triggers tap classification (single, double, or long-press).

    Tap Disambiguation Logic:

    1. First tap: Schedules a timeout for doubleTapDelay ms.
    2. Second tap (within delay): Cancels the timeout, fires onDoubleTapBefore, executes double-tap zoom, then fires onDoubleTapAfter.
    3. No second tap: If staticPinPosition is set, the view performs a 200 ms pan-to-pin animation, then fires onSingleTap.

    Long-press behavior: If a long-press is triggered, the onLongPress callback fires. Upon release, the library checks an internal sentinel; if a long-press was active, it suppresses the trailing tap, meaning onSingleTap will not fire after a long-press.

  7. Configure and position the Static Pin

    master

    The StaticPin is a sibling to the canvas <GestureDetector>. It is used to provide a stable anchor point.

    Positioning

    • The pin is anchored bottom-centre to the staticPinPosition via internal transforms.
    • The position is defined using CSS-like left/top values corresponding to staticPinPosition.x/.y.
    • The pin is opacity: 0 until its icon has been measured, then it becomes opacity: 1.

    Interaction & Pointer Events

    • The wrapper defaults to pointerEvents="box-none", allowing touches to pass through empty pin space to the canvas.
    • To make parts of the pin interactive, wrap them in a <GestureDetector>. These touches will be claimed by the consumer and will not trigger canvas tap/long-press callbacks.
    • Use pinProps.pointerEvents to override the default (e.g., 'auto' to catch all touches in the pin bounding box).

    Position Callbacks

    • onStaticPinPositionMoveWorklet (UI thread): Fires whenever the pin's content-space position changes. Requires both contentWidth and contentHeight to be set.
    • onStaticPinPositionChange (JS thread): Fires ~100 ms after motion stops. It uses epsilon-equality (0.001) to prevent redundant fires.
  8. Migrate from PanResponder/Animated to the new API

    master

    The library has moved from a PanResponder/Animated stack to a functional Reanimated/RNGH stack.

    Removed Props (No direct replacement)

    • bindToBorders, panBoundaryPadding (Boundary clamping is removed; implement manually in onTransformWorklet)
    • disableMomentum (Pan stops immediately on finger lift)
    • animatePin (Build effects into your own staticPinIcon)
    • zoomAnimatedValue, panAnimatedValueXY (Use useZoomableViewContext() to read zoom, offsetX, and offsetY)
    • onShiftingBefore/After, onZoomBefore/After (Use zoomEnabled and sensitivity props)
    • onLayout $\rightarrow$ onLayoutWorklet (UI thread; payload is { x, y, width, height })
    • onTransform $\rightarrow$ onTransformWorklet (UI thread)

    Renamed Props

    • movementSensibility $\rightarrow$ movementSensitivity (Legacy name still works with a warning).

    New Props & Exports

    • UI Thread Callbacks (must include 'worklet';): onLayoutWorklet, onTransformWorklet, onPanResponderMoveWorklet, onStaticPinPositionMoveWorklet.
    • JS Thread Callbacks: onPanResponderGrant, onPanResponderEnd, onPanResponderTerminate, onShiftingEnd, onZoomEnd.
    • Hooks/Utilities: useZoomableViewContext(), applyContainResizeMode, getImageOriginOnTransformSubject, viewportPositionToImagePosition.
  9. Basic Usage of ReactNativeZoomableView

    master

    Use ReactNativeZoomableView as a drop-in replacement for a standard View. It allows you to wrap content (like text or images) to enable zooming and panning.

    Note on Content Dimensions: When using images or content with intrinsic sizes, it is recommended to provide contentWidth and contentHeight props. These should represent the dimensions of the content as it is rendered naturally (the layout size), not its intrinsic pixel size, so the component can correctly apply boundaries.

    import { ReactNativeZoomableView } from '@openspacelabs/react-native-zoomable-view';
    
    <ReactNativeZoomableView
       maxZoom={1.5}
       minZoom={0.5}
       zoomStep={0.5}
       initialZoom={1}
       onZoomEnd={logOutZoomState}
       style={{
          padding: 10,
          backgroundColor: 'red',
       }}
    >
       <Text>This is the content</Text>
    </ReactNativeZoomableView>
  10. Install and configure react-native-zoomable-view

    master

    To use @openspacelabs/react-native-zoomable-view, ensure your project meets the following peer dependency requirements:

    • react >= 18.0.0
    • react-native >= 0.79.0
    • react-native-gesture-handler ^2.20.2
    • react-native-reanimated ^3.16.1

    Critical Setup Steps:

    1. The ReactNativeZoomableView component must be mounted inside a GestureHandlerRootView.
    2. The react-native-reanimated Babel plugin must be configured in your babel.config.js.
    npm install @openspacelabs/react-native-zoomable-view react-native-gesture-handler react-native-reanimated
  11. Install @openspacelabs/react-native-zoomable-view

    master

    Install the package using npm or yarn.

    Requirements:

    • React Native >= 0.79.0
    • React >= 18.0.0
    • react-native-reanimated ^3.16.1 (Peer dependency: install separately and configure the Babel plugin in babel.config.js)
    • react-native-gesture-handler ^2.20.2 (Peer dependency: install separately and wrap your app root with GestureHandlerRootView)

    Installation Commands:

    npm add @openspacelabs/react-native-zoomable-view

    or

    yarn add @openspacelabs/react-native-zoomable-view
    npm add @openspacelabs/react-native-zoomable-view
  12. Configure zoom and pan behavior via Props

    master

    The ReactNativeZoomableView component accepts several options to control zooming and panning behavior:

    nametypedescriptiondefault
    zoomEnabledbooleanEnable or disable zooming dynamicallytrue
    panEnabledbooleanEnable or disable panning dynamicallytrue
    initialZoomnumberInitial zoom level on startup1.0
    maxZoomnumberMaximum zoom level. Use Infinity for unbounded zoom-in1.5
    minZoomnumberMinimum zoom level. Use -Infinity for unbounded zoom-out0.5
    disablePanOnInitialZoombooleanIf true, panning is disabled when zoom level equals initialZoomfalse
    doubleTapDelaynumberDelay (ms) to recognize a double press300
    doubleTapZoomToCenterbooleanIf true, double tap zooms to center instead of tap direction
    zoomStepnumberAmount of zoom applied on double tap
    pinchToZoomInSensitivitynumberResistance to zoom in (0-10, higher is less sensitive)1
    pinchToZoomOutSensitivitynumberResistance to zoom out (0-10, higher is less sensitive)1
    movementSensitivitynumberResistance to shifting view (0.5-5, higher is less sensitive)1
    initialOffsetXnumberStarting horizontal offset0
    initialOffsetYnumberStarting vertical offset0
    contentHeightnumberTreat the height of the centered content as the subject's heightundefined
    contentWidthnumberTreat the width of the centered content as the subject's widthundefined
    longPressDurationnumberDuration (ms) until a press is a long press700
    visualTouchFeedbackEnabledbooleanShow touch feedback circle on touchtrue