Skip to content

openspacelabs/react-native-zoomable-view

 
 

Repository files navigation

@openspacelabs/react-native-zoomable-view

A view component for react-native with pinch to zoom, tap to move and double tap to zoom capability. You can zoom everything, from normal images, text and more complex nested views.

This library is a fork of @dudigital/react-native-zoomable-view. We've rewritten most of the logic in the original library to address the following items:

  • Fixed jittering during zooming and panning
  • Fixed incorrect zoom center (happens during pinching and double tapping)
  • Fixed incorrect pan boundaries
  • Added the ability to zoom and pan at the same time (before you can only perform 1 of these 2 at a time)
  • Added “zoom to” animations
  • Added onSingleTap (besides the existing onDoubleTap)
  • Added animated touch feedback when the zoom subject is tapped on
  • Added "react-native-builder-bob" as a framework for library management/maintenance
  • Better internal code organization and documentation
  • Allowed passing in a custom pan and zoom animation values via optional props

What sets this library apart from the other zoom-pan libraries?

This library offers a much better user experience than the others:

  • The ability to zoom and pan at the same time.
  • No jittering during zooming.
  • Zoom center correctly placed at the pinch center - currently this is the ONLY react-native library that offers this.
  • And many other goodies. Check out the documentation below for more details.

M1 Mac iOS Simulator

Note that if try to run this library on an M1 Mac iOS Simulator, the animations will be quite jittery/jumpy. Test it on a real physical device or non-M1 Mac to see the actual performance.

Demo

Want to run the library on your physical device right here right now? Check out this Expo Snack

Getting started

Installation

We are working with the original maintainers of this library to transfer the NPM alias for react-native-zoomable-view. In the meantime, you will want to use @openspacelabs/react-native-zoomable-view as the package identifier.

Requirements:

  • React Native >= 0.79.0
  • React >= 18.0.0
  • react-native-reanimated ^3.16.1 (peer dependency — install separately and follow its setup, including the Babel plugin)
  • react-native-gesture-handler ^2.20.2 (peer dependency — install separately and wrap your root with GestureHandlerRootView)

To add this package, run

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

or

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

Basic Usage

This component is built on react-native-reanimated and react-native-gesture-handler. Make sure both peer dependencies are installed and configured per their setup guides — in particular, add the Reanimated Babel plugin to your babel.config.js and wrap your app root in GestureHandlerRootView.

Just use it as a drop in component instead of a normal view.

Import ReactNativeZoomableView:

import { ReactNativeZoomableView } from '@openspacelabs/react-native-zoomable-view';

Use the component:

<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>

Example

Here is a full drop in example you can use in Expo, after installing the package.

import * as React from 'react';

import { StyleSheet, View, Text, Image } from 'react-native';
import { ReactNativeZoomableView } from '@openspacelabs/react-native-zoomable-view';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>ReactNativeZoomableView</Text>
      <View style={{ borderWidth: 5, flexShrink: 1, height: 500, width: 310 }}>
        <ReactNativeZoomableView
          maxZoom={30}
          // Give these to the zoomable view so it can apply the boundaries around the actual content.
          // Need to make sure the content is actually centered and the width and height are
          // dimensions when it's rendered naturally. Not the intrinsic size.
          // For example, an image with an intrinsic size of 400x200 will be rendered as 300x150 in this case.
          // Therefore, we'll feed the zoomable view the 300x150 size.
          contentWidth={300}
          contentHeight={150}
        >
          <Image
            style={{ width: '100%', height: '100%', resizeMode: 'contain' }}
            source={{ uri: 'https://via.placeholder.com/400x200.png' }}
          />
        </ReactNativeZoomableView>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 20,
  },
  box: {
    width: 60,
    height: 60,
    marginVertical: 20,
  },
});

Props

Options

These options can be used to limit and change the zoom behavior.

