about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-12-16 16:32:54 -0600
committerPaul Frazee <pfrazee@gmail.com>2022-12-16 16:32:54 -0600
commitc3caf4826e05623eeadd19c52402d6bf52494197 (patch)
tree86af23df2419d33d70029ed79eb10320bc006063 /src
parentd7e71e079f84b517a4b54799fc66882f450d010d (diff)
downloadvoidsky-c3caf4826e05623eeadd19c52402d6bf52494197.tar.zst
Add zooming to the lightbox
Diffstat (limited to 'src')
-rw-r--r--src/view/com/lightbox/Images.tsx16
-rw-r--r--src/view/com/lightbox/Lightbox.tsx28
-rw-r--r--src/view/com/util/gestures/SwipeAndZoom.tsx (renamed from src/view/com/util/gestures/Swipe.tsx)97
3 files changed, 115 insertions, 26 deletions
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) => (
         <Image
           key={i}
-          style={[styles.image, {left: i * winDim.width}]}
+          style={[
+            styles.image,
+            {left: i * winDim.width},
+            isZooming && i !== index ? {opacity: 0} : undefined,
+          ]}
           source={{uri}}
         />
       ))}
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 = (
       <ImagesLightbox.Component
+        isZooming={isZooming}
         {...(store.shell.activeLightbox as models.ImagesLightbox)}
       />
     )
@@ -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 (
     <View style={StyleSheet.absoluteFill}>
-      <Swipe
+      <SwipeAndZoom
         panX={panX}
         panY={panY}
+        zoom={zoom}
         swipeEnabled
+        zoomEnabled
         canSwipeLeft={store.shell.activeLightbox.canSwipeLeft}
         canSwipeRight={store.shell.activeLightbox.canSwipeRight}
         canSwipeUp
         canSwipeDown
         hasPriority
+        onSwipeStartDirection={onSwipeStartDirection}
         onSwipeEnd={onSwipeEnd}>
         <TouchableWithoutFeedback onPress={onClose}>
           <Animated.View style={[styles.bg, swipeOpacity]} />
@@ -95,7 +113,7 @@ export const Lightbox = observer(function Lightbox() {
           </View>
         </TouchableWithoutFeedback>
         <Animated.View style={swipeTransform}>{element}</Animated.View>
-      </Swipe>
+      </SwipeAndZoom>
     </View>
   )
 })
diff --git a/src/view/com/util/gestures/Swipe.tsx b/src/view/com/util/gestures/SwipeAndZoom.tsx
index f6d600d02..dc3a9f54c 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>(Dir.None)
+  const [initialDistance, setInitialDistance] = useState<number | undefined>(
+    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()
     }
   }