This commit is contained in:
imeepos
2026-01-29 16:42:42 +08:00
parent 1ccdc23355
commit a087cafc99
6 changed files with 553 additions and 80 deletions

View File

@@ -12,6 +12,7 @@ import {
Dimensions, Dimensions,
} from 'react-native' } from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { TitleBar, HeroSlider, TabNavigation, TemplateCard, TemplateGrid } from '@/components/blocks/home' import { TitleBar, HeroSlider, TabNavigation, TemplateCard, TemplateGrid } from '@/components/blocks/home'
import ErrorState from '@/components/ErrorState' import ErrorState from '@/components/ErrorState'
@@ -25,6 +26,7 @@ import { useTemplateFilter } from '@/hooks/use-template-filter'
import { useUserBalance } from '@/hooks/use-user-balance' import { useUserBalance } from '@/hooks/use-user-balance'
import { useTemplateLike, useTemplateFavorite } from '@/hooks' import { useTemplateLike, useTemplateFavorite } from '@/hooks'
import { useTemplateSocialStore } from '@/stores/templateSocialStore' import { useTemplateSocialStore } from '@/stores/templateSocialStore'
import { useSwipeNavigation } from '@/hooks/use-swipe-navigation'
import { root } from '@repo/core' import { root } from '@repo/core'
import { TemplateSocialController } from '@repo/sdk' import { TemplateSocialController } from '@repo/sdk'
import { handleError } from '@/hooks/use-error' import { handleError } from '@/hooks/use-error'
@@ -160,6 +162,26 @@ export default function HomeScreen() {
} }
}, [loadingMore, hasMore, isLoading, loadMore]) }, [loadingMore, hasMore, isLoading, loadMore])
// 左右滑动切换分类
const handleSwipeLeft = useCallback(() => {
if (activeIndex < tabs.length - 1) {
selectTab(activeIndex + 1)
}
}, [activeIndex, tabs.length, selectTab])
const handleSwipeRight = useCallback(() => {
if (activeIndex > 0) {
selectTab(activeIndex - 1)
}
}, [activeIndex, selectTab])
const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({
onSwipeLeft: handleSwipeLeft,
onSwipeRight: handleSwipeRight,
canSwipeLeft: activeIndex < tabs.length - 1,
canSwipeRight: activeIndex > 0,
})
// 导航处理 - 使用 useCallback memoize // 导航处理 - 使用 useCallback memoize
const handlePointsPress = useCallback(() => router.push('/membership' as any), [router]) const handlePointsPress = useCallback(() => router.push('/membership' as any), [router])
const handleSearchPress = useCallback(() => router.push('/searchTemplate'), [router]) const handleSearchPress = useCallback(() => router.push('/searchTemplate'), [router])
@@ -377,6 +399,12 @@ export default function HomeScreen() {
</View> </View>
)} )}
<PanGestureHandler
onGestureEvent={handleGestureEvent}
onHandlerStateChange={handleGestureStateChange}
activeOffsetX={[-20, 20]}
>
<View style={styles.scrollView}>
<FlatList <FlatList
data={showTemplateList ? validTemplates : []} data={showTemplateList ? validTemplates : []}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
@@ -412,6 +440,8 @@ export default function HomeScreen() {
/> />
} }
/> />
</View>
</PanGestureHandler>
</SafeAreaView> </SafeAreaView>
) )
} }

View File

