[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 不同断点下的布局效果

下表展示了不同断点下的布局效果:

断点列数适用设备
<320vp2列小屏手机
320vp-600vp3列中屏手机
600vp-840vp4列大屏手机/小屏平板
>840vp6列平板/桌面设备

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组件实现复杂的网格布局,以及如何添加各种交互功能,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如电商商品展示、照片墙、新闻列表等。


全栈若城
1 声望2 粉丝