name type description default
zoomEnabled boolean Can be used to enable or disable the zooming dynamically true
panEnabled boolean Can be used to enable or disable the panning dynamically true
initialZoom number Initial zoom level on startup 1.0
maxZoom number Maximum possible zoom level (zoom in). Pass Infinity to allow unbounded zoom-in 1.5
minZoom number Minimum possible zoom level (zoom out). Pass -Infinity to allow unbounded zoom-out 0.5
disablePanOnInitialZoom boolean If true, panning is disabled when zoom level is equal to the initial zoom level false
doubleTapDelay number How much delay will still be recognized as double press (ms) 300
doubleTapZoomToCenter boolean If true, double tapping will always zoom to center of View instead of the direction it was double tapped in
zoomStep number How much zoom should be applied on double tap 0.5
pinchToZoomInSensitivity number the level of resistance (sensitivity) to zoom in (0 - 10) - higher is less sensitive 1
pinchToZoomOutSensitivity number the level of resistance (sensitivity) to zoom out (0 - 10) - higher is less sensitive 1
movementSensitivity number how resistant should shifting the view around be? (0.5 - 5) - higher is less sensitive 1
initialOffsetX number The horizontal offset the image should start at 0
initialOffsetY number The vertical offset the image should start at 0
contentHeight number Specify if you want to treat the height of the centered content inside the zoom subject as the zoom subject's height undefined
contentWidth number Specify if you want to treat the width of the centered content inside the zoom subject as the zoom subject's width undefined
longPressDuration number Duration in ms until a press is considered a long press 700
visualTouchFeedbackEnabled boolean Whether to show a touch feedback circle on touch true

Static Pin Position

These optional props can be used to keep a "static" pin in the centre of the screen and move the map underneath it. This is very useful for maps.

name type description
staticPinPosition Vec2D Where in the viewport to put the pin
staticPinIcon Element The pin icon itself
onStaticPinPositionChange (position: Vec2D) => void Callback every time the pin is at rest
onStaticPinPositionMoveWorklet (position: Vec2D) => void Worklet callback live while the pin is moving (UI thread — must declare 'worklet';, see note below)
pinProps ViewProps Props forwarded to the pin wrapper

Note: onStaticPinPress and onStaticPinLongPress were removed in this revision and are tracked for restoration in a follow-up. Tap-to-select pin behaviour is not currently supported.

Callbacks

These events can be used to work with data after specific events.

Any prop ending in *Worklet runs on the UI thread; the function passed must declare 'worklet'; as its first statement so the Reanimated Babel plugin compiles it as a worklet — otherwise the UI-thread invocation will crash.

name description params expected return
onTransformWorklet Worklet called when the transformation configuration (zoom level and offset) changes (UI thread — must declare 'worklet';) zoomableViewEventObject void
onSingleTap Will be called once a tap is confirmed as a single tap (after doubleTapDelay) event, zoomableViewEventObject void
onDoubleTapBefore Will be called at the start of a double tap event, zoomableViewEventObject void
onDoubleTapAfter Will be called at the end of a double tap event, zoomableViewEventObject void
onShiftingEnd Will be called when user stops a tap and move gesture event, zoomableViewEventObject void
onZoomEnd Will be called after pinchzooming has ended event, zoomableViewEventObject void
onLongPress Will be called after the user pressed on the image for a while event, zoomableViewEventObject void
onLayoutWorklet Worklet called when the zoom subject's measured layout changes. Skipped while measurements are still zero (initial mount before the wrapper's onLayout fires). UI thread — must declare 'worklet'; layout: { x, y, width, height } void

Methods

The following methods allow you to control the ZoomableView zoom level & position from your component. (think of control buttons, ...)

name description params expected return
zoomTo Changes the zoom level to a specific number newZoomLevel: number, zoomCenter?: Vec2D boolean
zoomBy Changes the zoom level relative to the current level (use positive numbers to zoom in, negative numbers to zoom out) zoomLevelChange: number boolean
moveTo Shifts the zoomed part to a specific point (in px relative to x: 0, y: 0) newOffsetX: number, newOffsetY: number void
moveBy Shifts the zoomed part by a specific pixel number newOffsetX: number, newOffsetY: number void
moveStaticPinTo Pans so the static pin aligns with position (in content coordinates). Requires staticPinPosition, contentWidth, and contentHeight position: Vec2D, duration?: number void

