Skip to content

Commit

Permalink
feat: improve gestures, update types and examples (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
likashefqet authored Sep 29, 2024
1 parent a6667e8 commit 9482fa9
Show file tree
Hide file tree
Showing 19 changed files with 5,050 additions and 3,944 deletions.
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ Photo by <a href="https://unsplash.com/photos/XLqiL-rz4V8" title="Photo by Walli

## What's new

- **Support for Scale Animated Value:** Added the ability to provide a Reanimated shared value for the scale property, allowing you to access and utilize the current zoom scale in your own code.
- **Enhanced Pan Gesture Handling:** Improved the accuracy and responsiveness of pan gestures, ensuring smoother and more natural interactions when panning images.

- **Return Last Values on Reset:** Updated the `onResetAnimationEnd` callback, which now returns the last zoom and position values when the component resets (zooms out), providing more control and feedback for custom logic.
- **Refined Single Tap Detection:** The single tap gesture functionality has been enhanced to trigger more reliably, providing better consistency and control without interfering with other gestures.

- **Updated Example Integration:**
- Added new examples demonstrating how to leverage the scale value for custom animation effects.
- Provided an example showcasing how to integrate the Image Zoom Component with react-native-reanimated-carousel, allowing for animated, zoomable image carousels.
- **TypeScript Support for Animated Props:** Expanded TypeScript definitions to include support for animated props, ensuring better type safety and compatibility with Reanimated-based animations.

## Features

Expand All @@ -38,10 +43,14 @@ Photo by <a href="https://unsplash.com/photos/XLqiL-rz4V8" title="Photo by Walli

- **Customizable Zoom Settings:** Utilize `minScale`, `maxScale`, and `doubleTapScale` props for precise control over minimum, maximum, and double tap zoom levels, tailoring the zoom behavior to application requirements

- **Customizable Functionality:** Fine-tune the component's behavior with `minPanPointers` and `maxPanPointers` props to define the range of pointers necessary for pan gesture detection. Enable or disable features such as panning (`isPanEnabled`), pinching (`isPinchEnabled`), single tap handling (`isSingleTapEnabled`), and double tap zoom (`isDoubleTapEnabled`) based on specific application needs.
- **Customizable Functionality:** Enable or disable features such as panning (`isPanEnabled`), pinching (`isPinchEnabled`), single tap handling (`isSingleTapEnabled`), and double tap zoom (`isDoubleTapEnabled`) based on specific application needs.

- **Access Scale Animated Value:** Provide a Reanimated shared value for the scale property, allowing you to access and utilize the current zoom scale in your own code.

- **Interactive Callbacks:** The component provides interactive callbacks such as `onInteractionStart`, `onInteractionEnd`, `onPinchStart`, `onPinchEnd`, `onPanStart`, `onPanEnd`, `onSingleTap`, `onDoubleTap` and `onResetAnimationEnd` that allow you to handle image interactions.

- **Access Last Values on Reset:** The `onResetAnimationEnd` callback returns the last zoom and position values when the component resets (zooms out), providing more control and feedback for custom logic.

- **Ref Handle:** Customize the functionality further by utilizing the exposed `reset` and `zoom` methods. The 'reset' method allows you to programmatically reset the image zoom as a side effect to another user action or event, in addition to the default double tap and pinch functionalities. The 'zoom' method allows you to programmatically zoom in the image to a given point (x, y) at a given scale level.

- **Reanimated Compatibility**: Compatible with `Reanimated v2` & `Reanimated v3`, providing optimized performance and smoother animations during image manipulations`.
Expand Down Expand Up @@ -104,7 +113,6 @@ To use the `ImageZoom` component, simply pass the uri prop with the URL of the i
maxScale={maxScale}
scale={scale}
doubleTapScale={3}
minPanPointers={1}
isSingleTapEnabled
isDoubleTapEnabled
onInteractionStart={() => {
Expand Down Expand Up @@ -144,7 +152,6 @@ To use the `ImageZoom` component, simply pass the uri prop with the URL of the i
maxScale={maxScale}
scale={scale}
doubleTapScale={3}
minPanPointers={1}
isSingleTapEnabled
isDoubleTapEnabled
onInteractionStart={() => {
Expand Down Expand Up @@ -188,7 +195,6 @@ All `React Native Image Props` &
| minScale | Number | `1` | The minimum scale allowed for zooming. |
| maxScale | Number | `5` | The maximum scale allowed for zooming. |
| doubleTapScale | Number | `3` | The value of the image scale when a double-tap gesture is detected. |
| minPanPointers | Number | `2` | The minimum number of pointers required to enable panning. |
| maxPanPointers | Number | `2` | The maximum number of pointers required to enable panning. |
| isPanEnabled | Boolean | `true` | Determines whether panning is enabled within the range of the minimum and maximum pan pointers. |
| isPinchEnabled | Boolean | `true` | Determines whether pinching is enabled. |
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"react-native": "0.74.1",
"react-native-gesture-handler": "~2.16.1",
"react-native-reanimated": "~3.10.1",
"react-native-reanimated-carousel": "^3.5.1",
"react-native-redash": "^18.1.3",
"react-native-safe-area-context": "^4.10.8",
"react-native-svg": "^15.6.0",
Expand Down
158 changes: 4 additions & 154 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,160 +1,10 @@
import React, { useRef, useState } from 'react';
import { StyleSheet, View, Pressable, Text } from 'react-native';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
FadeIn,
FadeOut,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ImageZoomRef } from '../../src';
import { AnimatedCircle } from './components/AnimatedCircle';
import ExpoImageZoom from './components/ExpoImageZoom';
import ImageZoom from './components/ImageZoom';
import React from 'react';
import { TabScreen } from './screens/TabsScreen';
import safeAreaContextProviderHOC from './safeAreaContextProviderHOC';
import { COLORS } from './themes/colors';

// Photo by Walling [https://unsplash.com/photos/XLqiL-rz4V8] on Unsplash [https://unsplash.com/]
const IMAGE_URI =
'https://images.unsplash.com/photo-1596003906949-67221c37965c';
const MIN_SCALE = 0.5;
const MAX_SCALE = 5;
const ZOOM_IN_X = 146;
const ZOOM_IN_Y = 491;

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
const FadeInAnimation = FadeIn.duration(256);
const FadeOutAnimation = FadeOut.duration(256);
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';

function App() {
const imageZoomRef = useRef<ImageZoomRef>(null);
const { top, bottom } = useSafeAreaInsets();

const scale = useSharedValue(1);

const [useCustomComponent, setUseCustomComponent] = useState(false);
const [isZoomed, setIsZoomed] = useState(false);

const toggleComponent = () => {
setUseCustomComponent((current) => !current);
};
const zoomIn = () => {
imageZoomRef?.current?.zoom({ scale: 5, x: ZOOM_IN_X, y: ZOOM_IN_Y });
};
const zoomOut = () => {
imageZoomRef?.current?.reset();
};

return (
<View style={styles.container}>
{useCustomComponent ? (
<ExpoImageZoom
ref={imageZoomRef}
uri={IMAGE_URI}
scale={scale}
minScale={MIN_SCALE}
maxScale={MAX_SCALE}
setIsZoomed={setIsZoomed}
/>
) : (
<ImageZoom
ref={imageZoomRef}
uri={IMAGE_URI}
scale={scale}
minScale={MIN_SCALE}
maxScale={MAX_SCALE}
setIsZoomed={setIsZoomed}
/>
)}
<AnimatedPressable
onPress={toggleComponent}
entering={FadeInAnimation}
exiting={FadeOutAnimation}
style={[styles.button, styles.switchComponentButton, { top: top + 8 }]}
>
<Text style={styles.buttonText}>
Use {useCustomComponent ? 'React Native Image' : 'Expo Image'}
</Text>
</AnimatedPressable>

{isZoomed ? (
<>
<AnimatedPressable
onPress={zoomOut}
entering={FadeInAnimation}
exiting={FadeOutAnimation}
style={[styles.button, { top: top + 8 }]}
>
<Text style={styles.buttonText}>Zoom Out</Text>
</AnimatedPressable>
<AnimatedCircle
size={50}
scale={scale}
minScale={1}
maxScale={MAX_SCALE}
entering={FadeInAnimation}
exiting={FadeOutAnimation}
style={[styles.progressCircle, { bottom: bottom + 8 }]}
/>
</>
) : (
<>
<View pointerEvents="none" style={styles.zoomInCircle} />
<AnimatedPressable
onPress={zoomIn}
entering={FadeInAnimation}
exiting={FadeOutAnimation}
style={[styles.button, { bottom: bottom + 8 }]}
>
<Text style={styles.buttonText}>Zoom In the 🟡 Circle</Text>
</AnimatedPressable>
</>
)}
</View>
);
return <TabScreen />;
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
button: {
position: 'absolute',
zIndex: 10,
right: 8,
height: 40,
justifyContent: 'center',
backgroundColor: COLORS.mainDarkAlpha(0.16),
paddingHorizontal: 16,
paddingVertical: 8,
borderWidth: 2,
borderRadius: 20,
borderColor: COLORS.accent,
},
switchComponentButton: {
right: undefined,
left: 8,
},
zoomInCircle: {
position: 'absolute',
top: ZOOM_IN_Y,
left: ZOOM_IN_X,
width: 24,
height: 24,
borderWidth: 2,
borderRadius: 12,
borderColor: COLORS.accent,
transform: [{ translateX: -12 }, { translateY: -12 }],
},
buttonText: {
fontWeight: 'bold',
color: COLORS.white,
},
progressCircle: {
position: 'absolute',
left: 16,
bottom: 16,
},
});

export default safeAreaContextProviderHOC(gestureHandlerRootHOC(App));
45 changes: 35 additions & 10 deletions example/src/components/ExpoImageZoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,38 @@ import React, {
forwardRef,
} from 'react';
import { StyleSheet } from 'react-native';
import { SharedValue } from 'react-native-reanimated';
import Animated, {
FadeIn,
FadeOut,
Layout,
SharedValue,
} from 'react-native-reanimated';
import { Image } from 'expo-image';
import { ZOOM_TYPE, Zoomable, ZoomableRef } from '../../../src';
import { ZOOM_TYPE, Zoomable, ZoomableProps, ZoomableRef } from '../../../src';

const AnimatedImage = Animated.createAnimatedComponent(Image);

const styles = StyleSheet.create({
image: {
flex: 1,
overflow: 'hidden',
},
});

type ExpoImageZoomProps = {
type Props = {
uri: string;
scale?: SharedValue<number>;
minScale?: number;
maxScale?: number;
ref: ForwardedRef<ZoomableRef>;
setIsZoomed: (value: boolean) => void;
style?: ZoomableProps['style'];
};

const ExpoImageZoom: ForwardRefRenderFunction<
ZoomableRef,
ExpoImageZoomProps
> = ({ uri, scale, minScale = 0.5, maxScale = 5, setIsZoomed }, ref) => {
const ExpoImageZoom: ForwardRefRenderFunction<ZoomableRef, Props> = (
{ uri, scale, minScale = 0.5, maxScale = 5, setIsZoomed, style },
ref
) => {
const onZoom = (zoomType?: ZOOM_TYPE) => {
if (!zoomType || zoomType === ZOOM_TYPE.ZOOM_IN) {
setIsZoomed(true);
Expand All @@ -42,11 +51,13 @@ const ExpoImageZoom: ForwardRefRenderFunction<
return (
<Zoomable
ref={ref}
entering={FadeIn}
exiting={FadeOut}
layout={Layout}
minScale={minScale}
maxScale={maxScale}
scale={scale}
doubleTapScale={3}
minPanPointers={1}
isSingleTapEnabled
isDoubleTapEnabled
onInteractionStart={() => {
Expand All @@ -67,14 +78,28 @@ const ExpoImageZoom: ForwardRefRenderFunction<
console.log('onZoom', zoomType);
onZoom(zoomType);
}}
style={styles.image}
style={[styles.image, style]}
onResetAnimationEnd={(finished, values) => {
console.log('onResetAnimationEnd', finished);
console.log('lastScaleValue:', values?.SCALE.lastValue);
onAnimationEnd(finished);
}}
>
<Image style={styles.image} source={{ uri }} contentFit="cover" />
<AnimatedImage
entering={FadeIn}
exiting={FadeOut}
layout={Layout}
style={styles.image}
source={{ uri }}
contentFit="cover"
/>
{/* Without Layout Animations
<Image
style={styles.image}
source={{ uri }}
contentFit="cover"
/>
*/}
</Zoomable>
);
};
Expand Down
20 changes: 11 additions & 9 deletions example/src/components/ImageZoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@ import React, {
forwardRef,
} from 'react';
import { StyleSheet } from 'react-native';
import { SharedValue } from 'react-native-reanimated';
import { FadeIn, FadeOut, Layout, SharedValue } from 'react-native-reanimated';
import {
ImageZoom as RNImagedZoom,
ZOOM_TYPE,
ImageZoomRef,
ImageZoomProps,
} from '../../../src';

const styles = StyleSheet.create({
image: {
flex: 1,
},
image: { flex: 1 },
});

type ImageZoomProps = {
type Props = {
uri: string;
scale?: SharedValue<number>;
minScale?: number;
maxScale?: number;
ref: ForwardedRef<ImageZoomRef>;
setIsZoomed: (value: boolean) => void;
style?: ImageZoomProps['style'];
};
const ImageZoom: ForwardRefRenderFunction<ImageZoomRef, ImageZoomProps> = (
{ uri, scale, minScale = 0.5, maxScale = 5, setIsZoomed },
const ImageZoom: ForwardRefRenderFunction<ImageZoomRef, Props> = (
{ uri, scale, minScale = 0.5, maxScale = 5, setIsZoomed, style },
ref
) => {
const onZoom = (zoomType?: ZOOM_TYPE) => {
Expand All @@ -43,13 +43,15 @@ const ImageZoom: ForwardRefRenderFunction<ImageZoomRef, ImageZoomProps> = (

return (
<RNImagedZoom
entering={FadeIn}
exiting={FadeOut}
layout={Layout}
ref={ref}
uri={uri}
minScale={minScale}
maxScale={maxScale}
scale={scale}
doubleTapScale={3}
minPanPointers={1}
isSingleTapEnabled
isDoubleTapEnabled
onInteractionStart={() => {
Expand All @@ -70,7 +72,7 @@ const ImageZoom: ForwardRefRenderFunction<ImageZoomRef, ImageZoomProps> = (
console.log('onZoom', zoomType);
onZoom(zoomType);
}}
style={styles.image}
style={[styles.image, style]}
onResetAnimationEnd={(finished, values) => {
console.log('onResetAnimationEnd', finished);
console.log('lastScaleValue:', values?.SCALE.lastValue);
Expand Down
Loading

0 comments on commit 9482fa9

Please sign in to comment.