diff --git a/src/view/com/lightbox/Images.tsx b/src/view/com/lightbox/Images.tsx index 6f84dfe7..7179f088 100644 --- a/src/view/com/lightbox/Images.tsx +++ b/src/view/com/lightbox/Images.tsx @@ -1,7 +1,15 @@ import React from 'react' import {Image, StyleSheet, useWindowDimensions, View} from 'react-native' -export function Component({uris, index}: {uris: string[]; index: number}) { +export function Component({ + uris, + index, + isZooming, +}: { + uris: string[] + index: number + isZooming: boolean +}) { const winDim = useWindowDimensions() const left = index * winDim.width * -1 return ( @@ -9,7 +17,11 @@ export function Component({uris, index}: {uris: string[]; index: number}) { {uris.map((uri, i) => ( ))} diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index f6c89b69..36c51764 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useState} from 'react' import { Animated, StyleSheet, @@ -8,7 +8,7 @@ import { } from 'react-native' import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Swipe, Dir} from '../util/gestures/Swipe' +import {SwipeAndZoom, Dir} from '../util/gestures/SwipeAndZoom' import {useStores} from '../../../state' import {useAnimatedValue} from '../../lib/useAnimatedValue' @@ -21,12 +21,17 @@ import * as ImagesLightbox from './Images' export const Lightbox = observer(function Lightbox() { const store = useStores() const winDim = useWindowDimensions() + const [isZooming, setIsZooming] = useState(false) const panX = useAnimatedValue(0) const panY = useAnimatedValue(0) + const zoom = useAnimatedValue(0) const onClose = () => { store.shell.closeLightbox() } + const onSwipeStartDirection = (dir: Dir) => { + setIsZooming(dir === Dir.Zoom) + } const onSwipeEnd = (dir: Dir) => { if (dir === Dir.Up || dir === Dir.Down) { onClose() @@ -57,6 +62,7 @@ export const Lightbox = observer(function Lightbox() { } else if (store.shell.activeLightbox?.name === 'images') { element = ( ) @@ -66,7 +72,16 @@ export const Lightbox = observer(function Lightbox() { const translateX = Animated.multiply(panX, winDim.width * -1) const translateY = Animated.multiply(panY, winDim.height * -1) - const swipeTransform = {transform: [{translateX}, {translateY}]} + const scale = Animated.add(zoom, 1) + const swipeTransform = { + transform: [ + {translateY: winDim.height / 2}, + {scale}, + {translateY: winDim.height / -2}, + {translateX}, + {translateY}, + ], + } const swipeOpacity = { opacity: panY.interpolate({ inputRange: [-1, 0, 1], @@ -76,15 +91,18 @@ export const Lightbox = observer(function Lightbox() { return ( - @@ -95,7 +113,7 @@ export const Lightbox = observer(function Lightbox() { {element} - + ) }) diff --git a/src/view/com/util/gestures/Swipe.tsx b/src/view/com/util/gestures/SwipeAndZoom.tsx similarity index 66% rename from src/view/com/util/gestures/Swipe.tsx rename to src/view/com/util/gestures/SwipeAndZoom.tsx index f6d600d0..dc3a9f54 100644 --- a/src/view/com/util/gestures/Swipe.tsx +++ b/src/view/com/util/gestures/SwipeAndZoom.tsx @@ -16,16 +16,19 @@ export enum Dir { Down, Left, Right, + Zoom, } interface Props { panX: Animated.Value panY: Animated.Value + zoom: Animated.Value canSwipeLeft?: boolean canSwipeRight?: boolean canSwipeUp?: boolean canSwipeDown?: boolean swipeEnabled?: boolean + zoomEnabled?: boolean hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture horzDistThresholdDivisor?: number vertDistThresholdDivisor?: number @@ -36,14 +39,16 @@ interface Props { children: React.ReactNode } -export function Swipe({ +export function SwipeAndZoom({ panX, panY, + zoom, canSwipeLeft = false, canSwipeRight = false, canSwipeUp = false, canSwipeDown = false, - swipeEnabled = true, + swipeEnabled = false, + zoomEnabled = false, hasPriority = false, horzDistThresholdDivisor = 1.75, vertDistThresholdDivisor = 1.75, @@ -55,6 +60,9 @@ export function Swipe({ }: Props) { const winDim = useWindowDimensions() const [dir, setDir] = useState(Dir.None) + const [initialDistance, setInitialDistance] = useState( + undefined, + ) const swipeVelocityThreshold = 35 const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor @@ -84,6 +92,7 @@ export function Swipe({ if (d === Dir.Right) return canSwipeRight if (d === Dir.Up) return canSwipeUp if (d === Dir.Down) return canSwipeDown + if (d === Dir.Zoom) return zoomEnabled return false } const isHorz = (d: Dir) => d === Dir.Left || d === Dir.Right @@ -93,34 +102,40 @@ export function Swipe({ event: GestureResponderEvent, gestureState: PanResponderGestureState, ) => { - if (swipeEnabled === false) { - return false + if (zoomEnabled && gestureState.numberActiveTouches === 2) { + return true + } else if (swipeEnabled && gestureState.numberActiveTouches === 1) { + const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx + const dy = gestureState.dy + const willHandle = + (isMovingHorizontally(event, gestureState) && + ((dx > 0 && canSwipeLeft) || (dx < 0 && canSwipeRight))) || + (isMovingVertically(event, gestureState) && + ((dy > 0 && canSwipeUp) || (dy < 0 && canSwipeDown))) + return willHandle } - - const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx - const dy = gestureState.dy - const willHandle = - (isMovingHorizontally(event, gestureState) && - ((dx > 0 && canSwipeLeft) || (dx < 0 && canSwipeRight))) || - (isMovingVertically(event, gestureState) && - ((dy > 0 && canSwipeUp) || (dy < 0 && canSwipeDown))) - return willHandle + return false } const startGesture = () => { setDir(Dir.None) onSwipeStart?.() + // reset all state panX.stopAnimation() // @ts-expect-error: _value is private, but docs use it as well panX.setOffset(panX._value) panY.stopAnimation() // @ts-expect-error: _value is private, but docs use it as well panY.setOffset(panY._value) + zoom.stopAnimation() + // @ts-expect-error: _value is private, but docs use it as well + zoom.setOffset(zoom._value) + setInitialDistance(undefined) } const respondToGesture = ( - _: GestureResponderEvent, + e: GestureResponderEvent, gestureState: PanResponderGestureState, ) => { const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx @@ -128,8 +143,10 @@ export function Swipe({ let newDir = Dir.None if (dir === Dir.None) { - // establish if the user is swiping horz or vert - if (Math.abs(dx) > Math.abs(dy)) { + // establish if the user is swiping horz or vert, or zooming + if (gestureState.numberActiveTouches === 2) { + newDir = Dir.Zoom + } else if (Math.abs(dx) > Math.abs(dy)) { newDir = dx > 0 ? Dir.Left : Dir.Right } else { newDir = dy > 0 ? Dir.Up : Dir.Down @@ -140,9 +157,37 @@ export function Swipe({ } else if (isVert(dir)) { // direction update newDir = dy > 0 ? Dir.Up : Dir.Down + } else { + newDir = dir } - if (isHorz(newDir)) { + if (newDir === Dir.Zoom) { + if (zoomEnabled) { + if (gestureState.numberActiveTouches === 2) { + // zoom in/out + const x0 = e.nativeEvent.touches[0].pageX + const x1 = e.nativeEvent.touches[1].pageX + const y0 = e.nativeEvent.touches[0].pageY + const y1 = e.nativeEvent.touches[1].pageY + const zoomDx = Math.abs(x0 - x1) + const zoomDy = Math.abs(y0 - y1) + const dist = Math.sqrt(zoomDx * zoomDx + zoomDy * zoomDy) / 100 + if ( + typeof initialDistance === 'undefined' || + dist - initialDistance < 0 + ) { + setInitialDistance(dist) + } else { + zoom.setValue(dist - initialDistance) + } + } else { + // pan around after zooming + panX.setValue(clamp(dx / winDim.width, -1, 1) * -1) + panY.setValue(clamp(dy / winDim.height, -1, 1) * -1) + } + } + } else if (isHorz(newDir)) { + // swipe left/right panX.setValue( clamp( dx / swipeHorzDistanceThreshold, @@ -152,6 +197,7 @@ export function Swipe({ ) panY.setValue(0) } else if (isVert(newDir)) { + // swipe up/down panY.setValue( clamp( dy / swipeVertDistanceThreshold, @@ -175,7 +221,7 @@ export function Swipe({ _: GestureResponderEvent, gestureState: PanResponderGestureState, ) => { - const finish = (finalDir: dir) => () => { + const finish = (finalDir: Dir) => () => { if (finalDir !== Dir.None) { onSwipeEnd?.(finalDir) } @@ -190,6 +236,7 @@ export function Swipe({ (Math.abs(gestureState.dx) > swipeHorzDistanceThreshold / 4 || Math.abs(gestureState.vx) > swipeVelocityThreshold) ) { + // horizontal swipe reset Animated.timing(panX, { toValue: dir === Dir.Left ? -1 : 1, duration: 100, @@ -200,18 +247,30 @@ export function Swipe({ (Math.abs(gestureState.dy) > swipeVertDistanceThreshold / 8 || Math.abs(gestureState.vy) > swipeVelocityThreshold) ) { + // vertical swipe reset Animated.timing(panY, { toValue: dir === Dir.Up ? -1 : 1, duration: 100, useNativeDriver, }).start(finish(dir)) } else { + // zoom (or no direction) reset onSwipeEnd?.(Dir.None) Animated.timing(panX, { toValue: 0, duration: 100, useNativeDriver, - }).start(finish(Dir.None)) + }).start() + Animated.timing(panY, { + toValue: 0, + duration: 100, + useNativeDriver, + }).start() + Animated.timing(zoom, { + toValue: 0, + duration: 100, + useNativeDriver, + }).start() } }