Skip to content

Commit

Permalink
refactor: use the new gestures API
Browse files Browse the repository at this point in the history
  • Loading branch information
likashefqet committed Apr 15, 2023
1 parent 3e6e889 commit 8f76051
Show file tree
Hide file tree
Showing 5 changed files with 366 additions and 240 deletions.
43 changes: 10 additions & 33 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,22 @@
import React from 'react';
import { StatusBar, StyleSheet } from 'react-native';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { ImageZoom } from '@likashefqet/react-native-image-zoom';

// Photo by Walling [https://unsplash.com/photos/XLqiL-rz4V8] on Unsplash [https://unsplash.com/]
const imageUri = 'https://images.unsplash.com/photo-1596003906949-67221c37965c';

const styles = StyleSheet.create({
container: {
backgroundColor: 'black',
},
loader: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'black',
},
});

function App() {
return (
<>
<StatusBar
barStyle="light-content"
translucent
backgroundColor="transparent"
/>
<ImageZoom
uri={imageUri}
containerStyle={styles.container}
activityIndicatorProps={{
color: 'white',
style: styles.loader,
}}
onInteractionStart={() => console.log('onInteractionStart')}
onInteractionEnd={() => console.log('onInteractionEnd')}
onPanStart={() => console.log('onPanStart')}
onPanEnd={() => console.log('onPanEnd')}
onPinchStart={() => console.log('onPinchStart')}
onPinchEnd={() => console.log('onPinchEnd')}
minScale={0.6}
/>
</>
<ImageZoom
uri={imageUri}
onInteractionStart={() => console.log('onInteractionStart')}
onInteractionEnd={() => console.log('onInteractionEnd')}
onPanStart={() => console.log('onPanStart')}
onPanEnd={() => console.log('onPanEnd')}
onPinchStart={() => console.log('onPinchStart')}
onPinchEnd={() => console.log('onPinchEnd')}
minScale={0.6}
/>
);
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"react-native": "0.66.4",
"react-native-builder-bob": "^0.18.0",
"react-native-gesture-handler": "^2.1.0",
"react-native-reanimated": "^2.3.0",
"react-native-reanimated": "^2.3.3",
"release-it": "^14.2.2",
"typescript": "^4.1.3"
},
Expand Down
192 changes: 59 additions & 133 deletions src/ImageZoom.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import React, { useRef, useState } from 'react';
import { LayoutChangeEvent, StyleSheet } from 'react-native';
import {
ActivityIndicator,
Image,
LayoutChangeEvent,
StyleSheet,
} from 'react-native';
import {
PanGestureHandler,
Gesture,
GestureDetector,
GestureStateChangeEvent,
GestureUpdateEvent,
PanGestureHandlerEventPayload,
PanGestureHandlerGestureEvent,
PinchGestureHandler,
PinchGestureHandlerEventPayload,
PinchGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
Expand All @@ -24,21 +19,9 @@ import { clamp } from './helpers';

import type { ImageZoomProps } from './types';

const AnimatedImage = Animated.createAnimatedComponent(Image);

const styles = StyleSheet.create({
container: {
flex: 1,
},
content: {
image: {
flex: 1,
flexGrow: 1,
position: 'relative',
overflow: 'hidden',
},
loader: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'whitesmoke',
},
});

Expand All @@ -48,42 +31,40 @@ export default function ImageZoom({
maxScale = 5,
minPanPointers = 2,
maxPanPointers = 2,
isPanEnabled = true,
isPinchEnabled = true,
onLoadEnd,
onInteractionStart,
onInteractionEnd,
onPinchStart,
onPinchEnd,
onPanStart,
onPanEnd,
style = {},
containerStyle = {},
imageContainerStyle = {},
activityIndicatorProps = {},
renderLoader,
...props
}: ImageZoomProps) {
const panRef = useRef();
const pinchRef = useRef();

const isInteracting = useRef(false);
const isPanning = useRef(false);
const isPinching = useRef(false);

const [isLoading, setIsLoading] = useState(true);
const [state, setState] = useState({
canInteract: false,
center: { x: 0, y: 0 },
const [center, setCenter] = useState({
x: 0,
y: 0,
});

const { canInteract, center } = state;

const scale = useSharedValue(1);
const initialFocal = { x: useSharedValue(0), y: useSharedValue(0) };
const focal = { x: useSharedValue(0), y: useSharedValue(0) };
const translate = { x: useSharedValue(0), y: useSharedValue(0) };

const onLayout = ({
nativeEvent: {
layout: { x, y, width, height },
},
}: LayoutChangeEvent) => {
setCenter({
x: x + width / 2,
y: y + height / 2,
});
};

const onInteractionStarted = () => {
if (!isInteracting.current) {
isInteracting.current = true;
Expand Down Expand Up @@ -122,40 +103,42 @@ export default function ImageZoom({
onInteractionEnded();
};

const panHandler = useAnimatedGestureHandler<PanGestureHandlerGestureEvent>({
onActive: (event: PanGestureHandlerEventPayload) => {
const panGesture = Gesture.Pan()
.minPointers(minPanPointers)
.maxPointers(maxPanPointers)
.onStart(() => {
runOnJS(onPanStarted)();
})
.onUpdate((event: GestureUpdateEvent<PanGestureHandlerEventPayload>) => {
translate.x.value = event.translationX;
translate.y.value = event.translationY;
},
onFinish: () => {
})
.onEnd(() => {
translate.x.value = withTiming(0);
translate.y.value = withTiming(0);
},
});
runOnJS(onPanEnded)();
});

const pinchHandler =
useAnimatedGestureHandler<PinchGestureHandlerGestureEvent>({
onStart: (event: PinchGestureHandlerEventPayload) => {
const pinchGesture = Gesture.Pinch()
.onStart(
(event: GestureStateChangeEvent<PinchGestureHandlerEventPayload>) => {
runOnJS(onPinchStarted)();
initialFocal.x.value = event.focalX;
initialFocal.y.value = event.focalY;
},
onActive: (event: PinchGestureHandlerEventPayload) => {
// onStart: focalX & focalY result both to 0 on Android
if (initialFocal.x.value === 0 && initialFocal.x.value === 0) {
initialFocal.x.value = event.focalX;
initialFocal.y.value = event.focalY;
}
scale.value = clamp(event.scale, minScale, maxScale);
focal.x.value = (center.x - initialFocal.x.value) * (scale.value - 1);
focal.y.value = (center.y - initialFocal.y.value) * (scale.value - 1);
},
onFinish: () => {
scale.value = withTiming(1);
focal.x.value = withTiming(0);
focal.y.value = withTiming(0);
initialFocal.x.value = 0;
initialFocal.y.value = 0;
},
}
)
.onUpdate((event: GestureUpdateEvent<PinchGestureHandlerEventPayload>) => {
scale.value = clamp(event.scale, minScale, maxScale);
focal.x.value = (center.x - initialFocal.x.value) * (scale.value - 1);
focal.y.value = (center.y - initialFocal.y.value) * (scale.value - 1);
})
.onEnd(() => {
scale.value = withTiming(1);
focal.x.value = withTiming(0);
focal.y.value = withTiming(0);
initialFocal.x.value = 0;
initialFocal.y.value = 0;
runOnJS(onPinchEnded)();
});

const animatedStyle = useAnimatedStyle(() => ({
Expand All @@ -168,72 +151,15 @@ export default function ImageZoom({
],
}));

const onLayout = ({
nativeEvent: {
layout: { x, y, width, height },
},
}: LayoutChangeEvent) => {
setState((current) => ({
...current,
canInteract: true,
center: { x: x + width / 2, y: y + height / 2 },
}));
};

const onImageLoadEnd = () => {
onLoadEnd?.();
setIsLoading(false);
};

return (
<PinchGestureHandler
ref={pinchRef}
simultaneousHandlers={[panRef]}
onGestureEvent={pinchHandler}
onActivated={onPinchStarted}
onCancelled={onPinchEnded}
onEnded={onPinchEnded}
onFailed={onPinchEnded}
enabled={isPinchEnabled && canInteract}
>
<Animated.View style={[styles.container, containerStyle]}>
<PanGestureHandler
ref={panRef}
simultaneousHandlers={[pinchRef]}
onGestureEvent={panHandler}
onActivated={onPanStarted}
onCancelled={onPanEnded}
onEnded={onPanEnded}
onFailed={onPanEnded}
minPointers={minPanPointers}
maxPointers={maxPanPointers}
enabled={isPanEnabled && canInteract}
>
<Animated.View
onLayout={onLayout}
style={[styles.content, imageContainerStyle]}
>
<AnimatedImage
style={[styles.container, style, animatedStyle]}
source={{ uri }}
resizeMode="contain"
onLoadEnd={onImageLoadEnd}
{...props}
/>
{isLoading &&
(renderLoader ? (
renderLoader()
) : (
<ActivityIndicator
size="small"
style={styles.loader}
color="dimgrey"
{...activityIndicatorProps}
/>
))}
</Animated.View>
</PanGestureHandler>
</Animated.View>
</PinchGestureHandler>
<GestureDetector gesture={Gesture.Simultaneous(panGesture, pinchGesture)}>
<Animated.Image
style={[styles.image, style, animatedStyle]}
source={{ uri }}
resizeMode="contain"
onLayout={onLayout}
{...props}
/>
</GestureDetector>
);
}
37 changes: 1 addition & 36 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import type {
ActivityIndicatorProps,
ImageProps,
ImageSourcePropType,
StyleProp,
ViewStyle,
} from 'react-native';
import type { ImageProps, ImageSourcePropType } from 'react-native';

export type ImageZoomProps = Omit<ImageProps, 'source'> & {
/**
Expand Down Expand Up @@ -32,16 +26,6 @@ export type ImageZoomProps = Omit<ImageProps, 'source'> & {
* @default 2
*/
maxPanPointers?: number;
/**
* Determines whether panning is enabled within the range of the minimum and maximum pan pointers.
* @default true
*/
isPanEnabled?: boolean;
/**
* Determines whether pinching is enabled.
* @default true
*/
isPinchEnabled?: boolean;
/**
* A callback triggered when the image interaction starts.
*/
Expand All @@ -66,25 +50,6 @@ export type ImageZoomProps = Omit<ImageProps, 'source'> & {
* A callback triggered when the image panning ends.
*/
onPanEnd?: Function;
/**
* The style object applied to the container.
* @default {}
*/
containerStyle?: StyleProp<ViewStyle>;
/**
* The style object applied to the image container.
* @default {}
*/
imageContainerStyle?: StyleProp<ViewStyle>;
/**
* The `ActivityIndicator` props used to customize the default loader.
* @default {}
*/
activityIndicatorProps?: ActivityIndicatorProps;
/**
* A function that renders a custom loading component. Set to `null` to disable the loader.
*/
renderLoader?: Function;
/**
* @see https://facebook.github.io/react-native/docs/image.html#source
* @default undefined
Expand Down
Loading

0 comments on commit 8f76051

Please sign in to comment.