[HarmonyOS NEXT 实战案例三] 音乐专辑网格展示(下)
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 概述
在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的音乐专辑网格展示。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的音乐专辑展示页面。
2. 响应式布局实现
2.1 断点响应设置
为了适应不同屏幕尺寸的设备,我们可以使用GridRow组件的breakpoints属性设置断点响应:
GridRow({
columns: { xs: 2, sm: 3, md: 4, lg: 6 },
gutter: { x: 16, y: 24 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
})
这里我们设置了三个断点值:320vp、600vp和840vp,并根据窗口大小自动调整列数:
- 小于320vp:2列
- 320vp-600vp:3列
- 600vp-840vp:4列
- 大于840vp:6列
同时,我们还设置了不同的水平和垂直间距。
2.2 不同断点下的布局效果
下表展示了不同断点下的布局效果:
断点 | 列数 | 适用设备 |
---|---|---|
<320vp | 2列 | 小屏手机 |
320vp-600vp | 3列 | 中屏手机 |
600vp-840vp | 4列 | 大屏手机/小屏平板 |
>840vp | 6列 | 平板/桌面设备 |
3. 专辑卡片优化
3.1 添加阴影效果
为了提升专辑卡片的视觉层次感,我们可以添加阴影效果:
Column() {
Image(album.cover)
.width('100%')
.aspectRatio(1)
.borderRadius(8)
Text(album.name)
.fontSize(16)
.margin({ top: 8 })
.width('100%')
.textAlign(TextAlign.Start)
Text(album.artist)
.fontSize(12)
.fontColor('#9E9E9E')
.margin({ top: 4 })
}
.backgroundColor(Color.White)
.borderRadius(12)
.padding(8)
.shadow({
radius: 12,
color: '#1A000000',
offsetX: 2,
offsetY: 4
})
我们为Column容器添加了白色背景、12vp的圆角、8vp的内边距和阴影效果。阴影设置了12vp的模糊半径、10%透明度的黑色阴影颜色、2vp的X轴偏移和4vp的Y轴偏移。
3.2 添加播放量和时长信息
为了提供更多专辑信息,我们可以在专辑卡片中添加播放量和时长信息:
interface Album {
name: string;
artist: string;
cover: ResourceStr;
playCount: number;
duration: string;
}
private albums:Album[] = [
{ name: '流行热歌', artist: '多位艺人', cover: $r("app.media.big29"), playCount: 1250000, duration: '2小时35分' },
{ name: '古典精选', artist: '世界名曲', cover: $r("app.media.big28"), playCount: 860000, duration: '3小时20分' },
{ name: '摇滚经典', artist: '传奇乐队', cover: $r("app.media.big25"), playCount: 1520000, duration: '1小时50分' },
{ name: '电子舞曲', artist: 'DJ合集', cover: $r("app.media.big23"), playCount: 2100000, duration: '2小时10分' }
]
在专辑卡片中添加播放量和时长信息的UI实现:
Row() {
Text(this.formatPlayCount(album.playCount))
.fontSize(10)
.fontColor('#9E9E9E')
Text(album.duration)
.fontSize(10)
.fontColor('#9E9E9E')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Start)
.margin({ top: 4 })
其中,formatPlayCount方法用于格式化播放量:
private formatPlayCount(count: number): string {
if (count >= 10000) {
return (count / 10000).toFixed(1) + '万';
}
return count.toString();
}
3.3 添加专辑标签
为了更好地分类专辑,我们可以添加专辑标签:
interface Album {
name: string;
artist: string;
cover: ResourceStr;
playCount: number;
duration: string;
tags: string[];
}
在专辑数据中添加标签:
private albums:Album[] = [
{ name: '流行热歌', artist: '多位艺人', cover: $r("app.media.big29"), playCount: 1250000, duration: '2小时35分', tags: ['流行', '热门'] },
{ name: '古典精选', artist: '世界名曲', cover: $r("app.media.big28"), playCount: 860000, duration: '3小时20分', tags: ['古典', '轻音乐'] },
{ name: '摇滚经典', artist: '传奇乐队', cover: $r("app.media.big25"), playCount: 1520000, duration: '1小时50分', tags: ['摇滚', '经典'] },
{ name: '电子舞曲', artist: 'DJ合集', cover: $r("app.media.big23"), playCount: 2100000, duration: '2小时10分', tags: ['电子', 'DJ'] }
]
在专辑卡片中添加标签的UI实现:
Row() {
ForEach(album.tags, (tag: string) => {
Text(tag)
.fontSize(10)
.fontColor('#FF5722')
.backgroundColor('#FFF3EF')
.borderRadius(4)
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
.margin({ right: 4 })
})
}
.width('100%')
.justifyContent(FlexAlign.Start)
.margin({ top: 8 })
4. 交互功能实现
4.1 专辑点击事件
为专辑卡片添加点击事件,实现跳转到专辑详情页的功能:
Column() {
// 专辑卡片内容
}
.backgroundColor(Color.White)
.borderRadius(12)
.padding(8)
.shadow({
radius: 12,
color: '#1A000000',
offsetX: 2,
offsetY: 4
})
.onClick(() => {
this.onAlbumClick(album);
})
点击事件处理方法:
private onAlbumClick(album: Album): void {
console.info(`点击了专辑:${album.name}`);
// 这里可以添加跳转到专辑详情页的逻辑
}
4.2 添加收藏功能
为专辑添加收藏功能,用户可以收藏自己喜欢的专辑:
interface Album {
id: string; // 添加id字段
name: string;
artist: string;
cover: ResourceStr;
playCount: number;
duration: string;
tags: string[];
isFavorite: boolean; // 添加收藏状态字段
}
在专辑卡片中添加收藏按钮:
Stack() {
Image(album.cover)
.width('100%')
.aspectRatio(1)
.borderRadius(8)
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image(album.isFavorite ? $r('app.media.ic_favorite_filled') : $r('app.media.ic_favorite'))
.width(20)
.height(20)
.fillColor(album.isFavorite ? '#FF5722' : '#FFFFFF')
}
.width(36)
.height(36)
.backgroundColor('#80000000')
.position({ x: '100%', y: 0 })
.translate({ x: -44, y: 8 })
.onClick((event) => {
this.toggleFavorite(album.id);
event.stopPropagation();
})
}
.width('100%')
收藏状态切换方法:
private toggleFavorite(albumId: string): void {
this.albums = this.albums.map(item => {
if (item.id === albumId) {
return { ...item, isFavorite: !item.isFavorite };
}
return item;
});
}
4.3 添加下拉刷新功能
为了提供更好的用户体验,我们可以添加下拉刷新功能:
Refresh({ refreshing: this.refreshing }) {
Column() {
Text('推荐歌单')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
.width('100%')
.textAlign(TextAlign.Start)
GridRow({
columns: { xs: 2, sm: 3, md: 4, lg: 6 },
gutter: { x: 16, y: 24 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
// 专辑卡片内容
}
}
.width('100%')
.padding(16)
}
.onRefresh(() => {
this.refreshData();
})
刷新数据方法:
@State refreshing: boolean = false;
private refreshData(): void {
this.refreshing = true;
// 模拟网络请求
setTimeout(() => {
// 更新专辑数据
this.refreshing = false;
}, 2000);
}
5. 分类标签栏实现
为了方便用户浏览不同类型的专辑,我们可以添加分类标签栏:
@State currentCategory: string = '推荐';
private categories: string[] = ['推荐', '流行', '摇滚', '古典', '电子', '爵士', '民谣'];
build() {
Column() {
// 标题
Text('音乐专辑')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
.width('100%')
.textAlign(TextAlign.Start)
// 分类标签栏
Scroll(this.scroller) {
Row() {
ForEach(this.categories, (category: string) => {
Text(category)
.fontSize(14)
.fontColor(this.currentCategory === category ? '#FF5722' : '#333333')
.backgroundColor(this.currentCategory === category ? '#FFF3EF' : 'transparent')
.borderRadius(16)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8 })
.onClick(() => {
this.currentCategory = category;
this.filterAlbumsByCategory(category);
})
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.margin({ bottom: 16 })
// 专辑网格
Refresh({ refreshing: this.refreshing }) {
Column() {
GridRow({
columns: { xs: 2, sm: 3, md: 4, lg: 6 },
gutter: { x: 16, y: 24 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
// 专辑卡片内容
}
}
.width('100%')
.padding(16)
}
.onRefresh(() => {
this.refreshData();
})
}
.width('100%')
}
根据分类筛选专辑的方法:
@State displayAlbums: Album[] = [];
private allAlbums: Album[] = []; // 存储所有专辑数据
aboutToAppear() {
this.allAlbums = this.albums; // 初始化所有专辑数据
this.displayAlbums = this.allAlbums; // 初始显示所有专辑
}
private filterAlbumsByCategory(category: string): void {
if (category === '推荐') {
this.displayAlbums = this.allAlbums;
return;
}
this.displayAlbums = this.allAlbums.filter(album => {
return album.tags.includes(category);
});
}
6. 专辑详情页实现
当用户点击专辑卡片时,我们可以跳转到专辑详情页,展示更多专辑信息和歌曲列表:
interface Song {
id: string;
title: string;
artist: string;
duration: string;
}
@Component
struct AlbumDetailPage {
private album: Album;
private songs: Song[] = [];
aboutToAppear() {
// 模拟获取歌曲列表数据
this.songs = [
{ id: '1', title: '歌曲1', artist: '艺术家1', duration: '3:45' },
{ id: '2', title: '歌曲2', artist: '艺术家2', duration: '4:12' },
{ id: '3', title: '歌曲3', artist: '艺术家3', duration: '3:21' },
// 更多歌曲...
];
}
build() {
Column() {
// 专辑封面和信息
Row() {
Image(this.album.cover)
.width(120)
.height(120)
.borderRadius(8)
Column() {
Text(this.album.name)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(this.album.artist)
.fontSize(14)
.fontColor('#9E9E9E')
.margin({ top: 4 })
Row() {
ForEach(this.album.tags, (tag: string) => {
Text(tag)
.fontSize(10)
.fontColor('#FF5722')
.backgroundColor('#FFF3EF')
.borderRadius(4)
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
.margin({ right: 4, top: 8 })
})
}
Row() {
Text(this.formatPlayCount(this.album.playCount))
.fontSize(12)
.fontColor('#9E9E9E')
Text(this.album.duration)
.fontSize(12)
.fontColor('#9E9E9E')
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.layoutWeight(1)
.margin({ left: 16 })
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(16)
// 歌曲列表
Text('歌曲列表')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 8 })
.textAlign(TextAlign.Start)
List() {
ForEach(this.songs, (song: Song) => {
ListItem() {
Row() {
Column() {
Text(song.title)
.fontSize(14)
Text(song.artist)
.fontSize(12)
.fontColor('#9E9E9E')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(song.duration)
.fontSize(12)
.fontColor('#9E9E9E')
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
}
})
}
.width('100%')
.divider({ strokeWidth: 1, color: '#F0F0F0' })
}
.width('100%')
}
private formatPlayCount(count: number): string {
if (count >= 10000) {
return (count / 10000).toFixed(1) + '万';
}
return count.toString();
}
}
7. 完整代码
以下是优化后的音乐专辑网格展示的完整代码:
interface Album {
id: string;
name: string;
artist: string;
cover: ResourceStr;
playCount: number;
duration: string;
tags: string[];
isFavorite: boolean;
}
@Component
export struct MusicAlbumGrid {
@State refreshing: boolean = false;
@State currentCategory: string = '推荐';
@State displayAlbums: Album[] = [];
private scroller: Scroller = new Scroller();
private categories: string[] = ['推荐', '流行', '摇滚', '古典', '电子', '爵士', '民谣'];
private allAlbums: Album[] = [
{ id: '1', name: '流行热歌', artist: '多位艺人', cover: $r("app.media.big29"), playCount: 1250000, duration: '2小时35分', tags: ['流行', '热门'], isFavorite: false },
{ id: '2', name: '古典精选', artist: '世界名曲', cover: $r("app.media.big28"), playCount: 860000, duration: '3小时20分', tags: ['古典', '轻音乐'], isFavorite: false },
{ id: '3', name: '摇滚经典', artist: '传奇乐队', cover: $r("app.media.big25"), playCount: 1520000, duration: '1小时50分', tags: ['摇滚', '经典'], isFavorite: false },
{ id: '4', name: '电子舞曲', artist: 'DJ合集', cover: $r("app.media.big23"), playCount: 2100000, duration: '2小时10分', tags: ['电子', 'DJ'], isFavorite: false },
{ id: '5', name: '爵士名曲', artist: '爵士大师', cover: $r("app.media.big29"), playCount: 750000, duration: '1小时45分', tags: ['爵士', '蓝调'], isFavorite: false },
{ id: '6', name: '民谣集锦', artist: '民谣歌手', cover: $r("app.media.big28"), playCount: 980000, duration: '2小时25分', tags: ['民谣', '吉他'], isFavorite: false }
];
aboutToAppear() {
this.displayAlbums = this.allAlbums;
}
build() {
Column() {
// 标题
Text('音乐专辑')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
.width('100%')
.textAlign(TextAlign.Start)
.padding({ left: 16, right: 16 })
// 分类标签栏
Scroll(this.scroller) {
Row() {
ForEach(this.categories, (category: string) => {
Text(category)
.fontSize(14)
.fontColor(this.currentCategory === category ? '#FF5722' : '#333333')
.backgroundColor(this.currentCategory === category ? '#FFF3EF' : 'transparent')
.borderRadius(16)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8 })
.onClick(() => {
this.currentCategory = category;
this.filterAlbumsByCategory(category);
})
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.margin({ bottom: 16 })
// 专辑网格
Refresh({ refreshing: this.refreshing }) {
Column() {
if (this.displayAlbums.length > 0) {
GridRow({
columns: { xs: 2, sm: 3, md: 4, lg: 6 },
gutter: { x: 16, y: 24 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
ForEach(this.displayAlbums, (album: Album) => {
GridCol({ span: 1 }) {
Column() {
Stack() {
Image(album.cover)
.width('100%')
.aspectRatio(1)
.borderRadius(8)
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image(album.isFavorite ? $r('app.media.ic_favorite_filled') : $r('app.media.ic_favorite'))
.width(20)
.height(20)
.fillColor(album.isFavorite ? '#FF5722' : '#FFFFFF')
}
.width(36)
.height(36)
.backgroundColor('#80000000')
.position({ x: '100%', y: 0 })
.translate({ x: -44, y: 8 })
.onClick((event) => {
this.toggleFavorite(album.id);
event.stopPropagation();
})
}
.width('100%')
Text(album.name)
.fontSize(16)
.margin({ top: 8 })
.width('100%')
.textAlign(TextAlign.Start)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(album.artist)
.fontSize(12)
.fontColor('#9E9E9E')
.margin({ top: 4 })
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(this.formatPlayCount(album.playCount))
.fontSize(10)
.fontColor('#9E9E9E')
Text(album.duration)
.fontSize(10)
.fontColor('#9E9E9E')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Start)
.margin({ top: 4 })
Row() {
ForEach(album.tags, (tag: string) => {
Text(tag)
.fontSize(10)
.fontColor('#FF5722')
.backgroundColor('#FFF3EF')
.borderRadius(4)
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
.margin({ right: 4 })
})
}
.width('100%')
.justifyContent(FlexAlign.Start)
.margin({ top: 8 })
}
.backgroundColor(Color.White)
.borderRadius(12)
.padding(8)
.shadow({
radius: 12,
color: '#1A000000',
offsetX: 2,
offsetY: 4
})
.onClick(() => {
this.onAlbumClick(album);
})
}
})
}
} else {
Column() {
Text('暂无数据')
.fontSize(16)
.fontColor('#9E9E9E')
}
.width('100%')
.height(200)
.justifyContent(FlexAlign.Center)
}
}
.width('100%')
.padding(16)
}
.onRefresh(() => {
this.refreshData();
})
}
.width('100%')
}
private filterAlbumsByCategory(category: string): void {
if (category === '推荐') {
this.displayAlbums = this.allAlbums;
return;
}
this.displayAlbums = this.allAlbums.filter(album => {
return album.tags.includes(category);
});
}
private refreshData(): void {
this.refreshing = true;
// 模拟网络请求
setTimeout(() => {
// 更新专辑数据
this.refreshing = false;
}, 2000);
}
private formatPlayCount(count: number): string {
if (count >= 10000) {
return (count / 10000).toFixed(1) + '万';
}
return count.toString();
}
private toggleFavorite(albumId: string): void {
this.allAlbums = this.allAlbums.map(item => {
if (item.id === albumId) {
return { ...item, isFavorite: !item.isFavorite };
}
return item;
});
this.filterAlbumsByCategory(this.currentCategory);
}
private onAlbumClick(album: Album): void {
console.info(`点击了专辑:${album.name}`);
// 这里可以添加跳转到专辑详情页的逻辑
}
}
8. 总结
本教程详细讲解了如何优化音乐专辑网格展示布局,添加交互功能,以及实现更多高级特性。通过使用GridRow和GridCol组件的高级特性,我们实现了响应式布局,使应用能够适应不同屏幕尺寸的设备。同时,我们还添加了专辑卡片优化、交互功能和分类标签栏等功能,打造了一个功能完善的音乐专辑展示页面。
通过本教程,你应该已经掌握了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现复杂的网格布局,以及如何添加各种交互功能,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如电商商品展示、照片墙、新闻列表等。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。