rn_for_openharmony_steam资讯app实战-游戏视频实现
本文介绍了如何使用 React Native for OpenHarmony 开发 Steam 资讯 App 的游戏视频页面。该页面从 Steam API 获取游戏视频数据,包含视频列表展示和播放功能。文章详细讲解了数据结构、页面布局、核心代码实现,包括数据加载、视频列表渲染、播放器集成等关键功能。特别强调了性能优化措施如图片缓存、错误处理,以及用户体验设计如加载状态和空状态处理。该实现可作为 R
React Native for OpenHarmony 实战:Steam 资讯 App 游戏视频页面
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_steam
游戏视频是展示游戏动态效果的重要方式。视频页面需要展示游戏的所有视频,并支持视频播放、列表管理等功能。这个页面涉及视频播放器集成、列表渲染和交互设计。
视频数据的来源
游戏的视频数据同样来自 appdetails API 的返回结果。这个 API 返回的数据中包含 movies 数组:
export const getAppDetails = async (appId: number) => {
const res = await fetch(`${STORE_API}/appdetails?appids=${appId}`);
return res.json();
};
这里做了什么: 调用 appdetails 接口获取游戏详情。这个接口返回的数据包含游戏的所有信息,包括视频、截图等。
为什么复用 appdetails: 虽然可以单独调用视频 API,但 appdetails 已经包含了所有需要的信息。这样可以减少 API 调用次数,提高效率。
数据结构: appdetails 返回的 movies 数组大概是这样的:
{
"movies": [
{
"id": 256658,
"name": "Trailer",
"thumbnail": "https://cdn.cloudflare.steamstatic.com/steam/apps/256658/movie.293x165.jpg",
"webm": {
"480": "https://cdn.cloudflare.steamstatic.com/steam/apps/256658/movie480.webm",
"max": "https://cdn.cloudflare.steamstatic.com/steam/apps/256658/movie_max.webm"
},
"mp4": {
"480": "https://cdn.cloudflare.steamstatic.com/steam/apps/256658/movie480.mp4",
"max": "https://cdn.cloudflare.steamstatic.com/steam/apps/256658/movie_max.mp4"
}
}
]
}
关键字段说明:
id- 视频的唯一标识name- 视频名称(比如"Trailer"、“Gameplay”)thumbnail- 视频缩略图 URLwebm- WebM 格式的视频 URL(不同分辨率)mp4- MP4 格式的视频 URL(不同分辨率)
页面结构设计
视频页面的布局需要展示视频列表:
顶部 是 Header,显示游戏名称和返回按钮。
中间 是视频列表,每个视频显示缩略图、名称和时长。
点击视频 可以打开视频播放器进行播放。
底部 是 TabBar,保持应用的导航一致性。
核心代码实现
组件初始化和数据加载
export const GameVideosScreen = () => {
const {selectedAppId} = useApp();
const [videos, setVideos] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedVideo, setSelectedVideo] = useState<any>(null);
这里做了什么: 定义了视频列表、加载状态和当前选中的视频。selectedVideo 用来存储用户点击的视频,用于播放。
状态的作用: videos 存储所有视频数据,loading 控制加载动画,selectedVideo 控制视频播放器的显示。
为什么分离 selectedVideo: 这样可以独立控制视频播放器的显示和隐藏,不需要修改 videos 数组。
数据加载
useEffect(() => {
if (!selectedAppId) return;
getAppDetails(selectedAppId).then(data => {
const gameData = data?.[selectedAppId]?.data;
setVideos(gameData?.movies || []);
setLoading(false);
}).catch(() => setLoading(false));
}, [selectedAppId]);
这里做了什么: 当组件挂载或 selectedAppId 变化时,调用 getAppDetails() 获取游戏详情,然后提取 movies 数组。
数据提取的安全性: 使用可选链操作符 ?. 安全地访问嵌套属性。如果某个属性不存在,不会抛出错误。
默认值的处理: 如果 movies 不存在,使用空数组作为默认值。这样即使 API 返回的数据不完整,也不会导致应用崩溃。
错误处理: 即使请求失败,也会把 loading 设为 false,这样用户不会一直看到加载动画。
视频列表的渲染
<FlatList
data={videos}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => (
<TouchableOpacity
style={styles.videoItem}
onPress={() => setSelectedVideo(item)}
>
<Image
source={{uri: item.thumbnail}}
style={styles.thumbnail}
cache="force-cache"
/>
<View style={styles.playIcon}>
<Text style={styles.playIconText}>▶</Text>
</View>
<View style={styles.videoInfo}>
<Text style={styles.videoName} numberOfLines={2}>{item.name}</Text>
</View>
</TouchableOpacity>
)}
/>
这里做了什么:
- 缩略图显示 - 使用 thumbnail 显示视频缩略图
- 播放图标 - 在缩略图上显示播放按钮
- 视频名称 - 显示视频的名称
- 点击播放 - 点击时设置 selectedVideo,打开播放器
为什么显示播放图标: 播放图标可以清晰地表示这是一个可播放的视频。用户能一眼看出这是视频而不是图片。
缩略图的优势: 使用缩略图而不是完整视频可以显著减少加载时间。缩略图通常只有几十 KB。
图片缓存: cache="force-cache" 告诉 React Native 优先使用缓存的图片。这样用户滚动列表时,已经加载过的缩略图会从缓存读取。
视频播放器
{selectedVideo && (
<Modal visible={true} transparent={false}>
<View style={styles.playerContainer}>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => setSelectedVideo(null)}
>
<Text style={styles.closeBtnText}>✕</Text>
</TouchableOpacity>
<Video
source={{uri: selectedVideo.mp4.max}}
style={styles.video}
controls={true}
resizeMode="contain"
onError={(error) => console.log('Video error:', error)}
/>
</View>
</Modal>
)}
这里做了什么:
- Modal 显示 - 当 selectedVideo 不为 null 时显示 Modal
- Video 组件 - 使用 react-native-video 播放视频
- controls={true} - 显示播放控制条
- resizeMode=“contain” - 保持视频比例
为什么用 Modal: Modal 可以全屏显示视频播放器,提供更好的观看体验。用户可以通过关闭按钮返回列表。
选择 mp4.max: 这是最高质量的 MP4 视频。如果需要支持不同网络条件,可以根据网络速度选择不同分辨率。
controls 属性: 这个属性显示播放、暂停、进度条等控制按钮。用户可以方便地控制视频播放。
错误处理: onError 回调可以捕获视频播放错误。在实际项目中,应该显示错误提示给用户。
加载和空状态
if (loading) {
return (
<View style={styles.container}>
<Header title="游戏视频" showBack />
<Loading />
<TabBar />
</View>
);
}
if (videos.length === 0) {
return (
<View style={styles.container}>
<Header title="游戏视频" showBack />
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>暂无视频</Text>
</View>
<TabBar />
</View>
);
}
这里做了什么:
- 加载状态 - 显示 Loading 组件
- 空状态 - 如果没有视频,显示"暂无视频"提示
用户体验: 通过显示明确的状态提示,用户能理解当前的情况。不会因为看到空白页面而困惑。
提前返回模式: 这种模式让代码更清晰,避免嵌套多层 if-else。
样式设计
视频项的样式
videoItem: {
margin: 12,
borderRadius: 8,
overflow: 'hidden',
backgroundColor: '#1b2838',
},
thumbnail: {
width: '100%',
height: 180,
},
playIcon: {
position: 'absolute',
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
justifyContent: 'center',
alignItems: 'center',
top: '50%',
left: '50%',
marginTop: -30,
marginLeft: -30,
},
playIconText: {
fontSize: 24,
color: '#fff',
fontWeight: 'bold',
},
videoInfo: {
padding: 12,
},
videoName: {
fontSize: 14,
fontWeight: '600',
color: '#acdbf5',
},
这里的设计考量:
- margin: 12 - 视频项之间有 12 的间距
- borderRadius: 8 - 圆角使视频项看起来更现代
- overflow: ‘hidden’ - 超出边界的内容被隐藏
- position: ‘absolute’ - 播放图标绝对定位在缩略图中心
margin 的作用: 项之间的间距使列表看起来不拥挤。12 是一个常见的间距单位。
overflow: ‘hidden’ 的作用: 这个属性确保圆角效果正确显示。如果不设置,缩略图可能会超出圆角边界。
播放图标的定位: 使用 top: '50%' 和 left: '50%' 将播放图标放在中心,然后用负 margin 调整。这样可以精确控制位置。
半透明背景: 播放图标的背景是半透明的白色,这样可以在不同颜色的缩略图上都清晰显示。
播放器的样式
playerContainer: {
flex: 1,
backgroundColor: '#000',
justifyContent: 'center',
alignItems: 'center',
},
video: {
width: '100%',
height: '100%',
},
closeBtn: {
position: 'absolute',
top: 20,
right: 20,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
},
closeBtnText: {
fontSize: 24,
color: '#fff',
fontWeight: 'bold',
},
这里的设计考量:
- backgroundColor: ‘#000’ - 黑色背景,适合视频播放
- width: ‘100%’, height: ‘100%’ - 视频占据整个屏幕
- position: ‘absolute’ - 关闭按钮绝对定位
- zIndex: 10 - 确保关闭按钮在最上层
黑色背景的选择: 黑色背景可以让视频更突出,不会有其他颜色的干扰。这是视频播放器的标准做法。
全屏显示: 视频占据整个屏幕,提供沉浸式的观看体验。
关闭按钮的设计: 圆形按钮(borderRadius: 20)看起来更现代。半透明的白色背景可以在黑色背景上清晰显示。
视频播放的优化
自适应分辨率
const selectVideoUrl = (video: any) => {
// 优先选择 MP4 格式
if (video.mp4) {
// 如果网络较好,选择最高质量
return video.mp4.max || video.mp4['480'];
}
// 降级到 WebM 格式
if (video.webm) {
return video.webm.max || video.webm['480'];
}
return null;
};
这里做了什么:
- 格式优先级 - 优先选择 MP4,降级到 WebM
- 分辨率选择 - 优先选择最高质量,降级到 480p
为什么要自适应: 不同设备和网络条件下,用户需要不同质量的视频。高质量视频在 WiFi 下很好,但在 4G 下可能会卡顿。
MP4 vs WebM: MP4 的兼容性更好,大多数设备都支持。WebM 是备选方案。
分辨率降级: 如果最高质量的视频加载失败,可以自动降级到 480p。这样可以保证视频能播放。
预加载缩略图
const [thumbnailLoading, setThumbnailLoading] = useState<Record<number, boolean>>({});
<Image
source={{uri: item.thumbnail}}
style={styles.thumbnail}
onLoadStart={() => setThumbnailLoading({...thumbnailLoading, [item.id]: true})}
onLoadEnd={() => setThumbnailLoading({...thumbnailLoading, [item.id]: false})}
/>
{thumbnailLoading[item.id] && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#66c0f4" />
</View>
)}
这里做了什么:
- 加载状态追踪 - 记录每个缩略图的加载状态
- 加载动画 - 缩略图加载时显示加载动画
为什么要追踪加载状态: 这样可以在缩略图加载时显示占位符或加载动画。用户能看到缩略图正在加载。
Record 类型的使用: Record<number, boolean> 表示一个对象,键是视频 ID,值是加载状态。这样可以独立追踪每个缩略图的加载状态。
用户体验: 加载动画告诉用户缩略图正在加载,而不是应用卡住了。
视频播放事件处理
const [videoState, setVideoState] = useState({
duration: 0,
currentTime: 0,
isPlaying: false,
});
<Video
source={{uri: selectedVideo.mp4.max}}
style={styles.video}
controls={true}
resizeMode="contain"
onLoad={(data) => setVideoState({...videoState, duration: data.duration})}
onProgress={(data) => setVideoState({...videoState, currentTime: data.currentTime})}
onPlay={() => setVideoState({...videoState, isPlaying: true})}
onPause={() => setVideoState({...videoState, isPlaying: false})}
onError={(error) => console.log('Video error:', error)}
/>
这里做了什么:
- onLoad - 视频加载完成时调用,获取视频时长
- onProgress - 视频播放进度变化时调用
- onPlay/onPause - 视频播放/暂停时调用
为什么要追踪这些事件: 这样可以实现自定义的播放控制、进度显示等功能。
videoState 的作用: 这个状态存储视频的播放信息。可以用来显示当前播放时间、总时长等。
onProgress 的用途: 这个回调可以用来更新进度条、显示当前播放时间等。
完整页面示例
import React, {useEffect, useState} from 'react';
import {View, Text, FlatList, TouchableOpacity, Image, StyleSheet, Modal, ActivityIndicator} from 'react-native';
import Video from 'react-native-video';
import {Header} from '../components/Header';
import {TabBar} from '../components/TabBar';
import {Loading} from '../components/Loading';
import {useApp} from '../store/AppContext';
import {getAppDetails} from '../api/steam';
export const GameVideosScreen = () => {
const {selectedAppId} = useApp();
const [videos, setVideos] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedVideo, setSelectedVideo] = useState<any>(null);
const [thumbnailLoading, setThumbnailLoading] = useState<Record<number, boolean>>({});
useEffect(() => {
if (!selectedAppId) return;
getAppDetails(selectedAppId).then(data => {
const gameData = data?.[selectedAppId]?.data;
setVideos(gameData?.movies || []);
setLoading(false);
}).catch(() => setLoading(false));
}, [selectedAppId]);
const selectVideoUrl = (video: any) => {
if (video.mp4) {
return video.mp4.max || video.mp4['480'];
}
if (video.webm) {
return video.webm.max || video.webm['480'];
}
return null;
};
if (loading) {
return (
<View style={styles.container}>
<Header title="游戏视频" showBack />
<Loading />
<TabBar />
</View>
);
}
return (
<View style={styles.container}>
<Header title="游戏视频" showBack />
{videos.length === 0 ? (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>暂无视频</Text>
</View>
) : (
<FlatList
data={videos}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => (
<TouchableOpacity
style={styles.videoItem}
onPress={() => setSelectedVideo(item)}
>
{thumbnailLoading[item.id] && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color="#66c0f4" />
</View>
)}
<Image
source={{uri: item.thumbnail}}
style={styles.thumbnail}
cache="force-cache"
onLoadStart={() => setThumbnailLoading({...thumbnailLoading, [item.id]: true})}
onLoadEnd={() => setThumbnailLoading({...thumbnailLoading, [item.id]: false})}
/>
<View style={styles.playIcon}>
<Text style={styles.playIconText}>▶</Text>
</View>
<View style={styles.videoInfo}>
<Text style={styles.videoName} numberOfLines={2}>{item.name}</Text>
</View>
</TouchableOpacity>
)}
/>
)}
{selectedVideo && (
<Modal visible={true} transparent={false}>
<View style={styles.playerContainer}>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => setSelectedVideo(null)}
>
<Text style={styles.closeBtnText}>✕</Text>
</TouchableOpacity>
<Video
source={{uri: selectVideoUrl(selectedVideo)}}
style={styles.video}
controls={true}
resizeMode="contain"
onError={(error) => console.log('Video error:', error)}
/>
</View>
</Modal>
)}
<TabBar />
</View>
);
};
const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#171a21'},
emptyContainer: {flex: 1, justifyContent: 'center', alignItems: 'center'},
emptyText: {fontSize: 16, color: '#8f98a0'},
videoItem: {margin: 12, borderRadius: 8, overflow: 'hidden', backgroundColor: '#1b2838'},
thumbnail: {width: '100%', height: 180},
loadingOverlay: {position: 'absolute', width: '100%', height: 180, justifyContent: 'center', alignItems: 'center', backgroundColor: '#1b2838', zIndex: 5},
playIcon: {position: 'absolute', width: 60, height: 60, borderRadius: 30, backgroundColor: 'rgba(255, 255, 255, 0.3)', justifyContent: 'center', alignItems: 'center', top: '50%', left: '50%', marginTop: -30, marginLeft: -30},
playIconText: {fontSize: 24, color: '#fff', fontWeight: 'bold'},
videoInfo: {padding: 12},
videoName: {fontSize: 14, fontWeight: '600', color: '#acdbf5'},
playerContainer: {flex: 1, backgroundColor: '#000', justifyContent: 'center', alignItems: 'center'},
video: {width: '100%', height: '100%'},
closeBtn: {position: 'absolute', top: 20, right: 20, width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.3)', justifyContent: 'center', alignItems: 'center', zIndex: 10},
closeBtnText: {fontSize: 24, color: '#fff', fontWeight: 'bold'},
});
这里做了什么: 完整的游戏视频页面实现,包括:
- 视频列表的加载和渲染
- 视频缩略图显示
- 播放图标显示
- 视频播放器集成
- 加载状态显示
- 空状态处理
小结
游戏视频页面虽然功能相对简单,但涉及了很多实用的开发技巧:
- 视频列表 - 使用 FlatList 渲染视频列表
- 缩略图显示 - 使用缓存提高性能
- 播放器集成 - 使用 react-native-video 播放视频
- 加载状态 - 显示加载动画提升用户体验
- 自适应分辨率 - 根据条件选择合适的视频质量
- 事件处理 - 追踪视频播放事件
下一篇我们来实现游戏评测页面,这个页面会展示用户对游戏的评价和评分,涉及评测数据的展示和统计。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)