Move swipe gesture handling upwards
This commit is contained in:
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user