Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve gestures, update types and examples #66

Merged
merged 2 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading