import React, { useRef, useState, useEffect } from 'react'; import { View, StyleSheet, Platform, useWindowDimensions, PanResponder } from 'react-native'; import { Image } from 'expo-image'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { FlatList } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS, } from 'react-native-reanimated'; import CustomVideoPlayer from './CustomVideoPlayer'; interface FullScreenSwiperProps { assets: any[]; initialIndex: number; onClose: () => void; onIndexChange: (index: number) => void; } /* ───────────────────────────────────────────────────────────── ZoomableImage • Gesture.Pinch() — native multi-touch pinch-to-zoom • Gesture.Pan() — single-finger swipe-down-to-close • Both run on the UI thread via reanimated worklets ───────────────────────────────────────────────────────────── */ function ZoomableImage({ uri, thumbUri, width, height, onSwipeDown, onZoomChange, }: { uri: string; thumbUri?: string; width: number; onSwipeDown: () => void; onZoomChange: (zooming: boolean) => void; }) { const scale = useSharedValue(1); const savedScale = useSharedValue(1); const translateY = useSharedValue(0); const pinchGesture = Gesture.Pinch() .onStart(() => { 'worklet'; runOnJS(onZoomChange)(true); }) .onUpdate((e) => { 'worklet'; scale.value = Math.max(0.5, Math.min(5, savedScale.value * e.scale)); }) .onEnd(() => { 'worklet'; runOnJS(onZoomChange)(false); if (scale.value < 1.15) { scale.value = withSpring(1, { damping: 15, stiffness: 150 }); savedScale.value = 1; } else { savedScale.value = Math.min(5, scale.value); } }); const panGesture = Gesture.Pan() .minPointers(1) .maxPointers(1) .activeOffsetY(20) .failOffsetX([-15, 15]) .onStart(() => { 'worklet'; runOnJS(onZoomChange)(true); }) .onUpdate((e) => { 'worklet'; translateY.value = e.translationY; }) .onEnd((e) => { 'worklet'; runOnJS(onZoomChange)(false); if (e.translationY > 100) { runOnJS(onSwipeDown)(); } else { translateY.value = withSpring(0, { damping: 15, stiffness: 150 }); } }); const composedGesture = Gesture.Simultaneous(pinchGesture, panGesture); const animatedStyle = useAnimatedStyle(() => ({ transform: [ { scale: scale.value }, { translateY: translateY.value }, ], })); return ( ); } /* ───────────────────────────────────────────────────────────── FullScreenSwiper Uses RNGH's FlatList so gesture handlers inside items properly negotiate with the scroll gesture on Android. ───────────────────────────────────────────────────────────── */ export default function FullScreenSwiper({ assets, initialIndex, onClose, onIndexChange, }: FullScreenSwiperProps) { const { width } = useWindowDimensions(); const [activeIndex, setActiveIndex] = useState(initialIndex); const [scrollEnabled, setScrollEnabled] = useState(true); const onIndexChangeRef = useRef(onIndexChange); useEffect(() => { onIndexChangeRef.current = onIndexChange; }, [onIndexChange]); const onViewableItemsChanged = useRef(({ viewableItems }: any) => { if (viewableItems.length > 0) { const idx = viewableItems[0].index; setActiveIndex(idx); onIndexChangeRef.current(idx); } }).current; const viewabilityConfig = useRef({ itemVisiblePercentThreshold: 50 }).current; const renderItem = ({ item, index }: { item: any; index: number }) => { const isVideo = item.mediaType === 'video' || item.filename?.toLowerCase().match(/\.(mp4|mov|avi|webm)$/i); const isActive = index === activeIndex; if (isVideo) { return ( setScrollEnabled(false)} onScrubEnd={() => setScrollEnabled(true)} /> ); } return ( setScrollEnabled(!zooming)} /> ); }; return ( item.id} renderItem={renderItem} horizontal pagingEnabled scrollEnabled={scrollEnabled} showsHorizontalScrollIndicator={false} initialScrollIndex={activeIndex} getItemLayout={(_, index) => ({ length: width, offset: width * index, index, })} onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={viewabilityConfig} windowSize={3} maxToRenderPerBatch={3} removeClippedSubviews={Platform.OS === 'android'} extraData={activeIndex} /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', }, });