From c3caf4826e05623eeadd19c52402d6bf52494197 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 16 Dec 2022 16:32:54 -0600 Subject: Add zooming to the lightbox --- src/view/com/lightbox/Images.tsx | 16 +- src/view/com/lightbox/Lightbox.tsx | 28 ++- src/view/com/util/gestures/Swipe.tsx | 232 ---------------------- src/view/com/util/gestures/SwipeAndZoom.tsx | 291 ++++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 239 deletions(-) delete mode 100644 src/view/com/util/gestures/Swipe.tsx create mode 100644 src/view/com/util/gestures/SwipeAndZoom.tsx (limited to 'src') diff --git a/src/view/com/lightbox/Images.tsx b/src/view/com/lightbox/Images.tsx index 6f84dfe7c..7179f0887 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 f6c89b69b..36c51764f 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/Swipe.tsx deleted file mode 100644 index f6d600d02..000000000 --- a/src/view/com/util/gestures/Swipe.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React, {useState} from 'react' -import { - Animated, - GestureResponderEvent, - I18nManager, - PanResponder, - PanResponderGestureState, - useWindowDimensions, - View, -} from 'react-native' -import {clamp} from 'lodash' - -export enum Dir { - None, - Up, - Down, - Left, - Right, -} - -interface Props { - panX: Animated.Value - panY: Animated.Value - canSwipeLeft?: boolean - canSwipeRight?: boolean - canSwipeUp?: boolean - canSwipeDown?: boolean - swipeEnabled?: boolean - hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture - horzDistThresholdDivisor?: number - vertDistThresholdDivisor?: number - useNativeDriver?: boolean - onSwipeStart?: () => void - onSwipeStartDirection?: (dir: Dir) => void - onSwipeEnd?: (dir: Dir) => void - children: React.ReactNode -} - -export function Swipe({ - panX, - panY, - canSwipeLeft = false, - canSwipeRight = false, - canSwipeUp = false, - canSwipeDown = false, - swipeEnabled = true, - hasPriority = false, - horzDistThresholdDivisor = 1.75, - vertDistThresholdDivisor = 1.75, - useNativeDriver = false, - onSwipeStart, - onSwipeStartDirection, - onSwipeEnd, - children, -}: Props) { - const winDim = useWindowDimensions() - const [dir, setDir] = useState(Dir.None) - - const swipeVelocityThreshold = 35 - const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor - const swipeVertDistanceThreshold = winDim.height / vertDistThresholdDivisor - - const isMovingHorizontally = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - return ( - Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) && - Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25) - ) - } - const isMovingVertically = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - return ( - Math.abs(gestureState.dy) > Math.abs(gestureState.dx * 1.25) && - Math.abs(gestureState.vy) > Math.abs(gestureState.vx * 1.25) - ) - } - - const canDir = (d: Dir) => { - if (d === Dir.Left) return canSwipeLeft - if (d === Dir.Right) return canSwipeRight - if (d === Dir.Up) return canSwipeUp - if (d === Dir.Down) return canSwipeDown - return false - } - const isHorz = (d: Dir) => d === Dir.Left || d === Dir.Right - const isVert = (d: Dir) => d === Dir.Up || d === Dir.Down - - const canMoveScreen = ( - event: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - if (swipeEnabled === false) { - return false - } - - 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 startGesture = () => { - setDir(Dir.None) - onSwipeStart?.() - - 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) - } - - const respondToGesture = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx - const dy = gestureState.dy - - let newDir = Dir.None - if (dir === Dir.None) { - // establish if the user is swiping horz or vert - if (Math.abs(dx) > Math.abs(dy)) { - newDir = dx > 0 ? Dir.Left : Dir.Right - } else { - newDir = dy > 0 ? Dir.Up : Dir.Down - } - } else if (isHorz(dir)) { - // direction update - newDir = dx > 0 ? Dir.Left : Dir.Right - } else if (isVert(dir)) { - // direction update - newDir = dy > 0 ? Dir.Up : Dir.Down - } - - if (isHorz(newDir)) { - panX.setValue( - clamp( - dx / swipeHorzDistanceThreshold, - canSwipeRight ? -1 : 0, - canSwipeLeft ? 1 : 0, - ) * -1, - ) - panY.setValue(0) - } else if (isVert(newDir)) { - panY.setValue( - clamp( - dy / swipeVertDistanceThreshold, - canSwipeDown ? -1 : 0, - canSwipeUp ? 1 : 0, - ) * -1, - ) - panX.setValue(0) - } - - if (!canDir(newDir)) { - newDir = Dir.None - } - if (newDir !== dir) { - setDir(newDir) - onSwipeStartDirection?.(newDir) - } - } - - const finishGesture = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - const finish = (finalDir: dir) => () => { - if (finalDir !== Dir.None) { - onSwipeEnd?.(finalDir) - } - setDir(Dir.None) - panX.flattenOffset() - panX.setValue(0) - panY.flattenOffset() - panY.setValue(0) - } - if ( - isHorz(dir) && - (Math.abs(gestureState.dx) > swipeHorzDistanceThreshold / 4 || - Math.abs(gestureState.vx) > swipeVelocityThreshold) - ) { - Animated.timing(panX, { - toValue: dir === Dir.Left ? -1 : 1, - duration: 100, - useNativeDriver, - }).start(finish(dir)) - } else if ( - isVert(dir) && - (Math.abs(gestureState.dy) > swipeVertDistanceThreshold / 8 || - Math.abs(gestureState.vy) > swipeVelocityThreshold) - ) { - Animated.timing(panY, { - toValue: dir === Dir.Up ? -1 : 1, - duration: 100, - useNativeDriver, - }).start(finish(dir)) - } else { - onSwipeEnd?.(Dir.None) - Animated.timing(panX, { - toValue: 0, - duration: 100, - useNativeDriver, - }).start(finish(Dir.None)) - } - } - - const panResponder = PanResponder.create({ - onMoveShouldSetPanResponder: canMoveScreen, - onPanResponderGrant: startGesture, - onPanResponderMove: respondToGesture, - onPanResponderTerminate: finishGesture, - onPanResponderRelease: finishGesture, - onPanResponderTerminationRequest: () => !hasPriority, - }) - - return ( - - {children} - - ) -} diff --git a/src/view/com/util/gestures/SwipeAndZoom.tsx b/src/view/com/util/gestures/SwipeAndZoom.tsx new file mode 100644 index 000000000..dc3a9f54c --- /dev/null +++ b/src/view/com/util/gestures/SwipeAndZoom.tsx @@ -0,0 +1,291 @@ +import React, {useState} from 'react' +import { + Animated, + GestureResponderEvent, + I18nManager, + PanResponder, + PanResponderGestureState, + useWindowDimensions, + View, +} from 'react-native' +import {clamp} from 'lodash' + +export enum Dir { + None, + Up, + 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 + useNativeDriver?: boolean + onSwipeStart?: () => void + onSwipeStartDirection?: (dir: Dir) => void + onSwipeEnd?: (dir: Dir) => void + children: React.ReactNode +} + +export function SwipeAndZoom({ + panX, + panY, + zoom, + canSwipeLeft = false, + canSwipeRight = false, + canSwipeUp = false, + canSwipeDown = false, + swipeEnabled = false, + zoomEnabled = false, + hasPriority = false, + horzDistThresholdDivisor = 1.75, + vertDistThresholdDivisor = 1.75, + useNativeDriver = false, + onSwipeStart, + onSwipeStartDirection, + onSwipeEnd, + children, +}: Props) { + const winDim = useWindowDimensions() + const [dir, setDir] = useState(Dir.None) + const [initialDistance, setInitialDistance] = useState( + undefined, + ) + + const swipeVelocityThreshold = 35 + const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor + const swipeVertDistanceThreshold = winDim.height / vertDistThresholdDivisor + + const isMovingHorizontally = ( + _: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { + return ( + Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) && + Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25) + ) + } + const isMovingVertically = ( + _: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { + return ( + Math.abs(gestureState.dy) > Math.abs(gestureState.dx * 1.25) && + Math.abs(gestureState.vy) > Math.abs(gestureState.vx * 1.25) + ) + } + + const canDir = (d: Dir) => { + if (d === Dir.Left) return canSwipeLeft + 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 + const isVert = (d: Dir) => d === Dir.Up || d === Dir.Down + + const canMoveScreen = ( + event: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { + 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 + } + 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 = ( + e: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { + const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx + const dy = gestureState.dy + + let newDir = Dir.None + if (dir === Dir.None) { + // 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 + } + } else if (isHorz(dir)) { + // direction update + newDir = dx > 0 ? Dir.Left : Dir.Right + } else if (isVert(dir)) { + // direction update + newDir = dy > 0 ? Dir.Up : Dir.Down + } else { + newDir = dir + } + + 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, + canSwipeRight ? -1 : 0, + canSwipeLeft ? 1 : 0, + ) * -1, + ) + panY.setValue(0) + } else if (isVert(newDir)) { + // swipe up/down + panY.setValue( + clamp( + dy / swipeVertDistanceThreshold, + canSwipeDown ? -1 : 0, + canSwipeUp ? 1 : 0, + ) * -1, + ) + panX.setValue(0) + } + + if (!canDir(newDir)) { + newDir = Dir.None + } + if (newDir !== dir) { + setDir(newDir) + onSwipeStartDirection?.(newDir) + } + } + + const finishGesture = ( + _: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => { + const finish = (finalDir: Dir) => () => { + if (finalDir !== Dir.None) { + onSwipeEnd?.(finalDir) + } + setDir(Dir.None) + panX.flattenOffset() + panX.setValue(0) + panY.flattenOffset() + panY.setValue(0) + } + if ( + isHorz(dir) && + (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, + useNativeDriver, + }).start(finish(dir)) + } else if ( + isVert(dir) && + (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() + Animated.timing(panY, { + toValue: 0, + duration: 100, + useNativeDriver, + }).start() + Animated.timing(zoom, { + toValue: 0, + duration: 100, + useNativeDriver, + }).start() + } + } + + const panResponder = PanResponder.create({ + onMoveShouldSetPanResponder: canMoveScreen, + onPanResponderGrant: startGesture, + onPanResponderMove: respondToGesture, + onPanResponderTerminate: finishGesture, + onPanResponderRelease: finishGesture, + onPanResponderTerminationRequest: () => !hasPriority, + }) + + return ( + + {children} + + ) +} -- cgit 1.4.1