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()
}
}