Properties

name description
gestureStarted Indicates if a gesture is currently in progress

Example:

import { createRef } from 'react';
import {
  ReactNativeZoomableView,
  type ReactNativeZoomableViewRef,
} from '@openspacelabs/react-native-zoomable-view';

export default function App() {
  // you will need a reference to the ReactNativeZoomableView's imperative handle
  const zoomableViewRef = createRef<ReactNativeZoomableViewRef>();

  return (
    <View style={styles.container}>
      <View style={styles.zoomWrapper}>
        <ReactNativeZoomableView
          ref={zoomableViewRef}
        >
          <Text style={styles.caption}>HelloWorld</Text>
        </ReactNativeZoomableView>
      </View>

      <View style={styles.controlWrapperLeft}>
        {/* Here you see some examples of moveBy */}
        <Button onPress={() => zoomableViewRef.current!.moveBy(-30, 0)} title="⬅️" />
        <Button onPress={() => zoomableViewRef.current!.moveBy(30, 0)} title="➡️" />
        <Button onPress={() => zoomableViewRef.current!.moveBy(0, -30)} title="⬆️" />
        <Button onPress={() => zoomableViewRef.current!.moveBy(0, 30)} title="⬇️" />

        {/* Here you see an example of moveTo */}
        <Button onPress={() => zoomableViewRef.current!.moveTo(300, 200)} title="Move to" />
      </View>

      <View style={styles.controlWrapperRight}>
        {/* Here you see examples of zoomBy */}
        <Button onPress={() => zoomableViewRef.current!.zoomBy(-0.1)} title="-" />
        <Button onPress={() => zoomableViewRef.current!.zoomBy(0.1)} title="+" />

        {/* Here you see an example of zoomTo */}
        <Button onPress={() => zoomableViewRef.current!.zoomTo(1)} title="reset" />
      </View>
    </View>
  );
}

Pan Responder Hooks

react-native-gesture-handler is now used instead of the built-in PanResponder. As such, we have removed some hooks that are no longer supported and made the rest backward compatible.

name description params expected return
onPanResponderGrant Will be called when the pan gesture is granted (begins after RNGH activation) event, zoomableViewEventObject void
onPanResponderEnd Will be called when gesture ends (more accurately, on pan responder "release") event, zoomableViewEventObject void
onPanResponderTerminate Will be called when the gesture is force-interrupted by another handler event, zoomableViewEventObject void
onPanResponderMoveWorklet Will be called when user moves while touching event, zoomableViewEventObject {boolean} if true is returned, pinch and shift operations will not be processed

zoomableViewEventObject

The zoomableViewEventObject object is attached to every event and represents the current state of our zoomable view.

   {
      zoomLevel: number,         // current level of zooming denoting the scale applied to the zoom subject (usually a value between minZoom and maxZoom)
      offsetX: number,           // current offset left
      offsetY: number,           // current offset top
      originalHeight: number,    // original height of the zoom subject
      originalWidth: number,     // original width of the zoom subject
   }

Special configurations

Contributing

See the contributing guide to learn how to contribute to the repository and the development workflow.

OpenSpace Labs

OpenSpace_Logo

This library was authored by @SimonErich and is now maintained by OpenSpace Labs. Based in San Francisco, Openspace is "like Google StreetView plus Git for construction sites". We're doing a lot of cool things with AI, Machine Vision, 3D/2D Imagery, React/React Native, and more.

Join us and help revolutionize the construction industry. We're hiring on all fronts!

Check out our Glassdoor and blog posts.

License

MIT

About

A view component for react-native with pinch to zoom, tap to move and double tap to zoom capability.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • TypeScript 96.9%
  • JavaScript 3.1%