Unify dimensions cache between lightbox and feed (#6047)
* Remove useless memo * Use explicit values when useImageAspectRatio doesn't know It's not very good that you can't distingiush when we haven't loaded vs when we're certain. This shifts the burden of dealing with missing values to the caller. * Check cache early * Handle src change * Rewrite image-sizes.fetch to avoid mixing async styles * Make image-sizes LRU Code is copy paste from useImageDimensions.ts * Rm unused fields * Derive aspect on the fly * Factor useImageDimensions out of useImageAspectRatio * Move useImageDimensions into image-sizes * Make lightbox use the same cache * Wire up known dimensions to the lightbox * Handle division by zero in the hook * Use safe aspect for lightbox calculations
This commit is contained in:
@@ -16,4 +16,9 @@ export type Position = {
|
||||
y: number
|
||||
}
|
||||
|
||||
export type ImageSource = {uri: string; thumbUri: string; alt?: string}
|
||||
export type ImageSource = {
|
||||
uri: string
|
||||
thumbUri: string
|
||||
alt?: string
|
||||
dimensions: Dimensions | null
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {useImageDimensions} from '#/lib/media/image-sizes'
|
||||
import type {Dimensions as ImageDimensions, ImageSource} from '../../@types'
|
||||
import useImageDimensions from '../../hooks/useImageDimensions'
|
||||
import {
|
||||
applyRounding,
|
||||
createTransform,
|
||||
@@ -52,7 +52,10 @@ const ImageItem = ({
|
||||
isScrollViewBeingDragged,
|
||||
}: Props) => {
|
||||
const [isScaled, setIsScaled] = useState(false)
|
||||
const imageDimensions = useImageDimensions(imageSrc)
|
||||
const [imageAspect, imageDimensions] = useImageDimensions({
|
||||
src: imageSrc.uri,
|
||||
knownDimensions: imageSrc.dimensions,
|
||||
})
|
||||
const committedTransform = useSharedValue(initialTransform)
|
||||
const panTranslation = useSharedValue({x: 0, y: 0})
|
||||
const pinchOrigin = useSharedValue({x: 0, y: 0})
|
||||
@@ -119,12 +122,12 @@ const ImageItem = ({
|
||||
candidateTransform: TransformMatrix,
|
||||
) {
|
||||
'worklet'
|
||||
if (!imageDimensions) {
|
||||
if (!imageAspect) {
|
||||
return [0, 0]
|
||||
}
|
||||
const [nextTranslateX, nextTranslateY, nextScale] =
|
||||
readTransform(candidateTransform)
|
||||
const scaledDimensions = getScaledDimensions(imageDimensions, nextScale)
|
||||
const scaledDimensions = getScaledDimensions(imageAspect, nextScale)
|
||||
const clampedTranslateX = clampTranslation(
|
||||
nextTranslateX,
|
||||
scaledDimensions.width,
|
||||
@@ -248,7 +251,7 @@ const ImageItem = ({
|
||||
.numberOfTaps(2)
|
||||
.onEnd(e => {
|
||||
'worklet'
|
||||
if (!imageDimensions) {
|
||||
if (!imageDimensions || !imageAspect) {
|
||||
return
|
||||
}
|
||||
const [, , committedScale] = readTransform(committedTransform.value)
|
||||
@@ -260,7 +263,6 @@ const ImageItem = ({
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -363,11 +365,10 @@ const styles = StyleSheet.create({
|
||||
})
|
||||
|
||||
function getScaledDimensions(
|
||||
imageDimensions: ImageDimensions,
|
||||
imageAspect: number,
|
||||
scale: number,
|
||||
): ImageDimensions {
|
||||
'worklet'
|
||||
const imageAspect = imageDimensions.width / imageDimensions.height
|
||||
const screenAspect = SCREEN.width / SCREEN.height
|
||||
const isLandscape = imageAspect > screenAspect
|
||||
if (isLandscape) {
|
||||
|
||||
@@ -19,8 +19,8 @@ import Animated, {
|
||||
import {Image} from 'expo-image'
|
||||
|
||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||
import {Dimensions as ImageDimensions, ImageSource} from '../../@types'
|
||||
import useImageDimensions from '../../hooks/useImageDimensions'
|
||||
import {useImageDimensions} from '#/lib/media/image-sizes'
|
||||
import {ImageSource} from '../../@types'
|
||||
|
||||
const SWIPE_CLOSE_OFFSET = 75
|
||||
const SWIPE_CLOSE_VELOCITY = 1
|
||||
@@ -47,7 +47,10 @@ const ImageItem = ({
|
||||
const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
|
||||
const translationY = useSharedValue(0)
|
||||
const [scaled, setScaled] = useState(false)
|
||||
const imageDimensions = useImageDimensions(imageSrc)
|
||||
const [imageAspect, imageDimensions] = useImageDimensions({
|
||||
src: imageSrc.uri,
|
||||
knownDimensions: imageSrc.dimensions,
|
||||
})
|
||||
const maxZoomScale = imageDimensions
|
||||
? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM
|
||||
: 1
|
||||
@@ -99,7 +102,7 @@ const ImageItem = ({
|
||||
const willZoom = !scaled
|
||||
if (willZoom) {
|
||||
nextZoomRect = getZoomRectAfterDoubleTap(
|
||||
imageDimensions,
|
||||
imageAspect,
|
||||
absoluteX,
|
||||
absoluteY,
|
||||
)
|
||||
@@ -179,7 +182,7 @@ const styles = StyleSheet.create({
|
||||
})
|
||||
|
||||
const getZoomRectAfterDoubleTap = (
|
||||
imageDimensions: ImageDimensions | null,
|
||||
imageAspect: number | undefined,
|
||||
touchX: number,
|
||||
touchY: number,
|
||||
): {
|
||||
@@ -188,7 +191,7 @@ const getZoomRectAfterDoubleTap = (
|
||||
width: number
|
||||
height: number
|
||||
} => {
|
||||
if (!imageDimensions) {
|
||||
if (!imageAspect) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -199,7 +202,6 @@ const getZoomRectAfterDoubleTap = (
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* 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 {useEffect, useState} from 'react'
|
||||
import {Image, ImageURISource} from 'react-native'
|
||||
|
||||
import {Dimensions, ImageSource} from '../@types'
|
||||
|
||||
const CACHE_SIZE = 50
|
||||
|
||||
type CacheStorageItem = {key: string; value: any}
|
||||
|
||||
const createCache = (cacheSize: number) => ({
|
||||
_storage: [] as CacheStorageItem[],
|
||||
get(key: string): any {
|
||||
const {value} =
|
||||
this._storage.find(({key: storageKey}) => storageKey === key) || {}
|
||||
|
||||
return value
|
||||
},
|
||||
set(key: string, value: any) {
|
||||
if (this._storage.length >= cacheSize) {
|
||||
this._storage.shift()
|
||||
}
|
||||
|
||||
this._storage.push({key, value})
|
||||
},
|
||||
})
|
||||
|
||||
const imageDimensionsCache = createCache(CACHE_SIZE)
|
||||
|
||||
const useImageDimensions = (image: ImageSource): Dimensions | null => {
|
||||
const [dimensions, setDimensions] = useState<Dimensions | null>(null)
|
||||
|
||||
const getImageDimensions = (
|
||||
image: ImageSource,
|
||||
): Promise<Dimensions | null> => {
|
||||
return new Promise(resolve => {
|
||||
if (image.uri) {
|
||||
const source = image as ImageURISource
|
||||
const cacheKey = source.uri as string
|
||||
const imageDimensions = imageDimensionsCache.get(cacheKey)
|
||||
if (imageDimensions) {
|
||||
resolve(imageDimensions)
|
||||
} else {
|
||||
Image.getSizeWithHeaders(
|
||||
// @ts-ignore
|
||||
source.uri,
|
||||
source.headers,
|
||||
(width: number, height: number) => {
|
||||
if (width > 0 && height > 0) {
|
||||
imageDimensionsCache.set(cacheKey, {width, height})
|
||||
resolve({width, height})
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
resolve(null)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let isImageUnmounted = false
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
getImageDimensions(image).then(dimensions => {
|
||||
if (!isImageUnmounted) {
|
||||
setDimensions(dimensions)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
isImageUnmounted = true
|
||||
}
|
||||
}, [image])
|
||||
|
||||
return dimensions
|
||||
}
|
||||
|
||||
export default useImageDimensions
|
||||
Reference in New Issue
Block a user