diff --git a/docs/docs/.vitepress/config.mts b/docs/docs/.vitepress/config.mts index 46aeeed..89abd2d 100644 --- a/docs/docs/.vitepress/config.mts +++ b/docs/docs/.vitepress/config.mts @@ -12,7 +12,7 @@ export default defineConfig({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: '2.0.1', items: [ + { text: '2.1.0', items: [ {text: 'Releases', link: 'https://github.com/Glazzes/react-native-zoom-toolkit/releases'}, {text: 'Contributing', link: 'https://github.com/Glazzes/react-native-zoom-toolkit/blob/main/CONTRIBUTING.md'}, ]} diff --git a/docs/docs/components/gallery.md b/docs/docs/components/gallery.md index 5e9db30..d454fa5 100644 --- a/docs/docs/components/gallery.md +++ b/docs/docs/components/gallery.md @@ -199,6 +199,17 @@ For more information see this [Gesture Handler's issue](https://github.com/softw Lets the user drag the current item around as they pinch, it also provides a more accurate pinch gesture calculation to user interaction. +### pinchCenteringMode +| Type | Default | Additional Info | +|------|----------|-----------------| +| `PinchCenteringMode` | `PinchCenteringMode.CLAMP` | see [PinchCenteringMode](#pinchcenteringmode-enum) | + +::: tip Tip +To get the best out of this feature keep `allowPinchPanning` set to `true`. +::: + +Modify the way the pinch gesture reacts to the user interaction. + ### onIndexChange | Type | Default | |------|----------| @@ -213,6 +224,20 @@ Callback triggered when the list scrolls to the next or previous item. Callback triggered when the user taps the current item once, provides additional metadata like index if you need it. +### onVerticalPull +| Type | Default | Additional Info | +|------|----------|-----------------| +| `(translateY: number, released: boolean) => void` | `undefined` | see [worklets](https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/worklets/) | + +::: tip Conditions +- Gallery must be on horizontal mode +- The current item must be at a scale of one. +::: + +Worklet callback triggered as the user drags the component vertically when this one is at a scale of one, it includes metadata like `released` parameter which indicates whether the user stopped pulling. + +This property is useful for instance to animate the background color based on the translateY parameter. + ### onSwipe | Type | Default | |------|---------| @@ -329,6 +354,14 @@ Jump to the item at the given index. - Returns `void` ## Type Definitions +### PinchCenteringMode Enum +Determine the behavior used by the pinch gesture relative to the boundaries of its enclosing component. + +| Property | Description | +|--------------|--------------| +| `CLAMP` | Keeps the pinch gesture clamped to the borders or its enclosing container during the entirity of the gesture, just like seen on Android galleries. | +| `INTERACTION` | Keeps the pinch gesture in sync with user interaction, if the pinch gesture was released in an out bonds position it will animate back to a position within the bondaries of its enclosing container. | + ### ResumableZoomState | Property | Type | Description | |--------------|----------|------------------------------------------| diff --git a/src/components/gallery/Gallery.tsx b/src/components/gallery/Gallery.tsx index 2478f41..430ad55 100644 --- a/src/components/gallery/Gallery.tsx +++ b/src/components/gallery/Gallery.tsx @@ -15,7 +15,11 @@ import { getPanWithPinchStatus } from '../../commons/utils/getPanWithPinchStatus import Reflection from './Reflection'; import GalleryItem from './GalleryItem'; import { GalleryContext } from './context'; -import type { GalleryProps, GalleryType } from './types'; +import { + PinchCenteringMode, + type GalleryProps, + type GalleryType, +} from './types'; type GalleryPropsWithRef = GalleryProps & { reference?: React.ForwardedRef; @@ -32,6 +36,7 @@ const Gallery = (props: GalleryPropsWithRef) => { maxScale: userMaxScale = 6, vertical = false, tapOnEdgeToItem = true, + pinchCenteringMode = PinchCenteringMode.CLAMP, allowPinchPanning: pinchPanning, customTransition, onIndexChange, @@ -42,6 +47,7 @@ const Gallery = (props: GalleryPropsWithRef) => { onPinchStart, onPinchEnd, onSwipe, + onVerticalPull, } = props; const allowPinchPanning = pinchPanning ?? getPanWithPinchStatus(); @@ -93,9 +99,7 @@ const Gallery = (props: GalleryPropsWithRef) => { useAnimatedReaction( () => activeIndex.value, - (value) => { - if (onIndexChange) runOnJS(onIndexChange)(value); - }, + (value) => onIndexChange && runOnJS(onIndexChange)(value), [activeIndex] ); @@ -114,6 +118,7 @@ const Gallery = (props: GalleryPropsWithRef) => { [vertical, activeIndex, rootSize] ); + // Reference handling const setIndex = (index: number) => { const clamped = clamp(index, 0, data.length); activeIndex.value = clamped; @@ -173,12 +178,14 @@ const Gallery = (props: GalleryPropsWithRef) => { vertical={vertical} tapOnEdgeToItem={tapOnEdgeToItem} allowPinchPanning={allowPinchPanning} + pinchCenteringMode={pinchCenteringMode} onTap={onTap} onPanStart={onPanStart} onPanEnd={onPanEnd} onPinchStart={onPinchStart} onPinchEnd={onPinchEnd} onSwipe={onSwipe} + onVerticalPull={onVerticalPull} /> ); diff --git a/src/components/gallery/Reflection.tsx b/src/components/gallery/Reflection.tsx index 7e57320..9c1e5ff 100644 --- a/src/components/gallery/Reflection.tsx +++ b/src/components/gallery/Reflection.tsx @@ -15,21 +15,20 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { clamp } from '../../commons/utils/clamp'; import { pinchTransform } from '../../commons/utils/pinchTransform'; import { useVector } from '../../commons/hooks/useVector'; +import { snapPoint } from '../../commons/utils/snapPoint'; +import getSwipeDirection from '../../commons/utils/getSwipeDirection'; +import { GalleryContext } from './context'; +import { crop } from '../../commons/utils/crop'; +import { usePinchCommons } from '../../commons/hooks/usePinchCommons'; + +import { type GalleryProps, PinchCenteringMode } from './types'; import { PanMode, ScaleMode, SwipeDirection, type BoundsFuction, type PanGestureEvent, - type PanGestureEventCallback, - type PinchGestureEventCallback, - type TapGestureEvent, } from '../../commons/types'; -import { snapPoint } from '../../commons/utils/snapPoint'; -import getSwipeDirection from '../../commons/utils/getSwipeDirection'; -import { GalleryContext } from './context'; -import { crop } from '../../commons/utils/crop'; -import { usePinchCommons } from '../../commons/hooks/usePinchCommons'; const minScale = 1; const config = { duration: 300, easing: Easing.linear }; @@ -41,12 +40,14 @@ type ReflectionProps = { vertical: boolean; tapOnEdgeToItem: boolean; allowPinchPanning: boolean; - onTap?: (e: TapGestureEvent, index: number) => void; - onPanStart?: PanGestureEventCallback; - onPanEnd?: PanGestureEventCallback; - onPinchStart?: PinchGestureEventCallback; - onPinchEnd?: PinchGestureEventCallback; - onSwipe?: (direction: SwipeDirection) => void; + pinchCenteringMode: PinchCenteringMode; + onTap?: GalleryProps['onTap']; + onPanStart?: GalleryProps['onPanStart']; + onPanEnd?: GalleryProps['onPanEnd']; + onPinchStart?: GalleryProps['onPinchStart']; + onPinchEnd?: GalleryProps['onPinchEnd']; + onSwipe?: GalleryProps['onSwipe']; + onVerticalPull?: GalleryProps['onVerticalPull']; }; /* @@ -62,12 +63,14 @@ const Reflection = ({ vertical, tapOnEdgeToItem, allowPinchPanning, + pinchCenteringMode: pinchMode, onTap, onPanStart, onPanEnd, onPinchStart: onUserPinchStart, onPinchEnd: onUserPinchEnd, onSwipe: onUserSwipe, + onVerticalPull, }: ReflectionProps) => { const { activeIndex, @@ -93,6 +96,9 @@ const Reflection = ({ const time = useSharedValue(0); const position = useVector(0, 0); + const isPullingVertical = useSharedValue(false); + const pullReleased = useSharedValue(false); + const boundsFn: BoundsFuction = (scaleValue) => { 'worklet'; @@ -176,21 +182,28 @@ const Reflection = ({ }; useAnimatedReaction( - () => rootSize.width.value, - () => reset(0, 0, minScale, false), - [rootSize] - ); - - useAnimatedReaction( - () => activeIndex.value, - () => reset(0, 0, minScale, false), - [activeIndex] + () => ({ + translate: translate.y.value, + scale: scale.value, + isPulling: isPullingVertical.value, + released: pullReleased.value, + }), + (val) => { + if (!vertical && val.scale === 1 && val.isPulling) { + onVerticalPull?.(val.translate, pullReleased.value); + } + }, + [translate, scale, isPullingVertical, pullReleased] ); useAnimatedReaction( - () => resetIndex.value, + () => ({ + root: rootSize.width.value, + active: activeIndex.value, + reset: resetIndex.value, + }), () => reset(0, 0, minScale, false), - [resetIndex] + [rootSize, activeIndex, resetIndex] ); const { gesturesEnabled, onPinchStart, onPinchUpdate, onPinchEnd } = @@ -208,7 +221,8 @@ const Reflection = ({ delta, allowPinchPanning, scaleMode: ScaleMode.BOUNCE, - panMode: PanMode.CLAMP, + panMode: + pinchMode === PinchCenteringMode.CLAMP ? PanMode.CLAMP : PanMode.FREE, boundFn: boundsFn, userCallbacks: { onPinchStart: onUserPinchStart, @@ -236,6 +250,11 @@ const Reflection = ({ position.x.value = e.absoluteX; position.y.value = e.absoluteY; + const isVerticalPan = Math.abs(e.velocityY) > Math.abs(e.velocityX); + if (isVerticalPan && scale.value === 1 && !vertical) { + isPullingVertical.value = true; + } + cancelAnimation(translate.x); cancelAnimation(translate.y); cancelAnimation(detectorTranslate.x); @@ -245,6 +264,11 @@ const Reflection = ({ offset.y.value = translate.y.value; }) .onUpdate(({ translationX, translationY }) => { + if (isPullingVertical.value) { + translate.y.value = translationY; + return; + } + const toX = offset.x.value + translationX; const toY = offset.y.value + translationY; @@ -276,6 +300,17 @@ const Reflection = ({ detectorTranslate.y.value = clamp(toY, -1 * boundY, boundY); }) .onEnd((e) => { + if (isPullingVertical.value) { + pullReleased.value = true; + translate.y.value = withTiming(0, undefined, (finished) => { + if (finished) { + isPullingVertical.value = false; + pullReleased.value = false; + } + }); + return; + } + const boundaries = boundsFn(scale.value); const direction = getSwipeDirection(e, { boundaries, @@ -286,16 +321,12 @@ const Reflection = ({ if (direction !== undefined) { onSwipe(direction); - if (onUserSwipe !== undefined) runOnJS(onUserSwipe)(direction); - + onUserSwipe && runOnJS(onUserSwipe)(direction); return; } - if (onPanEnd !== undefined) { - runOnJS(onPanEnd)(e); - } - onScrollEnd(e); + onPanEnd && runOnJS(onPanEnd)(e); const clampX: [number, number] = [-1 * boundaries.x, boundaries.x]; const clampY: [number, number] = [-1 * boundaries.y, boundaries.y]; @@ -417,6 +448,8 @@ export default React.memo(Reflection, (prev, next) => { prev.length === next.length && prev.vertical === next.vertical && prev.tapOnEdgeToItem === next.tapOnEdgeToItem && - prev.allowPinchPanning === next.allowPinchPanning + prev.allowPinchPanning === next.allowPinchPanning && + prev.pinchCenteringMode === next.pinchCenteringMode && + prev.onVerticalPull === next.onVerticalPull ); }); diff --git a/src/components/gallery/types.ts b/src/components/gallery/types.ts index a6d2650..d7ea9e9 100644 --- a/src/components/gallery/types.ts +++ b/src/components/gallery/types.ts @@ -8,6 +8,11 @@ import type { } from '../../commons/types'; import type { ResumableZoomState } from '../resumable/types'; +export enum PinchCenteringMode { + CLAMP, + INTERACTION, +} + export type GalleryTransitionState = { index: number; activeIndex: number; @@ -31,11 +36,13 @@ export type GalleryProps = { vertical?: boolean; tapOnEdgeToItem?: boolean; 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; + onVerticalPull?: (translateY: number, released: boolean) => void; } & PinchGestureCallbacks & PanGestureCallbacks;