Move swipe gesture handling upwards

This commit is contained in:
Dan Abramov
2024-10-30 23:43:45 +00:00
parent 9c28b3a491
commit fe7d97b0bf
4 changed files with 59 additions and 78 deletions
@@ -1,14 +1,16 @@
import React, {useState} from 'react'
import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import {
Gesture,
GestureDetector,
PanGesture,
} from 'react-native-gesture-handler'
import Animated, {
runOnJS,
SharedValue,
useAnimatedReaction,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated'
import {Image} from 'expo-image'
@@ -44,15 +46,14 @@ type Props = {
onZoom: (isZoomed: boolean) => void
isScrollViewBeingDragged: boolean
showControls: boolean
dismissSwipeTranslateY: SharedValue<number>
dismissSwipePan: PanGesture
}
const ImageItem = ({
imageSrc,
onTap,
onZoom,
onRequestClose,
isScrollViewBeingDragged,
dismissSwipeTranslateY,
dismissSwipePan,
}: Props) => {
const [isScaled, setIsScaled] = useState(false)
const [imageAspect, imageDimensions] = useImageDimensions({
@@ -102,19 +103,8 @@ const ImageItem = ({
prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value)
prependTransform(t, committedTransform.value)
const [translateX, translateY, scale] = readTransform(t)
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},
],
transform: [{translateX}, {translateY: translateY}, {scale}],
}
})
@@ -292,33 +282,11 @@ const ImageItem = ({
committedTransform.value = withClampedSpring(finalTransform)
})
const dismissSwipePan = Gesture.Pan()
.enabled(!isScaled)
.activeOffsetY([-10, 10])
.failOffsetX([-10, 10])
.maxPointers(1)
.onUpdate(e => {
'worklet'
dismissSwipeTranslateY.value = e.translationY
})
.onEnd(e => {
'worklet'
if (Math.abs(e.velocityY) > 1000) {
dismissSwipeTranslateY.value = withDecay({velocity: e.velocityY})
runOnJS(onRequestClose)()
} else {
dismissSwipeTranslateY.value = withSpring(0, {
stiffness: 700,
damping: 50,
})
}
})
const composedGesture = isScrollViewBeingDragged
? // If the parent is not at rest, provide a no-op gesture.
Gesture.Manual()
: Gesture.Exclusive(
dismissSwipePan,
dismissSwipePan ?? Gesture.Manual(),
Gesture.Simultaneous(pinch, pan),
doubleTap,
singleTap,
@@ -7,23 +7,19 @@
*/
import React, {useState} from 'react'
import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import Animated, {
interpolate,
runOnJS,
SharedValue,
useAnimatedRef,
useAnimatedStyle,
} from 'react-native-reanimated'
import {ActivityIndicator, Dimensions, StyleSheet, View} from 'react-native'
import {
Gesture,
GestureDetector,
PanGesture,
} from 'react-native-gesture-handler'
import Animated, {runOnJS, useAnimatedRef} from 'react-native-reanimated'
import {Image} from 'expo-image'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useImageDimensions} from '#/lib/media/image-sizes'
import {ImageSource} from '../../@types'
const SWIPE_CLOSE_OFFSET = 75
const SWIPE_CLOSE_VELOCITY = 1
const SCREEN = Dimensions.get('screen')
const MAX_ORIGINAL_IMAGE_ZOOM = 2
const MIN_DOUBLE_TAP_SCALE = 2
@@ -35,16 +31,15 @@ type Props = {
onZoom: (scaled: boolean) => void
isScrollViewBeingDragged: boolean
showControls: boolean
dismissSwipeTranslateY: SharedValue<number>
dismissSwipePan: PanGesture | null
}
const ImageItem = ({
imageSrc,
onTap,
onZoom,
onRequestClose,
showControls,
dismissSwipeTranslateY,
dismissSwipePan,
}: Props) => {
const scrollViewRef = useAnimatedRef<Animated.ScrollView>()
@@ -57,33 +52,18 @@ const ImageItem = ({
? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM
: 1
const animatedStyle = useAnimatedStyle(() => {
return {
opacity: interpolate(
dismissSwipeTranslateY.value,
[-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
[0.5, 1, 0.5],
),
}
})
const scrollHandler = useAnimatedScrollHandler({
onScroll(e) {
const nextIsScaled = e.zoomScale > 1
dismissSwipeTranslateY.value = nextIsScaled ? 0 : e.contentOffset.y
if (scaled !== nextIsScaled) {
runOnJS(handleZoom)(nextIsScaled)
}
},
onEndDrag(e) {
const velocityY = e.velocity?.y ?? 0
const nextIsScaled = e.zoomScale > 1
if (scaled !== nextIsScaled) {
runOnJS(handleZoom)(nextIsScaled)
}
if (!nextIsScaled && Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY) {
runOnJS(onRequestClose)()
}
},
})
@@ -130,7 +110,11 @@ const ImageItem = ({
runOnJS(handleDoubleTap)(absoluteX, absoluteY)
})
const composedGesture = Gesture.Exclusive(doubleTap, singleTap)
const composedGesture = Gesture.Exclusive(
dismissSwipePan ?? Gesture.Manual(),
doubleTap,
singleTap,
)
return (
<GestureDetector gesture={composedGesture}>
@@ -142,8 +126,9 @@ const ImageItem = ({
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
maximumZoomScale={maxZoomScale}
onScroll={scrollHandler}>
<Animated.View style={[styles.imageScrollContainer, animatedStyle]}>
onScroll={scrollHandler}
bounces={scaled}>
<View style={styles.imageScrollContainer}>
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
<Image
contentFit="contain"
@@ -156,7 +141,7 @@ const ImageItem = ({
enableLiveTextInteraction={showControls && !scaled}
accessibilityIgnoresInvertColors
/>
</Animated.View>
</View>
</Animated.ScrollView>
</GestureDetector>
)
@@ -2,7 +2,7 @@
import React from 'react'
import {View} from 'react-native'
import {SharedValue} from 'react-native-reanimated'
import {PanGesture} from 'react-native-gesture-handler'
import {ImageSource} from '../../@types'
@@ -13,7 +13,7 @@ type Props = {
onZoom: (scaled: boolean) => void
isScrollViewBeingDragged: boolean
showControls: boolean
dismissSwipeTranslateY: SharedValue<number>
dismissSwipePan: PanGesture | null
}
const ImageItem = (_props: Props) => {
+30 -2
View File
@@ -16,11 +16,14 @@ import {
StyleSheet,
View,
} from 'react-native'
import {Gesture} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view'
import {MeasuredDimensions} from 'react-native-reanimated'
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withDecay,
withSpring,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
@@ -86,6 +89,29 @@ function ImageViewing({
],
}))
const dismissSwipePan = Gesture.Pan()
.enabled(!isScaled)
.activeOffsetY([-10, 10])
.failOffsetX([-10, 10])
.maxPointers(1)
.onUpdate(e => {
'worklet'
dismissSwipeTranslateY.value = e.translationY
console.log(dismissSwipeTranslateY.value)
})
.onEnd(e => {
'worklet'
if (Math.abs(e.velocityY) > 1000) {
dismissSwipeTranslateY.value = withDecay({velocity: e.velocityY})
runOnJS(onRequestClose)()
} else {
dismissSwipeTranslateY.value = withSpring(0, {
stiffness: 700,
damping: 50,
})
}
})
const onTap = useCallback(() => {
setShowControls(show => !show)
}, [])
@@ -130,7 +156,7 @@ function ImageViewing({
}}
overdrag={true}
style={styles.pager}>
{images.map(imageSrc => (
{images.map((imageSrc, i) => (
<View key={imageSrc.uri}>
<ImageItem
onTap={onTap}
@@ -139,7 +165,9 @@ function ImageViewing({
onRequestClose={onRequestClose}
isScrollViewBeingDragged={isDragging}
showControls={showControls}
dismissSwipeTranslateY={dismissSwipeTranslateY}
dismissSwipePan={
imageIndex === i && !isDragging ? dismissSwipePan : null
}
/>
</View>
))}