expo-popcore-old/components/video/fullscreen-video-modal.tsx

286 lines
6.8 KiB
TypeScript
Raw Permalink 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 { VideoView, useVideoPlayer } from 'expo-video';
import React, { useCallback, useEffect, useMemo, useRef, useState } 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 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) {
// 使用useRef存储player实例避免无限循环
const playerRef = useRef<any>(null);
// 稳定videoUrl使用useMemo
const stableVideoUrl = useMemo(() => ({ uri: videoUrl }), [videoUrl]);
// 始终调用useVideoPlayer但只在videoUrl变化时更新player
const newPlayer = useVideoPlayer(
stableVideoUrl,
(player) => {
player.loop = isLooping;
player.muted = isMuted;
if (autoPlay) {
player.play();
}
}
);
// 只有当newPlayer变化时才更新player
useEffect(() => {
playerRef.current = newPlayer;
}, [newPlayer]);
// 获取当前的player实例
const player = playerRef.current || newPlayer;
const [isLoading, setIsLoading] = useState(true);
// 重置状态当模态框打开/关闭时
useEffect(() => {
if (visible) {
setIsLoading(true);
} else {
const currentPlayer = playerRef.current;
if (currentPlayer) {
currentPlayer.pause();
}
}
}, [visible, autoPlay]);
// 处理视频加载完成
const handleVideoReady = () => {
setIsLoading(false);
if (autoPlay) {
const currentPlayer = playerRef.current;
currentPlayer?.play();
}
};
// 处理视频错误
const handleVideoError = (error: any) => {
console.error('视频播放错误:', error);
setIsLoading(false);
};
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
// 视频点击处理(直接关闭)
const handleVideoPress = () => {
handleClose();
};
// 创建手势处理器
const panResponder = React.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;
if (gestureState.dx > threshold && hasPrevious) {
onPrevious?.();
} else if (gestureState.dx < -threshold && hasNext) {
onNext?.();
}
},
})
).current;
// 处理返回键Android
useEffect(() => {
const handleBackPress = () => {
if (visible) {
onClose();
return true;
}
return false;
};
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () => subscription.remove();
}, [visible, onClose]);
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
src={videoUrl}
style={styles.video}
autoPlay={autoPlay}
loop={isLooping}
muted={isMuted}
playsInline
onLoadedData={() => setIsLoading(false)}
onError={handleVideoError}
/>
) : (
<VideoView
player={player}
style={styles.video}
contentFit="contain"
nativeControls={false}
/>
)}
{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',
},
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;