Fix gestures outside image bounds
This commit is contained in:
@@ -13,7 +13,9 @@ import Animated, {
|
|||||||
useSharedValue,
|
useSharedValue,
|
||||||
withSpring,
|
withSpring,
|
||||||
} from 'react-native-reanimated'
|
} 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 {useImageDimensions} from '#/lib/media/image-sizes'
|
||||||
import type {Dimensions as ImageDimensions, ImageSource} from '../../@types'
|
import type {Dimensions as ImageDimensions, ImageSource} from '../../@types'
|
||||||
@@ -47,6 +49,7 @@ type Props = {
|
|||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
showControls: boolean
|
showControls: boolean
|
||||||
dismissSwipePan: PanGesture
|
dismissSwipePan: PanGesture
|
||||||
|
animatedStyle: ImageStyle | null
|
||||||
}
|
}
|
||||||
const ImageItem = ({
|
const ImageItem = ({
|
||||||
imageSrc,
|
imageSrc,
|
||||||
@@ -54,6 +57,7 @@ const ImageItem = ({
|
|||||||
onZoom,
|
onZoom,
|
||||||
isScrollViewBeingDragged,
|
isScrollViewBeingDragged,
|
||||||
dismissSwipePan,
|
dismissSwipePan,
|
||||||
|
animatedStyle,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isScaled, setIsScaled] = useState(false)
|
const [isScaled, setIsScaled] = useState(false)
|
||||||
const [imageAspect, imageDimensions] = useImageDimensions({
|
const [imageAspect, imageDimensions] = useImageDimensions({
|
||||||
@@ -95,7 +99,7 @@ const ImageItem = ({
|
|||||||
onZoom(nextIsScaled)
|
onZoom(nextIsScaled)
|
||||||
}
|
}
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
const animatedContainerStyle = useAnimatedStyle(() => {
|
||||||
// Apply the active adjustments on top of the committed transform before the gestures.
|
// 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.
|
// This is matrix multiplication, so operations are applied in the reverse order.
|
||||||
let t = createTransform()
|
let t = createTransform()
|
||||||
@@ -293,19 +297,14 @@ const ImageItem = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<GestureDetector gesture={composedGesture}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
// Necessary to make opacity work for both children together.
|
style={[styles.container, animatedContainerStyle, {}]}>
|
||||||
renderToHardwareTextureAndroid
|
<Animated.View
|
||||||
style={[styles.container, animatedStyle, {}]}>
|
style={[
|
||||||
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
animatedStyle,
|
||||||
<GestureDetector gesture={composedGesture}>
|
{
|
||||||
<Image
|
|
||||||
contentFit="contain"
|
|
||||||
source={{uri: imageSrc.uri}}
|
|
||||||
placeholderContentFit="contain"
|
|
||||||
placeholder={{uri: imageSrc.thumbUri}}
|
|
||||||
style={{
|
|
||||||
width: SCREEN.width,
|
width: SCREEN.width,
|
||||||
height: imageAspect ? SCREEN.width / imageAspect : undefined,
|
height: imageAspect ? SCREEN.width / imageAspect : undefined,
|
||||||
borderRadius:
|
borderRadius:
|
||||||
@@ -314,14 +313,23 @@ const ImageItem = ({
|
|||||||
: imageSrc.type === 'rect-avi'
|
: imageSrc.type === 'rect-avi'
|
||||||
? 20
|
? 20
|
||||||
: 0,
|
: 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}
|
accessibilityLabel={imageSrc.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
cachePolicy="memory"
|
cachePolicy="memory"
|
||||||
/>
|
/>
|
||||||
</GestureDetector>
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
</Animated.View>
|
||||||
|
</GestureDetector>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import {
|
|||||||
PanGesture,
|
PanGesture,
|
||||||
} from 'react-native-gesture-handler'
|
} from 'react-native-gesture-handler'
|
||||||
import Animated, {runOnJS, useAnimatedRef} from 'react-native-reanimated'
|
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 {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||||
import {useImageDimensions} from '#/lib/media/image-sizes'
|
import {useImageDimensions} from '#/lib/media/image-sizes'
|
||||||
import {ImageSource} from '../../@types'
|
import {ImageSource} from '../../@types'
|
||||||
|
|
||||||
|
const AnimatedImage = Animated.createAnimatedComponent(Image)
|
||||||
|
|
||||||
const SCREEN = Dimensions.get('screen')
|
const SCREEN = Dimensions.get('screen')
|
||||||
const MAX_ORIGINAL_IMAGE_ZOOM = 2
|
const MAX_ORIGINAL_IMAGE_ZOOM = 2
|
||||||
const MIN_DOUBLE_TAP_SCALE = 2
|
const MIN_DOUBLE_TAP_SCALE = 2
|
||||||
@@ -32,6 +34,7 @@ type Props = {
|
|||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
showControls: boolean
|
showControls: boolean
|
||||||
dismissSwipePan: PanGesture | null
|
dismissSwipePan: PanGesture | null
|
||||||
|
animatedStyle: ImageStyle | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageItem = ({
|
const ImageItem = ({
|
||||||
@@ -40,6 +43,7 @@ const ImageItem = ({
|
|||||||
onZoom,
|
onZoom,
|
||||||
showControls,
|
showControls,
|
||||||
dismissSwipePan,
|
dismissSwipePan,
|
||||||
|
animatedStyle,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
|
const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
|
||||||
|
|
||||||
@@ -128,13 +132,10 @@ const ImageItem = ({
|
|||||||
onScroll={scrollHandler}
|
onScroll={scrollHandler}
|
||||||
bounces={scaled}
|
bounces={scaled}
|
||||||
centerContent>
|
centerContent>
|
||||||
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
|
<Animated.View
|
||||||
<Image
|
style={[
|
||||||
contentFit="contain"
|
animatedStyle,
|
||||||
source={{uri: imageSrc.uri}}
|
{
|
||||||
placeholderContentFit="contain"
|
|
||||||
placeholder={{uri: imageSrc.thumbUri}}
|
|
||||||
style={{
|
|
||||||
width: SCREEN.width,
|
width: SCREEN.width,
|
||||||
height: imageAspect ? SCREEN.width / imageAspect : undefined,
|
height: imageAspect ? SCREEN.width / imageAspect : undefined,
|
||||||
borderRadius:
|
borderRadius:
|
||||||
@@ -143,12 +144,21 @@ const ImageItem = ({
|
|||||||
: imageSrc.type === 'rect-avi'
|
: imageSrc.type === 'rect-avi'
|
||||||
? 20
|
? 20
|
||||||
: 0,
|
: 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}
|
accessibilityLabel={imageSrc.alt}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
enableLiveTextInteraction={showControls && !scaled}
|
enableLiveTextInteraction={showControls && !scaled}
|
||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
|
style={{flex: 1}}
|
||||||
/>
|
/>
|
||||||
|
</Animated.View>
|
||||||
</Animated.ScrollView>
|
</Animated.ScrollView>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {PanGesture} from 'react-native-gesture-handler'
|
import {PanGesture} from 'react-native-gesture-handler'
|
||||||
|
import {ImageStyle} from 'expo-image'
|
||||||
|
|
||||||
import {ImageSource} from '../../@types'
|
import {ImageSource} from '../../@types'
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ type Props = {
|
|||||||
isScrollViewBeingDragged: boolean
|
isScrollViewBeingDragged: boolean
|
||||||
showControls: boolean
|
showControls: boolean
|
||||||
dismissSwipePan: PanGesture | null
|
dismissSwipePan: PanGesture | null
|
||||||
|
animatedStyle: ImageStyle | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageItem = (_props: Props) => {
|
const ImageItem = (_props: Props) => {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ function ImageViewing({
|
|||||||
const [imageIndex, setImageIndex] = useState(initialImageIndex)
|
const [imageIndex, setImageIndex] = useState(initialImageIndex)
|
||||||
const [showControls, setShowControls] = useState(true)
|
const [showControls, setShowControls] = useState(true)
|
||||||
const dismissSwipeTranslateY = useSharedValue(0)
|
const dismissSwipeTranslateY = useSharedValue(0)
|
||||||
|
const isFlyingAway = useSharedValue(false)
|
||||||
|
|
||||||
const animatedHeaderStyle = useAnimatedStyle(() => {
|
const animatedHeaderStyle = useAnimatedStyle(() => {
|
||||||
const show = showControls && dismissSwipeTranslateY.value === 0
|
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(() => {
|
const activeImageStyle = useAnimatedStyle(() => {
|
||||||
if (openProgress.value === 1 || dismissSwipeTranslateY.value !== 0) {
|
if (openProgress.value === 1 || dismissSwipeTranslateY.value !== 0) {
|
||||||
return {
|
return {
|
||||||
pointerEvents: 'auto',
|
|
||||||
transform: [{translateY: dismissSwipeTranslateY.value}],
|
transform: [{translateY: dismissSwipeTranslateY.value}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,12 +121,10 @@ function ImageViewing({
|
|||||||
image.dimensions,
|
image.dimensions,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
pointerEvents: 'none',
|
|
||||||
transform: interpolatedTransform,
|
transform: interpolatedTransform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
pointerEvents: 'auto',
|
|
||||||
transform: [],
|
transform: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -136,6 +141,7 @@ function ImageViewing({
|
|||||||
.onEnd(e => {
|
.onEnd(e => {
|
||||||
'worklet'
|
'worklet'
|
||||||
if (Math.abs(e.velocityY) > 1000) {
|
if (Math.abs(e.velocityY) > 1000) {
|
||||||
|
isFlyingAway.value = true
|
||||||
dismissSwipeTranslateY.value = withDecay({
|
dismissSwipeTranslateY.value = withDecay({
|
||||||
velocity: e.velocityY,
|
velocity: e.velocityY,
|
||||||
velocityFactor: Math.max(3000 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
|
velocityFactor: Math.max(3000 / Math.abs(e.velocityY), 1), // Speed up if it's too slow.
|
||||||
@@ -198,7 +204,7 @@ function ImageViewing({
|
|||||||
edges={edges}
|
edges={edges}
|
||||||
aria-modal
|
aria-modal
|
||||||
accessibilityViewIsModal>
|
accessibilityViewIsModal>
|
||||||
<View style={[styles.container]}>
|
<Animated.View style={[styles.container, containerStyle]}>
|
||||||
<Animated.View style={[styles.backdrop, backdropStyle]} />
|
<Animated.View style={[styles.backdrop, backdropStyle]} />
|
||||||
<Animated.View style={[styles.header, animatedHeaderStyle]}>
|
<Animated.View style={[styles.header, animatedHeaderStyle]}>
|
||||||
<ImageDefaultHeader onRequestClose={onRequestClose} />
|
<ImageDefaultHeader onRequestClose={onRequestClose} />
|
||||||
@@ -216,9 +222,7 @@ function ImageViewing({
|
|||||||
overdrag={true}
|
overdrag={true}
|
||||||
style={styles.pager}>
|
style={styles.pager}>
|
||||||
{images.map((imageSrc, i) => (
|
{images.map((imageSrc, i) => (
|
||||||
<Animated.View
|
<View key={imageSrc.uri}>
|
||||||
key={imageSrc.uri}
|
|
||||||
style={imageIndex === i ? activeImageStyle : null}>
|
|
||||||
<ImageItem
|
<ImageItem
|
||||||
onTap={onTap}
|
onTap={onTap}
|
||||||
onZoom={onZoom}
|
onZoom={onZoom}
|
||||||
@@ -226,11 +230,12 @@ function ImageViewing({
|
|||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
isScrollViewBeingDragged={isDragging}
|
isScrollViewBeingDragged={isDragging}
|
||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
|
animatedStyle={imageIndex === i ? activeImageStyle : null}
|
||||||
dismissSwipePan={
|
dismissSwipePan={
|
||||||
imageIndex === i && !isDragging ? dismissSwipePan : null
|
imageIndex === i && !isDragging ? dismissSwipePan : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</PagerView>
|
</PagerView>
|
||||||
<Animated.View style={[styles.footer, animatedFooterStyle]}>
|
<Animated.View style={[styles.footer, animatedFooterStyle]}>
|
||||||
@@ -241,7 +246,7 @@ function ImageViewing({
|
|||||||
onPressShare={onPressShare}
|
onPressShare={onPressShare}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</Animated.View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user