🐛 修复视频播放器无限循环渲染问题
## 主要修复 - 修复 FullscreenMediaModal 和 FullscreenVideoModal 中的 useEffect 循环依赖 - 重构 VideoPlayer 组件的视频属性管理逻辑 - 优化 useVideoPlayer 的初始化回调机制 ## 新增功能 - 新增标签 API 支持 (lib/api/tags.ts) - 新增内容骨架屏组件 (components/profile/content-skeleton.tsx) - 新增返回按钮组件 (components/ui/back-button.tsx) ## 改进优化 - 优化视频播放器的性能,避免重复初始化 - 修复 useEffect 依赖项导致的无限循环更新 - 完善类型定义和 API 接口 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ThemedText } from '@/components/themed-text';
|
||||
import { type VideoReadyForDisplayEvent, ResizeMode, Video } from 'expo-av';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { VideoView, useVideoPlayer } from 'expo-video';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
@@ -43,7 +43,19 @@ export function FullscreenVideoModal({
|
||||
hasNext = false,
|
||||
hasPrevious = false,
|
||||
}: FullscreenVideoModalProps) {
|
||||
const videoRef = useRef<Video>(null);
|
||||
const player = useVideoPlayer(
|
||||
{
|
||||
uri: videoUrl,
|
||||
},
|
||||
(player) => {
|
||||
player.loop = isLooping;
|
||||
player.muted = isMuted;
|
||||
if (autoPlay) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 重置状态当模态框打开/关闭时
|
||||
@@ -51,18 +63,17 @@ export function FullscreenVideoModal({
|
||||
if (visible) {
|
||||
setIsLoading(true);
|
||||
} else {
|
||||
// 停止播放当关闭时
|
||||
if (videoRef.current) {
|
||||
videoRef.current.stopAsync();
|
||||
if (player) {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
}, [visible, autoPlay]);
|
||||
|
||||
// 处理视频加载完成
|
||||
const handleVideoReady = (_event: VideoReadyForDisplayEvent) => {
|
||||
const handleVideoReady = () => {
|
||||
setIsLoading(false);
|
||||
if (autoPlay) {
|
||||
videoRef.current?.playAsync();
|
||||
player?.play();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,20 +93,16 @@ export function FullscreenVideoModal({
|
||||
};
|
||||
|
||||
// 创建手势处理器
|
||||
const panResponder = useRef(
|
||||
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; // 滑动屏幕宽度的1/4触发切换
|
||||
|
||||
const threshold = screenWidth / 4;
|
||||
if (gestureState.dx > threshold && hasPrevious) {
|
||||
// 向右滑动,显示上一个
|
||||
onPrevious?.();
|
||||
} else if (gestureState.dx < -threshold && hasNext) {
|
||||
// 向左滑动,显示下一个
|
||||
onNext?.();
|
||||
}
|
||||
},
|
||||
@@ -106,7 +113,7 @@ export function FullscreenVideoModal({
|
||||
useEffect(() => {
|
||||
const handleBackPress = () => {
|
||||
if (visible) {
|
||||
handleClose();
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -114,7 +121,7 @@ export function FullscreenVideoModal({
|
||||
|
||||
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
||||
return () => subscription.remove();
|
||||
}, [visible, handleClose]);
|
||||
}, [visible, onClose]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
@@ -127,10 +134,8 @@ export function FullscreenVideoModal({
|
||||
statusBarTranslucent={true}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{/* 状态栏处理 */}
|
||||
{Platform.OS === 'android' && <StatusBar hidden />}
|
||||
|
||||
{/* 视频容器 */}
|
||||
<TouchableOpacity
|
||||
style={styles.videoContainer}
|
||||
onPress={handleVideoPress}
|
||||
@@ -140,67 +145,58 @@ export function FullscreenVideoModal({
|
||||
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}
|
||||
onError={handleVideoError}
|
||||
/>
|
||||
)}
|
||||
{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" as any
|
||||
nativeControls={false}
|
||||
onError={handleVideoError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 加载指示器 */}
|
||||
{isLoading && (
|
||||
<View style={styles.loadingOverlay}>
|
||||
<ActivityIndicator size="large" color="#4ECDC4" />
|
||||
</View>
|
||||
)}
|
||||
{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>
|
||||
{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>
|
||||
@@ -228,7 +224,6 @@ const styles = StyleSheet.create({
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
backgroundColor: '#000',
|
||||
objectFit: 'cover'
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: 'absolute',
|
||||
|
||||
Reference in New Issue
Block a user