
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、核心知识点
视频列表播放是视频应用的核心功能之一,支持用户浏览多个视频并快速切换。在鸿蒙端,通过 react-native-video 配合 FlatList 和 ScrollView 可以实现流畅的视频列表播放体验。
视频列表核心概念
typescript
import Video, { VideoRef } from 'react-native-video';
import { FlatList, ScrollView } from 'react-native';
// 视频列表数据结构
interface VideoItem {
id: string;
uri: string;
title: string;
thumbnail: string;
duration: number;
}
// 基础视频列表
const videoList: VideoItem[] = [
{ id: '1', uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4', title: '视频1', thumbnail: '', duration: 120 },
{ id: '2', uri: 'https://media.w3.org/2010/05/bunny/trailer.mp4', title: '视频2', thumbnail: '', duration: 180 },
];
视频列表播放模式
视频列表播放支持以下两种展示方式:
- 垂直列表播放 - 使用
FlatList垂直滚动,当前视频播放时其他暂停 - 网格展示播放 - 多列网格布局,点击播放全屏视频
二、实战核心代码解析
1. 垂直视频列表播放
typescript
// 垂直视频列表
const VerticalVideoList = () => {
const [videoList] = useState<VideoItem[]>([
{ id: '1', uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4', title: 'Sintel 预告片', thumbnail: '', duration: 60 },
{ id: '2', uri: 'https://media.w3.org/2010/05/bunny/trailer.mp4', title: 'Big Buck Bunny', thumbnail: '', duration: 120 },
{ id: '3', uri: 'https://media.w3.org/2010/05/video/movie_300.mp4', title: '电影片段', thumbnail: '', duration: 300 },
]);
const [currentPlayingId, setCurrentPlayingId] = useState<string>('1');
const renderVideoItem = ({ item }: { item: VideoItem }) => {
const isPlaying = currentPlayingId === item.id;
return (
<TouchableOpacity onPress={() => setCurrentPlayingId(item.id)}>
<Video
source={{ uri: item.uri }}
style={styles.videoItem}
paused={!isPlaying}
resizeMode="cover"
repeat={true}
muted={!isPlaying}
/>
<Text style={styles.videoTitle}>{item.title}</Text>
</TouchableOpacity>
);
};
return (
<FlatList
data={videoList}
renderItem={renderVideoItem}
keyExtractor={(item) => item.id}
pagingEnabled={true}
showsVerticalScrollIndicator={false}
/>
);
};
2. 视频网格展示
typescript
// 视频网格展示
const VideoGrid = () => {
const [selectedVideo, setSelectedVideo] = useState<VideoItem | null>(null);
const videoList: VideoItem[] = [
{ id: '1', uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4', title: '网格视频 1', thumbnail: '', duration: 60 },
{ id: '2', uri: 'https://media.w3.org/2010/05/bunny/trailer.mp4', title: '网格视频 2', thumbnail: '', duration: 120 },
{ id: '3', uri: 'https://media.w3.org/2010/05/video/movie_300.mp4', title: '网格视频 3', thumbnail: '', duration: 300 },
{ id: '4', uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4', title: '网格视频 4', thumbnail: '', duration: 60 },
];
if (selectedVideo) {
return (
<View style={styles.fullscreenContainer}>
<Video
source={{ uri: selectedVideo.uri }}
style={styles.fullscreenVideo}
resizeMode="contain"
repeat={true}
/>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setSelectedVideo(null)}
>
<Text style={styles.closeButtonText}>✕</Text>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.gridContainer}>
{videoList.map((item) => (
<TouchableOpacity
key={item.id}
style={styles.gridItem}
onPress={() => setSelectedVideo(item)}
>
<Video
source={{ uri: item.uri }}
style={styles.gridVideo}
paused={true}
resizeMode="cover"
/>
<View style={styles.gridOverlay}>
<Text style={styles.playIcon}>▶</Text>
</View>
<Text style={styles.gridTitle}>{item.title}</Text>
</TouchableOpacity>
))}
</View>
);
};
三、实战完整版:视频列表播放器
typescript
import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
FlatList,
ScrollView,
Dimensions,
StatusBar,
SafeAreaView,
ActivityIndicator,
Image,
} from 'react-native';
import Video, { VideoRef, ResizeMode } from 'react-native-video';
interface VideoItem {
id: string;
uri: string;
title: string;
description: string;
thumbnail: string;
duration: number;
views: number;
}
const VideoListCarouselScreen = () => {
const [activeTab, setActiveTab] = useState<'list' | 'grid'>('list');
const [currentPlayingId, setCurrentPlayingId] = useState<string>('1');
const [selectedVideo, setSelectedVideo] = useState<VideoItem | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const videoList: VideoItem[] = [
{
id: '1',
uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
title: 'Sintel 动画预告片',
description: '一部精彩的3D动画短片',
thumbnail: '',
duration: 60,
views: 1000000,
},
{
id: '2',
uri: 'https://media.w3.org/2010/05/bunny/trailer.mp4',
title: 'Big Buck Bunny',
description: '可爱的大雄兔动画故事',
thumbnail: '',
duration: 120,
views: 800000,
},
{
id: '3',
uri: 'https://media.w3.org/2010/05/video/movie_300.mp4',
title: '经典电影片段',
description: '精选电影精彩片段',
thumbnail: '',
duration: 300,
views: 1200000,
},
{
id: '4',
uri: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
title: '动画世界探索',
description: '探索奇妙的动画世界',
thumbnail: '',
duration: 90,
views: 600000,
},
{
id: '5',
uri: 'https://media.w3.org/2010/05/bunny/trailer.mp4',
title: '自然风光',
description: '美丽的自然风光展示',
thumbnail: '',
duration: 150,
views: 900000,
},
{
id: '6',
uri: 'https://media.w3.org/2010/05/video/movie_300.mp4',
title: '电影精彩剪辑',
description: '经典电影精彩瞬间',
thumbnail: '',
duration: 200,
views: 750000,
},
];
useEffect(() => {
setTimeout(() => setIsLoading(false), 1000);
}, []);
const formatDuration = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const formatViews = (views: number): string => {
if (views >= 1000000) {
return `${(views / 1000000).toFixed(1)}M`;
} else if (views >= 1000) {
return `${(views / 1000).toFixed(1)}K`;
}
return views.toString();
};
const renderTabButton = (tab: 'list' | 'grid', title: string) => (
<TouchableOpacity
style={[styles.tabButton, activeTab === tab && styles.tabButtonActive]}
onPress={() => setActiveTab(tab)}
>
<Text style={[styles.tabButtonText, activeTab === tab && styles.tabButtonTextActive]}>
{title}
</Text>
</TouchableOpacity>
);
const renderVerticalList = () => (
<FlatList
data={videoList}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
const isPlaying = currentPlayingId === item.id;
return (
<TouchableOpacity
style={styles.listItem}
onPress={() => setCurrentPlayingId(item.id)}
>
<View style={styles.listVideoContainer}>
<Video
source={{ uri: item.uri }}
style={styles.listVideo}
paused={!isPlaying}
resizeMode="cover"
repeat={true}
muted={!isPlaying}
volume={isPlaying ? 1.0 : 0}
/>
{!isPlaying && (
<View style={styles.playOverlay}>
<Text style={styles.playIcon}>▶</Text>
</View>
)}
<View style={styles.durationBadge}>
<Text style={styles.durationText}>{formatDuration(item.duration)}</Text>
</View>
</View>
<View style={styles.listInfo}>
<Text style={styles.listTitle} numberOfLines={2}>
{item.title}
</Text>
<Text style={styles.listDescription} numberOfLines={2}>
{item.description}
</Text>
<Text style={styles.listStats}>
{formatViews(item.views)} 次观看
</Text>
</View>
{isPlaying && (
<View style={styles.playingIndicator}>
<Text style={styles.playingText}>播放中</Text>
</View>
)}
</TouchableOpacity>
);
}}
showsVerticalScrollIndicator={false}
/>
);
const renderGrid = () => (
<View style={styles.gridContainer}>
{videoList.map((item) => (
<TouchableOpacity
key={item.id}
style={styles.gridItem}
onPress={() => setSelectedVideo(item)}
>
<View style={styles.gridVideoContainer}>
<Video
source={{ uri: item.uri }}
style={styles.gridVideo}
paused={true}
resizeMode="cover"
/>
<View style={styles.gridOverlay}>
<Text style={styles.gridPlayIcon}>▶</Text>
</View>
<View style={styles.gridDurationBadge}>
<Text style={styles.gridDurationText}>{formatDuration(item.duration)}</Text>
</View>
</View>
<Text style={styles.gridTitle} numberOfLines={2}>
{item.title}
</Text>
<Text style={styles.gridViews}>{formatViews(item.views)} 次观看</Text>
</TouchableOpacity>
))}
</View>
);
if (isLoading) {
return (
<SafeAreaView style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007DFF" />
<Text style={styles.loadingText}>加载中...</Text>
</SafeAreaView>
);
}
if (selectedVideo) {
return (
<SafeAreaView style={styles.fullscreenContainer}>
<StatusBar hidden={true} />
<Video
source={{ uri: selectedVideo.uri }}
style={styles.fullscreenVideo}
resizeMode={ResizeMode.CONTAIN}
repeat={true}
volume={1.0}
/>
<View style={styles.fullscreenOverlay}>
<Text style={styles.fullscreenTitle}>{selectedVideo.title}</Text>
<Text style={styles.fullscreenDescription}>{selectedVideo.description}</Text>
</View>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setSelectedVideo(null)}
>
<Text style={styles.closeButtonText}>✕</Text>
</TouchableOpacity>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<View style={styles.header}>
<Text style={styles.headerTitle}>视频播放器</Text>
<Text style={styles.headerSubtitle}>列表 / 网格</Text>
</View>
<View style={styles.tabContainer}>
{renderTabButton('list', '列表')}
{renderTabButton('grid', '网格')}
</View>
<View style={styles.contentContainer}>
{activeTab === 'list' && renderVerticalList()}
{activeTab === 'grid' && renderGrid()}
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>共 {videoList.length} 个视频</Text>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F2F3F5',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F2F3F5',
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: '#666',
},
header: {
padding: 20,
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#E5E6EB',
},
headerTitle: {
fontSize: 20,
fontWeight: '600',
color: '#333',
},
headerSubtitle: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
tabContainer: {
flexDirection: 'row',
backgroundColor: '#fff',
paddingHorizontal: 20,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#E5E6EB',
},
tabButton: {
flex: 1,
paddingVertical: 8,
marginRight: 12,
borderRadius: 8,
backgroundColor: '#F2F3F5',
alignItems: 'center',
},
tabButtonActive: {
backgroundColor: '#007DFF',
},
tabButtonText: {
fontSize: 14,
color: '#666',
fontWeight: '500',
},
tabButtonTextActive: {
color: '#fff',
},
contentContainer: {
flex: 1,
},
listItem: {
flexDirection: 'row',
backgroundColor: '#fff',
marginHorizontal: 16,
marginVertical: 8,
borderRadius: 12,
overflow: 'hidden',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
listVideoContainer: {
width: 160,
height: 120,
position: 'relative',
},
listVideo: {
width: '100%',
height: '100%',
},
playOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
playIcon: {
fontSize: 32,
color: '#fff',
},
durationBadge: {
position: 'absolute',
bottom: 8,
right: 8,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 6,
paddingVertical: 3,
borderRadius: 4,
},
durationText: {
fontSize: 12,
color: '#fff',
fontWeight: '500',
},
listInfo: {
flex: 1,
padding: 12,
justifyContent: 'center',
},
listTitle: {
fontSize: 15,
fontWeight: '600',
color: '#333',
marginBottom: 6,
},
listDescription: {
fontSize: 13,
color: '#666',
marginBottom: 8,
},
listStats: {
fontSize: 12,
color: '#999',
},
playingIndicator: {
position: 'absolute',
top: 8,
right: 8,
backgroundColor: '#007DFF',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
playingText: {
fontSize: 12,
color: '#fff',
fontWeight: '500',
},
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: 8,
},
gridItem: {
width: '48%',
margin: '1%',
backgroundColor: '#fff',
borderRadius: 12,
overflow: 'hidden',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
gridVideoContainer: {
aspectRatio: 16 / 9,
position: 'relative',
},
gridVideo: {
width: '100%',
height: '100%',
},
gridOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
},
gridPlayIcon: {
fontSize: 40,
color: '#fff',
},
gridDurationBadge: {
position: 'absolute',
bottom: 8,
right: 8,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 6,
paddingVertical: 3,
borderRadius: 4,
},
gridDurationText: {
fontSize: 12,
color: '#fff',
fontWeight: '500',
},
gridTitle: {
fontSize: 14,
fontWeight: '600',
color: '#333',
padding: 12,
paddingTop: 8,
},
gridViews: {
fontSize: 12,
color: '#999',
paddingHorizontal: 12,
paddingBottom: 12,
},
fullscreenContainer: {
flex: 1,
backgroundColor: '#000',
},
fullscreenVideo: {
width: '100%',
height: '100%',
},
fullscreenOverlay: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 24,
backgroundColor: 'linear-gradient(to top, rgba(0,0,0,0.8), transparent)',
},
fullscreenTitle: {
fontSize: 22,
fontWeight: '600',
color: '#fff',
marginBottom: 8,
},
fullscreenDescription: {
fontSize: 15,
color: 'rgba(255, 255, 255, 0.8)',
},
closeButton: {
position: 'absolute',
top: 50,
right: 20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 24,
color: '#fff',
fontWeight: '600',
},
footer: {
padding: 16,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#E5E6EB',
alignItems: 'center',
},
footerText: {
fontSize: 14,
color: '#666',
},
});
export default VideoListCarouselScreen;
四、TypeScript 类型错误修复说明
在使用视频列表功能时,建议正确定义类型以避免类型错误:
typescript
import { NativeSyntheticEvent, NativeScrollEvent } from 'react-native';
// 正确定义滚动事件处理函数类型
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
console.log('滚动事件:', event.nativeEvent.contentOffset);
};
五、OpenHarmony6.0 专属避坑指南
以下是鸿蒙 RN 开发中实现「视频列表播放」的所有真实高频踩坑点,按出现频率排序,问题现象贴合开发实际,解决方案均为「一行代码/简单配置」,所有方案均为鸿蒙端专属最优解:
| 问题现象 | 问题原因 | 鸿蒙端最优解决方案 |
|---|---|---|
| 列表滚动时视频卡顿 | 多个视频同时加载导致内存占用过高 | ✅ 只播放当前可见区域的视频,使用 paused 控制其他视频暂停 |
| 网格布局视频尺寸不一致 | 未正确设置视频容器的宽高比 | ✅ 使用 aspectRatio: 16/9 保持统一的视频比例 |
| 视频列表内存占用过高 | 所有视频组件同时渲染 | ✅ 使用 FlatList 的 removeClippedSubviews 属性优化渲染 |
| 全屏播放返回后列表错位 | 全屏状态未正确处理 | ✅ 使用 SafeAreaView 确保布局在状态栏下方 |
| 视频切换时音频重叠 | 旧视频未正确暂停 | ✅ 切换前先暂停当前播放的视频 |
| 视频列表滚动性能差 | 未启用视图回收机制 | ✅ 确保 FlatList 使用 getItemLayout 优化性能 |
| 网格点击视频无反应 | 视频容器遮挡了点击事件 | ✅ 确保点击事件绑定在最外层容器上 |
⚠️ 特别注意:鸿蒙端使用视频列表的要求:
- 内存优化 - ✅ 同时播放的视频不超过 2 个
- 性能优化 - ✅ 使用
removeClippedSubviews减少渲染负担 - 用户体验 - ✅ 提供清晰的视频切换指示和加载状态
- 网络优化 - ✅ 使用低码率的缩略图,高清视频按需加载
六、扩展用法:视频列表高频进阶优化(纯原生 无依赖 鸿蒙适配)
基于本次的核心视频列表代码,结合 RN 的内置能力,可轻松实现鸿蒙端开发中所有高频的视频列表进阶需求:
✔️ 扩展1:视频预加载
提前加载下一个视频,提升切换流畅度:
typescript
const [preloadIndex, setPreloadIndex] = useState<number>(1);
useEffect(() => {
setPreloadIndex((currentIndex + 1) % videoList.length);
}, [currentIndex]);
<Video
source={{ uri: videoList[currentIndex].uri }}
preload={videoList[preloadIndex].uri}
/>
✔️ 扩展2:视频缓存管理
实现视频缓存清理和管理:
typescript
const clearVideoCache = () => {
if (videoRef.current) {
videoRef.current.seek(0);
}
};
✔️ 扩展3:视频播放进度记忆
记住每个视频的播放进度:
typescript
const [videoProgress, setVideoProgress] = useState<Record<string, number>>({});
const handleProgress = (item: VideoItem, currentTime: number) => {
setVideoProgress(prev => ({
...prev,
[item.id]: currentTime,
}));
};
<Video
source={{ uri: item.uri }}
onProgress={(data) => handleProgress(item, data.currentTime)}
initialPosition={videoProgress[item.id]}
/>
✔️ 扩展4:视频分享功能
支持分享当前播放的视频:
typescript
import { Share } from 'react-native';
const shareVideo = (item: VideoItem) => {
Share.share({
message: `观看精彩视频:${item.title}\n${item.uri}`,
url: item.uri,
});
};