Fix gestures outside image bounds

This commit is contained in:
Dan Abramov
2024-11-02 18:56:01 +00:00
parent 01b350175e
commit b3f4338d21
4 changed files with 87 additions and 62 deletions
@@ -13,7 +13,9 @@ import Animated, {
useSharedValue,
withSpring,
} from 'react-native-reanimated'
import {Image} from 'expo-image'
import {Image, ImageStyle} from 'expo-image'
const AnimatedImage = Animated.createAnimatedComponent(Image)
import {useImageDimensions} from '#/lib/media/image-sizes'
import type {Dimensions as ImageDimensions, ImageSource} from '../../@types'
@@ -47,6 +49,7 @@ type Props = {
isScrollViewBeingDragged: boolean
showControls: boolean
dismissSwipePan: PanGesture
animatedStyle: ImageStyle | null
}
const ImageItem = ({
imageSrc,
@@ -54,6 +57,7 @@ const ImageItem = ({
onZoom,
isScrollViewBeingDragged,
dismissSwipePan,
animatedStyle,
}: Props) => {
const [isScaled, setIsScaled] = useState(false)
const [imageAspect, imageDimensions] = useImageDimensions({
@@ -95,7 +99,7 @@ const ImageItem = ({
onZoom(nextIsScaled)
}
const animatedStyle = useAnimatedStyle(() => {
const animatedContainerStyle = 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()
@@ -293,35 +297,39 @@ const ImageItem = ({
)
return (
<Animated.View
ref={containerRef}
// Necessary to make opacity work for both children together.
renderToHardwareTextureAndroid
style={[styles.container, animatedStyle, {}]}>
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
<GestureDetector gesture={composedGesture}>
<Image
contentFit="contain"
source={{uri: imageSrc.uri}}
placeholderContentFit="contain"
placeholder={{uri: imageSrc.thumbUri}}
style={{
width: SCREEN.width,
height: imageAspect ? SCREEN.width / imageAspect : undefined,
borderRadius:
imageSrc.type === 'circle-avi'
? SCREEN.width / 2
: imageSrc.type === 'rect-avi'
? 20
: 0,
}}
accessibilityLabel={imageSrc.alt}
accessibilityHint=""
accessibilityIgnoresInvertColors
cachePolicy="memory"
/>
</GestureDetector>
</Animated.View>
<GestureDetector gesture={composedGesture}>
<Animated.View
ref={containerRef}
style={[styles.container, animatedContainerStyle, {}]}>
<Animated.View
style={[
animatedStyle,
{
width: SCREEN.width,
height: imageAspect ? SCREEN.width / imageAspect : undefined,
borderRadius:
imageSrc.type === 'circle-avi'
? SCREEN.width / 2
: imageSrc.type === 'rect-avi'
? 20
: 0,
},
]}>
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
<AnimatedImage
contentFit="contain"
source={{uri: imageSrc.uri}}
placeholderContentFit="contain"
placeholder={{uri: imageSrc.thumbUri}}
style={{flex: 1}}
accessibilityLabel={imageSrc.alt}
accessibilityHint=""
accessibilityIgnoresInvertColors
cachePolicy="memory"
/>
</Animated.View>
</Animated.View>
</GestureDetector>
)
}
@@ -14,12 +14,14 @@ import {
PanGesture,
} from 'react-native-gesture-handler'
import Animated, {runOnJS, useAnimatedRef} from 'react-native-reanimated'
import {Image} from 'expo-image'
import {Image, ImageStyle} from 'expo-image'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useImageDimensions} from '#/lib/media/image-sizes'
import {ImageSource} from '../../@types'
const AnimatedImage = Animated.createAnimatedComponent(Image)
const SCREEN = Dimensions.get('screen')
const MAX_ORIGINAL_IMAGE_ZOOM = 2
const MIN_DOUBLE_TAP_SCALE = 2
@@ -32,6 +34,7 @@ type Props = {
isScrollViewBeingDragged: boolean
showControls: boolean
dismissSwipePan: PanGesture | null
animatedStyle: ImageStyle | null
}
const ImageItem = ({
@@ -40,6 +43,7 @@ const ImageItem = ({
onZoom,
showControls,
dismissSwipePan,
animatedStyle,
}: Props) => {
const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
@@ -128,27 +132,33 @@ const ImageItem = ({
onScroll={scrollHandler}
bounces={scaled}
centerContent>
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
<Image
contentFit="contain"
source={{uri: imageSrc.uri}}
placeholderContentFit="contain"
placeholder={{uri: imageSrc.thumbUri}}
style={{
width: SCREEN.width,
height: imageAspect ? SCREEN.width / imageAspect : undefined,
borderRadius:
imageSrc.type === 'circle-avi'
? SCREEN.width / 2
: imageSrc.type === 'rect-avi'
? 20
: 0,
}}
accessibilityLabel={imageSrc.alt}
accessibilityHint=""
enableLiveTextInteraction={showControls && !scaled}
accessibilityIgnoresInvertColors
/>
<Animated.View
style={[
animatedStyle,
{
width: SCREEN.width,
height: imageAspect ? SCREEN.width / imageAspect : undefined,
borderRadius:
imageSrc.type === 'circle-avi'
? SCREEN.width / 2
: imageSrc.type === 'rect-avi'
? 20
: 0,
},
]}>
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
<AnimatedImage
contentFit="cover"
source={{uri: imageSrc.uri}}
placeholderContentFit="cover"
placeholder={{uri: imageSrc.thumbUri}}
accessibilityLabel={imageSrc.alt}
accessibilityHint=""
enableLiveTextInteraction={showControls && !scaled}
accessibilityIgnoresInvertColors
style={{flex: 1}}
/>
</Animated.View>
</Animated.ScrollView>
</GestureDetector>
)
@@ -3,6 +3,7 @@
import React from 'react'
import {View} from 'react-native'
import {PanGesture} from 'react-native-gesture-handler'
import {ImageStyle} from 'expo-image'
import {ImageSource} from '../../@types'
@@ -14,6 +15,7 @@ type Props = {
isScrollViewBeingDragged: boolean
showControls: boolean
dismissSwipePan: PanGesture | null
animatedStyle: ImageStyle | null
}
const ImageItem = (_props: Props) => {
+14 -9
View File
@@ -72,6 +72,7 @@ function ImageViewing({
const [imageIndex, setImageIndex] = useState(initialImageIndex)
const [showControls, setShowControls] = useState(true)
const dismissSwipeTranslateY = useSharedValue(0)
const isFlyingAway = useSharedValue(false)
const animatedHeaderStyle = useAnimatedStyle(() => {
const show = showControls && dismissSwipeTranslateY.value === 0
@@ -98,10 +99,16 @@ function ImageViewing({
}
})
const containerStyle = useAnimatedStyle(() => {
if (openProgress.value < 1 || isFlyingAway.value) {
return {pointerEvents: 'none'}
}
return {pointerEvents: 'auto'}
})
const activeImageStyle = useAnimatedStyle(() => {
if (openProgress.value === 1 || dismissSwipeTranslateY.value !== 0) {
return {
pointerEvents: 'auto',
transform: [{translateY: dismissSwipeTranslateY.value}],
}
}
@@ -114,12 +121,10 @@ function ImageViewing({
image.dimensions,
)
return {
pointerEvents: 'none',
transform: interpolatedTransform,
}
}
return {
pointerEvents: 'auto',
transform: [],
}
})
@@ -136,6 +141,7 @@ function ImageViewing({
.onEnd(e => {
'worklet'
if (Math.abs(e.velocityY) > 1000) {
isFlyingAway.value = true
dismissSwipeTranslateY.value = withDecay({
velocity: e.velocityY,
velocityFactor: Math.max(3000 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
@@ -198,7 +204,7 @@ function ImageViewing({
edges={edges}
aria-modal
accessibilityViewIsModal>
<View style={[styles.container]}>
<Animated.View style={[styles.container, containerStyle]}>
<Animated.View style={[styles.backdrop, backdropStyle]} />
<Animated.View style={[styles.header, animatedHeaderStyle]}>
<ImageDefaultHeader onRequestClose={onRequestClose} />
@@ -216,9 +222,7 @@ function ImageViewing({
overdrag={true}
style={styles.pager}>
{images.map((imageSrc, i) => (
<Animated.View
key={imageSrc.uri}
style={imageIndex === i ? activeImageStyle : null}>
<View key={imageSrc.uri}>
<ImageItem
onTap={onTap}
onZoom={onZoom}
@@ -226,11 +230,12 @@ function ImageViewing({
onRequestClose={onRequestClose}
isScrollViewBeingDragged={isDragging}
showControls={showControls}
animatedStyle={imageIndex === i ? activeImageStyle : null}
dismissSwipePan={
imageIndex === i && !isDragging ? dismissSwipePan : null
}
/>
</Animated.View>
</View>
))}
</PagerView>
<Animated.View style={[styles.footer, animatedFooterStyle]}>
@@ -241,7 +246,7 @@ function ImageViewing({
onPressShare={onPressShare}
/>
</Animated.View>
</View>
</Animated.View>
</SafeAreaView>
)
}