1. 项目背景与目标
作为一名移动应用开发者,我最近在探索HarmonyOS的ArkUI框架时,发现它的声明式UI开发方式与传统命令式布局有很大不同。为了快速掌握ArkUI的核心布局能力,我决定通过实现一个"歌曲列表"的界面来练习。这个看似简单的练习项目,实际上涵盖了现代UI开发中的多个关键技术点:
- 列表数据的动态渲染
- 复杂列表项的自定义布局
- 滚动性能的优化考虑
- 响应式布局的适配方案
选择歌曲列表作为练习对象有几个优势:首先,音乐类应用是移动端最常见的使用场景之一;其次,歌曲列表通常包含封面图、歌曲名、歌手、时长等多种元素,能充分锻炼布局能力;最后,这类界面在真实项目中往往需要处理大量数据,能让我们提前考虑性能问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与项目创建
2.1 开发环境配置
在开始之前,我们需要确保开发环境正确配置。我使用的是最新版的DevEco Studio(3.1版本),配套的HarmonyOS SDK也已更新至API 9。这里有个小技巧:在创建项目时,建议选择"Empty Ability"模板,而不是直接使用列表模板,这样可以让我们从零开始构建,更深入地理解每个布局组件的作用。
注意:确保你的Gradle版本与DevEco Studio兼容,我遇到过因版本不匹配导致的构建失败问题。推荐使用Gradle 7.4+版本。
2.2 项目结构初始化
创建项目后,重点关注以下几个关键文件:
code复制resources/
├── base/
│ ├── element/ # 尺寸、颜色等常量定义
│ └── profile/ # 页面布局文件
entry/src/main/
├── ets/
│ ├── pages/ # 页面代码
│ └── model/ # 数据模型
我习惯先在element/string.json中定义好所有文本常量,比如:
json复制{
"string": [
{
"name": "app_name",
"value": "MusicPlayer"
},
{
"name": "unknown_artist",
"value": "未知歌手"
}
]
}
3. 数据模型设计
3.1 歌曲数据结构
在实现UI之前,我们需要先定义好数据模型。一个歌曲对象通常包含以下属性:
typescript复制class SongItem {
id: string; // 歌曲唯一标识
title: string; // 歌曲名称
artist: string; // 演唱者
album: string; // 专辑名称
duration: number; // 时长(秒)
coverUrl: string; // 封面图URL
isFavorite: boolean; // 是否收藏
}
3.2 模拟数据生成
为了快速验证UI效果,我创建了一个数据生成工具函数:
typescript复制function generateMockSongs(count: number): Array<SongItem> {
const artists = ['周杰伦', '林俊杰', '邓紫棋', '五月天', 'Taylor Swift'];
const albums = ['七里香', '伟大的渺小', '摩天动物园', '自传', '1989'];
return Array.from({length: count}, (_, i) => ({
id: `song_${i}`,
title: `歌曲示例 ${i + 1}`,
artist: artists[i % artists.length],
album: albums[i % albums.length],
duration: 180 + (i % 60),
coverUrl: `https://example.com/covers/${i % 10}.jpg`,
isFavorite: i % 4 === 0
}));
}
实际项目中,这些数据应该来自网络API,但开发初期使用模拟数据能提高效率。我习惯将模拟数据与实际数据接口保持相同结构,便于后续切换。
4. 列表布局实现
4.1 基础列表构建
ArkUI提供了List组件来实现滚动列表。最基本的歌曲列表实现如下:
typescript复制@Component
struct SongListPage {
private songs: SongItem[] = generateMockSongs(50);
build() {
List({ space: 10 }) {
ForEach(this.songs, (song: SongItem) => {
ListItem() {
// 列表项内容将在4.2节实现
}
}, (song: SongItem) => song.id)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
这里有几个关键点需要注意:
ForEach的第三个参数是键值生成函数,必须保证唯一性space参数控制列表项间距,根据设计规范通常设置为10-20vp- 列表必须显式设置宽度和高度,否则可能无法正常显示
4.2 列表项UI设计
一个专业的歌曲列表项通常包含左侧封面图、中间歌曲信息、右侧操作按钮。以下是具体实现:
typescript复制ListItem() {
Row() {
// 封面图
Image(song.coverUrl)
.width(80)
.height(80)
.borderRadius(8)
.objectFit(ImageFit.Cover)
// 歌曲信息
Column() {
Text(song.title)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({overflow: TextOverflow.Ellipsis})
Text(`${song.artist} · ${song.album}`)
.fontSize(14)
.fontColor('#666')
.maxLines(1)
.textOverflow({overflow: TextOverflow.Ellipsis})
}
.layoutWeight(1)
.padding(10)
// 右侧区域
Row() {
Text(this.formatDuration(song.duration))
.fontSize(14)
.fontColor('#999')
Image($r('app.media.ic_favorite'))
.width(20)
.height(20)
.margin({left: 15})
.opacity(song.isFavorite ? 1 : 0.3)
}
.alignItems(VerticalAlign.Center)
}
.padding(10)
.backgroundColor('#FFFFFF')
.borderRadius(12)
}
实现过程中的几个技巧:
- 使用
layoutWeight让中间内容自适应宽度 - 文本必须设置
maxLines和textOverflow防止溢出 - 通过
opacity变化表示收藏状态,比切换图片更流畅 - 圆角和阴影能提升视觉层次感
4.3 性能优化实践
当列表数据量较大时(如超过100条),需要特别注意性能优化:
1. 图片懒加载
typescript复制Image(song.coverUrl)
.syncLoad(false) // 启用异步加载
2. 列表项复用优化
typescript复制List() {
ForEach(this.songs, (song: SongItem) => {
ListItem() {
SongItemComponent({ song: song })
}
}, (song: SongItem) => song.id)
}
.cachedCount(5) // 缓存屏幕外5个item
3. 避免复杂计算
typescript复制// 不推荐:每次build都会执行
Text(this.formatDuration(song.duration))
// 推荐:预先计算好
Text(song.formattedDuration)
5. 交互功能实现
5.1 点击事件处理
为列表项添加点击事件:
typescript复制ListItem() {
// ...列表项内容
.onClick(() => {
router.push({
url: 'pages/SongDetailPage',
params: { songId: song.id }
})
})
}
实际项目中,点击效果需要添加视觉反馈。我通常这样做:
typescript复制.stateStyles({ pressed: { .opacity(0.6) } })
5.2 收藏功能实现
收藏按钮需要独立处理点击事件,防止冒泡:
typescript复制Image($r('app.media.ic_favorite'))
.onClick((e: ClickEvent) => {
e.stopPropagation(); // 阻止事件冒泡
this.toggleFavorite(song.id);
})
对应的状态管理方法:
typescript复制@State songs: SongItem[] = generateMockSongs(50);
toggleFavorite(songId: string) {
this.songs = this.songs.map(song => {
if (song.id === songId) {
return { ...song, isFavorite: !song.isFavorite };
}
return song;
});
}
5.3 滑动删除功能
ArkUI的ListItem提供了滑动删除的API:
typescript复制ListItem() {
// ...列表项内容
}
.swipeAction({
end: new Button() {
Text('删除')
.fontColor('#FFFFFF')
}
.backgroundColor('#FF0000')
.width(80)
.height('100%')
.onClick(() => {
this.deleteSong(song.id);
})
})
对应的删除方法:
typescript复制deleteSong(songId: string) {
const index = this.songs.findIndex(song => song.id === songId);
if (index >= 0) {
this.songs.splice(index, 1);
this.songs = [...this.songs]; // 触发UI更新
}
}
6. 样式与主题适配
6.1 尺寸单位选择
在ArkUI中,推荐使用vp(虚拟像素)作为尺寸单位:
typescript复制Image(song.coverUrl)
.width(80)
.height(80)
对于字体大小,使用fp(字体像素)能更好地适应系统字体大小设置:
typescript复制Text(song.title)
.fontSize(18)
6.2 暗黑模式适配
通过资源文件实现多主题支持。首先在resources/base/element/下创建:
color.json- 定义颜色变量theme.json- 定义主题映射
示例color.json:
json复制{
"color": [
{
"name": "text_primary",
"value": "#333333"
},
{
"name": "background_secondary",
"value": "#F5F5F5"
}
]
}
然后在代码中引用:
typescript复制Text(song.title)
.fontColor($r('app.color.text_primary'))
6.3 全局样式管理
对于重复使用的样式,可以提取为公共组件或样式函数:
typescript复制// 在utils/Styles.ts中
export const textEllipsis = () => {
return {
.maxLines(1)
.textOverflow({overflow: TextOverflow.Ellipsis})
};
};
// 使用处
Text(song.title)
.fontSize(18)
.extend(textEllipsis())
7. 测试与调试技巧
7.1 边界情况测试
在实际测试中,我发现以下几种边界情况需要特别注意:
- 超长文本:歌曲名或歌手名特别长时的显示效果
- 空数据状态:歌曲列表为空时的占位UI
- 网络图片加载失败:封面图URL无效时的降级处理
对应的解决方案:
typescript复制// 图片加载失败处理
Image(song.coverUrl)
.error($r('app.media.ic_default_cover'))
// 空状态处理
if (this.songs.length === 0) {
Text('暂无歌曲')
.fontSize(16)
.fontColor('#999')
}
7.2 性能分析工具
DevEco Studio提供了性能分析工具,我通常关注:
- FPS:列表滚动时的帧率,应保持在50FPS以上
- 内存占用:列表数据增长时的内存变化
- CPU使用率:交互时的CPU负载
调试技巧:在开发者选项中开启"显示布局边界",可以直观查看列表项的复用情况。
7.3 真机测试要点
在真机测试时,有几个容易忽略的点:
- 不同屏幕尺寸的适配情况
- 低端设备上的性能表现
- 全面屏手机的边缘手势冲突
我通常会准备以下测试设备矩阵:
- 小屏手机(如iPhone SE)
- 大屏手机(如Mate X系列)
- 中端处理器设备
8. 项目扩展思路
完成基础功能后,可以考虑以下扩展方向:
8.1 高级交互功能
- 拖拽排序:实现歌曲列表的手动排序
- 多选操作:批量选择歌曲进行操作
- 分组列表:按字母或分类分组显示
8.2 动画效果增强
- 列表项入场动画
- 收藏按钮的点赞动效
- 滑动删除的弹性效果
8.3 架构优化
- 状态管理:引入Redux或MobX
- 数据持久化:本地缓存歌曲列表
- 组件拆分:将列表项拆分为独立组件
实现这些扩展功能时,我发现保持代码的可维护性非常重要。我的经验是:
- 早期就建立良好的组件结构
- 为自定义组件定义清晰的props接口
- 将业务逻辑与UI表现分离
这个歌曲列表练习项目虽然看似简单,但涵盖了现代移动UI开发的诸多核心概念。通过反复迭代和优化,我不仅掌握了ArkUI的基本用法,还对性能优化和交互设计有了更深的理解。在实际工作中,这种基础组件的实现质量往往决定了整个应用的用户体验水平。
