Skip to content

(WIP) fix: fixed image-viewer pan getsure pan values according to image and… #2729

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

Open
wants to merge 1 commit into
base: patch
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ const ImageViewerBasic = ({ ...props }: any) => {
<ImageViewerBackdrop>
<ImageViewerContent
images={Images}
renderImages={({ item }) => {
return <ImageViewerImage source={{ uri: item.url }} />;
renderImages={({ item, ...triggerProps }) => {
return (
<ImageViewerImage
source={{ uri: item.url }}
{...triggerProps}
/>
);
}}
keyExtractor={(item, index) => item.id + '-' + index}
>
Expand Down
3 changes: 2 additions & 1 deletion packages/unstyled/image-viewer/src/ImageViewerBackdrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ const ImageViewerBackdrop = (StyledImageViewerBackdrop: any) =>
forwardRef(({ children, ...props }: any, ref?: any) => {
const { scale } = useContext(ImageViewerContext);
const animatedStyle = useAnimatedStyle(() => {
const absScale = Number(scale?.toFixed(2));
return {
opacity: scale,
opacity: absScale,
};
});

Expand Down
174 changes: 129 additions & 45 deletions packages/unstyled/image-viewer/src/ImageViewerContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { forwardRef, useContext } from 'react';
import React, { forwardRef, useContext, useEffect } from 'react';
import { ImageViewerContext } from './ImageViewerContext';
import {
Easing,
runOnJS,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
withSpring,
Expand Down Expand Up @@ -38,22 +40,35 @@ const ImageViewerContent = (
const focalY = useSharedValue(0);
const lastTranslateX = useSharedValue(0);
const lastTranslateY = useSharedValue(0);
const isPinching = useSharedValue(false);
const imageWidth = useSharedValue(0);
const imageHeight = useSharedValue(0);

useEffect(() => {
if (scale.value === 1) {
isPinching.value = false;
}
}, [scale.value, isPinching]);

const pinchGesture = Gesture.Pinch()
.onStart(() => {
isPinching.value = true;
savedScale.value = scale.value;
})
.onUpdate((event: any) => {
// Apply the new scale based on the saved scale value
const newScale = savedScale.value * event.scale;
scale.value = Math.min(Math.max(newScale, 0.5), 10);
scale.value = Math.min(Math.max(newScale, 0.3), 10);
focalX.value = event.focalX;
focalY.value = event.focalY;
})
.onEnd(() => {
if (scale.value < 1) {
scale.value = withSpring(1);
scale.value = 1;
savedScale.value = 1;
}
if (scale.value < 0.9) {
runOnJS(onClose)();
} else {
savedScale.value = scale.value;
}
Expand All @@ -64,77 +79,117 @@ const ImageViewerContent = (
.maxDuration(DOUBLE_TAP_DELAY)
.onStart((event: any) => {
if (scale.value > 1) {
// If already zoomed in, reset to normal
scale.value = withTiming(1);
// Reset to normal
scale.value = withTiming(1, { easing: Easing.ease });
savedScale.value = 1;
translateX.value = withTiming(0);
translateY.value = withTiming(0);
translateX.value = 0;
translateY.value = 0;
} else {
// Zoom in to 2x at the tap location
scale.value = withTiming(2);
// Zoom in to 2x
scale.value = withTiming(2, { easing: Easing.ease });
savedScale.value = 2;

// Calculate the focal point for zooming
// Calculate the scaled dimensions at 2x
const scaledWidth = imageWidth.value * 2;
const scaledHeight = imageHeight.value * 2;

// Calculate tap point relative to center
const centerX = SCREEN_WIDTH / 2;
const centerY = SCREEN_HEIGHT / 2;
const focusX = event.x - centerX;
const focusY = event.y - centerY;

// Adjust translation to zoom into the tapped point
translateX.value = withTiming(-focusX);
translateY.value = withTiming(-focusY);
// Calculate maximum allowed translation
const maxTranslateX = Math.max(0, (scaledWidth - SCREEN_WIDTH) / 2);
const maxTranslateY = Math.max(
0,
(scaledHeight - SCREEN_HEIGHT) / 2
);

// Apply bounded translation
translateX.value = Math.max(
-maxTranslateX,
Math.min(maxTranslateX, -focusX)
);
translateY.value = Math.max(
-maxTranslateY,
Math.min(maxTranslateY, -focusY)
);
}
});

const panGesture = Gesture.Pan()
.onStart(() => {
// Store the current translation values when starting the pan
lastTranslateX.value = translateX.value;
lastTranslateY.value = translateY.value;
})
.onUpdate((event: any) => {
if (scale.value > 1) {
// When zoomed in, allow panning within bounds
// Calculate new positions based on the start position plus the new translation
translateX.value = lastTranslateX.value + event.translationX;
translateY.value = lastTranslateY.value + event.translationY;
// Calculate the scaled dimensions
const scaledWidth = imageWidth.value * scale.value;
const scaledHeight = imageHeight.value * scale.value;

// Calculate the maximum allowed translation based on scaled dimensions
const maxTranslateX = Math.max(0, (scaledWidth - SCREEN_WIDTH) / 2);
const maxTranslateY = Math.max(
0,
(scaledHeight - SCREEN_HEIGHT) / 2
);

// Calculate new positions with bounds
const newTranslateX = lastTranslateX.value + event.translationX;
const newTranslateY = lastTranslateY.value + event.translationY;

// Apply bounds with smooth clamping
translateX.value = Math.max(
-maxTranslateX,
Math.min(maxTranslateX, newTranslateX)
);
translateY.value = Math.max(
-maxTranslateY,
Math.min(maxTranslateY, newTranslateY)
);
} else {
// Normal swipe behavior when not zoomed
// When not zoomed in, allow dragging to dismiss
translateX.value = event.translationX;
translateY.value = event.translationY;
scale.value = withSpring(
Math.max(0.5, 1 - Math.abs(event.translationY) / SCREEN_HEIGHT)
);
if (!isPinching.value) {
scale.value = withSpring(
Math.max(0.5, 1 - Math.abs(event.translationY) / SCREEN_HEIGHT)
);
}
}
})
.onEnd((event: any) => {
if (scale.value <= 1) {
if (Math.abs(event.translationY) > SCREEN_HEIGHT * 0.03) {
runOnJS(onClose)();
} else {
// Reset position
translateX.value = 0;
translateY.value = 0;
scale.value = 1;
savedScale.value = 1;
}
}

// Reset position if not zoomed
if (scale.value <= 1) {
translateX.value = 0;
translateY.value = withSpring(0);
scale.value = withTiming(1);
savedScale.value = 1;
} else {
// When zoomed, bound the pan values
const maxTranslateX = ((scale.value - 1) * SCREEN_WIDTH) / 2;
const maxTranslateY = ((scale.value - 1) * SCREEN_HEIGHT) / 2;

translateX.value = withSpring(
Math.min(
Math.max(translateX.value, -maxTranslateX),
maxTranslateX
)
// Calculate final bounds for zoomed state
const scaledWidth = imageWidth.value * scale.value;
const scaledHeight = imageHeight.value * scale.value;

const maxTranslateX = Math.max(0, (scaledWidth - SCREEN_WIDTH) / 2);
const maxTranslateY = Math.max(
0,
(scaledHeight - SCREEN_HEIGHT) / 2
);

// ensure position stays within bounds
translateX.value = Math.max(
-maxTranslateX,
Math.min(maxTranslateX, translateX.value)
);
translateY.value = withSpring(
Math.min(
Math.max(translateY.value, -maxTranslateY),
maxTranslateY
)
translateY.value = Math.max(
-maxTranslateY,
Math.min(maxTranslateY, translateY.value)
);
}
});
Expand All @@ -148,7 +203,6 @@ const ImageViewerContent = (
// https://github.com/software-mansion/react-native-reanimated/issues/4548
// @ts-ignore
const animatedStyle = useAnimatedStyle(() => {
runOnJS(setScale)(scale.value);
return {
transform: [
{ translateX: translateX.value },
Expand All @@ -158,6 +212,15 @@ const ImageViewerContent = (
};
});

// Add a separate worklet to handle scale changes
useAnimatedReaction(
() => scale.value,
(currentScale) => {
runOnJS(setScale)(currentScale);
},
[scale.value]
);

return (
<StyledGestureHandlerRootView ref={ref}>
{children}
Expand All @@ -170,6 +233,27 @@ const ImageViewerContent = (
key={keyExtractor ? keyExtractor(item, index) : index}
item={item}
index={index}
onLoad={(event) => {
if (event.nativeEvent) {
const { width, height } = event.nativeEvent.source;
// Calculate scaled dimensions to fit screen while maintaining aspect ratio
let scaledWidth = width;
let scaledHeight = height;
const screenRatio = SCREEN_WIDTH / SCREEN_HEIGHT;
const imageRatio = width / height;
if (imageRatio > screenRatio) {
// Image is wider than screen ratio
scaledWidth = SCREEN_WIDTH;
scaledHeight = SCREEN_WIDTH / imageRatio;
} else {
// Image is taller than screen ratio
scaledHeight = SCREEN_HEIGHT;
scaledWidth = SCREEN_HEIGHT * imageRatio;
}
imageWidth.value = scaledWidth;
imageHeight.value = scaledHeight;
}
}}
/>
);
})}
Expand Down
2 changes: 2 additions & 0 deletions packages/unstyled/image-viewer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ export interface InterfaceImageViewerContentProps {
renderImages: ({
item,
index,
onLoad,
}: {
item: any;
index: number;
onLoad: (event: any) => void;
}) => React.ReactNode;
/**
* Callback function to extract the key for the images.
Expand Down
Loading