about summary refs log tree commit diff
path: root/src/view/com/lightbox/ImageViewing/hooks/useDoubleTapToZoom.ts
blob: ea81d9f1c491fdcad2c1c9a8a1fa038d471cb799 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
 * Copyright (c) JOB TODAY S.A. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

import React, {useCallback} from 'react'
import {ScrollView, NativeTouchEvent, NativeSyntheticEvent} from 'react-native'

import {Dimensions} from '../@types'

const DOUBLE_TAP_DELAY = 300
const MIN_ZOOM = 2

let lastTapTS: number | null = null

/**
 * This is iOS only.
 * Same functionality for Android implemented inside usePanResponder hook.
 */
function useDoubleTapToZoom(
  scrollViewRef: React.RefObject<ScrollView>,
  scaled: boolean,
  screen: Dimensions,
  imageDimensions: Dimensions | null,
) {
  const handleDoubleTap = useCallback(
    (event: NativeSyntheticEvent<NativeTouchEvent>) => {
      const nowTS = new Date().getTime()
      const scrollResponderRef = scrollViewRef?.current?.getScrollResponder()

      const getZoomRectAfterDoubleTap = (
        touchX: number,
        touchY: number,
      ): {
        x: number
        y: number
        width: number
        height: number
      } => {
        if (!imageDimensions) {
          return {
            x: 0,
            y: 0,
            width: screen.width,
            height: screen.height,
          }
        }

        // First, let's figure out how much we want to zoom in.
        // We want to try to zoom in at least close enough to get rid of black bars.
        const imageAspect = imageDimensions.width / imageDimensions.height
        const screenAspect = screen.width / screen.height
        const zoom = Math.max(
          imageAspect / screenAspect,
          screenAspect / imageAspect,
          MIN_ZOOM,
        )
        // Unlike in the Android version, we don't constrain the *max* zoom level here.
        // Instead, this is done in the ScrollView props so that it constraints pinch too.

        // Next, we'll be calculating the rectangle to "zoom into" in screen coordinates.
        // We already know the zoom level, so this gives us the rectangle size.
        let rectWidth = screen.width / zoom
        let rectHeight = screen.height / zoom

        // Before we settle on the zoomed rect, figure out the safe area it has to be inside.
        // We don't want to introduce new black bars or make existing black bars unbalanced.
        let minX = 0
        let minY = 0
        let maxX = screen.width - rectWidth
        let maxY = screen.height - rectHeight
        if (imageAspect >= screenAspect) {
          // The image has horizontal black bars. Exclude them from the safe area.
          const renderedHeight = screen.width / imageAspect
          const horizontalBarHeight = (screen.height - renderedHeight) / 2
          minY += horizontalBarHeight
          maxY -= horizontalBarHeight
        } else {
          // The image has vertical black bars. Exclude them from the safe area.
          const renderedWidth = screen.height * imageAspect
          const verticalBarWidth = (screen.width - renderedWidth) / 2
          minX += verticalBarWidth
          maxX -= verticalBarWidth
        }

        // Finally, we can position the rect according to its size and the safe area.
        let rectX
        if (maxX >= minX) {
          // Content fills the screen horizontally so we have horizontal wiggle room.
          // Try to keep the tapped point under the finger after zoom.
          rectX = touchX - touchX / zoom
          rectX = Math.min(rectX, maxX)
          rectX = Math.max(rectX, minX)
        } else {
          // Keep the rect centered on the screen so that black bars are balanced.
          rectX = screen.width / 2 - rectWidth / 2
        }
        let rectY
        if (maxY >= minY) {
          // Content fills the screen vertically so we have vertical wiggle room.
          // Try to keep the tapped point under the finger after zoom.
          rectY = touchY - touchY / zoom
          rectY = Math.min(rectY, maxY)
          rectY = Math.max(rectY, minY)
        } else {
          // Keep the rect centered on the screen so that black bars are balanced.
          rectY = screen.height / 2 - rectHeight / 2
        }

        return {
          x: rectX,
          y: rectY,
          height: rectHeight,
          width: rectWidth,
        }
      }

      if (lastTapTS && nowTS - lastTapTS < DOUBLE_TAP_DELAY) {
        let nextZoomRect = {
          x: 0,
          y: 0,
          width: screen.width,
          height: screen.height,
        }

        const willZoom = !scaled
        if (willZoom) {
          const {pageX, pageY} = event.nativeEvent
          nextZoomRect = getZoomRectAfterDoubleTap(pageX, pageY)
        }

        // @ts-ignore
        scrollResponderRef?.scrollResponderZoomTo({
          ...nextZoomRect, // This rect is in screen coordinates
          animated: true,
        })
      } else {
        lastTapTS = nowTS
      }
    },
    [imageDimensions, scaled, screen.height, screen.width, scrollViewRef],
  )

  return handleDoubleTap
}

export default useDoubleTapToZoom