From 99aadbbf511e549a937ac364ff2e3270d8c9f85d Mon Sep 17 00:00:00 2001 From: Santiago Date: Thu, 17 Oct 2024 15:39:17 -0500 Subject: [PATCH 01/18] docs: fix ResumableZoom example --- docs/docs/components/resumablezoom.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/docs/components/resumablezoom.md b/docs/docs/components/resumablezoom.md index d7c2f74..0c569f9 100644 --- a/docs/docs/components/resumablezoom.md +++ b/docs/docs/components/resumablezoom.md @@ -31,7 +31,7 @@ This component is best utilized when at least one of the two dimensions of the w ```jsx import React from 'react'; -import { Image, View, useWindowDimensions } from 'react-native'; +import { Image, useWindowDimensions } from 'react-native'; import { ResumableZoom, getAspectRatioSize, @@ -57,11 +57,9 @@ const App = () => { }); return ( - - - - - + + + ); }; From 389ed6afa24d9114d0c716afc8daad150c893494 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 22 Nov 2024 11:57:19 -0500 Subject: [PATCH 02/18] feat(Snapback): add scrollRef property --- src/components/snapback/SnapbackZoom.tsx | 52 ++++++++++++++---------- src/components/snapback/types.ts | 7 ++++ 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/components/snapback/SnapbackZoom.tsx b/src/components/snapback/SnapbackZoom.tsx index 8f44a0f..4f8f994 100644 --- a/src/components/snapback/SnapbackZoom.tsx +++ b/src/components/snapback/SnapbackZoom.tsx @@ -25,6 +25,7 @@ const SnapbackZoom: React.FC = ({ resizeConfig, timingConfig, gesturesEnabled = true, + scrollRef, onTap, onDoubleTap, onPinchStart, @@ -47,6 +48,18 @@ const SnapbackZoom: React.FC = ({ resizeConfig?.size.height ?? 0 ); + const measureContainer = () => { + 'worklet'; + + const measuremet = measure(containerRef); + if (measuremet !== null) { + containerSize.width.value = measuremet.width; + containerSize.height.value = measuremet.height; + position.x.value = measuremet.pageX; + position.y.value = measuremet.pageY; + } + }; + const childrenSize = useDerivedValue(() => { return resizeToAspectRatio({ resizeConfig, @@ -86,15 +99,8 @@ const SnapbackZoom: React.FC = ({ currentFocal.y.value = (one.absoluteY + two.absoluteY) / 2; }) .onStart((e) => { - onPinchStart && runOnJS(onPinchStart)(e); - - const measuremet = measure(containerRef); - if (measuremet !== null) { - containerSize.width.value = measuremet.width; - containerSize.height.value = measuremet.height; - position.x.value = measuremet.pageX; - position.y.value = measuremet.pageY; - } + measureContainer(); + onPinchStart && onPinchStart(e); initialFocal.x.value = currentFocal.x.value; initialFocal.y.value = currentFocal.y.value; @@ -103,6 +109,8 @@ const SnapbackZoom: React.FC = ({ origin.y.value = e.focalY - containerSize.height.value / 2; }) .onUpdate((e) => { + measureContainer(); + const deltaX = currentFocal.x.value - initialFocal.x.value; const deltaY = currentFocal.y.value - initialFocal.y.value; @@ -123,6 +131,10 @@ const SnapbackZoom: React.FC = ({ }); }); + if (scrollRef !== undefined) { + pinch.blocksExternalGesture(scrollRef); + } + const tap = Gesture.Tap() .withTestId('tap') .enabled(gesturesEnabled) @@ -139,17 +151,15 @@ const SnapbackZoom: React.FC = ({ .runOnJS(true) .onEnd((e) => onDoubleTap?.(e)); - const containerStyle = useAnimatedStyle(() => { - const width = containerSize.width.value; - const height = containerSize.height.value; - - return { - width: width === 0 ? undefined : width, - height: height === 0 ? undefined : height, + const containerStyle = useAnimatedStyle( + () => ({ + width: resizeConfig?.size.width, + height: resizeConfig?.size.height, justifyContent: 'center', alignItems: 'center', - }; - }, [containerSize]); + }), + [resizeConfig] + ); const childStyle = useAnimatedStyle(() => { const { width, height, deltaX, deltaY } = childrenSize.value; @@ -169,10 +179,8 @@ const SnapbackZoom: React.FC = ({ return ( - - - {children} - + + {children} ); diff --git a/src/components/snapback/types.ts b/src/components/snapback/types.ts index 17b8be1..271ff6b 100644 --- a/src/components/snapback/types.ts +++ b/src/components/snapback/types.ts @@ -4,6 +4,7 @@ import type { EasingFunctionFactory, ReduceMotion, } from 'react-native-reanimated'; +import type { GestureType } from 'react-native-gesture-handler'; import type { HitSlop } from 'react-native-gesture-handler/lib/typescript/handlers/gestureHandlerCommon'; import type { @@ -13,6 +14,11 @@ import type { TapGestureCallbacks, } from '../../commons/types'; +export type BlocksGesture = + | GestureType + | React.RefObject + | React.RefObject | undefined>; + export type TimingConfig = Partial<{ duration: number; easing: EasingFunction | EasingFunctionFactory; @@ -39,6 +45,7 @@ export type SnapBackZoomProps = { children: React.ReactNode } & Partial<{ onUpdate: (e: SnapbackZoomState) => void; hitSlop: HitSlop; timingConfig: TimingConfig; + scrollRef: BlocksGesture; }> & PinchGestureCallbacks & TapGestureCallbacks; From 21e001c206387ad6190c27556040e3a7d5407648 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 22 Nov 2024 13:21:05 -0500 Subject: [PATCH 03/18] docs(Snapback): document scrollRef property --- docs/docs/components/snapbackzoom.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/docs/components/snapbackzoom.md b/docs/docs/components/snapbackzoom.md index aa0bacf..334a573 100644 --- a/docs/docs/components/snapbackzoom.md +++ b/docs/docs/components/snapbackzoom.md @@ -1,5 +1,5 @@ --- -title: Snapback Zoom +title: SnapbackZoom description: An ideal zoom component for preview handling outline: deep --- @@ -161,6 +161,28 @@ if you need to mirror the current state of the gesture to some other component, Callback triggered once the snap back animation has finished. +### scrollRef + +| Type | Default | +| ------------------------------------------ | ----------- | +| `React.RefObject>` | `undefined` | + +Improve gesture detection when SnapbackZoom is rendered within a vertical ScrollView, see the following example. + +```tsx +const scrollViewRef = useRef(null); + + + {images.map((uri) => { + return ( + + + + ); + })} +; +``` + ## About resizeConfig Property Before you start reading, for a visual reference watch the video above and pay attention to the parrot image. From 8cdaa050bf2695b11a60c6519149a952f162ef4c Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 22 Nov 2024 13:27:57 -0500 Subject: [PATCH 04/18] fix(Resumable): update pinch and double tap implmentation --- src/commons/hooks/useDoubleTapCommons.ts | 3 +- src/commons/hooks/usePinchCommons.ts | 4 +-- src/components/resumable/ResumableZoom.tsx | 38 +++++++++++++--------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/commons/hooks/useDoubleTapCommons.ts b/src/commons/hooks/useDoubleTapCommons.ts index 4a2fa5e..236c4fc 100644 --- a/src/commons/hooks/useDoubleTapCommons.ts +++ b/src/commons/hooks/useDoubleTapCommons.ts @@ -31,10 +31,9 @@ export const useDoubleTapCommons = ({ boundsFn, onGestureEnd, }: DoubleTapOptions) => { - const onDoubleTapEnd = (e: TapGestureEvent) => { + const onDoubleTapEnd = (event: TapGestureEvent) => { 'worklet'; - const event = { ...e, x: e.x / scale.value, y: e.y / scale.value }; const originX = event.x - container.width.value / 2; const originY = event.y - container.height.value / 2; const toScale = diff --git a/src/commons/hooks/usePinchCommons.ts b/src/commons/hooks/usePinchCommons.ts index b8968c9..3537ccb 100644 --- a/src/commons/hooks/usePinchCommons.ts +++ b/src/commons/hooks/usePinchCommons.ts @@ -102,8 +102,8 @@ export const usePinchCommons = (options: PinchOptions) => { initialFocal.x.value = currentFocal.x.value; initialFocal.y.value = currentFocal.y.value; - origin.x.value = e.focalX / scale.value - container.width.value / 2; - origin.y.value = e.focalY / scale.value - container.height.value / 2; + origin.x.value = e.focalX - container.width.value / 2; + origin.y.value = e.focalY - container.height.value / 2; offset.x.value = translate.x.value; offset.y.value = translate.y.value; diff --git a/src/components/resumable/ResumableZoom.tsx b/src/components/resumable/ResumableZoom.tsx index 2fbb0cc..51d61d7 100644 --- a/src/components/resumable/ResumableZoom.tsx +++ b/src/components/resumable/ResumableZoom.tsx @@ -17,10 +17,12 @@ import { usePanCommons } from '../../commons/hooks/usePanCommons'; import { usePinchCommons } from '../../commons/hooks/usePinchCommons'; import { useDoubleTapCommons } from '../../commons/hooks/useDoubleTapCommons'; import withResumableValidation from '../../commons/hoc/withResumableValidation'; +import { getVisibleRect as getRect } from '../../commons/utils/getVisibleRect'; import type { BoundsFuction, CommonZoomState, + Rect, Vector, } from '../../commons/types'; import type { @@ -217,21 +219,15 @@ const ResumableZoom: React.FC = (props) => { const detectorStyle = useAnimatedStyle(() => { return { - width: extendedSize.width.value * scaleOffset.value, - height: extendedSize.height.value * scaleOffset.value, + width: extendedSize.width.value, + height: extendedSize.height.value, transform: [ { translateX: translate.x.value }, { translateY: translate.y.value }, + { scale: scale.value }, ], }; - }, [extendedSize, scaleOffset, translate]); - - const childStyle = useAnimatedStyle( - () => ({ - transform: [{ scale: scale.value }], - }), - [scale] - ); + }, [extendedSize, translate, scale]); const requestState = (): CommonZoomState => { return { @@ -282,11 +278,27 @@ const ResumableZoom: React.FC = (props) => { set(toX, toY, toScale, true); }; + const getVisibleRect = (): Rect => { + return getRect({ + scale: scale.value, + itemSize: { + width: childSize.width.value, + height: childSize.height.value, + }, + containerSize: { + width: rootSize.width.value, + height: rootSize.height.value, + }, + translation: { x: translate.x.value, y: translate.y.value }, + }); + }; + useImperativeHandle(reference, () => ({ reset: (animate = true) => set(0, 0, minScale, animate), requestState: requestState, assignState: assignState, zoom: zoom, + getVisibleRect: getVisibleRect, })); const composedTap = Gesture.Exclusive(doubleTap, tap); @@ -296,11 +308,7 @@ const ResumableZoom: React.FC = (props) => { - + {children} From 0a1b2638ab897d87d0ddc7d7990212d3c0b54c05 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 22 Nov 2024 13:31:29 -0500 Subject: [PATCH 05/18] feat(Resuamble): add getVisibleRect method to ref --- src/components/resumable/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/resumable/types.ts b/src/components/resumable/types.ts index c419086..fbdb77b 100644 --- a/src/components/resumable/types.ts +++ b/src/components/resumable/types.ts @@ -7,6 +7,7 @@ import type { PanGestureCallbacks, PinchCenteringMode, PinchGestureCallbacks, + Rect, SizeVector, SwipeDirection, TapGestureCallbacks, @@ -40,4 +41,5 @@ export type ResumableZoomType = { requestState: () => CommonZoomState; assignState: (state: ResumableZoomAssignableState, animate?: boolean) => void; zoom: (accScale: number, xy?: Vector) => void; + getVisibleRect: () => Rect; }; From 407649bdfe5fc1044e3a02b1bd42ac579aefa7f1 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 22 Nov 2024 14:12:03 -0500 Subject: [PATCH 06/18] feat: replace getAspectRatioSize with fitContainer --- src/index.ts | 2 +- src/utils/fitContainer.ts | 18 +++++++++++++++++ src/utils/getAspectRatioSize.ts | 35 --------------------------------- 3 files changed, 19 insertions(+), 36 deletions(-) create mode 100644 src/utils/fitContainer.ts delete mode 100644 src/utils/getAspectRatioSize.ts diff --git a/src/index.ts b/src/index.ts index b0f0c2f..1e89c11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,5 +37,5 @@ export { export { useTransformationState } from './hooks/useTransformationState'; -export { getAspectRatioSize } from './utils/getAspectRatioSize'; +export { fitContainer } from './utils/fitContainer'; export { stackTransition } from './commons/misc/stacktransition'; diff --git a/src/utils/fitContainer.ts b/src/utils/fitContainer.ts new file mode 100644 index 0000000..cf52c87 --- /dev/null +++ b/src/utils/fitContainer.ts @@ -0,0 +1,18 @@ +import type { SizeVector } from '../commons/types'; + +export const fitContainer = ( + aspectRatio: number, + container: SizeVector +): SizeVector => { + 'worklet'; + + let width = container.width; + let height = container.width / aspectRatio; + + if (height > container.height) { + width = container.height * aspectRatio; + height = container.height; + } + + return { width, height }; +}; diff --git a/src/utils/getAspectRatioSize.ts b/src/utils/getAspectRatioSize.ts deleted file mode 100644 index 59150eb..0000000 --- a/src/utils/getAspectRatioSize.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { SizeVector } from '../commons/types'; - -type Options = { - aspectRatio: number; - width?: number; - height?: number; -}; - -/** - * @description Gets width and height based on the aspect ratio. - * @param options An object describing the aspect ratio of an image/video, and optional width and height - * parameters, calculates height if this is undefined based on width and vice versa. - * @returns An object containing the computed width and height by the aspect ratio. - */ -export const getAspectRatioSize = (options: Options): SizeVector => { - 'worklet'; - const { aspectRatio, width, height } = options; - if (width === undefined && height === undefined) { - throw Error( - 'maxWidth and maxHeight parameters are undefined, provide at least one of them' - ); - } - - if (width !== undefined) { - return { - width: Math.floor(width), - height: Math.floor(width / aspectRatio), - }; - } - - return { - width: Math.floor(height! * aspectRatio), - height: Math.floor(height!), - }; -}; From 9e5002e4b1f6b6e72e3d5c229219fbc9e42eb0cd Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 22 Nov 2024 14:15:23 -0500 Subject: [PATCH 07/18] docs: update docs according to the last changes --- docs/docs/.vitepress/config.mts | 2 +- docs/docs/components/resumablezoom.md | 24 +++++++----- docs/docs/utilities/fitContainer.md | 27 +++++++++++++ docs/docs/utilities/getAspectRatioSize.md | 31 --------------- docs/docs/utilities/useimageresolution.md | 46 ++++++++++------------- 5 files changed, 62 insertions(+), 68 deletions(-) create mode 100644 docs/docs/utilities/fitContainer.md delete mode 100644 docs/docs/utilities/getAspectRatioSize.md diff --git a/docs/docs/.vitepress/config.mts b/docs/docs/.vitepress/config.mts index 8f19a7f..7d3a49e 100644 --- a/docs/docs/.vitepress/config.mts +++ b/docs/docs/.vitepress/config.mts @@ -46,12 +46,12 @@ export default defineConfig({ text: 'Utilities', collapsed: true, items: [ + { text: 'fitContainer', link: '/utilities/fitContainer' }, { text: 'useImageResolution', link: '/utilities/useimageresolution' }, { text: 'useTransformationState', link: '/utilities/usetransformationstate', }, - { text: 'getAspectRatioSize', link: '/utilities/getAspectRatioSize' }, ], }, { diff --git a/docs/docs/components/resumablezoom.md b/docs/docs/components/resumablezoom.md index 0c569f9..e3d3c32 100644 --- a/docs/docs/components/resumablezoom.md +++ b/docs/docs/components/resumablezoom.md @@ -33,8 +33,8 @@ This component is best utilized when at least one of the two dimensions of the w import React from 'react'; import { Image, useWindowDimensions } from 'react-native'; import { + fitContainer, ResumableZoom, - getAspectRatioSize, useImageResolution, } from 'react-native-zoom-toolkit'; @@ -42,23 +42,20 @@ const uri = 'https://assets-global.website-files.com/63634f4a7b868a399577cf37/64665685a870fadf4bb171c2_labrador%20americano.jpg'; const App = () => { - const { width } = useWindowDimensions(); - - // Gets the resolution of your image + const { width, height } = useWindowDimensions(); const { isFetching, resolution } = useImageResolution({ uri }); if (isFetching || resolution === undefined) { return null; } - // An utility function to get the size without compromising the aspect ratio - const imageSize = getAspectRatioSize({ - aspectRatio: resolution.width / resolution.height, - width: width, + const size = fitContainer(resolution.width / resolution.height, { + width, + height, }); return ( - + ); }; @@ -318,12 +315,19 @@ Programmatically zoom in or out to a xy position within the child component. | multiplier | `number` | Value to multiply the current scale for, values greater than one zoom in and values less than one zoom out. | | xy | `Vector \| undefined` | Position of the point to zoom in or out starting from the top left corner of your component, leaving this value as undefined will be infered as zooming in or out from the center of the child component's current visible area. | +### getVisibleRect + +Get the coordinates of the current visible rectangle within ResumableZoom's frame. + +- type definition: `() => Rect` +- return type: `{x: number, y: number, width: number, height: number}` + ### requestState Request internal transformation values of this component at the moment of the calling. - type definition: `() => CommonZoomState` -- return type: [CommonZoomState](#commonzoomstate) +- return type: [CommonZoomState\](#commonzoomstate) ### assignState diff --git a/docs/docs/utilities/fitContainer.md b/docs/docs/utilities/fitContainer.md new file mode 100644 index 0000000..b1f535c --- /dev/null +++ b/docs/docs/utilities/fitContainer.md @@ -0,0 +1,27 @@ +--- +title: fitContainer +description: Get width and height of an element to fit a container +outline: deep +--- + +# fitContainer + +Get the width and height for an element based on its aspect ratio and the container it's meant to fit. + +## Type Definition + +| Name | Type | Description | +| ----------- | -------------------- | ----------------------------------- | +| aspectRatio | `number` | Aspect ratio of the element to fit. | +| container | `SizeVector` | Width and height of the container. | + +## How to use + +```js +const container = useWindowDimensions(); +const resolution = { width: 1920, height: 1080 }; +const size = fitContainer(resolution.width / resolution.height, { + width: container.width, + height: container.height, +}); +``` diff --git a/docs/docs/utilities/getAspectRatioSize.md b/docs/docs/utilities/getAspectRatioSize.md deleted file mode 100644 index 42490ab..0000000 --- a/docs/docs/utilities/getAspectRatioSize.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: getAspectRatioSize -description: Gets width and height based on the aspect ratio -outline: deep ---- - -# getAspectRatioSize -Gets width and height based on the aspect ratio. - -## How to use -Let's assume you've got a HD image and you want to render this image with a max width of 200px, however you -don't want to compromise its aspect ratio. - -```js -import { getAspectRatioSize } from 'react-native-zoom-toolkit'; - -const hdResolution = { width: 1920, height: 1080 }; - -// width is 200 and height is 112.5 -const { width, height } = getAspectRatioSize({ - aspectRatio: hdResolution.width / hdResolution.height, - width: 200 -}); - -// Alternatively if you want to render your image with a height of 200 -// use height property, width is 355 here. -const { width, height } = getAspectRatioSize({ - aspectRatio: hdResolution.width / hdResolution.height, - height: 200 -}); -``` diff --git a/docs/docs/utilities/useimageresolution.md b/docs/docs/utilities/useimageresolution.md index 25a5bab..5b3e55b 100644 --- a/docs/docs/utilities/useimageresolution.md +++ b/docs/docs/utilities/useimageresolution.md @@ -5,45 +5,39 @@ outline: deep --- # useImageResolution hook -Get the resolution of a bundle or network image. + +Get the resolution of a network, bundle or base64 image. ### How to use + ```jsx import { useImageResolution } from 'react-native-zoom-toolkit'; -// Get resolution of a bundle image +// Network image +const { isFetching, resolution, error } = useImageResolution({ + uri: 'url to some network image', + headers: { + Authorization: 'some bearer token', + }, +}); + +// Bundle image const { isFetching, resolution, error } = useImageResolution( require('path to your bundle image asset') ); -// Get resolution of a network image +// Base64 image const { isFetching, resolution, error } = useImageResolution({ - uri: 'url to some network image', - headers: { - 'Authorization': 'some bearer token', - } -}) - + uri: 'your base64 string', +}); ``` -- parameter information - -| Property | Type |Description | -|----------|------|------------| -| `source` | `Source \| number` | An url pointing to a network image and headers or a require statement to a bundle image asset. | - -- returns [FetchImageResolutionResult](#fetchimageresolutionresult) ## Type Definitions -### Source -| Property | Type |Description | -|----------|------|------------| -| `uri` | `string` | An url pointing to a network image. | -| `headers` | `Record \| undefined` | Optional headers, in case you are accesing network protected images. | ### FetchImageResolutionResult -| Property | Type |Description | -|----------|------|------------| -| `isFetching` | `boolean` | Whether the hook is fetching or not. | -| `resolution` | `SizeVector \| undefined` | Width and height of the image. | -| `error` | `Error \| undefined` | An error in case the image fetching fails. | +| Property | Type | Description | +| ------------ | ------------------------- | ------------------------------------------ | +| `isFetching` | `boolean` | Whether the hook is fetching or not. | +| `resolution` | `SizeVector \| undefined` | Width and height of the image. | +| `error` | `Error \| undefined` | An error in case the image fetching fails. | From 631b05ac9b19ba85d458871812622734fd64ead5 Mon Sep 17 00:00:00 2001 From: Santiago Date: Thu, 28 Nov 2024 14:45:50 -0500 Subject: [PATCH 08/18] refactor: improve readability of some utility functions --- src/commons/utils/crop.ts | 54 ++++++++++++------------ src/commons/utils/getCropRotatedSize.ts | 27 +++++++++--- src/commons/utils/getSwipeDirection.ts | 22 ++++------ src/commons/utils/getVisibleRect.ts | 54 +++++++++--------------- src/commons/utils/resizeToAspectRatio.ts | 7 +-- 5 files changed, 78 insertions(+), 86 deletions(-) diff --git a/src/commons/utils/crop.ts b/src/commons/utils/crop.ts index 422c116..cad4998 100644 --- a/src/commons/utils/crop.ts +++ b/src/commons/utils/crop.ts @@ -1,46 +1,49 @@ import { getVisibleRect } from './getVisibleRect'; import type { SizeVector, Vector } from '../types'; -import type { CropContextResult } from '../../components/crop/types'; type CanvasToSizeOptions = { - context: CropContextResult['context']; + scale: number; cropSize: SizeVector; - canvas: SizeVector; + itemSize: SizeVector; resolution: SizeVector; - offset: Vector; - scale: number; + translation: Vector; + isRotated: boolean; fixedWidth?: number; }; -const flipVector = (vector: SizeVector): SizeVector => { - return { - width: vector.height, - height: vector.width, - }; -}; - -export const crop = (options: CanvasToSizeOptions): CropContextResult => { +export const crop = (options: CanvasToSizeOptions) => { 'worklet'; - const { cropSize, canvas, resolution, offset, scale, fixedWidth, context } = - options; - - const isFlipped = context.rotationAngle % 180 !== 0; - const actualCanvasSize = isFlipped ? flipVector(canvas) : canvas; - const actualResolution = isFlipped ? flipVector(resolution) : resolution; + const { + cropSize, + itemSize, + resolution, + translation, + scale, + isRotated, + fixedWidth, + } = options; - let resize: SizeVector | undefined; - const { x, y, width, height } = getVisibleRect({ + const rect = getVisibleRect({ scale, - visibleSize: cropSize, - canvasSize: actualCanvasSize, - elementSize: actualResolution, - offset, + containerSize: cropSize, + itemSize: { + width: isRotated ? itemSize.height : itemSize.width, + height: isRotated ? itemSize.width : itemSize.height, + }, + translation, }); + const relativeScale = resolution.width / itemSize.width; + const x = rect.x * relativeScale; + const y = rect.y * relativeScale; + const width = rect.width * relativeScale; + const height = rect.height * relativeScale; + // Make a normal crop, if the fixedWidth is defined just resize everything to meet the ratio // between fixedWidth and the width of the crop. let sizeModifier = 1; + let resize: SizeVector | undefined; if (fixedWidth !== undefined) { sizeModifier = fixedWidth / width; resize = { @@ -56,7 +59,6 @@ export const crop = (options: CanvasToSizeOptions): CropContextResult => { width: Math.round(width * sizeModifier), height: Math.round(height * sizeModifier), }, - context, resize, }; }; diff --git a/src/commons/utils/getCropRotatedSize.ts b/src/commons/utils/getCropRotatedSize.ts index e19b43e..96178ea 100644 --- a/src/commons/utils/getCropRotatedSize.ts +++ b/src/commons/utils/getCropRotatedSize.ts @@ -1,5 +1,4 @@ import type { SizeVector } from '../types'; -import { getAspectRatioSize } from '../../utils/getAspectRatioSize'; type Options = { crop: SizeVector; @@ -7,6 +6,25 @@ type Options = { angle: number; }; +export const getRatioSize = ( + aspectRatio: number, + container: Partial> +): SizeVector => { + 'worklet'; + + if (container.width !== undefined) { + return { + width: container.width, + height: container.width / aspectRatio, + }; + } + + return { + width: container.height! * aspectRatio, + height: container.height!, + }; +}; + export const getCropRotatedSize = (options: Options): SizeVector => { 'worklet'; const { crop, angle, resolution } = options; @@ -17,8 +35,8 @@ export const getCropRotatedSize = (options: Options): SizeVector => { const aspectRatio = resolution.width / resolution.height; const inverseAspectRatio = resolution.height / resolution.width; - base = getAspectRatioSize({ - aspectRatio: flipped ? aspectRatio : inverseAspectRatio, + const currentAspectRatio = flipped ? aspectRatio : inverseAspectRatio; + base = getRatioSize(currentAspectRatio, { width: cropAspectRatio >= 1 ? undefined : crop.width, height: cropAspectRatio >= 1 ? crop.height : undefined, }); @@ -37,8 +55,7 @@ export const getCropRotatedSize = (options: Options): SizeVector => { Math.abs(base.height * Math.cos(angle)) + Math.abs(base.width * Math.sin(angle)); - return getAspectRatioSize({ - aspectRatio: aspectRatio, + return getRatioSize(aspectRatio, { width: aspectRatio >= 1 ? undefined : maxWidth, height: aspectRatio >= 1 ? maxHeight : undefined, }); diff --git a/src/commons/utils/getSwipeDirection.ts b/src/commons/utils/getSwipeDirection.ts index 9535086..a359965 100644 --- a/src/commons/utils/getSwipeDirection.ts +++ b/src/commons/utils/getSwipeDirection.ts @@ -20,38 +20,32 @@ export const getSwipeDirection = ( const { time, boundaries, position, translate } = options; const deltaTime = performance.now() - time; - const deltaX = Math.abs(position.x - e.absoluteX); - const deltaY = Math.abs(position.y - e.absoluteY); const { x: boundX, y: boundY } = boundaries; + const swipedDistanceX = Math.abs(position.x - e.absoluteX) >= SWIPE_DISTANCE; + const swipedDistanceY = Math.abs(position.y - e.absoluteY) >= SWIPE_DISTANCE; + const swipedInTime = deltaTime <= SWIPE_TIME; + const swipeRight = - e.velocityX >= SWIPE_VELOCITY && - deltaX >= SWIPE_DISTANCE && - deltaTime <= SWIPE_TIME; + e.velocityX >= SWIPE_VELOCITY && swipedDistanceX && swipedInTime; const inRightBound = translate.x === boundX; if (swipeRight && inRightBound) return 'right'; const swipeLeft = - e.velocityX <= -1 * SWIPE_VELOCITY && - deltaX >= SWIPE_DISTANCE && - deltaTime <= SWIPE_TIME; + e.velocityX <= -1 * SWIPE_VELOCITY && swipedDistanceX && swipedInTime; const inLeftBound = translate.x === -1 * boundX; if (swipeLeft && inLeftBound) return 'left'; const swipeUp = - e.velocityY <= -1 * SWIPE_VELOCITY && - deltaY >= SWIPE_DISTANCE && - deltaTime <= SWIPE_TIME; + e.velocityY <= -1 * SWIPE_VELOCITY && swipedDistanceY && swipedInTime; const inUpperBound = translate.y === -1 * boundY; if (swipeUp && inUpperBound) return 'up'; const swipeDown = - e.velocityY >= SWIPE_VELOCITY && - deltaY >= SWIPE_DISTANCE && - deltaTime <= SWIPE_TIME; + e.velocityY >= SWIPE_VELOCITY && swipedDistanceY && swipedInTime; const inLowerBound = translate.y === boundY; if (swipeDown && inLowerBound) return 'down'; diff --git a/src/commons/utils/getVisibleRect.ts b/src/commons/utils/getVisibleRect.ts index 21bcb91..fd00674 100644 --- a/src/commons/utils/getVisibleRect.ts +++ b/src/commons/utils/getVisibleRect.ts @@ -1,51 +1,35 @@ -import { interpolate } from 'react-native-reanimated'; -import type { SizeVector, Vector } from '../types'; - -type Rect = { - x: number; - y: number; - width: number; - height: number; -}; +import type { SizeVector, Vector, Rect } from '../types'; type Options = { scale: number; - canvasSize: SizeVector; // Element size on screen - visibleSize: SizeVector; // Expected visible area - elementSize: SizeVector; // Real dimensions, eg Resolution - offset: Vector; // Translation values + translation: Vector; // cartesian system values with the y axis flipped + itemSize: SizeVector; // Size of the wrapped component + containerSize: SizeVector; // Size of zoom component }; export const getVisibleRect = (options: Options): Rect => { 'worklet'; - const { scale, canvasSize, visibleSize, elementSize, offset } = options; - - const boundX = (canvasSize.width * scale - visibleSize.width) / 2; - const boundY = (canvasSize.height * scale - visibleSize.height) / 2; + const { scale, translation, itemSize, containerSize } = options; - const normalizedOffsetX = Math.abs(boundX) + offset.x; - const normalizedOffsetY = Math.abs(boundY) + offset.y; + const offsetX = (itemSize.width * scale - containerSize.width) / 2; + const offsetY = (itemSize.height * scale - containerSize.height) / 2; + const clampedX = Math.max(offsetX, 0); + const clampedY = Math.max(offsetY, 0); - const relativeWidth = visibleSize.width / (canvasSize.width * scale); - const relativeHeight = visibleSize.height / (canvasSize.height * scale); + const reducerX = (-1 * translation.x + clampedX) / (itemSize.width * scale); + const reducerY = (-1 * translation.y + clampedY) / (itemSize.height * scale); - const relativeX = interpolate( - normalizedOffsetX, - [0, 2 * boundX], - [1 - relativeWidth, 0] - ); + const x = itemSize.width * reducerX; + const y = itemSize.height * reducerY; - const relativeY = interpolate( - normalizedOffsetY, - [0, 2 * boundY], - [1 - relativeHeight, 0] - ); + const width = + itemSize.width * + Math.min(1, containerSize.width / (itemSize.width * scale)); - const x = elementSize.width * relativeX; - const y = elementSize.height * relativeY; - const width = elementSize.width * relativeWidth; - const height = elementSize.height * relativeHeight; + const height = + itemSize.height * + Math.min(1, containerSize.height / (itemSize.height * scale)); return { x, y, width, height }; }; diff --git a/src/commons/utils/resizeToAspectRatio.ts b/src/commons/utils/resizeToAspectRatio.ts index 65c092d..2b4b326 100644 --- a/src/commons/utils/resizeToAspectRatio.ts +++ b/src/commons/utils/resizeToAspectRatio.ts @@ -51,10 +51,5 @@ export const resizeToAspectRatio = ({ const deltaX = (finalWidth - width) / 2; const deltaY = (finalHeight - height) / 2; - return { - width: finalWidth, - height: finalHeight, - deltaX, - deltaY, - }; + return { width: finalWidth, height: finalHeight, deltaX, deltaY }; }; From 1fb6afd6c0bc07a3dae95d640ce2d70e5538e999 Mon Sep 17 00:00:00 2001 From: Santiago Date: Thu, 28 Nov 2024 17:19:42 -0500 Subject: [PATCH 09/18] fix(Crop): fix gesture detection --- src/components/crop/CropZoom.tsx | 55 ++++++++++++++++---------------- src/components/crop/types.ts | 4 +-- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/components/crop/CropZoom.tsx b/src/components/crop/CropZoom.tsx index 532bbb6..ccc8688 100644 --- a/src/components/crop/CropZoom.tsx +++ b/src/components/crop/CropZoom.tsx @@ -7,11 +7,7 @@ import Animated, { useSharedValue, withTiming, } from 'react-native-reanimated'; -import { - Gesture, - GestureDetector, - GestureHandlerRootView, -} from 'react-native-gesture-handler'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { crop } from '../../commons/utils/crop'; import { useSizeVector } from '../../commons/hooks/useSizeVector'; @@ -191,15 +187,16 @@ const CropZoom: React.FC = (props) => { const detectorStyle = useAnimatedStyle(() => { return { - width: detector.width.value * scaleOffset.value, - height: detector.height.value * scaleOffset.value, + width: detector.width.value, + height: detector.height.value, position: 'absolute', transform: [ { translateX: translate.x.value }, { translateY: translate.y.value }, + { scale: scale.value }, ], }; - }, [detector, scaleOffset, translate]); + }, [detector, translate, scale]); const childStyle = useAnimatedStyle(() => { return { @@ -274,19 +271,30 @@ const CropZoom: React.FC = (props) => { }; const handleCrop = (fixedWidth?: number): CropContextResult => { - return crop({ + const context: CropContextResult['context'] = { + rotationAngle: rotation.value * RAD2DEG, + flipHorizontal: rotate.y.value === Math.PI, + flipVertical: rotate.x.value === Math.PI, + }; + + const result = crop({ + scale: scale.value, cropSize: cropSize, resolution: resolution, - canvas: { width: container.width.value, height: container.height.value }, - offset: { x: translate.x.value, y: translate.y.value }, - scale: scale.value, - context: { - rotationAngle: rotation.value * RAD2DEG, - flipHorizontal: rotate.y.value === Math.PI, - flipVertical: rotate.x.value === Math.PI, + itemSize: { + width: container.width.value, + height: container.height.value, }, + translation: { x: translate.x.value, y: translate.y.value }, + isRotated: context.rotationAngle % 180 !== 0, fixedWidth, }); + + return { + crop: result.crop, + resize: result.resize, + context, + }; }; const handleRequestState = (): CropZoomState => ({ @@ -351,22 +359,15 @@ const CropZoom: React.FC = (props) => { minHeight: cropSize.height, }; - const cropStyle: ViewStyle = { - width: cropSize.width, - height: cropSize.height, - }; - return ( - - - {children} - {OverlayComponent?.()} - + + {children} + {OverlayComponent?.()} - + ); }; diff --git a/src/components/crop/types.ts b/src/components/crop/types.ts index 14034fe..7e96d3e 100644 --- a/src/components/crop/types.ts +++ b/src/components/crop/types.ts @@ -67,7 +67,7 @@ export type CropZoomProps = React.PropsWithChildren<{ onUpdate?: CropGestureEventCallBack; OverlayComponent?: () => React.ReactElement; }> & + CommonResumableProps & PanGestureCallbacks & PinchGestureCallbacks & - Omit & - CommonResumableProps; + Omit; From 580146fd1f1a878baf5825dc0cc31eb27e46a7c3 Mon Sep 17 00:00:00 2001 From: Santiago Date: Thu, 28 Nov 2024 17:28:52 -0500 Subject: [PATCH 10/18] fix(Gallery): fix gesture detection --- src/commons/misc/stacktransition.ts | 4 +- src/commons/types.ts | 7 ++ src/commons/utils/getScrollPosition.ts | 12 +++ src/components/gallery/Gallery.tsx | 55 +++++++----- src/components/gallery/GalleryItem.tsx | 21 +++-- src/components/gallery/GalleryProvider.tsx | 2 - src/components/gallery/Reflection.tsx | 97 +++++++++++++++------- src/components/gallery/context.ts | 1 - src/components/gallery/types.ts | 46 +++++----- 9 files changed, 160 insertions(+), 85 deletions(-) create mode 100644 src/commons/utils/getScrollPosition.ts diff --git a/src/commons/misc/stacktransition.ts b/src/commons/misc/stacktransition.ts index 30ddc09..8c48a4d 100644 --- a/src/commons/misc/stacktransition.ts +++ b/src/commons/misc/stacktransition.ts @@ -4,10 +4,10 @@ import type { GalleryTransitionCallback } from '../../components/gallery/types'; export const stackTransition: GalleryTransitionCallback = (options) => { 'worklet'; - const { index, activeIndex, vertical, gallerySize, scroll, isScrolling } = + const { index, activeIndex, direction, gallerySize, scroll, isScrolling } = options; - if (vertical) { + if (direction === 'vertical') { const translateY = index * gallerySize.height - scroll; return { transform: [{ translateY }] }; } diff --git a/src/commons/types.ts b/src/commons/types.ts index 18df7fd..0bd08fa 100644 --- a/src/commons/types.ts +++ b/src/commons/types.ts @@ -5,6 +5,13 @@ import type { PanGestureHandlerEventPayload, } from 'react-native-gesture-handler'; +export type Rect = { + x: number; + y: number; + width: number; + height: number; +}; + export type Vector = { x: T; y: T; diff --git a/src/commons/utils/getScrollPosition.ts b/src/commons/utils/getScrollPosition.ts new file mode 100644 index 0000000..65e93d4 --- /dev/null +++ b/src/commons/utils/getScrollPosition.ts @@ -0,0 +1,12 @@ +type ScrollOptions = { + index: number; + itemSize: number; + gap: number; +}; + +export const getScrollPosition = (options: ScrollOptions): number => { + 'worklet'; + + const { index, itemSize, gap } = options; + return index * itemSize + index * gap; +}; diff --git a/src/components/gallery/Gallery.tsx b/src/components/gallery/Gallery.tsx index e195703..b5bdf42 100644 --- a/src/components/gallery/Gallery.tsx +++ b/src/components/gallery/Gallery.tsx @@ -10,6 +10,7 @@ import Animated, { import { clamp } from '../../commons/utils/clamp'; import { getMaxScale } from '../../commons/utils/getMaxScale'; +import { getScrollPosition } from '../../commons/utils/getScrollPosition'; import Reflection from './Reflection'; import GalleryItem from './GalleryItem'; @@ -30,6 +31,7 @@ const Gallery = (props: GalleryPropsWithRef) => { windowSize = 5, maxScale: userMaxScale = 6, vertical = false, + gap = 0, allowOverflow = false, tapOnEdgeToItem = true, zoomEnabled = true, @@ -54,7 +56,6 @@ const Gallery = (props: GalleryPropsWithRef) => { const { activeIndex, - fetchIndex, rootSize, rootChildSize, scroll, @@ -89,11 +90,15 @@ const Gallery = (props: GalleryPropsWithRef) => { const measureRoot = (e: LayoutChangeEvent) => { const { width, height } = e.nativeEvent.layout; - rootSize.width.value = width; - rootSize.height.value = height; - - const direction = vertical ? height : width; - scroll.value = activeIndex.value * direction; + const scrollPosition = getScrollPosition({ + index: activeIndex.get(), + itemSize: vertical ? height : width, + gap, + }); + + rootSize.width.set(width); + rootSize.height.set(height); + scroll.set(scrollPosition); }; const animatedStyles = useAnimatedStyle( @@ -126,24 +131,24 @@ const Gallery = (props: GalleryPropsWithRef) => { useAnimatedReaction( () => activeIndex.value, - (value) => onIndexChange && runOnJS(onIndexChange)(value), + (value) => { + onIndexChange && runOnJS(onIndexChange)(value); + runOnJS(setScrollIndex)(value); + }, [activeIndex] ); - useAnimatedReaction( - () => fetchIndex.value, - (value) => runOnJS(setScrollIndex)(value), - [fetchIndex] - ); - useAnimatedReaction( () => ({ vertical, size: { width: rootSize.width.value, height: rootSize.height.value }, }), (value) => { - const direction = value.vertical ? value.size.height : value.size.width; - scroll.value = activeIndex.value * direction; + scroll.value = getScrollPosition({ + index: activeIndex.value, + itemSize: value.vertical ? value.size.height : value.size.width, + gap, + }); }, [vertical, rootSize] ); @@ -166,16 +171,20 @@ const Gallery = (props: GalleryPropsWithRef) => { const setIndex = (index: number) => { const clamped = clamp(index, 0, data.length - 1); activeIndex.value = clamped; - fetchIndex.value = clamped; - scroll.value = clamped * itemSize.value; + + scroll.value = getScrollPosition({ + index: activeIndex.get(), + itemSize: itemSize.get(), + gap, + }); }; const requestState = () => ({ - width: rootChildSize.width.value, - height: rootChildSize.height.value, - translateX: translate.x.value, - translateY: translate.y.value, - scale: scale.value, + width: rootChildSize.width.get(), + height: rootChildSize.height.get(), + translateX: translate.x.get(), + translateY: translate.y.get(), + scale: scale.get(), }); const reset = (animate = true) => { @@ -208,6 +217,7 @@ const Gallery = (props: GalleryPropsWithRef) => { key={key} zIndex={data.length - index} index={index} + gap={gap} item={item} vertical={vertical} renderItem={renderItem} @@ -217,6 +227,7 @@ const Gallery = (props: GalleryPropsWithRef) => { })} React.ReactElement; @@ -22,6 +24,7 @@ type GalleryItemProps = { const GalleryItem = ({ index, + gap, zIndex, item, vertical, @@ -77,13 +80,15 @@ const GalleryItem = ({ [overflow, activeIndex, index, hideAdjacentItems] ); + // @ts-ignore const transitionStyle = useAnimatedStyle(() => { if (customTransition !== undefined) { return customTransition({ index, + gap, activeIndex: activeIndex.value, isScrolling: isScrolling.value, - vertical, + direction: vertical ? 'vertical' : 'horizontal', scroll: scroll.value, gallerySize: { width: rootSize.width.value, @@ -92,16 +97,18 @@ const GalleryItem = ({ }); } - const sizeNotDefined = + const currentScroll = -1 * scroll.value + index * gap; + + const isSizeNotDefined = rootSize.width.value === 0 && rootSize.height.value === 0; - const opacity = sizeNotDefined && index !== activeIndex.value ? 0 : 1; + const opacity = isSizeNotDefined && index !== activeIndex.value ? 0 : 1; if (vertical) { - const translateY = index * rootSize.height.value - scroll.value; + const translateY = index * rootSize.height.value + currentScroll; return { transform: [{ translateY }], opacity }; } - const translateX = index * rootSize.width.value - scroll.value; + const translateX = index * rootSize.width.value + currentScroll; return { transform: [{ translateX }], opacity }; }); @@ -138,6 +145,7 @@ const GalleryItem = ({ return ( @@ -150,6 +158,7 @@ const GalleryItem = ({ export default React.memo(GalleryItem, (prev, next) => { return ( prev.index === next.index && + prev.gap === next.gap && prev.zIndex === next.zIndex && prev.vertical === next.vertical && prev.customTransition === next.customTransition && diff --git a/src/components/gallery/GalleryProvider.tsx b/src/components/gallery/GalleryProvider.tsx index a87165d..e270ba0 100644 --- a/src/components/gallery/GalleryProvider.tsx +++ b/src/components/gallery/GalleryProvider.tsx @@ -15,7 +15,6 @@ const GalleryProvider = ( ) => { const startIndex = clamp(props.initialIndex ?? 0, 0, props.data.length - 1); const activeIndex = useSharedValue(startIndex); - const fetchIndex = useSharedValue(startIndex); const rootSize = useSizeVector(0, 0); const rootChildSize = useSizeVector(0, 0); @@ -38,7 +37,6 @@ const GalleryProvider = ( scrollOffset, translate, activeIndex, - fetchIndex, isScrolling, scale, hasZoomed, diff --git a/src/components/gallery/Reflection.tsx b/src/components/gallery/Reflection.tsx index 449dd0a..dd6ba9a 100644 --- a/src/components/gallery/Reflection.tsx +++ b/src/components/gallery/Reflection.tsx @@ -22,20 +22,22 @@ import { useDoubleTapCommons } from '../../commons/hooks/useDoubleTapCommons'; import { getSwipeDirection } from '../../commons/utils/getSwipeDirection'; import type { - PinchCenteringMode, - ScaleMode, SwipeDirection, BoundsFuction, PanGestureEvent, + ScaleMode, + PinchCenteringMode, } from '../../commons/types'; import { GalleryContext } from './context'; import { type GalleryProps } from './types'; +import { getScrollPosition } from '../../commons/utils/getScrollPosition'; const minScale = 1; const config = { duration: 300, easing: Easing.linear }; type ReflectionProps = { length: number; + gap: number; maxScale: SharedValue; itemSize: Readonly>; vertical: boolean; @@ -62,6 +64,7 @@ type ReflectionProps = { */ const Reflection = ({ length, + gap, maxScale, itemSize, vertical, @@ -82,7 +85,6 @@ const Reflection = ({ }: ReflectionProps) => { const { activeIndex, - fetchIndex, scroll, scrollOffset, isScrolling, @@ -134,19 +136,34 @@ const Reflection = ({ const snapToScrollPosition = (e: PanGestureEvent) => { 'worklet'; - const index = activeIndex.value; - const prev = itemSize.value * clamp(index - 1, 0, length - 1); - const current = itemSize.value * index; - const next = itemSize.value * clamp(index + 1, 0, length - 1); + + cancelAnimation(scroll); + + const prev = getScrollPosition({ + index: clamp(activeIndex.value - 1, 0, length - 1), + itemSize: itemSize.value, + gap, + }); + const current = getScrollPosition({ + index: activeIndex.value, + itemSize: itemSize.value, + gap, + }); + const next = getScrollPosition({ + index: clamp(activeIndex.value + 1, 0, length - 1), + itemSize: itemSize.value, + gap, + }); const velocity = vertical ? e.velocityY : e.velocityX; const toScroll = snapPoint(scroll.value, velocity, [prev, current, next]); - if (toScroll !== current) - fetchIndex.value = index + (toScroll === next ? 1 : -1); + scroll.value = withTiming(toScroll, config, (finished) => { + if (!finished) return; + if (toScroll !== current) { + activeIndex.value += toScroll === next ? 1 : -1; + } - scroll.value = withTiming(toScroll, config, () => { - activeIndex.value = fetchIndex.value; isScrolling.value = false; toScroll !== current && reset(0, 0, minScale, false); }); @@ -155,6 +172,8 @@ const Reflection = ({ const onSwipe = (direction: SwipeDirection) => { 'worklet'; + cancelAnimation(scroll); + let toIndex = activeIndex.value; if (direction === 'up' && vertical) toIndex += 1; if (direction === 'down' && vertical) toIndex -= 1; @@ -162,10 +181,19 @@ const Reflection = ({ if (direction === 'right' && !vertical) toIndex -= 1; toIndex = clamp(toIndex, 0, length - 1); - if (toIndex === activeIndex.value) return; + if (toIndex === activeIndex.value) { + return; + } + + const newScrollPosition = getScrollPosition({ + index: toIndex, + itemSize: itemSize.value, + gap, + }); + + scroll.value = withTiming(newScrollPosition, config, (finished) => { + if (!finished) return; - fetchIndex.value = toIndex; - scroll.value = withTiming(toIndex * itemSize.value, config, () => { activeIndex.value = toIndex; isScrolling.value = false; reset(0, 0, minScale, false); @@ -193,7 +221,7 @@ const Reflection = ({ [rootSize] ); - const onGestueEndWrapper = () => { + const onGestureEndWrapper = () => { overflow.value = 'hidden'; hideAdjacentItems.value = false; onGestureEnd?.(); @@ -220,7 +248,7 @@ const Reflection = ({ userCallbacks: { onPinchStart: onUserPinchStart, onPinchEnd: onUserPinchEnd, - onGestureEnd: onGestueEndWrapper, + onGestureEnd: onGestureEndWrapper, }, }); @@ -253,11 +281,13 @@ const Reflection = ({ const pan = Gesture.Pan() .withTestId('pan') .maxPointers(1) + .minVelocity(100) .enabled(gesturesEnabled) .onStart((e) => { onPanStart && runOnJS(onPanStart)(e); cancelAnimation(translate.x); cancelAnimation(translate.y); + cancelAnimation(scroll); const isVerticalPan = Math.abs(e.velocityY) > Math.abs(e.velocityX); isPullingVertical.value = isVerticalPan && scale.value === 1 && !vertical; @@ -287,7 +317,9 @@ const Reflection = ({ const scrollX = -1 * Math.sign(toX) * exceedX; const scrollY = -1 * Math.sign(toY) * exceedY; const to = scrollOffset.value + (vertical ? scrollY : scrollX); - scroll.value = clamp(to, 0, (length - 1) * itemSize.value); + + const items = length - 1; + scroll.value = clamp(to, 0, items * itemSize.value + items * gap); translate.x.value = clamp(toX, -1 * boundX, boundX); translate.y.value = clamp(toY, -1 * boundY, boundY); @@ -345,9 +377,7 @@ const Reflection = ({ .enabled(gesturesEnabled) .numberOfTaps(1) .maxDuration(250) - .onEnd((e) => { - const event = { ...e, x: e.x / scale.value, y: e.y / scale.value }; - + .onEnd((event) => { const gallerySize = { width: rootSize.width.value, height: rootSize.height.value, @@ -355,10 +385,9 @@ const Reflection = ({ const { x, width } = getVisibleRect({ scale: scale.value, - visibleSize: gallerySize, - canvasSize: gallerySize, - elementSize: gallerySize, - offset: { x: translate.x.value, y: translate.y.value }, + containerSize: gallerySize, + itemSize: gallerySize, + translation: { x: translate.x.value, y: translate.y.value }, }); const tapEdge = 44 / scale.value; @@ -370,15 +399,20 @@ const Reflection = ({ if (event.x <= leftEdge && canGoToItem) toIndex -= 1; if (event.x >= rightEdge && canGoToItem) toIndex += 1; + toIndex = clamp(toIndex, 0, length - 1); if (toIndex === activeIndex.value) { onTap && runOnJS(onTap)(event, activeIndex.value); return; } - toIndex = clamp(toIndex, 0, length - 1); - scroll.value = toIndex * itemSize.value; + const toScroll = getScrollPosition({ + index: toIndex, + itemSize: itemSize.value, + gap, + }); + + scroll.value = toScroll; activeIndex.value = toIndex; - fetchIndex.value = toIndex; reset(0, 0, minScale, false); }); @@ -395,16 +429,17 @@ const Reflection = ({ const height = Math.max(rootSize.height.value, rootChildSize.height.value); return { - width: width * scaleOffset.value, - height: height * scaleOffset.value, + width: width, + height: height, position: 'absolute', - zIndex: Number.MAX_SAFE_INTEGER, + zIndex: 2_147_483_647, transform: [ { translateX: translate.x.value }, { translateY: translate.y.value }, + { scale: scale.value }, ], }; - }, [rootSize, rootChildSize, translate, scaleOffset]); + }, [rootSize, rootChildSize, translate, scale]); const composed = Gesture.Race(pan, pinch, Gesture.Exclusive(doubleTap, tap)); diff --git a/src/components/gallery/context.ts b/src/components/gallery/context.ts index b7653b3..b1e916b 100644 --- a/src/components/gallery/context.ts +++ b/src/components/gallery/context.ts @@ -10,7 +10,6 @@ export type GalleryContextType = { scroll: SharedValue; scrollOffset: SharedValue; activeIndex: SharedValue; - fetchIndex: SharedValue; isScrolling: SharedValue; hasZoomed: SharedValue; overflow: SharedValue<'hidden' | 'visible'>; diff --git a/src/components/gallery/types.ts b/src/components/gallery/types.ts index 838868b..e4c7c66 100644 --- a/src/components/gallery/types.ts +++ b/src/components/gallery/types.ts @@ -14,7 +14,8 @@ import type { export type GalleryTransitionState = { index: number; activeIndex: number; - vertical: boolean; + gap: number; + direction: 'vertical' | 'horizontal'; isScrolling: boolean; scroll: number; gallerySize: SizeVector; @@ -27,26 +28,29 @@ export type GalleryTransitionCallback = ( export type GalleryProps = { data: T[]; renderItem: (item: T, index: number) => React.ReactElement; - keyExtractor?: (item: T, index: number) => string; - maxScale?: number | SizeVector[]; - initialIndex?: number; - windowSize?: number; - vertical?: boolean; - allowOverflow?: boolean; - tapOnEdgeToItem?: boolean; - zoomEnabled?: boolean; - scaleMode?: ScaleMode; - allowPinchPanning?: boolean; - pinchCenteringMode?: PinchCenteringMode; - customTransition?: GalleryTransitionCallback; - onTap?: (e: TapGestureEvent, index: number) => void; - onSwipe?: (direction: SwipeDirection) => void; - onIndexChange?: (index: number) => void; - onScroll?: (scroll: number, contentOffset: number) => void; - onUpdate?: (state: CommonZoomState) => void; - onVerticalPull?: (translateY: number, released: boolean) => void; - onGestureEnd?: () => void; -} & PinchGestureCallbacks & +} & Partial<{ + keyExtractor: (item: T, index: number) => string; + maxScale: number | SizeVector[]; + initialIndex: number; + windowSize: number; + gap: number; + vertical: boolean; + allowOverflow: boolean; + tapOnEdgeToItem: boolean; + zoomEnabled: boolean; + scaleMode: ScaleMode; + allowPinchPanning: boolean; + pinchCenteringMode: PinchCenteringMode; + customTransition: GalleryTransitionCallback; + onTap: (e: TapGestureEvent, index: number) => void; + onSwipe: (direction: SwipeDirection) => void; + onIndexChange: (index: number) => void; + onScroll: (scroll: number, contentOffset: number) => void; + onUpdate: (state: CommonZoomState) => void; + onVerticalPull: (translateY: number, released: boolean) => void; + onGestureEnd: () => void; +}> & + PinchGestureCallbacks & PanGestureCallbacks & ZoomEventCallbacks; From 62adebed93c21cd86f1fabb93c11e6d3d02ff525 Mon Sep 17 00:00:00 2001 From: Santiago Date: Thu, 28 Nov 2024 17:43:26 -0500 Subject: [PATCH 11/18] chore: update example app to Expo 52 --- example/app.json | 3 +- example/app/_layout.tsx | 3 +- example/app/snapback.tsx | 2 +- example/package.json | 42 +- .../src/cropzoom/common-example/Controls.tsx | 46 +- example/src/cropzoom/commons/SVGOverlay.tsx | 2 +- example/src/gallery/GalleryExample.tsx | 14 +- example/src/gallery/GalleryImage.tsx | 10 +- example/src/gallery/GalleryVideo.tsx | 10 +- example/src/gallery/utils/utils.ts | 39 - example/src/navigation/Drawer.tsx | 1 + .../src/resumable/ResumableZoomExample.tsx | 19 +- example/src/resumable/ResumableZoomSkia.tsx | 8 +- .../ReflectionContext.tsx => context.tsx} | 4 +- example/src/snapback/index.ts | 1 - .../{SnapbackZoomExample.tsx => index.tsx} | 3 +- example/src/snapback/list/MessageList.tsx | 73 +- .../snapback/list/{ => components}/Appbar.tsx | 7 +- .../list/{ => components}/TextArea.tsx | 2 +- .../{ => list}/messages/CellRenderer.tsx | 16 +- .../{ => list}/messages/ImageMessage.tsx | 73 +- .../{ => list}/messages/VideoMessage.tsx | 8 +- .../src/snapback/reflection/Reflection.tsx | 2 +- yarn.lock | 3190 +++++++++++------ 24 files changed, 2288 insertions(+), 1290 deletions(-) delete mode 100644 example/src/gallery/utils/utils.ts rename example/src/snapback/{reflection/ReflectionContext.tsx => context.tsx} (87%) delete mode 100644 example/src/snapback/index.ts rename example/src/snapback/{SnapbackZoomExample.tsx => index.tsx} (96%) rename example/src/snapback/list/{ => components}/Appbar.tsx (95%) rename example/src/snapback/list/{ => components}/TextArea.tsx (97%) rename example/src/snapback/{ => list}/messages/CellRenderer.tsx (63%) rename example/src/snapback/{ => list}/messages/ImageMessage.tsx (78%) rename example/src/snapback/{ => list}/messages/VideoMessage.tsx (95%) diff --git a/example/app.json b/example/app.json index ea1e869..4808f75 100644 --- a/example/app.json +++ b/example/app.json @@ -4,6 +4,7 @@ "slug": "example", "scheme": "example", "version": "1.0.0", + "newArchEnabled": true, "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -28,6 +29,6 @@ "favicon": "./assets/favicon.png", "bundler": "metro" }, - "plugins": ["expo-router"] + "plugins": ["expo-router", "expo-video"] } } diff --git a/example/app/_layout.tsx b/example/app/_layout.tsx index e4bb623..ffa75c4 100644 --- a/example/app/_layout.tsx +++ b/example/app/_layout.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import { StyleSheet } from 'react-native'; + import { Drawer } from 'expo-router/drawer'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import * as Orientation from 'expo-screen-orientation'; @@ -6,7 +8,6 @@ import * as Orientation from 'expo-screen-orientation'; Orientation.lockAsync(Orientation.OrientationLock.PORTRAIT_UP); import { default as CustomDrawer } from '../src/navigation/Drawer'; -import { StyleSheet } from 'react-native'; const _layout = () => { return ( diff --git a/example/app/snapback.tsx b/example/app/snapback.tsx index 5abc77b..f2294fa 100644 --- a/example/app/snapback.tsx +++ b/example/app/snapback.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { SnapbackZoomExample } from '../src/snapback'; +import SnapbackZoomExample from '../src/snapback'; const snapback = () => { return ; diff --git a/example/package.json b/example/package.json index 2928f81..526cd01 100644 --- a/example/package.json +++ b/example/package.json @@ -9,32 +9,32 @@ "web": "expo start --web" }, "dependencies": { - "@expo/metro-runtime": "~3.2.1", + "@expo/metro-runtime": "~4.0.0", "@react-navigation/drawer": "^6.6.15", - "@shopify/react-native-skia": "1.2.3", - "expo": "^51.0.8", - "expo-av": "~14.0.5", - "expo-constants": "~16.0.1", - "expo-image": "~1.12.9", - "expo-image-manipulator": "~12.0.5", - "expo-linking": "~6.3.1", - "expo-media-library": "~16.0.3", - "expo-router": "~3.5.14", - "expo-screen-orientation": "~7.0.5", - "expo-status-bar": "~1.12.1", + "@shopify/react-native-skia": "1.5.0", + "expo": "^52.0.4", + "expo-av": "~15.0.1", + "expo-constants": "~17.0.2", + "expo-image": "~2.0.0", + "expo-image-manipulator": "~13.0.5", + "expo-linking": "~7.0.2", + "expo-media-library": "~17.0.2", + "expo-router": "~4.0.2", + "expo-screen-orientation": "~8.0.0", + "expo-status-bar": "~2.0.0", "fbemitter": "^3.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-native": "0.74.1", - "react-native-gesture-handler": "~2.16.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-native": "0.76.1", + "react-native-gesture-handler": "~2.20.2", "react-native-image-viewing": "^0.2.2", - "react-native-reanimated": "~3.10.1", - "react-native-safe-area-context": "4.10.1", - "react-native-screens": "3.31.1", - "react-native-web": "~0.19.6" + "react-native-reanimated": "~3.16.1", + "react-native-safe-area-context": "4.12.0", + "react-native-screens": "~4.0.0", + "react-native-web": "~0.19.13" }, "devDependencies": { - "@babel/core": "^7.20.0", + "@babel/core": "^7.25.2", "@expo/webpack-config": "~19.0.1", "@types/fbemitter": "^2.0.35", "babel-loader": "^8.1.0", diff --git a/example/src/cropzoom/common-example/Controls.tsx b/example/src/cropzoom/common-example/Controls.tsx index 7f3a060..af63516 100644 --- a/example/src/cropzoom/common-example/Controls.tsx +++ b/example/src/cropzoom/common-example/Controls.tsx @@ -1,11 +1,13 @@ import React, { useState } from 'react'; import { StyleSheet, View, Pressable, ActivityIndicator } from 'react-native'; -import { FlipType, type Action, manipulateAsync } from 'expo-image-manipulator'; + +import { FlipType, SaveFormat, ImageManipulator } from 'expo-image-manipulator'; import Icon from '@expo/vector-icons/MaterialCommunityIcons'; import type { CropZoomType } from 'react-native-zoom-toolkit'; import { theme } from '../../constants'; import { activeColor, baseColor } from '../commons/contants'; +import { createAlbumAsync, createAssetAsync } from 'expo-media-library'; type ControlProps = { uri: string; @@ -44,32 +46,34 @@ const Controls: React.FC = ({ uri, cropRef, setCrop }) => { } setIsCropping(true); - const cropResult = cropRef.current.crop(200); + const cropContext = cropRef.current.crop(300); + const manipulateContext = ImageManipulator.manipulate(uri); - const actions: Action[] = []; - if (cropResult.resize !== undefined) { - actions.push({ resize: cropResult.resize }); - } + if (cropContext.resize !== undefined) + manipulateContext.resize(cropContext.resize); - if (cropResult.context.flipHorizontal) { - actions.push({ flip: FlipType.Horizontal }); - } + if (cropContext.context.flipHorizontal) + manipulateContext.flip(FlipType.Horizontal); - if (cropResult.context.flipVertical) { - actions.push({ flip: FlipType.Vertical }); - } + if (cropContext.context.flipVertical) + manipulateContext.flip(FlipType.Vertical); - if (cropResult.context.rotationAngle !== 0) { - actions.push({ rotate: cropResult.context.rotationAngle }); - } + if (cropContext.context.rotationAngle !== 0) + manipulateContext.rotate(cropContext.context.rotationAngle); + + manipulateContext.crop(cropContext.crop); + + const imageRef = await manipulateContext.renderAsync(); + const result = await imageRef.saveAsync({ + compress: 1, + format: SaveFormat.PNG, + }); - actions.push({ crop: cropResult.crop }); + const asset = await createAssetAsync(result.uri); + await createAlbumAsync('cropping', asset); - manipulateAsync(uri, actions) - .then((manipulationResult) => { - setCrop(manipulationResult.uri); - }) - .finally(() => setIsCropping(false)); + setCrop(result.uri); + setIsCropping(false); }; return ( diff --git a/example/src/cropzoom/commons/SVGOverlay.tsx b/example/src/cropzoom/commons/SVGOverlay.tsx index 59b3535..e5e0056 100644 --- a/example/src/cropzoom/commons/SVGOverlay.tsx +++ b/example/src/cropzoom/commons/SVGOverlay.tsx @@ -37,7 +37,7 @@ const SVGOverlay: React.FC = ({ cropSize }) => { return ( - + ); }; diff --git a/example/src/gallery/GalleryExample.tsx b/example/src/gallery/GalleryExample.tsx index 48f974d..b784f50 100644 --- a/example/src/gallery/GalleryExample.tsx +++ b/example/src/gallery/GalleryExample.tsx @@ -12,15 +12,12 @@ import { MediaType, type Asset, } from 'expo-media-library'; -import { - stackTransition, - Gallery, - type GalleryType, -} from 'react-native-zoom-toolkit'; +import { Gallery, type GalleryType } from 'react-native-zoom-toolkit'; import GalleryImage from './GalleryImage'; import VideoControls from './controls/VideoControls'; import GalleryVideo from './GalleryVideo'; +import { StatusBar } from 'expo-status-bar'; type SizeVector = { width: number; height: number }; @@ -60,7 +57,6 @@ const GalleryExample = () => { ); const keyExtractor = useCallback((item, index) => `${item.uri}-${index}`, []); - const customTransition = useCallback(stackTransition, []); // Toogle video controls opacity if the current item is a video const onTap = useCallback(() => { @@ -96,7 +92,7 @@ const GalleryExample = () => { if (granted) { const page = await getAssetsAsync({ first: 100, - mediaType: ['photo', 'video'], + mediaType: ['photo'], sortBy: 'creationTime', }); @@ -124,6 +120,7 @@ const GalleryExample = () => { data={assets} keyExtractor={keyExtractor} renderItem={renderItem} + gap={24} maxScale={scales} onIndexChange={(idx) => { activeIndex.value = idx; @@ -131,7 +128,6 @@ const GalleryExample = () => { onTap={onTap} pinchCenteringMode={'sync'} onVerticalPull={onVerticalPulling} - customTransition={customTransition} /> { isSeeking={isSeeking} opacity={opacityControls} /> + + ); }; diff --git a/example/src/gallery/GalleryImage.tsx b/example/src/gallery/GalleryImage.tsx index 6a5d83b..0bc2654 100644 --- a/example/src/gallery/GalleryImage.tsx +++ b/example/src/gallery/GalleryImage.tsx @@ -8,7 +8,7 @@ import { import { Image } from 'expo-image'; import { type Asset } from 'expo-media-library'; -import { calculateItemSize } from './utils/utils'; +import { fitContainer } from 'react-native-zoom-toolkit'; type GalleryImageProps = { asset: Asset; @@ -21,14 +21,10 @@ const GalleryImage: React.FC = ({ index, activeIndex, }) => { - const [downScale, setDownScale] = useState(true); const { width, height } = useWindowDimensions(); + const size = fitContainer(asset.width / asset.height, { width, height }); - const size = calculateItemSize( - { width: asset.width, height: asset.height }, - { width, height }, - width / height - ); + const [downScale, setDownScale] = useState(true); const wrapper = (active: number) => { if (index === active) setDownScale(false); diff --git a/example/src/gallery/GalleryVideo.tsx b/example/src/gallery/GalleryVideo.tsx index 640f574..aff3ccd 100644 --- a/example/src/gallery/GalleryVideo.tsx +++ b/example/src/gallery/GalleryVideo.tsx @@ -4,13 +4,13 @@ import { type SharedValue } from 'react-native-reanimated'; import { ResizeMode, Video, type AVPlaybackStatus } from 'expo-av'; import type { Asset } from 'expo-media-library'; -import { calculateItemSize } from './utils/utils'; import { listenToPauseVideoEvent, listenToPlayVideoEvent, listenToSeekVideoEvent, listenToStopVideoEvent, } from './utils/emitter'; +import { fitContainer } from '../../../src/utils/fitContainer'; type GalleryVideoProps = { asset: Asset; @@ -29,13 +29,7 @@ const GalleryVideo: React.FC = ({ }) => { const videoRef = useRef