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