Skip to content

Commit

Permalink
feat: add scale animated value & return last values in onResetAnimati…
Browse files Browse the repository at this point in the history
…onEnd callback (#59)

* feat: add scale animated value & return last values in onResetAnimationEnd callback

* chore: update docs
  • Loading branch information
likashefqet authored Aug 25, 2024
1 parent f23e4a7 commit 104d6e9
Show file tree
Hide file tree
Showing 14 changed files with 416 additions and 103 deletions.
30 changes: 18 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ Photo by <a href="https://unsplash.com/photos/XLqiL-rz4V8" title="Photo by Walli

## What's new

- **Zoomable Component:** This component makes any child elements zoomable, ensuring they behave like the image zoom component. This is particularly useful when you need to replace the default image component with alternatives like Expo Image (see example) or Fast Image.
- **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.

- **Updated Ref Handle:** Customize the functionality further by utilizing the exposed `zoom` method. This method allows you to programmatically zoom in the image to a given point (x, y) at a given scale level.
- **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.

## Features

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

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

- **Ref Handle:** Customize the functionality further by utilizing the exposed `reset` method. This 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.
- **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`.

- **TypeScript Support:** Developed with `TypeScript` to enhance codebase maintainability and ensure type safety, reducing potential errors during development and refactoring processes

- **Full React Native Image Props Support:** The component supports all React Native Image props, making it easy to integrate with existing code and utilize all the features that React Native Image provides.

- **Zoomable Component:** This component makes any child elements zoomable, ensuring they behave like the image zoom component. This is particularly useful when you need to replace the default image component with alternatives like Expo Image (see example) or Fast Image.

## Getting Started

To use the `ImageZoom` component, you first need to install the package via npm or yarn. Run either of the following commands:
Expand Down Expand Up @@ -96,12 +98,13 @@ To use the `ImageZoom` component, simply pass the uri prop with the URL of the i

```javascript
<ImageZoom
ref={imageZoomRef}
uri={imageUri}
minScale={0.5}
maxScale={5}
ref={ref}
uri={uri}
minScale={minScale}
maxScale={maxScale}
scale={scale}
doubleTapScale={3}
minPanPointers={2}
minPanPointers={1}
isSingleTapEnabled
isDoubleTapEnabled
onInteractionStart={() => {
Expand All @@ -123,8 +126,9 @@ To use the `ImageZoom` component, simply pass the uri prop with the URL of the i
onZoom(zoomType);
}}
style={styles.image}
onResetAnimationEnd={(finished) => {
onResetAnimationEnd={(finished, values) => {
console.log('onResetAnimationEnd', finished);
console.log('lastScaleValue:', values?.SCALE.lastValue);
onAnimationEnd(finished);
}}
resizeMode="cover"
Expand All @@ -136,8 +140,9 @@ To use the `ImageZoom` component, simply pass the uri prop with the URL of the i
```javascript
<Zoomable
ref={ref}
minScale={0.5}
maxScale={5}
minScale={minScale}
maxScale={maxScale}
scale={scale}
doubleTapScale={3}
minPanPointers={1}
isSingleTapEnabled
Expand All @@ -161,8 +166,9 @@ To use the `ImageZoom` component, simply pass the uri prop with the URL of the i
onZoom(zoomType);
}}
style={styles.image}
onResetAnimationEnd={(finished) => {
onResetAnimationEnd={(finished, values) => {
console.log('onResetAnimationEnd', finished);
console.log('lastScaleValue:', values?.SCALE.lastValue);
onAnimationEnd(finished);
}}
>
Expand Down
2 changes: 2 additions & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"react-native": "0.74.1",
"react-native-gesture-handler": "~2.16.1",
"react-native-reanimated": "~3.10.1",
"react-native-redash": "^18.1.3",
"react-native-safe-area-context": "^4.10.8",
"react-native-svg": "^15.6.0",
"react-native-web": "~0.19.10"
},
"devDependencies": {
Expand Down
185 changes: 114 additions & 71 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,160 @@
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 safeAreaContextProviderHOC from './safeAreaContextProviderHOC';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

const styles = StyleSheet.create({
container: {
flex: 1,
},
image: {
overflow: 'hidden',
},
button: {
zIndex: 10,
position: 'absolute',
right: 8,
height: 40,
justifyContent: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.64)',
paddingHorizontal: 16,
paddingVertical: 8,
borderWidth: 2,
borderRadius: 20,
borderColor: 'yellow',
},
zoomInButton: {
bottom: 48,
},
zoomOutButton: {
top: 48,
},
switchComponentButton: {
right: undefined,
left: 8,
},
circle: {
position: 'absolute',
top: 491,
left: 146,
width: 24,
height: 24,
borderWidth: 2,
borderRadius: 12,
borderColor: 'yellow',
transform: [{ translateX: -12 }, { translateY: -12 }],
},
buttonText: {
fontWeight: 'bold',
},
});
import { COLORS } from './themes/colors';

// Photo by Walling [https://unsplash.com/photos/XLqiL-rz4V8] on Unsplash [https://unsplash.com/]
const imageUri = 'https://images.unsplash.com/photo-1596003906949-67221c37965c';
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);

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={imageUri}
uri={IMAGE_URI}
scale={scale}
minScale={MIN_SCALE}
maxScale={MAX_SCALE}
setIsZoomed={setIsZoomed}
/>
) : (
<ImageZoom
ref={imageZoomRef}
uri={imageUri}
uri={IMAGE_URI}
scale={scale}
minScale={MIN_SCALE}
maxScale={MAX_SCALE}
setIsZoomed={setIsZoomed}
/>
)}
<Pressable
onPress={() => {
setUseCustomComponent((current) => !current);
}}
style={[styles.button, styles.switchComponentButton, { top: top + 16 }]}
<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>
</Pressable>
</AnimatedPressable>

{isZoomed ? (
<Pressable
onPress={() => {
imageZoomRef?.current?.reset();
}}
style={[styles.button, { top: top + 16 }]}
>
<Text style={styles.buttonText}>Zoom Out</Text>
</Pressable>
<>
<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.circle} />
<Pressable
onPress={() => {
imageZoomRef?.current?.zoom({ scale: 5, x: 146, y: 491 });
}}
style={[styles.button, { bottom: bottom + 16 }]}
<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>
</Pressable>
</AnimatedPressable>
</>
)}
</View>
);
}

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));
Loading

0 comments on commit 104d6e9

Please sign in to comment.