HarmonyOS7 ArkUI 多媒体播放列表 - 视频列表、分类Tab与播放状态实战:把案例真正写懂
文章目录

前言
我比较喜欢这种能看出业务味道的案例。代码里不只有布局,还有筛选、统计、切换、校验这些真正项目里常见的东西。
这个案例的主线是 多媒体播放列表,后面的 视频列表、分类Tab与播放状态 则把页面做得更像真实业务页。读代码时,建议把交互、状态和列表这三条线一起看。
为什么这个案例值得写
MediaPlaylistPage 对应的是 多媒体播放列表 - 视频列表、分类Tab与播放状态 这个业务场景。别把它只当成一个 UI 练习页,它真正有价值的地方在于:同一个页面里同时出现了状态切换、列表渲染、条件展示和用户反馈。
| 观察点 | 在这个案例里的表现 |
|---|---|
| 页面主题 | 多媒体播放列表 |
| 主要文案 | 技术, 鸿蒙开发从入门到项目实战, 码农小王, 8.2万, TypeScript 高级特性详解, 前端进阶 |
| 常用组件 | Column, Divider, ForEach, Row, Scroll, Stack, Text, TextInput |
| 适合练习 | 状态驱动 UI、局部刷新、事件回调、页面结构拆分 |
使用方法
把完整代码放进 ArkTS 页面文件后,就可以直接在预览器里运行。实际使用时,建议先手动点一遍页面里的主要交互,看状态有没有跟着变化;然后再去对照代码,理解每个 @State 字段到底控制了哪一块区域。
如果你想把这个案例改成自己的版本,我建议先做三件事:换掉模拟数据、调整筛选规则、再把重复结构抽成 Builder 或子组件。顺序别反,先改结构往往最容易把自己绕进去。
界面背后的数据流
ArkUI 页面写顺手之后,你会发现很多问题其实都不是样式问题,而是状态没放对位置。这个案例里能直接看到哪些数据是输入、哪些是选择、哪些是列表源,读起来会比较顺。
| 状态字段 | 类型 | 说明 |
|---|---|---|
activeCategory |
number |
当前选中项,决定内容切换、高亮或过滤结果 |
playingId |
number |
布尔状态,控制弹层、模式或显示隐藏 |
videos |
VideoItem_zjd6[] |
记录当前展示状态或页面选择 |
searchText |
string |
用户输入值,会直接参与计算、筛选或提交 |
我自己看这类示例时,会先把
@State和事件函数圈出来,再去看布局。这样不会被大段Column、Row搞乱节奏。
单独看这段代码
片段 1:get filteredVideos(): VideoItem_zjd6[] {
这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。
get filteredVideos(): VideoItem_zjd6[] {
const cat = this.categories[this.activeCategory]
return this.videos.filter((v: VideoItem_zjd6) => {
const matchCat = cat === '全部' || v.category === cat
const matchSearch = this.searchText === '' || v.title.includes(this.searchText) || v.author.includes(this.searchText)
return matchCat && matchSearch
})
}

片段 2:toggleLike(id: number) {
这段代码我建议单独看。它通常负责派生数据、交互动作或者局部渲染逻辑,把这部分抽出去之后,页面主结构会干净很多,后续要改规则也更省事。
toggleLike(id: number) {
this.videos = this.videos.map((v: VideoItem_zjd6) => {
if (v.id === id) {
v.likes = v.isLiked ? v.likes - 1 : v.likes + 1
v.isLiked = !v.isLiked
return v
}
return v
})
}
必读小结
适合拿来练手的能力:页面拆分、状态联动、条件渲染、交互反馈。
推荐阅读顺序:先看前言 -> 再跑页面 -> 再看状态表 -> 最后啃完整代码
如果只想快速上手,优先改模拟数据和交互函数,收益最高
我会重点检查的几个地方
- 点击或输入之后,界面有没有立即更新
- 列表渲染是否依赖了稳定的数据结构
- 筛选、统计、派生数据是不是单独放进函数里
- 是否还有重复布局可以继续抽出去
- 文案、颜色、间距是不是同一套风格
完整代码
// MediaPlaylistPage - 多媒体播放列表 - 视频列表、分类Tab与播放状态
interface VideoItem_zjd6 {
id: number
title: string
author: string
duration: string
views: string
likes: number
coverColor: string
category: string
isLiked: boolean
isCollected: boolean
}
@Entry
@Component
struct MediaPlaylistPage {
@State activeCategory: number = 0
@State playingId: number = 1
@State videos: VideoItem_zjd6[] = [
{ id: 1, title: 'HarmonyOS ArkUI 完整教程 2024', author: '技术达人', duration: '2:30:15', views: '12.5万', likes: 3210, coverColor: '#667eea', category: '技术', isLiked: false, isCollected: true },
{ id: 2, title: '鸿蒙开发从入门到项目实战', author: '码农小王', duration: '1:45:30', views: '8.2万', likes: 1890, coverColor: '#f64f59', category: '技术', isLiked: true, isCollected: false },
{ id: 3, title: 'TypeScript 高级特性详解', author: '前端进阶', duration: '58:20', views: '5.6万', likes: 990, coverColor: '#00b894', category: '技术', isLiked: false, isCollected: false },
{ id: 4, title: '深圳旅游4K风景大片', author: '旅行者', duration: '15:40', views: '23.1万', likes: 8900, coverColor: '#fd79a8', category: '旅行', isLiked: false, isCollected: false },
{ id: 5, title: '西藏自驾游全程记录', author: '探险家', duration: '42:10', views: '18.7万', likes: 6230, coverColor: '#74b9ff', category: '旅行', isLiked: true, isCollected: true },
{ id: 6, title: '健身房胸部训练完整计划', author: '健身教练', duration: '25:05', views: '4.8万', likes: 2100, coverColor: '#6c5ce7', category: '运动', isLiked: false, isCollected: false },
{ id: 7, title: '零基础学瑜伽 第一课', author: '瑜伽老师', duration: '30:00', views: '11.2万', likes: 4500, coverColor: '#a29bfe', category: '运动', isLiked: false, isCollected: false },
{ id: 8, title: '家常川菜10道合集', author: '厨艺大师', duration: '22:15', views: '31.8万', likes: 12000, coverColor: '#ff7675', category: '美食', isLiked: true, isCollected: true },
]
@State searchText: string = ''
private categories: string[] = ['全部', '技术', '旅行', '运动', '美食']
get filteredVideos(): VideoItem_zjd6[] {
const cat = this.categories[this.activeCategory]
return this.videos.filter((v: VideoItem_zjd6) => {
const matchCat = cat === '全部' || v.category === cat
const matchSearch = this.searchText === '' || v.title.includes(this.searchText) || v.author.includes(this.searchText)
return matchCat && matchSearch
})
}
toggleLike(id: number) {
this.videos = this.videos.map((v: VideoItem_zjd6) => {
if (v.id === id) {
v.likes = v.isLiked ? v.likes - 1 : v.likes + 1
v.isLiked = !v.isLiked
return v
}
return v
})
}
toggleCollect(id: number) {
this.videos = this.videos.map((v) => { if (v.id === id) { v.isCollected = !v.isCollected } return v })
}
formatLikes(n: number): string {
return n >= 10000 ? `${(n / 10000).toFixed(1)}万` : `${n}`
}
@Builder
VideoCard(item: VideoItem_zjd6) {
Column({ space: 0 }) {
// 封面
Stack({ alignContent: Alignment.BottomStart }) {
Column()
.width('100%')
.height(200)
.linearGradient({ angle: 135, colors: [[item.coverColor, 0], ['#00000080', 1]] })
// 播放按钮
Stack() {
Circle().width(52).height(52).fill('#ffffff40')
Text(this.playingId === item.id ? '⏸' : '▶')
.fontSize(22)
.fontColor('#ffffff')
.margin({ left: this.playingId === item.id ? 0 : 2 })
}
.position({ x: '50%', y: '50%' })
.translate({ x: -26, y: -26 })
.onClick(() => { this.playingId = item.id })
// 时长
Text(item.duration)
.fontSize(12)
.fontColor('#ffffff')
.backgroundColor('#00000080')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(4)
.margin(10)
if (this.playingId === item.id) {
Text('正在播放')
.fontSize(11)
.fontColor('#ffffff')
.backgroundColor('#1890ff')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(4)
.position({ x: 0, y: 0 })
.margin(10)
}
}
.width('100%')
// 视频信息
Column({ space: 10 }) {
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#1a1a1a')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.lineHeight(22)
.width('100%')
Row({ space: 10 }) {
// 作者头像
Stack() {
Circle().width(28).height(28).fill(item.coverColor + '60')
Text(item.author.charAt(0)).fontSize(12).fontColor('#ffffff').fontWeight(FontWeight.Bold)
}
Text(item.author)
.fontSize(13)
.fontColor('#666666')
Blank()
Text(`${item.views}次观看`)
.fontSize(12)
.fontColor('#aaaaaa')
}
.width('100%')
Row({ space: 0 }) {
Text(item.category)
.fontSize(11)
.fontColor(item.coverColor)
.backgroundColor(item.coverColor + '20')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
Blank()
// 点赞
Row({ space: 4 }) {
Text(item.isLiked ? '❤️' : '🤍').fontSize(16)
Text(this.formatLikes(item.likes)).fontSize(13).fontColor(item.isLiked ? '#ff4d4f' : '#aaaaaa')
}
.padding({ left: 10, right: 10 })
.onClick(() => this.toggleLike(item.id))
// 收藏
Row({ space: 4 }) {
Text(item.isCollected ? '⭐' : '☆').fontSize(16)
}
.padding({ left: 6, right: 6 })
.onClick(() => this.toggleCollect(item.id))
// 分享
Text('📤').fontSize(16).padding({ left: 6 })
}
.width('100%')
}
.padding(14)
}
.width('100%')
.backgroundColor('#ffffff')
.borderRadius(12)
.clip(true)
}
build() {
Column({ space: 0 }) {
// 顶部
Column({ space: 12 }) {
Row() {
Text('视频').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')
Blank()
Text('🔍').fontSize(20).fontColor('#666666')
}
.width('100%')
Row({ space: 10 }) {
Text('🔍').fontSize(16).fontColor('#aaaaaa')
TextInput({ placeholder: '搜索视频或作者...', text: this.searchText })
.layoutWeight(1).height(36).fontSize(14).backgroundColor('transparent')
.onChange((v: string) => { this.searchText = v })
}
.padding({ left: 12, right: 12 })
.backgroundColor('#f5f5f5')
.borderRadius(20)
.height(44)
}
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
.backgroundColor('#ffffff')
// 分类Tab
Scroll() {
Row({ space: 0 }) {
ForEach(this.categories, (cat: string, idx: number) => {
Column() {
Text(cat)
.fontSize(15)
.fontWeight(this.activeCategory === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.activeCategory === idx ? '#1890ff' : '#888888')
.padding({ left: 16, right: 16, top: 10, bottom: 8 })
Divider()
.strokeWidth(2.5)
.color(this.activeCategory === idx ? '#1890ff' : 'transparent')
.margin({ left: 12, right: 12 })
}
.onClick(() => { this.activeCategory = idx })
})
}
.padding({ left: 4, right: 4 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.backgroundColor('#ffffff')
.border({ width: { bottom: 0.5 }, color: '#f0f0f0' })
// 视频列表
Scroll() {
Column({ space: 12 }) {
ForEach(this.filteredVideos, (item: VideoItem_zjd6) => {
this.VideoCard(item)
}, (item: VideoItem_zjd6) => item.id.toString())
Column().height(24)
}
.padding({ left: 16, right: 16, top: 12 })
}
.layoutWeight(1)
.backgroundColor('#f5f7fa')
}
.width('100%')
.height('100%')
.backgroundColor('#f5f7fa')
}
}
继续扩展
这类案例最值得学的,其实不是某一个控件怎么写,而是页面组织方式。把状态放对、把交互函数收好、把布局压简单,后面你接正式项目时会轻松很多。
更多推荐


所有评论(0)