
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、核心知识点
全屏播放和画中画(Picture-in-Picture, PiP)是视频播放器的核心功能,能够提升用户观看体验。在鸿蒙端,react-native-video 提供了完善的全屏和画中画支持,让开发者可以轻松实现专业级的视频播放功能。
全屏播放核心概念
typescript
import Video, { VideoRef } from 'react-native-video';
import { StatusBar, SafeAreaView } from 'react-native';
// 全屏播放基础实现
const FullscreenVideo = () => {
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
return (
<SafeAreaView style={[styles.container, isFullscreen && styles.fullscreen]}>
<StatusBar hidden={isFullscreen} />
<Video
source={{ uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4' }}
style={[styles.video, isFullscreen && styles.fullscreenVideo]}
resizeMode="contain"
/>
</SafeAreaView>
);
};
画中画核心概念
typescript
// 画中画基础实现
const PictureInPictureVideo = () => {
const [isPictureInPicture, setIsPictureInPicture] = useState<boolean>(false);
const [enterPictureInPictureOnLeave, setEnterPictureInPictureOnLeave] = useState<boolean>(true);
return (
<Video
source={{ uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4' }}
enterPictureInPictureOnLeave={enterPictureInPictureOnLeave}
onPictureInPictureStatusChanged={(e) => {
setIsPictureInPicture(e.isActive || false);
}}
/>
);
};
全屏与画中画模式对比
视频播放模式
全屏播放
画中画播放
嵌入式播放
隐藏状态栏
视频占据全屏
沉浸式体验
小窗口悬浮
可拖动调整
后台持续播放
固定尺寸容器
与其他内容共存
页面内嵌显示
二、实战核心代码解析
1. 全屏播放切换
typescript
// 全屏播放切换
const ToggleFullscreenPlayer = () => {
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const videoRef = useRef<VideoRef>(null);
const toggleFullscreen = () => {
setIsFullscreen(prev => !prev);
};
return (
<View style={styles.container}>
<TouchableOpacity
onPress={toggleFullscreen}
style={styles.fullscreenButton}
>
<Video
ref={videoRef}
source={{ uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4' }}
style={[styles.video, isFullscreen && styles.fullscreenVideo]}
resizeMode="contain"
paused={false}
repeat={true}
/>
<Text style={styles.fullscreenButtonText}>
{isFullscreen ? '退出全屏' : '全屏播放'}
</Text>
</TouchableOpacity>
</View>
);
};
2. 画中画模式
typescript
// 画中画播放器
const PictureInPicturePlayer = () => {
const [isPictureInPicture, setIsPictureInPicture] = useState<boolean>(false);
const [enterPictureInPictureOnLeave, setEnterPictureInPictureOnLeave] = useState<boolean>(true);
const videoRef = useRef<VideoRef>(null);
const handleEnterPictureInPicture = () => {
videoRef.current?.enterPictureInPicture();
setIsPictureInPicture(true);
};
const handleExitPictureInPicture = () => {
videoRef.current?.exitPictureInPicture();
setIsPictureInPicture(false);
};
return (
<View style={styles.container}>
<View style={styles.controls}>
<Text style={styles.controlLabel}>
应用返回桌面时自动启动画中画:
</Text>
<TouchableOpacity
style={[styles.switch, enterPictureInPictureOnLeave && styles.switchActive]}
onPress={() => setEnterPictureInPictureOnLeave(prev => !prev)}
>
<Text style={[styles.switchText, enterPictureInPictureOnLeave && styles.switchTextActive]}>
{enterPictureInPictureOnLeave ? '开' : '关'}
</Text>
</TouchableOpacity>
</View>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.pipButton, isPictureInPicture && styles.pipButtonActive]}
onPress={handleEnterPictureInPicture}
disabled={isPictureInPicture}
>
<Text style={styles.pipButtonText}>开启画中画</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.pipButton, !isPictureInPicture && styles.pipButtonActive]}
onPress={handleExitPictureInPicture}
disabled={!isPictureInPicture}
>
<Text style={styles.pipButtonText}>关闭画中画</Text>
</TouchableOpacity>
</View>
<Video
ref={videoRef}
source={{ uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4' }}
style={styles.video}
resizeMode="contain"
enterPictureInPictureOnLeave={enterPictureInPictureOnLeave}
onPictureInPictureStatusChanged={(e: any) => {
console.log('画中画状态变化:', e);
if (isPictureInPicture !== e.isActive) {
setIsPictureInPicture(e.isActive || false);
}
}}
/>
</View>
);
};
3. 全屏 + 画中画组合
typescript
// 全屏 + 画中画组合模式
const CombinedModePlayer = () => {
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const [isPictureInPicture, setIsPictureInPicture] = useState<boolean>(false);
const videoRef = useRef<VideoRef>(null);
const toggleFullscreen = () => {
if (isPictureInPicture) {
videoRef.current?.exitPictureInPicture();
setIsPictureInPicture(false);
}
if (isFullscreen) {
videoRef.current?.dismissFullscreenPlayer();
setIsFullscreen(false);
} else {
videoRef.current?.presentFullscreenPlayer();
setIsFullscreen(true);
}
};
const togglePictureInPicture = () => {
if (isFullscreen) {
videoRef.current?.dismissFullscreenPlayer();
setIsFullscreen(false);
}
if (isPictureInPicture) {
videoRef.current?.exitPictureInPicture();
setIsPictureInPicture(false);
} else {
videoRef.current?.enterPictureInPicture();
setIsPictureInPicture(true);
}
};
return (
<SafeAreaView style={[styles.container, isFullscreen && styles.fullscreenContainer]}>
<StatusBar hidden={isFullscreen} />
<Video
ref={videoRef}
source={{ uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4' }}
style={[styles.video, isFullscreen && styles.fullscreenVideo]}
resizeMode="contain"
enterPictureInPictureOnLeave={true}
onPictureInPictureStatusChanged={(e: any) => {
setIsPictureInPicture(e.isActive || false);
}}
onFullscreenPlayerDidPresent={() => {
setIsFullscreen(true);
}}
onFullscreenPlayerWillDismiss={() => {
setIsFullscreen(false);
}}
/>
<View style={styles.controls}>
<TouchableOpacity
style={styles.controlButton}
onPress={toggleFullscreen}
>
<Text style={styles.controlButtonText}>
{isFullscreen ? '退出全屏' : '全屏'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={togglePictureInPicture}
>
<Text style={styles.controlButtonText}>
{isPictureInPicture ? '关闭画中画' : '画中画'}
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
三、实战完整版:全屏播放与画中画播放器
typescript
import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
StatusBar,
Dimensions,
Switch,
ScrollView,
ActivityIndicator,
AppState,
} from 'react-native';
import Video, { VideoRef, ResizeMode } from 'react-native-video';
const FullscreenPiPScreen = () => {
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const [isPictureInPicture, setIsPictureInPicture] = useState<boolean>(false);
const [enterPictureInPictureOnLeave, setEnterPictureInPictureOnLeave] = useState<boolean>(true);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [currentTime, setCurrentTime] = useState<number>(0);
const [duration, setDuration] = useState<number>(0);
const [volume, setVolume] = useState<number>(1.0);
const [resizeMode, setResizeMode] = useState<ResizeMode>(ResizeMode.CONTAIN);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isControlsVisible, setIsControlsVisible] = useState<boolean>(true);
const videoRef = useRef<VideoRef>(null);
const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const videoUri = 'https://media.w3.org/2010/05/sintel/trailer.mp4';
const toggleFullscreen = () => {
if (isPictureInPicture) {
videoRef.current?.exitPictureInPicture();
setIsPictureInPicture(false);
}
if (isFullscreen) {
videoRef.current?.dismissFullscreenPlayer();
setIsFullscreen(false);
} else {
videoRef.current?.presentFullscreenPlayer();
setIsFullscreen(true);
}
showControls();
};
const togglePictureInPicture = () => {
if (isPictureInPicture) {
videoRef.current?.exitPictureInPicture();
setIsPictureInPicture(false);
} else {
videoRef.current?.enterPictureInPicture();
setIsPictureInPicture(true);
}
showControls();
};
// 监听应用状态变化,处理画中画
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background' && enterPictureInPictureOnLeave && !isPictureInPicture) {
videoRef.current?.enterPictureInPicture();
setIsPictureInPicture(true);
}
});
return () => subscription?.remove();
}, [enterPictureInPictureOnLeave, isPictureInPicture]);
const togglePlayPause = () => {
setIsPlaying(prev => !prev);
showControls();
};
const handleSeek = (value: number) => {
if (videoRef.current) {
videoRef.current.seek(value);
}
showControls();
};
const handleProgress = (data: any) => {
setCurrentTime(data.currentTime);
};
const handleLoad = (data: any) => {
setDuration(data.duration);
setIsLoading(false);
};
const handleLoadStart = () => {
setIsLoading(true);
};
const handleEnd = () => {
setIsPlaying(false);
setCurrentTime(0);
};
const showControls = () => {
setIsControlsVisible(true);
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
controlsTimeoutRef.current = setTimeout(() => {
setIsControlsVisible(false);
}, 4000);
};
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
const getResizeModeLabel = (mode: ResizeMode): string => {
switch (mode) {
case ResizeMode.CONTAIN:
return '适应';
case ResizeMode.COVER:
return '填充';
case ResizeMode.STRETCH:
return '拉伸';
case ResizeMode.NONE:
return '原始';
default:
return '适应';
}
};
useEffect(() => {
return () => {
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
};
}, []);
// 横屏全屏渲染
if (isFullscreen) {
return (
<SafeAreaView style={styles.fullscreenLandscapeContainer}>
<StatusBar hidden={true} />
<View style={styles.fullscreenLandscapeVideoWrapper}>
<TouchableOpacity
activeOpacity={1}
style={styles.videoContainer}
onPress={showControls}
>
<Video
ref={videoRef}
source={{ uri: videoUri, isNetwork: true }}
style={styles.fullscreenLandscapeVideo}
resizeMode={resizeMode}
paused={!isPlaying}
volume={volume}
enterPictureInPictureOnLeave={enterPictureInPictureOnLeave}
onProgress={handleProgress}
onLoad={handleLoad}
onLoadStart={handleLoadStart}
onEnd={handleEnd}
onPictureInPictureStatusChanged={(e: any) => {
console.log('画中画状态变化:', e);
if (isPictureInPicture !== e.isActive) {
setIsPictureInPicture(e.isActive || false);
}
}}
onFullscreenPlayerDidPresent={() => {
setIsFullscreen(true);
}}
onFullscreenPlayerWillDismiss={() => {
setIsFullscreen(false);
}}
repeat={false}
controls={false}
/>
{isLoading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007DFF" />
<Text style={styles.loadingText}>加载中...</Text>
</View>
)}
{isControlsVisible && (
<View style={styles.controls}>
<TouchableOpacity
style={styles.controlButton}
onPress={togglePlayPause}
>
<Text style={styles.controlButtonText}>
{isPlaying ? '⏸' : '▶'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={() => handleSeek(currentTime - 10)}
>
<Text style={styles.controlButtonText}>-10s</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={() => handleSeek(currentTime + 10)}
>
<Text style={styles.controlButtonText}>+10s</Text>
</TouchableOpacity>
<View style={styles.progressContainer}>
<Text style={styles.timeText}>{formatTime(currentTime)}</Text>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${(currentTime / duration) * 100}%` }
]}
/>
</View>
<Text style={styles.timeText}>{formatTime(duration)}</Text>
</View>
<TouchableOpacity
style={styles.controlButton}
onPress={toggleFullscreen}
>
<Text style={styles.controlButtonText}>
{'⬇'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={togglePictureInPicture}
>
<Text style={styles.controlButtonText}>
{isPictureInPicture ? '🔲' : '📺'}
</Text>
</TouchableOpacity>
</View>
)}
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={[styles.container, isFullscreen && styles.fullscreenContainer]}>
<StatusBar hidden={isFullscreen} />
<View style={[styles.videoWrapper, isFullscreen && styles.fullscreenVideoWrapper]}>
<TouchableOpacity
activeOpacity={1}
style={styles.videoContainer}
onPress={showControls}
>
<Video
ref={videoRef}
source={{ uri: videoUri, isNetwork: true }}
style={styles.video}
resizeMode={resizeMode}
paused={!isPlaying}
volume={volume}
enterPictureInPictureOnLeave={enterPictureInPictureOnLeave}
onProgress={handleProgress}
onLoad={handleLoad}
onLoadStart={handleLoadStart}
onEnd={handleEnd}
onPictureInPictureStatusChanged={(e: any) => {
console.log('画中画状态变化:', e);
if (isPictureInPicture !== e.isActive) {
setIsPictureInPicture(e.isActive || false);
}
}}
repeat={false}
controls={false}
/>
{isLoading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007DFF" />
<Text style={styles.loadingText}>加载中...</Text>
</View>
)}
{isControlsVisible && (
<View style={styles.controls}>
<TouchableOpacity
style={styles.controlButton}
onPress={togglePlayPause}
>
<Text style={styles.controlButtonText}>
{isPlaying ? '⏸' : '▶'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={() => {
if (videoRef.current) {
videoRef.current.seek(Math.max(0, currentTime - 10));
}
showControls();
}}
>
<Text style={styles.controlButtonText}>-10s</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={() => {
if (videoRef.current) {
videoRef.current.seek(Math.min(duration, currentTime + 10));
}
showControls();
}}
>
<Text style={styles.controlButtonText}>+10s</Text>
</TouchableOpacity>
<View style={styles.progressContainer}>
<Text style={styles.timeText}>{formatTime(currentTime)}</Text>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${(currentTime / duration) * 100}%` }
]}
/>
</View>
<Text style={styles.timeText}>{formatTime(duration)}</Text>
</View>
<TouchableOpacity
style={styles.controlButton}
onPress={toggleFullscreen}
>
<Text style={styles.controlButtonText}>
{isFullscreen ? '⬇' : '⬆'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={togglePictureInPicture}
>
<Text style={styles.controlButtonText}>
{isPictureInPicture ? '📺' : '📺'}
</Text>
</TouchableOpacity>
</View>
)}
</TouchableOpacity>
</View>
{!isFullscreen && (
<ScrollView style={styles.settingsContainer}>
<View style={styles.card}>
<Text style={styles.cardTitle}>播放模式</Text>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>全屏播放</Text>
<TouchableOpacity
style={[styles.modeButton, isFullscreen && styles.modeButtonActive]}
onPress={toggleFullscreen}
>
<Text style={[styles.modeButtonText, isFullscreen && styles.modeButtonTextActive]}>
{isFullscreen ? '已开启' : '点击开启'}
</Text>
</TouchableOpacity>
</View>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>画中画</Text>
<TouchableOpacity
style={[styles.modeButton, isPictureInPicture && styles.modeButtonActive]}
onPress={togglePictureInPicture}
>
<Text style={[styles.modeButtonText, isPictureInPicture && styles.modeButtonTextActive]}>
{isPictureInPicture ? '已开启' : '点击开启'}
</Text>
</TouchableOpacity>
</View>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>
返回桌面自动画中画
</Text>
<Switch
trackColor={{ false: '#E5E6EB', true: '#007DFF' }}
thumbColor={enterPictureInPictureOnLeave ? '#007DFF' : '#fff'}
onValueChange={setEnterPictureInPictureOnLeave}
value={enterPictureInPictureOnLeave}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>视频设置</Text>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>缩放模式</Text>
<View style={styles.resizeModeButtons}>
{[
ResizeMode.CONTAIN,
ResizeMode.COVER,
ResizeMode.STRETCH,
ResizeMode.NONE,
].map((mode) => (
<TouchableOpacity
key={mode}
style={[
styles.resizeModeButton,
resizeMode === mode && styles.resizeModeButtonActive,
]}
onPress={() => setResizeMode(mode)}
>
<Text style={[
styles.resizeModeButtonText,
resizeMode === mode && styles.resizeModeButtonTextActive,
]}>
{getResizeModeLabel(mode)}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>音量: {Math.round(volume * 100)}%</Text>
<View style={styles.volumeContainer}>
<TouchableOpacity
style={styles.volumeButton}
onPress={() => setVolume(Math.max(0, volume - 0.1))}
>
<Text style={styles.volumeButtonText}>-</Text>
</TouchableOpacity>
<View style={styles.volumeBar}>
<View
style={[
styles.volumeFill,
{ width: `${volume * 100}%` }
]}
/>
</View>
<TouchableOpacity
style={styles.volumeButton}
onPress={() => setVolume(Math.min(1, volume + 0.1))}
>
<Text style={styles.volumeButtonText}>+</Text>
</TouchableOpacity>
</View>
</View>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>使用说明</Text>
<Text style={styles.instructionText}>
1. 点击全屏按钮进入全屏模式
</Text>
<Text style={styles.instructionText}>
2. 点击画中画按钮开启画中画
</Text>
<Text style={styles.instructionText}>
3. 画中画模式下可以返回桌面继续观看
</Text>
<Text style={styles.instructionText}>
4. 开启自动画中画返回桌面时自动启动
</Text>
<Text style={[styles.instructionText, { color: '#F44336', fontWeight: '600' }]}>
⚠️ 注意: 画中画需要鸿蒙系统支持
</Text>
<Text style={[styles.instructionText, { color: '#9C27B0', fontWeight: '600' }]}>
💡 提示: 全屏模式隐藏状态栏
</Text>
<Text style={[styles.instructionText, { color: '#4CAF50', fontWeight: '600' }]}>
💡 提示: 支持 4 种缩放模式
</Text>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>常用属性</Text>
<Text style={styles.instructionText}>
• pictureInPicture: 是否开启画中画
</Text>
<Text style={styles.instructionText}>
• enterPictureInPictureOnLeave: 返回桌面时自动画中画
</Text>
<Text style={styles.instructionText}>
• onPictureInPictureStatusChanged: 画中画状态变化回调
</Text>
<Text style={styles.instructionText}>
• resizeMode: 视频缩放模式
</Text>
<Text style={styles.instructionText}>
• volume: 音量大小(0-1)
</Text>
<Text style={styles.instructionText}>
• onProgress: 播放进度回调
</Text>
<Text style={styles.instructionText}>
• onLoad: 视频加载完成回调
</Text>
<Text style={styles.instructionText}>
• onEnd: 播放结束回调
</Text>
</View>
</ScrollView>
)}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F2F3F5',
},
fullscreenContainer: {
backgroundColor: '#000',
},
videoWrapper: {
width: '90%',
aspectRatio: 16 / 9,
backgroundColor: '#000',
borderRadius: 12,
overflow: 'hidden',
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
alignSelf: 'center',
marginTop: 20,
},
fullscreenVideoWrapper: {
width: '100%',
height: '100%',
borderRadius: 0,
elevation: 0,
shadowOpacity: 0,
marginTop: 0,
aspectRatio: 16 / 9,
},
videoContainer: {
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
video: {
width: '100%',
height: '100%',
},
loadingContainer: {
position: 'absolute',
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 8,
fontSize: 14,
color: '#fff',
},
controls: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: 15,
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
controlButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
justifyContent: 'center',
alignItems: 'center',
},
controlButtonText: {
fontSize: 18,
color: '#fff',
fontWeight: '600',
},
progressContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
progressBar: {
flex: 1,
height: 6,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 3,
overflow: 'hidden',
},
progressFill: {
height: '100%',
backgroundColor: '#007DFF',
},
timeText: {
fontSize: 12,
color: '#fff',
minWidth: 40,
textAlign: 'center',
},
settingsContainer: {
flex: 1,
padding: 20,
},
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
cardTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 16,
color: '#333',
},
settingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
settingLabel: {
fontSize: 14,
color: '#333',
fontWeight: '500',
},
modeButton: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#E5E6EB',
borderRadius: 8,
},
modeButtonActive: {
backgroundColor: '#007DFF',
},
modeButtonText: {
fontSize: 14,
color: '#333',
fontWeight: '500',
},
modeButtonTextActive: {
color: '#fff',
},
resizeModeButtons: {
flexDirection: 'row',
gap: 8,
},
resizeModeButton: {
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: '#E5E6EB',
borderRadius: 6,
},
resizeModeButtonActive: {
backgroundColor: '#007DFF',
},
resizeModeButtonText: {
fontSize: 13,
color: '#333',
fontWeight: '500',
},
resizeModeButtonTextActive: {
color: '#fff',
},
volumeContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
flex: 1,
},
volumeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#E5E6EB',
justifyContent: 'center',
alignItems: 'center',
},
volumeButtonText: {
fontSize: 16,
color: '#333',
fontWeight: '600',
},
volumeBar: {
flex: 1,
height: 6,
backgroundColor: '#E5E6EB',
borderRadius: 3,
overflow: 'hidden',
},
volumeFill: {
height: '100%',
backgroundColor: '#007DFF',
},
instructionText: {
fontSize: 14,
lineHeight: 22,
marginBottom: 8,
color: '#333',
},
fullscreenLandscapeContainer: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
backgroundColor: '#000',
zIndex: 9999,
},
fullscreenLandscapeVideoWrapper: {
width: '100%',
height: '100%',
},
fullscreenLandscapeVideo: {
width: '100%',
height: '100%',
},
});
export default FullscreenPiPScreen;
四、鸿蒙端画中画与全屏实现说明
重要提示
在鸿蒙端使用画中画和全屏功能时,需要注意以下关键点:
- 使用 ref 方法控制画中画 :通过
videoRef.current?.enterPictureInPicture()和videoRef.current?.exitPictureInPicture()方法来控制画中画的开启和关闭 - 使用官方全屏方法 :通过
videoRef.current?.presentFullscreenPlayer()和videoRef.current?.dismissFullscreenPlayer()方法来控制全屏的进入和退出 - pictureInPicture 属性不支持 :根据官方文档,
pictureInPicture属性在鸿蒙端不支持,请勿使用 - 使用 enterPictureInPictureOnLeave :通过设置
enterPictureInPictureOnLeave={true}来实现应用返回桌面时自动启动画中画 - 监听全屏事件 :使用
onFullscreenPlayerDidPresent和onFullscreenPlayerWillDismiss来监听全屏状态变化 - 监听画中画状态 :使用
onPictureInPictureStatusChanged事件来监听画中画状态的变化
正确的实现方式
typescript
const videoRef = useRef<VideoRef>(null);
const [isPictureInPicture, setIsPictureInPicture] = useState<boolean>(false);
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
// 全屏控制
const toggleFullscreen = () => {
if (isFullscreen) {
videoRef.current?.dismissFullscreenPlayer();
setIsFullscreen(false);
} else {
videoRef.current?.presentFullscreenPlayer();
setIsFullscreen(true);
}
};
// 开启画中画
const handleEnterPictureInPicture = () => {
videoRef.current?.enterPictureInPicture();
setIsPictureInPicture(true);
};
// 关闭画中画
const handleExitPictureInPicture = () => {
videoRef.current?.exitPictureInPicture();
setIsPictureInPicture(false);
};
// Video 组件配置
<Video
ref={videoRef}
source={{ uri: videoUri }}
enterPictureInPictureOnLeave={enterPictureInPictureOnLeave} // 支持的属性
onPictureInPictureStatusChanged={(e: any) => {
setIsPictureInPicture(e.isActive || false);
}}
onFullscreenPlayerDidPresent={() => {
setIsFullscreen(true);
}}
onFullscreenPlayerWillDismiss={() => {
setIsFullscreen(false);
}}
// ... 其他配置
/>
功能说明
全屏功能:
- presentFullscreenPlayer():通过 ref 方法进入全屏模式(鸿蒙端支持)
- dismissFullscreenPlayer():通过 ref 方法退出全屏模式(鸿蒙端支持)
- onFullscreenPlayerDidPresent:全屏播放器已进入全屏时触发(鸿蒙端支持)
- onFullscreenPlayerWillDismiss:全屏播放器即将退出全屏时触发(鸿蒙端支持)
画中画功能:
- enterPictureInPicture():通过 ref 方法进入画中画模式(鸿蒙端支持)
- exitPictureInPicture():通过 ref 方法退出画中画模式(鸿蒙端支持)
- enterPictureInPictureOnLeave:当用户离开应用时是否自动进入画中画模式(鸿蒙端支持)
- onPictureInPictureStatusChanged:画中画激活或失效时触发的回调(鸿蒙端支持)
- pictureInPicture:决定媒体是否以画中画模式播放(鸿蒙端不支持,请勿使用)
五、OpenHarmony6.0 专属避坑指南
以下是鸿蒙 RN 开发中实现「全屏播放与画中画」的所有真实高频踩坑点,按出现频率排序,问题现象贴合开发实际,解决方案均为「一行代码/简单配置」,所有方案均为鸿蒙端专属最优解:
| 问题现象 | 问题原因 | 鸿蒙端最优解决方案 |
|---|---|---|
| 全屏切换时视频变形 | ResizeMode 未正确设置或容器尺寸未调整 | ✅ 使用 ResizeMode.CONTAIN 保持比例,全屏时使用横屏布局 |
| 画中画无法启动 | 未使用正确的 ref 方法或系统未授权 | ✅ 使用 videoRef.current?.enterPictureInPicture() 方法,确保应用有画中画权限 |
| 全屏状态栏未隐藏 | StatusBar 的 hidden 属性未正确设置 |
✅ 使用 <StatusBar hidden={isFullscreen} /> 控制状态栏 |
| 画中画返回后状态错误 | 画中画状态变化回调未正确处理 | ✅ 在 onPictureInPictureStatusChanged 中同步状态,使用 ref 方法控制 |
| 画中画视频无声音 | 音量未正确设置或被静音 | ✅ 确保 volume 属性正确设置为 1.0 |
| 全屏控制栏无法隐藏 | 控制栏自动隐藏定时器未设置 | ✅ 使用 setTimeout 4秒后隐藏控制栏 |
| 画中画小窗口位置错误 | 未正确设置画中画窗口参数 | ✅ 使用系统默认画中画位置,无需手动设置 |
| 全屏播放时返回黑屏 | 全屏状态未正确清理 | ✅ 退出全屏时重置容器尺寸和状态 |
| 画中画切换到全屏失败 | 两种模式切换时状态冲突 | ✅ 切换前先使用 videoRef.current?.exitPictureInPicture() 关闭画中画 |
⚠️ 特别注意:鸿蒙端使用全屏和画中画的要求:
- 系统权限 - ✅ 画中画需要在系统设置中开启
- ref 方法控制 - ✅ 画中画必须使用
videoRef.current?.enterPictureInPicture()和videoRef.current?.exitPictureInPicture()方法控制 - 状态栏处理 - ✅ 全屏时隐藏状态栏,非全屏时显示
- 状态同步 - ✅ 画中画状态变化时需要同步到应用状态
- 模式互斥 - ✅ 全屏和画中画不能同时开启
- 横屏全屏 - ✅ 全屏模式默认为横屏布局,无需监听屏幕旋转
六、扩展用法:全屏与画中画高频进阶优化(纯原生 无依赖 鸿蒙适配)
基于本次的核心代码,结合 RN 的内置能力,可轻松实现鸿蒙端开发中所有高频的全屏和画中画进阶需求:
✔️ 扩展1:双击全屏
双击视频快速切换全屏:
typescript
const [lastTap, setLastTap] = useState<number>(0);
const handleDoubleTap = () => {
const now = Date.now();
if (now - lastTap < 300) {
toggleFullscreen();
}
setLastTap(now);
};
<TouchableOpacity onPress={handleDoubleTap}>
<Video />
</TouchableOpacity>
✔️ 扩展2:画中画手势控制
在画中画模式下支持拖动和缩放:
typescript
const onPictureInPictureStatusChanged = (e: any) => {
if (e.isActive) {
console.log('画中画已开启,支持手势控制');
}
};
✔️ 扩展3:全屏横竖屏锁定
锁定全屏时的屏幕方向:
typescript
const [lockOrientation, setLockOrientation] = useState<boolean>(false);
useEffect(() => {
if (isFullscreen && lockOrientation) {
// 锁定横屏
console.log('锁定横屏');
}
}, [isFullscreen, lockOrientation]);
✔️ 扩展4:画中画自定义尺寸
自定义画中画窗口的大小:
typescript
const customPictureInPictureSize = {
width: 200,
height: 150,
};
// 注意:鸿蒙端画中画尺寸由系统控制,开发者无法自定义