Rewrite Android lightbox (#1624)
This commit is contained in:
parent
8366fe2c4a
commit
64153067e3
7 changed files with 540 additions and 598 deletions
|
|
@ -1,159 +1,398 @@
|
|||
/**
|
||||
* 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, {MutableRefObject, useState} from 'react'
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react'
|
||||
|
||||
import {
|
||||
Animated,
|
||||
ScrollView,
|
||||
Dimensions,
|
||||
StyleSheet,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
NativeMethodsMixin,
|
||||
} from 'react-native'
|
||||
import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import Animated, {
|
||||
measure,
|
||||
runOnJS,
|
||||
useAnimatedRef,
|
||||
useAnimatedStyle,
|
||||
useAnimatedReaction,
|
||||
useSharedValue,
|
||||
withDecay,
|
||||
withSpring,
|
||||
} from 'react-native-reanimated'
|
||||
import {
|
||||
GestureDetector,
|
||||
Gesture,
|
||||
GestureType,
|
||||
} from 'react-native-gesture-handler'
|
||||
import useImageDimensions from '../../hooks/useImageDimensions'
|
||||
import usePanResponder from '../../hooks/usePanResponder'
|
||||
import {
|
||||
createTransform,
|
||||
readTransform,
|
||||
applyRounding,
|
||||
prependPan,
|
||||
prependPinch,
|
||||
prependTransform,
|
||||
TransformMatrix,
|
||||
} from '../../transforms'
|
||||
import type {ImageSource, Dimensions as ImageDimensions} from '../../@types'
|
||||
|
||||
import {getImageTransform} from '../../utils'
|
||||
import {ImageSource} from '../../@types'
|
||||
import {ImageLoading} from './ImageLoading'
|
||||
|
||||
const SWIPE_CLOSE_OFFSET = 75
|
||||
const SWIPE_CLOSE_VELOCITY = 1.75
|
||||
const SCREEN = Dimensions.get('window')
|
||||
const SCREEN_WIDTH = SCREEN.width
|
||||
const SCREEN_HEIGHT = SCREEN.height
|
||||
const MIN_DOUBLE_TAP_SCALE = 2
|
||||
const MAX_ORIGINAL_IMAGE_ZOOM = 2
|
||||
|
||||
const AnimatedImage = Animated.createAnimatedComponent(Image)
|
||||
const initialTransform = createTransform()
|
||||
|
||||
type Props = {
|
||||
imageSrc: ImageSource
|
||||
onRequestClose: () => void
|
||||
onZoom: (isZoomed: boolean) => void
|
||||
pinchGestureRef: MutableRefObject<GestureType | undefined>
|
||||
isScrollViewBeingDragged: boolean
|
||||
}
|
||||
|
||||
const AnimatedImage = Animated.createAnimatedComponent(Image)
|
||||
|
||||
const ImageItem = ({imageSrc, onZoom, onRequestClose}: Props) => {
|
||||
const imageContainer = useRef<ScrollView & NativeMethodsMixin>(null)
|
||||
const ImageItem = ({
|
||||
imageSrc,
|
||||
onZoom,
|
||||
onRequestClose,
|
||||
isScrollViewBeingDragged,
|
||||
pinchGestureRef,
|
||||
}: Props) => {
|
||||
const [isScaled, setIsScaled] = useState(false)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const imageDimensions = useImageDimensions(imageSrc)
|
||||
const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
|
||||
const scrollValueY = new Animated.Value(0)
|
||||
const [isLoaded, setLoadEnd] = useState(false)
|
||||
const committedTransform = useSharedValue(initialTransform)
|
||||
const panTranslation = useSharedValue({x: 0, y: 0})
|
||||
const pinchOrigin = useSharedValue({x: 0, y: 0})
|
||||
const pinchScale = useSharedValue(1)
|
||||
const pinchTranslation = useSharedValue({x: 0, y: 0})
|
||||
const dismissSwipeTranslateY = useSharedValue(0)
|
||||
const containerRef = useAnimatedRef()
|
||||
|
||||
const onLoaded = useCallback(() => setLoadEnd(true), [])
|
||||
const onZoomPerformed = useCallback(
|
||||
(isZoomed: boolean) => {
|
||||
onZoom(isZoomed)
|
||||
if (imageContainer?.current) {
|
||||
imageContainer.current.setNativeProps({
|
||||
scrollEnabled: !isZoomed,
|
||||
})
|
||||
function getCommittedScale(): number {
|
||||
'worklet'
|
||||
const [, , committedScale] = readTransform(committedTransform.value)
|
||||
return committedScale
|
||||
}
|
||||
|
||||
// Keep track of when we're entering or leaving scaled rendering.
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
return pinchScale.value !== 1 || getCommittedScale() !== 1
|
||||
},
|
||||
(nextIsScaled, prevIsScaled) => {
|
||||
if (nextIsScaled !== prevIsScaled) {
|
||||
runOnJS(handleZoom)(nextIsScaled)
|
||||
}
|
||||
},
|
||||
[onZoom],
|
||||
)
|
||||
|
||||
const [panHandlers, scaleValue, translateValue] = usePanResponder({
|
||||
initialScale: scale || 1,
|
||||
initialTranslate: translate || {x: 0, y: 0},
|
||||
onZoom: onZoomPerformed,
|
||||
})
|
||||
function handleZoom(nextIsScaled: boolean) {
|
||||
setIsScaled(nextIsScaled)
|
||||
onZoom(nextIsScaled)
|
||||
}
|
||||
|
||||
const imagesStyles = getImageStyles(
|
||||
imageDimensions,
|
||||
translateValue,
|
||||
scaleValue,
|
||||
)
|
||||
const imageOpacity = scrollValueY.interpolate({
|
||||
inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
|
||||
outputRange: [0.7, 1, 0.7],
|
||||
})
|
||||
const imageStylesWithOpacity = {...imagesStyles, opacity: imageOpacity}
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
// Apply the active adjustments on top of the committed transform before the gestures.
|
||||
// This is matrix multiplication, so operations are applied in the reverse order.
|
||||
let t = createTransform()
|
||||
prependPan(t, panTranslation.value)
|
||||
prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value)
|
||||
prependTransform(t, committedTransform.value)
|
||||
const [translateX, translateY, scale] = readTransform(t)
|
||||
|
||||
const onScrollEndDrag = ({
|
||||
nativeEvent,
|
||||
}: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const velocityY = nativeEvent?.velocity?.y ?? 0
|
||||
const offsetY = nativeEvent?.contentOffset?.y ?? 0
|
||||
|
||||
if (
|
||||
(Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY &&
|
||||
offsetY > SWIPE_CLOSE_OFFSET) ||
|
||||
offsetY > SCREEN_HEIGHT / 2
|
||||
) {
|
||||
onRequestClose()
|
||||
const dismissDistance = dismissSwipeTranslateY.value
|
||||
const dismissProgress = Math.min(
|
||||
Math.abs(dismissDistance) / (SCREEN.height / 2),
|
||||
1,
|
||||
)
|
||||
return {
|
||||
opacity: 1 - dismissProgress,
|
||||
transform: [
|
||||
{translateX},
|
||||
{translateY: translateY + dismissDistance},
|
||||
{scale},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
// On Android, stock apps prevent going "out of bounds" on pan or pinch. You should "bump" into edges.
|
||||
// If the user tried to pan too hard, this function will provide the negative panning to stay in bounds.
|
||||
function getExtraTranslationToStayInBounds(
|
||||
candidateTransform: TransformMatrix,
|
||||
) {
|
||||
'worklet'
|
||||
if (!imageDimensions) {
|
||||
return [0, 0]
|
||||
}
|
||||
const [nextTranslateX, nextTranslateY, nextScale] =
|
||||
readTransform(candidateTransform)
|
||||
const scaledDimensions = getScaledDimensions(imageDimensions, nextScale)
|
||||
const clampedTranslateX = clampTranslation(
|
||||
nextTranslateX,
|
||||
scaledDimensions.width,
|
||||
SCREEN.width,
|
||||
)
|
||||
const clampedTranslateY = clampTranslation(
|
||||
nextTranslateY,
|
||||
scaledDimensions.height,
|
||||
SCREEN.height,
|
||||
)
|
||||
const dx = clampedTranslateX - nextTranslateX
|
||||
const dy = clampedTranslateY - nextTranslateY
|
||||
return [dx, dy]
|
||||
}
|
||||
|
||||
const onScroll = ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const offsetY = nativeEvent?.contentOffset?.y ?? 0
|
||||
// This is a hack.
|
||||
// We need to disallow any gestures (and let the native parent scroll view scroll) while you're scrolling it.
|
||||
// However, there is no great reliable way to coordinate this yet in RGNH.
|
||||
// This "fake" manual gesture handler whenever you're trying to touch something while the parent scrollview is not at rest.
|
||||
const consumeHScroll = Gesture.Manual().onTouchesDown((e, manager) => {
|
||||
if (isScrollViewBeingDragged) {
|
||||
// Steal the gesture (and do nothing, so native ScrollView does its thing).
|
||||
manager.activate()
|
||||
return
|
||||
}
|
||||
const measurement = measure(containerRef)
|
||||
if (!measurement || measurement.pageX !== 0) {
|
||||
// Steal the gesture (and do nothing, so native ScrollView does its thing).
|
||||
manager.activate()
|
||||
return
|
||||
}
|
||||
// Fail this "fake" gesture so that the gestures after it can proceed.
|
||||
manager.fail()
|
||||
})
|
||||
|
||||
scrollValueY.setValue(offsetY)
|
||||
}
|
||||
const pinch = Gesture.Pinch()
|
||||
.withRef(pinchGestureRef)
|
||||
.onStart(e => {
|
||||
pinchOrigin.value = {
|
||||
x: e.focalX - SCREEN.width / 2,
|
||||
y: e.focalY - SCREEN.height / 2,
|
||||
}
|
||||
})
|
||||
.onChange(e => {
|
||||
if (!imageDimensions) {
|
||||
return
|
||||
}
|
||||
// Don't let the picture zoom in so close that it gets blurry.
|
||||
// Also, like in stock Android apps, don't let the user zoom out further than 1:1.
|
||||
const committedScale = getCommittedScale()
|
||||
const maxCommittedScale =
|
||||
(imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM
|
||||
const minPinchScale = 1 / committedScale
|
||||
const maxPinchScale = maxCommittedScale / committedScale
|
||||
const nextPinchScale = Math.min(
|
||||
Math.max(minPinchScale, e.scale),
|
||||
maxPinchScale,
|
||||
)
|
||||
pinchScale.value = nextPinchScale
|
||||
|
||||
// Zooming out close to the corner could push us out of bounds, which we don't want on Android.
|
||||
// Calculate where we'll end up so we know how much to translate back to stay in bounds.
|
||||
const t = createTransform()
|
||||
prependPan(t, panTranslation.value)
|
||||
prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value)
|
||||
prependTransform(t, committedTransform.value)
|
||||
const [dx, dy] = getExtraTranslationToStayInBounds(t)
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
pinchTranslation.value = {
|
||||
x: pinchTranslation.value.x + dx,
|
||||
y: pinchTranslation.value.y + dy,
|
||||
}
|
||||
}
|
||||
})
|
||||
.onEnd(() => {
|
||||
// Commit just the pinch.
|
||||
let t = createTransform()
|
||||
prependPinch(
|
||||
t,
|
||||
pinchScale.value,
|
||||
pinchOrigin.value,
|
||||
pinchTranslation.value,
|
||||
)
|
||||
prependTransform(t, committedTransform.value)
|
||||
applyRounding(t)
|
||||
committedTransform.value = t
|
||||
|
||||
// Reset just the pinch.
|
||||
pinchScale.value = 1
|
||||
pinchOrigin.value = {x: 0, y: 0}
|
||||
pinchTranslation.value = {x: 0, y: 0}
|
||||
})
|
||||
|
||||
const pan = Gesture.Pan()
|
||||
.averageTouches(true)
|
||||
// Unlike .enabled(isScaled), this ensures that an initial pinch can turn into a pan midway:
|
||||
.minPointers(isScaled ? 1 : 2)
|
||||
.onChange(e => {
|
||||
if (!imageDimensions) {
|
||||
return
|
||||
}
|
||||
const nextPanTranslation = {x: e.translationX, y: e.translationY}
|
||||
let t = createTransform()
|
||||
prependPan(t, nextPanTranslation)
|
||||
prependPinch(
|
||||
t,
|
||||
pinchScale.value,
|
||||
pinchOrigin.value,
|
||||
pinchTranslation.value,
|
||||
)
|
||||
prependTransform(t, committedTransform.value)
|
||||
|
||||
// Prevent panning from going out of bounds.
|
||||
const [dx, dy] = getExtraTranslationToStayInBounds(t)
|
||||
nextPanTranslation.x += dx
|
||||
nextPanTranslation.y += dy
|
||||
panTranslation.value = nextPanTranslation
|
||||
})
|
||||
.onEnd(() => {
|
||||
// Commit just the pan.
|
||||
let t = createTransform()
|
||||
prependPan(t, panTranslation.value)
|
||||
prependTransform(t, committedTransform.value)
|
||||
applyRounding(t)
|
||||
committedTransform.value = t
|
||||
|
||||
// Reset just the pan.
|
||||
panTranslation.value = {x: 0, y: 0}
|
||||
})
|
||||
|
||||
const doubleTap = Gesture.Tap()
|
||||
.numberOfTaps(2)
|
||||
.onEnd(e => {
|
||||
if (!imageDimensions) {
|
||||
return
|
||||
}
|
||||
const committedScale = getCommittedScale()
|
||||
if (committedScale !== 1) {
|
||||
// Go back to 1:1 using the identity vector.
|
||||
let t = createTransform()
|
||||
committedTransform.value = withClampedSpring(t)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to zoom in so that we get rid of the black bars (whatever the orientation was).
|
||||
const imageAspect = imageDimensions.width / imageDimensions.height
|
||||
const screenAspect = SCREEN.width / SCREEN.height
|
||||
const candidateScale = Math.max(
|
||||
imageAspect / screenAspect,
|
||||
screenAspect / imageAspect,
|
||||
MIN_DOUBLE_TAP_SCALE,
|
||||
)
|
||||
// But don't zoom in so close that the picture gets blurry.
|
||||
const maxScale =
|
||||
(imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM
|
||||
const scale = Math.min(candidateScale, maxScale)
|
||||
|
||||
// Calculate where we would be if the user pinched into the double tapped point.
|
||||
// We won't use this transform directly because it may go out of bounds.
|
||||
const candidateTransform = createTransform()
|
||||
const origin = {
|
||||
x: e.absoluteX - SCREEN.width / 2,
|
||||
y: e.absoluteY - SCREEN.height / 2,
|
||||
}
|
||||
prependPinch(candidateTransform, scale, origin, {x: 0, y: 0})
|
||||
|
||||
// Now we know how much we went out of bounds, so we can shoot correctly.
|
||||
const [dx, dy] = getExtraTranslationToStayInBounds(candidateTransform)
|
||||
const finalTransform = createTransform()
|
||||
prependPinch(finalTransform, scale, origin, {x: dx, y: dy})
|
||||
committedTransform.value = withClampedSpring(finalTransform)
|
||||
})
|
||||
|
||||
const dismissSwipePan = Gesture.Pan()
|
||||
.enabled(!isScaled)
|
||||
.activeOffsetY([-10, 10])
|
||||
.failOffsetX([-10, 10])
|
||||
.maxPointers(1)
|
||||
.onUpdate(e => {
|
||||
dismissSwipeTranslateY.value = e.translationY
|
||||
})
|
||||
.onEnd(e => {
|
||||
if (Math.abs(e.velocityY) > 1000) {
|
||||
dismissSwipeTranslateY.value = withDecay({velocity: e.velocityY})
|
||||
runOnJS(onRequestClose)()
|
||||
} else {
|
||||
dismissSwipeTranslateY.value = withSpring(0, {
|
||||
stiffness: 700,
|
||||
damping: 50,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const isLoading = !isLoaded || !imageDimensions
|
||||
return (
|
||||
<ScrollView
|
||||
ref={imageContainer}
|
||||
style={styles.listItem}
|
||||
pagingEnabled
|
||||
nestedScrollEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.imageScrollContainer}
|
||||
scrollEnabled={true}
|
||||
onScroll={onScroll}
|
||||
onScrollEndDrag={onScrollEndDrag}>
|
||||
<AnimatedImage
|
||||
{...panHandlers}
|
||||
source={imageSrc}
|
||||
style={imageStylesWithOpacity}
|
||||
onLoad={onLoaded}
|
||||
accessibilityLabel={imageSrc.alt}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
{(!isLoaded || !imageDimensions) && <ImageLoading />}
|
||||
</ScrollView>
|
||||
<Animated.View ref={containerRef} style={styles.container}>
|
||||
{isLoading && (
|
||||
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
||||
)}
|
||||
<GestureDetector
|
||||
gesture={Gesture.Exclusive(
|
||||
consumeHScroll,
|
||||
dismissSwipePan,
|
||||
Gesture.Simultaneous(pinch, pan),
|
||||
doubleTap,
|
||||
)}>
|
||||
<AnimatedImage
|
||||
source={imageSrc}
|
||||
contentFit="contain"
|
||||
style={[styles.image, animatedStyle]}
|
||||
accessibilityLabel={imageSrc.alt}
|
||||
accessibilityHint=""
|
||||
onLoad={() => setIsLoaded(true)}
|
||||
/>
|
||||
</GestureDetector>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listItem: {
|
||||
width: SCREEN_WIDTH,
|
||||
height: SCREEN_HEIGHT,
|
||||
container: {
|
||||
width: SCREEN.width,
|
||||
height: SCREEN.height,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
imageScrollContainer: {
|
||||
height: SCREEN_HEIGHT * 2,
|
||||
image: {
|
||||
flex: 1,
|
||||
},
|
||||
loading: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const getImageStyles = (
|
||||
image: {width: number; height: number} | null,
|
||||
translate: Animated.ValueXY,
|
||||
scale?: Animated.Value,
|
||||
) => {
|
||||
if (!image?.width || !image?.height) {
|
||||
return {width: 0, height: 0}
|
||||
}
|
||||
|
||||
const transform = translate.getTranslateTransform()
|
||||
|
||||
if (scale) {
|
||||
// @ts-ignore TODO - is scale incorrect? might need to remove -prf
|
||||
transform.push({scale}, {perspective: new Animated.Value(1000)})
|
||||
}
|
||||
|
||||
return {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
transform,
|
||||
function getScaledDimensions(
|
||||
imageDimensions: ImageDimensions,
|
||||
scale: number,
|
||||
): ImageDimensions {
|
||||
'worklet'
|
||||
const imageAspect = imageDimensions.width / imageDimensions.height
|
||||
const screenAspect = SCREEN.width / SCREEN.height
|
||||
const isLandscape = imageAspect > screenAspect
|
||||
if (isLandscape) {
|
||||
return {
|
||||
width: scale * SCREEN.width,
|
||||
height: (scale * SCREEN.width) / imageAspect,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
width: scale * SCREEN.height * imageAspect,
|
||||
height: scale * SCREEN.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clampTranslation(
|
||||
value: number,
|
||||
scaledSize: number,
|
||||
screenSize: number,
|
||||
): number {
|
||||
'worklet'
|
||||
// Figure out how much the user should be allowed to pan, and constrain the translation.
|
||||
const panDistance = Math.max(0, (scaledSize - screenSize) / 2)
|
||||
const clampedValue = Math.min(Math.max(-panDistance, value), panDistance)
|
||||
return clampedValue
|
||||
}
|
||||
|
||||
function withClampedSpring(value: any) {
|
||||
'worklet'
|
||||
return withSpring(value, {overshootClamping: true})
|
||||
}
|
||||
|
||||
export default React.memo(ImageItem)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react'
|
||||
import React, {MutableRefObject, useCallback, useRef, useState} from 'react'
|
||||
|
||||
import {
|
||||
Animated,
|
||||
|
|
@ -20,11 +20,11 @@ import {
|
|||
TouchableWithoutFeedback,
|
||||
} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {GestureType} from 'react-native-gesture-handler'
|
||||
|
||||
import useImageDimensions from '../../hooks/useImageDimensions'
|
||||
|
||||
import {getImageTransform} from '../../utils'
|
||||
import {ImageSource} from '../../@types'
|
||||
import {ImageSource, Dimensions as ImageDimensions} from '../../@types'
|
||||
import {ImageLoading} from './ImageLoading'
|
||||
|
||||
const DOUBLE_TAP_DELAY = 300
|
||||
|
|
@ -40,6 +40,8 @@ type Props = {
|
|||
imageSrc: ImageSource
|
||||
onRequestClose: () => void
|
||||
onZoom: (scaled: boolean) => void
|
||||
pinchGestureRef: MutableRefObject<GestureType>
|
||||
isScrollViewBeingDragged: boolean
|
||||
}
|
||||
|
||||
const AnimatedImage = Animated.createAnimatedComponent(Image)
|
||||
|
|
@ -164,7 +166,7 @@ const styles = StyleSheet.create({
|
|||
})
|
||||
|
||||
const getZoomRectAfterDoubleTap = (
|
||||
imageDimensions: {width: number; height: number} | null,
|
||||
imageDimensions: ImageDimensions | null,
|
||||
touchX: number,
|
||||
touchY: number,
|
||||
): {
|
||||
|
|
@ -252,7 +254,7 @@ const getZoomRectAfterDoubleTap = (
|
|||
}
|
||||
|
||||
const getImageStyles = (
|
||||
image: {width: number; height: number} | null,
|
||||
image: ImageDimensions | null,
|
||||
translate: {readonly x: number; readonly y: number} | undefined,
|
||||
scale?: number,
|
||||
) => {
|
||||
|
|
@ -275,4 +277,37 @@ const getImageStyles = (
|
|||
}
|
||||
}
|
||||
|
||||
const getImageTransform = (
|
||||
image: ImageDimensions | null,
|
||||
screen: ImageDimensions,
|
||||
) => {
|
||||
if (!image?.width || !image?.height) {
|
||||
return [] as const
|
||||
}
|
||||
|
||||
const wScale = screen.width / image.width
|
||||
const hScale = screen.height / image.height
|
||||
const scale = Math.min(wScale, hScale)
|
||||
const {x, y} = getImageTranslate(image, screen)
|
||||
|
||||
return [{x, y}, scale] as const
|
||||
}
|
||||
|
||||
const getImageTranslate = (
|
||||
image: ImageDimensions,
|
||||
screen: ImageDimensions,
|
||||
): {x: number; y: number} => {
|
||||
const getTranslateForAxis = (axis: 'x' | 'y'): number => {
|
||||
const imageSize = axis === 'x' ? image.width : image.height
|
||||
const screenSize = axis === 'x' ? screen.width : screen.height
|
||||
|
||||
return (screenSize - imageSize) / 2
|
||||
}
|
||||
|
||||
return {
|
||||
x: getTranslateForAxis('x'),
|
||||
y: getTranslateForAxis('y'),
|
||||
}
|
||||
}
|
||||
|
||||
export default React.memo(ImageItem)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
// default implementation fallback for web
|
||||
|
||||
import React from 'react'
|
||||
import React, {MutableRefObject} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {GestureType} from 'react-native-gesture-handler'
|
||||
import {ImageSource} from '../../@types'
|
||||
|
||||
type Props = {
|
||||
imageSrc: ImageSource
|
||||
onRequestClose: () => void
|
||||
onZoom: (scaled: boolean) => void
|
||||
pinchGestureRef: MutableRefObject<GestureType | undefined>
|
||||
isScrollViewBeingDragged: boolean
|
||||
}
|
||||
|
||||
const ImageItem = (_props: Props) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue