Fix gestures outside image bounds
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user