bw-expo-app/components/image/fullscreen-image-modal.tsx

242 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ThemedText } from '@/components/themed-text';
import { Image } from 'expo-image';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import {
ActivityIndicator,
Dimensions,
Modal,
PanResponder,
Platform,
StatusBar,
StyleSheet,
TouchableOpacity,
View,
BackHandler,
} from 'react-native';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
export interface FullscreenImageModalProps {
visible: boolean;
onClose: () => void;
imageUrl: string;
onPrevious?: () => void;
onNext?: () => void;
hasNext?: boolean;
hasPrevious?: boolean;
}
export function FullscreenImageModal({
visible,
onClose,
imageUrl,
onPrevious,
onNext,
hasNext = false,
hasPrevious = false,
}: FullscreenImageModalProps) {
const [isLoading, setIsLoading] = useState(true);
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
// 重置状态当模态框打开/关闭时
useEffect(() => {
if (visible) {
setIsLoading(true);
}
}, [visible]);
// 处理图片加载完成
const handleImageLoad = () => {
setIsLoading(false);
};
// 处理图片加载错误
const handleImageError = (error: any) => {
console.error('图片加载错误:', error);
setIsLoading(false);
};
// 图片点击处理(直接关闭)
const handleImagePress = () => {
handleClose();
};
// 创建手势处理器
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (_, gestureState) => {
// 只有水平移动时才响应
return Math.abs(gestureState.dx) > Math.abs(gestureState.dy) && Math.abs(gestureState.dx) > 10;
},
onPanResponderRelease: (_, gestureState) => {
const threshold = screenWidth / 4; // 滑动屏幕宽度的1/4触发切换
if (gestureState.dx > threshold && hasPrevious) {
// 向右滑动,显示上一个
onPrevious?.();
} else if (gestureState.dx < -threshold && hasNext) {
// 向左滑动,显示下一个
onNext?.();
}
},
})
).current;
// 处理返回键Android
useEffect(() => {
const handleBackPress = () => {
if (visible) {
handleClose();
return true;
}
return false;
};
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => subscription.remove();
}, [visible, handleClose]);
if (!visible) return null;
return (
<Modal
visible={visible}
transparent={false}
animationType="fade"
onRequestClose={handleClose}
statusBarTranslucent={true}
>
<View style={styles.container}>
{/* 状态栏处理 */}
{Platform.OS === 'android' && <StatusBar hidden />}
{/* 图片容器 */}
<TouchableOpacity
style={styles.imageContainer}
onPress={handleImagePress}
activeOpacity={1}
>
<View
style={styles.imageWrapper}
{...panResponder.panHandlers}
>
{/* 图片显示 */}
<Image
source={{ uri: imageUrl }}
style={styles.image}
contentFit="contain"
onLoad={handleImageLoad}
onError={handleImageError}
placeholder={{ blurhash: 'L6PZfSi_.AyE_3t7t7R**0o#DgR4' }}
transition={300}
/>
{/* 加载指示器 */}
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#4ECDC4" />
</View>
)}
{/* 左右导航指示器 */}
{hasPrevious && (
<TouchableOpacity
style={styles.leftIndicator}
onPress={(e) => {
e.stopPropagation();
onPrevious?.();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
{hasNext && (
<TouchableOpacity
style={styles.rightIndicator}
onPress={(e) => {
e.stopPropagation();
onNext?.();
}}
activeOpacity={0.7}
>
<ThemedText style={styles.indicatorIcon}></ThemedText>
</TouchableOpacity>
)}
</View>
</TouchableOpacity>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
imageContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
imageWrapper: {
flex: 1,
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: screenWidth,
height: screenHeight,
backgroundColor: '#000',
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
leftIndicator: {
position: 'absolute',
left: 20,
top: '50%',
marginTop: -20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
rightIndicator: {
position: 'absolute',
right: 20,
top: '50%',
marginTop: -20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
indicatorIcon: {
fontSize: 24,
color: '#fff',
fontWeight: 'bold',
},
});
export default FullscreenImageModal;