This commit is contained in:
imeepos
2025-10-21 10:21:45 +08:00
parent 7e3f94bae3
commit 3a99ff96d5
28 changed files with 6981 additions and 176 deletions

View File

@@ -0,0 +1,294 @@
import { ThemedText } from '@/components/themed-text';
import { AVPlaybackStatus, ResizeMode, Video } from 'expo-av';
import React, { useEffect, useRef, useState } from 'react';
import {
ActivityIndicator,
Dimensions,
Modal,
PanResponder,
Platform,
StatusBar,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
export interface FullscreenVideoModalProps {
visible: boolean;
onClose: () => void;
videoUrl: string;
poster?: string;
autoPlay?: boolean;
isLooping?: boolean;
isMuted?: boolean;
onPrevious?: () => void;
onNext?: () => void;
hasNext?: boolean;
hasPrevious?: boolean;
}
export function FullscreenVideoModal({
visible,
onClose,
videoUrl,
poster,
autoPlay = true,
isLooping = true,
isMuted = false,
onPrevious,
onNext,
hasNext = false,
hasPrevious = false,
}: FullscreenVideoModalProps) {
const videoRef = useRef<Video>(null);
const [isLoading, setIsLoading] = useState(true);
const [isPlaying, setIsPlaying] = useState(autoPlay);
const [showControls, setShowControls] = useState(true);
// 重置状态当模态框打开/关闭时
useEffect(() => {
if (visible) {
setIsLoading(true);
setIsPlaying(autoPlay);
setShowControls(true);
} else {
// 停止播放当关闭时
if (videoRef.current) {
videoRef.current.stopAsync();
}
}
}, [visible, autoPlay]);
// 处理视频加载完成
const handleVideoReady = (status: AVPlaybackStatus) => {
if (status.isLoaded) {
setIsLoading(false);
if (autoPlay) {
videoRef.current?.playAsync();
}
}
};
// 处理视频播放状态变化
const handlePlaybackStatusUpdate = (status: AVPlaybackStatus) => {
if (status.isLoaded) {
setIsPlaying(status.isPlaying);
}
};
// 处理视频错误
const handleVideoError = (error: any) => {
console.error('视频播放错误:', error);
setIsLoading(false);
};
// 视频点击处理(直接关闭)
const handleVideoPress = () => {
handleClose();
};
// 关闭模态框
const handleClose = () => {
onClose();
};
// 创建手势处理器
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 backHandler = () => {
if (visible) {
handleClose();
return true;
}
return false;
};
// 在实际应用中,这里需要添加返回键监听
// 对于演示,我们只做概念性实现
return () => {
// 清理返回键监听
};
}, [visible]);
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.videoContainer}
onPress={handleVideoPress}
activeOpacity={1}
>
<View
style={styles.videoWrapper}
{...panResponder.panHandlers}
>
{/* 视频播放器 */}
{Platform.OS === 'web' ? (
<video
ref={videoRef as any}
src={videoUrl}
style={styles.video}
autoPlay={autoPlay}
loop={isLooping}
muted={isMuted}
playsInline
onLoadedData={() => setIsLoading(false)}
onError={handleVideoError}
/>
) : (
<Video
ref={videoRef}
source={{ uri: videoUrl }}
style={styles.video}
resizeMode={ResizeMode.CONTAIN}
shouldPlay={autoPlay}
isLooping={isLooping}
isMuted={isMuted}
useNativeControls={false}
onReadyForDisplay={handleVideoReady}
onPlaybackStatusUpdate={handlePlaybackStatusUpdate}
onError={handleVideoError}
/>
)}
{/* 加载指示器 */}
{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',
},
videoContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
videoWrapper: {
flex: 1,
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
video: {
width: screenWidth,
height: screenHeight,
backgroundColor: '#000',
objectFit: 'cover'
},
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 FullscreenVideoModal;