@@ -12,16 +12,18 @@ import { StatusBar } from 'expo-status-bar'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useRouter, useFocusEffect } from 'expo-router' import { useRouter, useFocusEffect } from 'expo-router'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { useMessages, type Message } from '@/hooks/use-messages' import { useMessages, type Message } from '@/hooks/use-messages'
import { useMessageActions } from '@/hooks/use-message-actions' import { useMessageActions } from '@/hooks/use-message-actions'
import { useAnnouncements } from '@/hooks/use-announcements' import { useAnnouncements } from '@/hooks/use-announcements'
import { useSwipeNavigation } from '@/hooks/use-swipe-navigation'
import LoadingState from '@/components/LoadingState' import LoadingState from '@/components/LoadingState'
import ErrorState from '@/components/ErrorState' import ErrorState from '@/components/ErrorState'
import PaginationLoader from '@/components/PaginationLoader' import PaginationLoader from '@/components/PaginationLoader'
import { MessageCard } from '@/components/message/MessageCard' import { MessageCard } from '@/components/message/MessageCard'
import { MessageTabBar, type MessageType } from '@/components/message/MessageTabBar' import { MessageTabBar, type MessageType, TAB_ITEMS } from '@/components/message/MessageTabBar'
import { AnnouncementBanner } from '@/components/message/AnnouncementBanner' import { AnnouncementBanner } from '@/components/message/AnnouncementBanner'
import { SwipeToDelete } from '@/components/message/SwipeToDelete' import { SwipeToDelete } from '@/components/message/SwipeToDelete'
import { MessageEmptyState } from '@/components/message/MessageEmptyState' import { MessageEmptyState } from '@/components/message/MessageEmptyState'
@@ -50,6 +52,9 @@ export default function MessageScreen() {
const router = useRouter() const router = useRouter()
const [activeTab, setActiveTab] = useState<MessageType>(undefined) const [activeTab, setActiveTab] = useState<MessageType>(undefined)
// 获取当前 Tab 索引
const activeTabIndex = TAB_ITEMS.findIndex(tab => tab.type === activeTab)
// Hooks // Hooks
const { const {
messages: allMessages, messages: allMessages,
@@ -95,6 +100,26 @@ export default function MessageScreen() {
setActiveTab(type) setActiveTab(type)
}, []) }, [])
// 左右滑动切换消息类型 Tab
const handleSwipeLeft = useCallback(() => {
if (activeTabIndex < TAB_ITEMS.length - 1) {
setActiveTab(TAB_ITEMS[activeTabIndex + 1].type)
}
}, [activeTabIndex])
const handleSwipeRight = useCallback(() => {
if (activeTabIndex > 0) {
setActiveTab(TAB_ITEMS[activeTabIndex - 1].type)
}
}, [activeTabIndex])
const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({
onSwipeLeft: handleSwipeLeft,
onSwipeRight: handleSwipeRight,
canSwipeLeft: activeTabIndex < TAB_ITEMS.length - 1,
canSwipeRight: activeTabIndex > 0,
})
// Handle mark all read // Handle mark all read
const handleMarkAllRead = useCallback(async () => { const handleMarkAllRead = useCallback(async () => {
const unreadMessages = filteredMessages.filter(m => !m.isRead) const unreadMessages = filteredMessages.filter(m => !m.isRead)
@@ -235,6 +260,12 @@ export default function MessageScreen() {
<MessageTabBar activeTab={activeTab} onTabChange={handleTabChange} /> <MessageTabBar activeTab={activeTab} onTabChange={handleTabChange} />
{/* Message List */} {/* Message List */}
<PanGestureHandler
onGestureEvent={handleGestureEvent}
onHandlerStateChange={handleGestureStateChange}
activeOffsetX={[-20, 20]}
>
<View style={styles.gestureContainer}>
<FlatList <FlatList
testID="message-list" testID="message-list"
data={filteredMessages} data={filteredMessages}
@@ -258,6 +289,8 @@ export default function MessageScreen() {
/> />
} }
/> />
</View>
</PanGestureHandler>
</SafeAreaView> </SafeAreaView>
) )
} }
@@ -267,6 +300,9 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: '#090A0B', backgroundColor: '#090A0B',
}, },
gestureContainer: {
flex: 1,
},
header: { header: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',

View File

@@ -17,6 +17,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { Image } from 'expo-image' import { Image } from 'expo-image'
import { useRouter } from 'expo-router' import { useRouter } from 'expo-router'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { PointsIcon, SearchIcon, SettingsIcon } from '@/components/icon' import { PointsIcon, SearchIcon, SettingsIcon } from '@/components/icon'
import EditProfileDrawer from '@/components/drawer/EditProfileDrawer' import EditProfileDrawer from '@/components/drawer/EditProfileDrawer'
import Dropdown from '@/components/ui/dropdown' import Dropdown from '@/components/ui/dropdown'
@@ -30,6 +31,7 @@ import {
} from '@/hooks' } from '@/hooks'
import { MySkeleton } from '@/components/skeleton/MySkeleton' import { MySkeleton } from '@/components/skeleton/MySkeleton'
import { TabNavigation } from '@/components/blocks/home/TabNavigation' import { TabNavigation } from '@/components/blocks/home/TabNavigation'
import { useSwipeNavigation } from '@/hooks/use-swipe-navigation'
import { useUserBalance } from '@/hooks/use-user-balance' import { useUserBalance } from '@/hooks/use-user-balance'
@@ -115,6 +117,26 @@ export default function My() {
else if (index === 2) setActiveTab('likes') else if (index === 2) setActiveTab('likes')
}, []) }, [])
// 左右滑动切换 Tab
const handleSwipeLeft = useCallback(() => {
if (activeTabIndex < tabs.length - 1) {
handleTabPress(activeTabIndex + 1)
}
}, [activeTabIndex, tabs.length, handleTabPress])
const handleSwipeRight = useCallback(() => {
if (activeTabIndex > 0) {
handleTabPress(activeTabIndex - 1)
}
}, [activeTabIndex, handleTabPress])
const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({
onSwipeLeft: handleSwipeLeft,
onSwipeRight: handleSwipeRight,
canSwipeLeft: activeTabIndex < tabs.length - 1,
canSwipeRight: activeTabIndex > 0,
})
// 调试日志 // 调试日志
useEffect(() => { useEffect(() => {
console.log('📊 作品列表状态:', { console.log('📊 作品列表状态:', {
@@ -346,7 +368,13 @@ export default function My() {
onTabPress={handleTabPress} onTabPress={handleTabPress}
/> />
{/* 作品九宫格 */} {/* 作品九宫格 - 包裹在手势处理器中 */}
<PanGestureHandler
onGestureEvent={handleGestureEvent}
onHandlerStateChange={handleGestureStateChange}
activeOffsetX={[-20, 20]}
>
<View style={styles.gestureContainer}>
{activeTab === 'works' && loading ? ( {activeTab === 'works' && loading ? (
<MySkeleton /> <MySkeleton />
) : activeTab === 'works' ? ( ) : activeTab === 'works' ? (
@@ -549,6 +577,8 @@ export default function My() {
</View> </View>
</ScrollView> </ScrollView>
) : null} ) : null}
</View>
</PanGestureHandler>
{/* 编辑资料抽屉 */} {/* 编辑资料抽屉 */}
<EditProfileDrawer <EditProfileDrawer
@@ -569,6 +599,9 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: '#090A0B', backgroundColor: '#090A0B',
}, },
gestureContainer: {
flex: 1,
},
topBar: { topBar: {
paddingHorizontal: GALLERY_HORIZONTAL_PADDING, paddingHorizontal: GALLERY_HORIZONTAL_PADDING,
paddingTop: 19, paddingTop: 19,

View File

@@ -15,6 +15,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { Image, ImageLoadEventData } from 'expo-image' import { Image, ImageLoadEventData } from 'expo-image'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Ionicons } from '@expo/vector-icons' import { Ionicons } from '@expo/vector-icons'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { SameStyleIcon, WhiteStarIcon } from '@/components/icon' import { SameStyleIcon, WhiteStarIcon } from '@/components/icon'
import { useRouter } from 'expo-router' import { useRouter } from 'expo-router'
@@ -22,6 +23,7 @@ import { useTemplates, type TemplateDetail } from '@/hooks'
import { VideoSocialButton } from '@/components/blocks/ui/VideoSocialButton' import { VideoSocialButton } from '@/components/blocks/ui/VideoSocialButton'
import { useTemplateLike } from '@/hooks/use-template-like' import { useTemplateLike } from '@/hooks/use-template-like'
import { useTemplateFavorite } from '@/hooks/use-template-favorite' import { useTemplateFavorite } from '@/hooks/use-template-favorite'
import { useSwipeNavigation } from '@/hooks/use-swipe-navigation'
import { import {
useTemplateSocialStore, useTemplateSocialStore,
useTemplateLiked, useTemplateLiked,
@@ -219,6 +221,7 @@ export const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; vi
}) })
export default function VideoScreen() { export default function VideoScreen() {
const router = useRouter()
const flatListRef = useRef<FlatList>(null) const flatListRef = useRef<FlatList>(null)
const videoHeight = screenHeight - TAB_BAR_HEIGHT const videoHeight = screenHeight - TAB_BAR_HEIGHT
const PAGE_SIZE = 10 // 每页10个数据 const PAGE_SIZE = 10 // 每页10个数据
@@ -247,6 +250,21 @@ export default function VideoScreen() {
loadMore() loadMore()
}, [hasMore, loading, loadMore, templates.length]) }, [hasMore, loading, loadMore, templates.length])
// 左右滑动切换底部 Tab 导航
// 视频页是第二个 Tab (index=1),左滑去消息页,右滑去首页
const handleSwipeLeft = useCallback(() => {
router.push('/(tabs)/message')
}, [router])
const handleSwipeRight = useCallback(() => {
router.push('/(tabs)/')
}, [router])
const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({
onSwipeLeft: handleSwipeLeft,
onSwipeRight: handleSwipeRight,
})
const renderItem = useCallback(({ item }: { item: TemplateDetail }) => ( const renderItem = useCallback(({ item }: { item: TemplateDetail }) => (
<VideoItem item={item} videoHeight={videoHeight} /> <VideoItem item={item} videoHeight={videoHeight} />
), [videoHeight]) ), [videoHeight])
@@ -310,6 +328,12 @@ export default function VideoScreen() {
return ( return (
<Layout> <Layout>
<PanGestureHandler
onGestureEvent={handleGestureEvent}
onHandlerStateChange={handleGestureStateChange}
activeOffsetX={[-20, 20]}
>
<View style={styles.gestureContainer}>
<FlatList <FlatList
ref={flatListRef} ref={flatListRef}
data={filteredTemplates} data={filteredTemplates}
@@ -328,6 +352,8 @@ export default function VideoScreen() {
windowSize={7} windowSize={7}
initialNumToRender={3} initialNumToRender={3}
/> />
</View>
</PanGestureHandler>
</Layout> </Layout>
) )
} }
@@ -337,6 +363,9 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: '#090A0B', backgroundColor: '#090A0B',
}, },
gestureContainer: {
flex: 1,
},
centerContainer: { centerContainer: {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',

View File

@@ -0,0 +1,227 @@
/**
* @file use-swipe-navigation.test.ts
* @description Tests for useSwipeNavigation hook
*
* This hook provides swipe gesture handling for tab navigation.
* It calculates swipe direction and triggers tab changes based on gesture velocity and distance.
*/
import { renderHook, act } from '@testing-library/react-native'
import { useSwipeNavigation } from './use-swipe-navigation'
describe('useSwipeNavigation', () => {
const mockOnSwipeLeft = jest.fn()
const mockOnSwipeRight = jest.fn()
beforeEach(() => {
jest.clearAllMocks()
})
describe('initialization', () => {
it('should initialize with default values', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
})
)
expect(result.current.handleGestureEvent).toBeDefined()
expect(result.current.handleGestureStateChange).toBeDefined()
})
it('should accept custom threshold', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
threshold: 100,
})
)
expect(result.current.handleGestureEvent).toBeDefined()
})
it('should accept custom velocity threshold', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
velocityThreshold: 500,
})
)
expect(result.current.handleGestureEvent).toBeDefined()
})
})
describe('swipe detection', () => {
it('should call onSwipeLeft when swiping left with sufficient distance', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
threshold: 50,
})
)
// Simulate gesture state change with left swipe
act(() => {
result.current.handleGestureStateChange({
nativeEvent: {
state: 5, // END state
translationX: -100,
velocityX: -200,
},
} as any)
})
expect(mockOnSwipeLeft).toHaveBeenCalledTimes(1)
expect(mockOnSwipeRight).not.toHaveBeenCalled()
})
it('should call onSwipeRight when swiping right with sufficient distance', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
threshold: 50,
})
)
// Simulate gesture state change with right swipe
act(() => {
result.current.handleGestureStateChange({
nativeEvent: {
state: 5, // END state
translationX: 100,
velocityX: 200,
},
} as any)
})
expect(mockOnSwipeRight).toHaveBeenCalledTimes(1)
expect(mockOnSwipeLeft).not.toHaveBeenCalled()
})
it('should call onSwipeLeft when velocity is high even with small distance', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
threshold: 100,
velocityThreshold: 300,
})
)
// Simulate gesture with high velocity but small distance
act(() => {
result.current.handleGestureStateChange({
nativeEvent: {
state: 5, // END state
translationX: -30,
velocityX: -500,
},
} as any)
})
expect(mockOnSwipeLeft).toHaveBeenCalledTimes(1)
})
it('should not trigger swipe when distance and velocity are below thresholds', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
threshold: 100,
velocityThreshold: 500,
})
)
// Simulate gesture with small distance and low velocity
act(() => {
result.current.handleGestureStateChange({
nativeEvent: {
state: 5, // END state
translationX: -30,
velocityX: -100,
},
} as any)
})
expect(mockOnSwipeLeft).not.toHaveBeenCalled()
expect(mockOnSwipeRight).not.toHaveBeenCalled()
})
})
describe('disabled state', () => {
it('should not trigger swipe when disabled', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
enabled: false,
})
)
act(() => {
result.current.handleGestureStateChange({
nativeEvent: {
state: 5, // END state
translationX: -100,
velocityX: -500,
},
} as any)
})
expect(mockOnSwipeLeft).not.toHaveBeenCalled()
expect(mockOnSwipeRight).not.toHaveBeenCalled()
})
})
describe('boundary conditions', () => {
it('should not call onSwipeLeft when canSwipeLeft is false', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
canSwipeLeft: false,
})
)
act(() => {
result.current.handleGestureStateChange({
nativeEvent: {
state: 5, // END state
translationX: -100,
velocityX: -500,
},
} as any)
})
expect(mockOnSwipeLeft).not.toHaveBeenCalled()
})
it('should not call onSwipeRight when canSwipeRight is false', () => {
const { result } = renderHook(() =>
useSwipeNavigation({
onSwipeLeft: mockOnSwipeLeft,
onSwipeRight: mockOnSwipeRight,
canSwipeRight: false,
})
)
act(() => {
result.current.handleGestureStateChange({
nativeEvent: {
state: 5, // END state
translationX: 100,
velocityX: 500,
},
} as any)
})
expect(mockOnSwipeRight).not.toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,118 @@
/**
* @file use-swipe-navigation.ts
* @description Hook for handling swipe gestures to navigate between tabs
*
* This hook provides gesture handlers for PanGestureHandler that detect
* horizontal swipes and trigger navigation callbacks.
*/
import { useCallback, useRef } from 'react'
import { Dimensions } from 'react-native'
import type { PanGestureHandlerGestureEvent, PanGestureHandlerStateChangeEvent } from 'react-native-gesture-handler'
const SCREEN_WIDTH = Dimensions.get('window').width
const DEFAULT_THRESHOLD = SCREEN_WIDTH * 0.2 // 20% of screen width
const DEFAULT_VELOCITY_THRESHOLD = 300 // pixels per second
// Gesture handler state constants (to avoid import issues in tests)
const GESTURE_STATE_END = 5
export interface UseSwipeNavigationOptions {
/** Callback when user swipes left (to go to next tab) */
onSwipeLeft: () => void
/** Callback when user swipes right (to go to previous tab) */
onSwipeRight: () => void
/** Minimum distance to trigger swipe (default: 20% of screen width) */
threshold?: number
/** Minimum velocity to trigger swipe regardless of distance (default: 300) */
velocityThreshold?: number
/** Whether swipe navigation is enabled (default: true) */
enabled?: boolean
/** Whether user can swipe left (default: true) */
canSwipeLeft?: boolean
/** Whether user can swipe right (default: true) */
canSwipeRight?: boolean
}
export interface UseSwipeNavigationReturn {
/** Handler for gesture events (for tracking) */
handleGestureEvent: (event: PanGestureHandlerGestureEvent) => void
/** Handler for gesture state changes (for triggering navigation) */
handleGestureStateChange: (event: PanGestureHandlerStateChangeEvent) => void
}
/**
* Hook for handling swipe gestures to navigate between tabs
*
* @example
* ```tsx
* const { handleGestureEvent, handleGestureStateChange } = useSwipeNavigation({
* onSwipeLeft: () => setActiveTab(prev => prev + 1),
* onSwipeRight: () => setActiveTab(prev => prev - 1),
* canSwipeLeft: activeTab < tabs.length - 1,
* canSwipeRight: activeTab > 0,
* })
*
* return (
* <PanGestureHandler
* onGestureEvent={handleGestureEvent}
* onHandlerStateChange={handleGestureStateChange}
* >
* <View>{children}</View>
* </PanGestureHandler>
* )
* ```
*/
export function useSwipeNavigation({
onSwipeLeft,
onSwipeRight,
threshold = DEFAULT_THRESHOLD,
velocityThreshold = DEFAULT_VELOCITY_THRESHOLD,
enabled = true,
canSwipeLeft = true,
canSwipeRight = true,
}: UseSwipeNavigationOptions): UseSwipeNavigationReturn {
// Track translation for potential animation use
const translationX = useRef(0)
const handleGestureEvent = useCallback(
(event: PanGestureHandlerGestureEvent) => {
if (!enabled) return
translationX.current = event.nativeEvent.translationX
},
[enabled]
)
const handleGestureStateChange = useCallback(
(event: PanGestureHandlerStateChangeEvent) => {
if (!enabled) return
const { state, translationX: tx, velocityX } = event.nativeEvent
// Only process when gesture ends
if (state !== GESTURE_STATE_END) return
const isSwipeLeft = tx < 0
const isSwipeRight = tx > 0
const absTranslation = Math.abs(tx)
const absVelocity = Math.abs(velocityX)
// Check if swipe meets threshold (distance OR velocity)
const meetsThreshold = absTranslation >= threshold || absVelocity >= velocityThreshold
if (!meetsThreshold) return
if (isSwipeLeft && canSwipeLeft) {
onSwipeLeft()
} else if (isSwipeRight && canSwipeRight) {
onSwipeRight()
}
},
[enabled, threshold, velocityThreshold, canSwipeLeft, canSwipeRight, onSwipeLeft, onSwipeRight]
)
return {
handleGestureEvent,
handleGestureStateChange,
}
}