React Native Image Panning/Zooming

My dog actually wrote the package…

I recently wrote a wrapper for Images (or any child, really) to allow you to zoom and pan as if you were using a native image editing tool on your smartphone or the editing tools available on Twitter, Instagram, and Facebook. Even though the wrapper accepts any child, I’ll assume we are using an <Image /> since that’s the most commonly used case.

Here is a video of our example code below:

Let’s get into the specifics of what this package is actually capable of doing:

  1. You can resize an Image by pinching.
  2. You can rotate the Image with a two finger gesture (rotate was off in my video above but you can turn it on by passing rotate={true} as a prop).
  3. You can pan an Image with a one touch gesture (swiping in the direction the Image should move).
  4. The finalized image container won’t allow white space behind the Image. This means that if the Image size is less than the size of the container the Image will animate back out to the container dimensions. This also means that if you swipe too far in any direction (revealing some white space) then the Image will animate back to a position such that the image container has no white space.
  5. You can zoom into a specific area of the Image. In other words, the Image should resize while panning simultaneously to ensure that where you’re zooming is the center point of your two fingers. This is difficult to see on the simulator, but if you try this on the device you will see what I’m talking about.
  6. Often times, you want some type of mask over the Image. So with this component you’re able to pass in a rectangular shaped mask so that the Image will adhere to the mask’s dimensions rather than the container’s dimensions.
  7. The wrapper accepts a function as its child. The arguments of the function are the animated height and width values of our wrapping <Animated.View />. If you just care about passing a View or Image as the returned value of the function, then just set flex: 1. If you want to use something like gl-react-native so that you can use captureFrame() then you will need to set the height and width of the Surface since you cannot flex the surface (please see https://github.com/ProjectSeptemberInc/gl-react-native/issues/84 for more information).
  8. The wrapper works on iOS and Android (I’ve tested it on an actual device too).
  9. The wrapper has no dependencies except React and React-Native, making it very lightweight.

Here is where the package is lacking:

  1. The image container can have white space if you rotate the Image. I accounted for adjustments with sizing and panning, but I have not accounted for how to adjust the Image if it’s rotated.
  2. As the Image expands, you’ll notice a dramatic slow down in the actual zooming. Zooming needs to take into account the actual size of the Image relative to the original size. Right now this isn’t happening. This is something I intend to work on soon.
  3. I resize the actual size and width of the image which isn’t good for performance. This is again something that I intend on working on. I’d like to change this from height and width to scale and then pass the animated scale value as the argument in the child function. This is probably my number one priority with this package.
  4. The image mask must be a rectangular shape. This is because of the adjustments made to the Image after the gesture is complete. Accounting for a custom shaped masks would be very tricky math.

So the package is by no means perfect, but it’s good start if you want a native looking Photo editing/viewing tool that looks native and is lightweight. Pull requests are welcomed if you’d like to contribute.

Here is the example code that created the movie above:

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
Image,
Dimensions,
} from 'react-native';
import ViewEditor from 'react-native-view-editor';
const { width, height } = Dimensions.get('window');
function Mask() {
return (
<View style={styles.maskContainer}>
<View style={[styles.mask, styles.topBottom, styles.top]} />
<View style={[styles.mask, styles.topBottom, styles.bottom]} />
<View style={[styles.mask, styles.side, styles.left]} />
<View style={[styles.mask, styles.side, styles.right]} />
</View>
)
}
function App() {
return (
<ViewEditor
style={styles.container}
imageHeight={width}
imageWidth={width}
imageContainerHeight={width}
imageMask={Mask}
maskHeight={width}
maskPadding={50}
>
{() => (
<Image
source={{ uri: 'https://unsplash.it/750'}}
style={styles.flex}
/>
)}
</ViewEditor>
)
}
const styles = StyleSheet.create({
container: {
marginTop: 20,
},
flex: {
flex: 1,
},
maskContainer: {
position: 'absolute',
top: 0,
left: 0,
},
mask: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
position: 'absolute',
overflow: 'hidden',
},
topBottom: {
height: 50,
width: width - 100,
left: 50,
},
top: {
top: 0,
},
bottom: {
top: width - 50,
},
side: {
width: 50,
height: width,
top: 0,
},
left: {
left: 0,
},
right: {
left: width - 50,
},
});
AppRegistry.registerComponent('sampleViewEditor', () => App);

And that’s it. The NPM package is available at https://www.npmjs.com/package/react-native-view-editor. The source code is also available at https://github.com/doubleqliq/react-native-view-editor. Again, I welcome PR. Also many thanks to https://github.com/kiddkai/react-native-gestures for some of the math used in this package.


I am a software engineer working primarily with React, React Native, FeathersJS, MeteorJS, AWS, and other technologies. I love what I do, and hope you find this article useful.