148.[HarmonyOS NEXT 实战案例八 :List系列] 粘性头部列表进阶篇

2025-06-30 22:25:11
103次阅读
0个评论

[HarmonyOS NEXT 实战案例八 :List系列] 粘性头部列表进阶篇

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

image.png

在基础篇中,我们学习了如何实现一个基本的粘性头部列表。本篇教程将深入探讨粘性头部列表的进阶功能和优化技巧,帮助你构建更加强大、用户体验更佳的粘性头部列表应用。

一、进阶功能概述

在本教程中,我们将探讨以下进阶功能:

  1. 自定义粘性头部样式:定制粘性头部的外观和行为
  2. 头部交互增强:为头部添加更丰富的交互功能
  3. 分组折叠与展开:实现分组的折叠和展开功能
  4. 动画与过渡效果:为列表添加流畅的动画和过渡效果
  5. 列表项交互优化:改善列表项的点击、长按等交互体验
  6. 播放控制功能增强:实现更完善的音乐播放控制功能
  7. 列表性能优化:提高大量数据下的列表性能

二、自定义粘性头部样式

1. 粘性头部样式变化

在基础实现中,粘性头部在滚动时保持原样。我们可以为粘性头部添加滚动时的样式变化,使其更加紧凑:

// 在AlbumHeader构建器中添加isSticky参数
@Builder
AlbumHeader(album: string, artist: string, cover: Resource, year: string, isSticky: boolean = false) {
    Row() {
        // 专辑封面
        Image(cover)
            .width(isSticky ? 40 : 60)
            .height(isSticky ? 40 : 60)
            .borderRadius(isSticky ? 4 : 8)
            .margin({ right: 16 })

        // 专辑信息
        Column() {
            Text(album)
                .fontSize(isSticky ? 16 : 18)
                .fontWeight(FontWeight.Bold)

            if (!isSticky) {
                Text(artist)
                    .fontSize(16)
                    .fontColor('#666666')
                    .margin({ top: 4 })

                Text(year)
                    .fontSize(14)
                    .fontColor('#999999')
                    .margin({ top: 2 })
            } else {
                Text(artist)
                    .fontSize(14)
                    .fontColor('#666666')
            }
        }
        .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .backgroundColor(isSticky ? '#F8F8F8' : '#FFFFFF')
    .padding({ 
        left: 16, 
        right: 16, 
        top: isSticky ? 8 : 12, 
        bottom: isSticky ? 8 : 12 
    })
    .shadow(isSticky ? {
        radius: 6,
        color: 'rgba(0, 0, 0, 0.1)',
        offsetY: 2
    } : null)
}

然后,我们需要在ListItemGroup中检测粘性状态:

ListItemGroup({
    header: this.AlbumHeader(albumData.album, albumData.artist, albumData.cover, albumData.year, false),
    stickyHeader: this.AlbumHeader(albumData.album, albumData.artist, albumData.cover, albumData.year, true),
    space: 0
})

这样,当头部变为粘性状态时,会使用更紧凑的样式。

2. 粘性头部背景模糊效果

为了增强视觉效果,我们可以为粘性头部添加背景模糊效果:

// 在粘性状态下添加背景模糊
.backdropBlur(isSticky ? 10 : 0)
.backgroundColor(isSticky ? 'rgba(255, 255, 255, 0.8)' : '#FFFFFF')

三、头部交互增强

1. 头部点击展开详情

我们可以为专辑头部添加点击事件,点击时展开专辑详情:

// 专辑详情状态
@State expandedAlbum: string | null = null

// 切换专辑详情展开状态
toggleAlbumDetails(album: string) {
    if (this.expandedAlbum === album) {
        this.expandedAlbum = null
    } else {
        this.expandedAlbum = album
    }
}

// 在AlbumHeader中添加点击事件
.onClick(() => {
    if (!isSticky) { // 只有在非粘性状态下才响应点击
        this.toggleAlbumDetails(album)
    }
})

然后,在ListItemGroup中添加专辑详情视图:

ListItemGroup({
    header: this.AlbumHeader(albumData.album, albumData.artist, albumData.cover, albumData.year, false),
    stickyHeader: this.AlbumHeader(albumData.album, albumData.artist, albumData.cover, albumData.year, true),
    space: 0
}) {
    // 专辑详情(展开时显示)
    if (this.expandedAlbum === albumData.album) {
        this.AlbumDetails(albumData)
    }
    
    // 歌曲列表
    ForEach(albumData.songs, (song: SongsType) => {
        ListItem() { /* ... */ }
    })
}

2. 专辑详情构建器

@Builder
AlbumDetails(albumData: MusicType) {
    Column() {
        // 专辑详细信息
        Row() {
            // 大封面
            Image(albumData.cover)
                .width(120)
                .height(120)
                .borderRadius(8)
                .margin({ right: 16 })

            // 详细信息
            Column() {
                Text(albumData.album)
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)

                Text(albumData.artist)
                    .fontSize(18)
                    .fontColor('#666666')
                    .margin({ top: 8 })

                Text(`发行年份: ${albumData.year}`)
                    .fontSize(14)
                    .fontColor('#999999')
                    .margin({ top: 4 })

                Text(`${albumData.songs.length} 首歌曲`)
                    .fontSize(14)
                    .fontColor('#999999')
                    .margin({ top: 4 })

                // 操作按钮
                Row({ space: 16 }) {
                    Button('播放全部')
                        .fontSize(14)
                        .backgroundColor('#007DFF')
                        .height(32)
                        .onClick(() => {
                            if (albumData.songs.length > 0) {
                                this.playSong(albumData.album, albumData.songs[0].title)
                            }
                        })

                    Button('收藏')
                        .fontSize(14)
                        .backgroundColor('#4CAF50')
                        .height(32)
                }
                .margin({ top: 12 })
            }
            .alignItems(HorizontalAlign.Start)
        }
        .padding({ left: 16, right: 16, top: 16, bottom: 16 })
        .width('100%')
    }
    .backgroundColor('#F5F5F5')
    .width('100%')
}

四、分组折叠与展开

1. 实现分组折叠功能

我们可以为每个专辑分组添加折叠/展开功能:

// 折叠的专辑列表
@State collapsedAlbums: string[] = []

// 切换专辑折叠状态
toggleAlbumCollapse(album: string) {
    const index = this.collapsedAlbums.indexOf(album)
    if (index === -1) {
        this.collapsedAlbums.push(album)
    } else {
        this.collapsedAlbums.splice(index, 1)
    }
}

// 在AlbumHeader中添加折叠/展开按钮
Row() {
    // ... 原有内容 ...
    
    // 折叠/展开按钮
    Image(this.collapsedAlbums.includes(album) ? $r('app.media.arrow_down') : $r('app.media.arrow_up'))
        .width(24)
        .height(24)
        .onClick((event) => {
            this.toggleAlbumCollapse(album)
            event.stopPropagation() // 阻止事件冒泡,避免触发头部点击事件
        })
}

然后,在ListItemGroup中根据折叠状态显示或隐藏歌曲:

ListItemGroup({
    header: this.AlbumHeader(albumData.album, albumData.artist, albumData.cover, albumData.year, false),
    stickyHeader: this.AlbumHeader(albumData.album, albumData.artist, albumData.cover, albumData.year, true),
    space: 0
}) {
    // 专辑详情(展开时显示)
    if (this.expandedAlbum === albumData.album) {
        this.AlbumDetails(albumData)
    }
    
    // 歌曲列表(非折叠状态才显示)
    if (!this.collapsedAlbums.includes(albumData.album)) {
        ForEach(albumData.songs, (song: SongsType) => {
            ListItem() { /* ... */ }
        })
    }
}

2. 添加折叠/展开动画

为了提升用户体验,我们可以为折叠/展开操作添加动画效果:

// 在ListItemGroup中添加动画
.transition({
    type: TransitionType.All,
    opacity: !this.collapsedAlbums.includes(albumData.album) ? 1 : 0,
    scale: { y: !this.collapsedAlbums.includes(albumData.album) ? 1 : 0.8 },
    translate: { y: !this.collapsedAlbums.includes(albumData.album) ? 0 : -20 }
})

五、动画与过渡效果

1. 列表项动画

为列表项添加进入和退出动画:

// 在ListItem中添加动画
.transition({
    type: TransitionType.All,
    opacity: 1,
    scale: { x: 1, y: 1 },
    translate: { x: 0, y: 0 }
})
.animation({
    duration: 300,
    curve: Curve.EaseInOut,
    delay: index * 50, // 根据索引添加延迟,创建级联效果
    iterations: 1,
    playMode: PlayMode.Normal
})

2. 当前播放歌曲高亮动画

为当前播放的歌曲添加呼吸灯效果:

// 在ListItem中添加呼吸灯动画
.animation(this.currentSong?.album === albumData.album && this.currentSong?.title === song.title ? {
    duration: 1500,
    curve: Curve.Linear,
    delay: 0,
    iterations: -1, // 无限循环
    playMode: PlayMode.Alternate // 交替播放
} : null)
.backgroundColor(
    this.currentSong?.album === albumData.album && this.currentSong?.title === song.title ?
        '#F0F7FF' : '#FFFFFF'
)
.opacity(this.currentSong?.album === albumData.album && this.currentSong?.title === song.title ?
    this.breatheOpacity : 1) // 呼吸灯透明度

// 添加呼吸灯透明度状态
@State breatheOpacity: number = 0.8

六、列表项交互优化

1. 滑动操作

为歌曲项添加滑动操作功能:

// 在ListItem中添加滑动操作
.swipeAction({
    end: [
        {
            action: () => {
                // 从播放列表中移除
                console.info(`从播放列表中移除: ${song.title}`)
            },
            backgroundcolor: '#F44336',
            label: '移除'
        }
    ],
    start: [
        {
            action: () => {
                // 切换喜欢状态
                this.toggleLiked(albumData.album, song.title)
            },
            backgroundcolor: song.isLiked ? '#F44336' : '#FFC107',
            label: song.isLiked ? '取消喜欢' : '喜欢'
        }
    ]
})

2. 切换喜欢状态

// 切换歌曲喜欢状态
toggleLiked(album: string, title: string) {
    // 查找并更新歌曲的喜欢状态
    this.musicData.forEach((albumData) => {
        if (albumData.album === album) {
            albumData.songs.forEach((song) => {
                if (song.title === title) {
                    song.isLiked = !song.isLiked
                }
            })
        }
    })
}

3. 长按菜单

为歌曲项添加长按菜单:

// 在ListItem中添加长按手势
.gesture(
    LongPressGesture()
        .onAction(() => {
            this.showSongMenu(albumData.album, song.title)
        })
)

// 显示歌曲菜单
showSongMenu(album: string, title: string) {
    // 实现一个简单的菜单
    this.menuVisible = true
    this.menuAlbum = album
    this.menuSong = title
}

// 添加菜单状态
@State menuVisible: boolean = false
@State menuAlbum: string = ''
@State menuSong: string = ''

然后,在UI中添加菜单面板:

// 在Stack中添加菜单面板
if (this.menuVisible) {
    Column() {
        // 菜单标题
        Text('歌曲操作')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 16 })

        // 菜单选项
        this.MenuOption('播放', $r('app.media.play_icon'), () => {
            this.playSong(this.menuAlbum, this.menuSong)
            this.menuVisible = false
        })

        this.MenuOption('添加到播放列表', $r('app.media.add_icon'), () => {
            console.info('添加到播放列表')
            this.menuVisible = false
        })

        this.MenuOption('分享', $r('app.media.share_icon'), () => {
            console.info('分享歌曲')
            this.menuVisible = false
        })

        // 取消按钮
        Button('取消')
            .width('100%')
            .height(48)
            .margin({ top: 16 })
            .onClick(() => {
                this.menuVisible = false
            })
    }
    .width('90%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .shadow({
        radius: 12,
        color: 'rgba(0, 0, 0, 0.2)',
        offsetY: 4
    })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.5)')
.onClick(() => {
    this.menuVisible = false
})

4. 菜单选项构建器

@Builder
MenuOption(text: string, icon: Resource, action: () => void) {
    Row() {
        Image(icon)
            .width(24)
            .height(24)
            .margin({ right: 16 })

        Text(text)
            .fontSize(16)
    }
    .width('100%')
    .height(48)
    .padding({ left: 16, right: 16 })
    .onClick(action)
    .stateStyles({
        pressed: {
            backgroundColor: '#F5F5F5'
        },
        normal: {
            backgroundColor: '#FFFFFF'
        }
    })
}

七、播放控制功能增强

1. 播放状态管理

添加播放状态管理:

// 播放状态
@State isPlaying: boolean = false
@State currentTime: number = 0 // 当前播放时间(秒)
@State duration: number = 0 // 歌曲总时长(秒)

// 播放/暂停切换
togglePlay() {
    this.isPlaying = !this.isPlaying
}

// 播放歌曲(增强版)
playSong(album: string, title: string) {
    this.currentSong = { album, title }
    this.isPlaying = true
    
    // 查找歌曲时长
    this.musicData.forEach((albumData) => {
        if (albumData.album === album) {
            albumData.songs.forEach((song) => {
                if (song.title === title) {
                    // 将时长字符串转换为秒数
                    const [minutes, seconds] = song.duration.split(':').map(Number)
                    this.duration = minutes * 60 + seconds
                    this.currentTime = 0
                }
            })
        }
    })
    
    // 如果正在播放,启动定时器更新进度
    if (this.isPlaying) {
        this.startProgressTimer()
    }
}

// 启动进度定时器
startProgressTimer() {
    // 清除现有定时器
    if (this.progressTimer) {
        clearInterval(this.progressTimer)
    }
    
    // 创建新定时器,每秒更新一次进度
    this.progressTimer = setInterval(() => {
        if (this.isPlaying) {
            this.currentTime += 1
            if (this.currentTime >= this.duration) {
                // 播放完毕,可以实现自动播放下一首
                this.playNextSong()
            }
        }
    }, 1000)
}

// 播放下一首歌曲
playNextSong() {
    if (!this.currentSong) return
    
    let found = false
    let nextSong: { album: string, title: string } | null = null
    
    // 查找当前歌曲的下一首
    for (const albumData of this.musicData) {
        if (albumData.album === this.currentSong.album) {
            for (let i = 0; i < albumData.songs.length; i++) {
                if (found) {
                    nextSong = { album: albumData.album, title: albumData.songs[i].title }
                    break
                }
                if (albumData.songs[i].title === this.currentSong.title) {
                    found = true
                }
            }
        }
        if (nextSong) break
    }
    
    // 如果找到下一首,播放它
    if (nextSong) {
        this.playSong(nextSong.album, nextSong.title)
    } else {
        // 没有下一首,停止播放
        this.isPlaying = false
        clearInterval(this.progressTimer)
    }
}

// 播放上一首歌曲
playPrevSong() {
    // 类似playNextSong的实现,但查找前一首歌曲
}

// 组件销毁时清除定时器
aboutToDisappear() {
    if (this.progressTimer) {
        clearInterval(this.progressTimer)
    }
}

// 进度定时器
private progressTimer: number | null = null

2. 增强的播放控制栏

// 底部播放控制栏(当有歌曲播放时显示)
if (this.currentSong) {
    Column() {
        // 进度条
        Slider({
            value: this.currentTime,
            min: 0,
            max: this.duration,
            step: 1
        })
        .width('100%')
        .height(20)
        .onChange((value) => {
            this.currentTime = value
        })

        // 播放控制和信息
        Row() {
            // 当前播放歌曲信息
            Column() {
                Text(this.currentSong.title)
                    .fontSize(16)
                    .fontWeight(FontWeight.Medium)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })

                Text(this.currentSong.album)
                    .fontSize(14)
                    .fontColor('#666666')
                    .margin({ top: 2 })
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            // 播放控制按钮
            Row({ space: 16 }) {
                // 上一首
                Image($r('app.media.prev_icon'))
                    .width(24)
                    .height(24)
                    .onClick(() => this.playPrevSong())

                // 播放/暂停
                Image(this.isPlaying ? $r('app.media.pause_icon') : $r('app.media.play_icon'))
                    .width(32)
                    .height(32)
                    .onClick(() => this.togglePlay())

                // 下一首
                Image($r('app.media.next_icon'))
                    .width(24)
                    .height(24)
                    .onClick(() => this.playNextSong())
            }
        }
        .width('100%')
        .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderColor('#E5E5E5')
    .borderWidth({ top: 1 })
}

八、列表性能优化

1. 懒加载与虚拟化

对于大量数据,可以使用懒加载和虚拟化技术:

// 在List中启用懒加载
.cachedCount(5) // 缓存5个不在可视区域的列表项
.onReachStart(() => {
    // 当滚动到顶部时加载更多数据
})
.onReachEnd(() => {
    // 当滚动到底部时加载更多数据
})

2. 分页加载

实现分页加载功能,逐步加载更多专辑:

// 分页加载状态
@State currentPage: number = 1
@State pageSize: number = 5
@State hasMoreData: boolean = true

// 获取当前页的数据
get paginatedData() {
    return this.musicData.slice(0, this.currentPage * this.pageSize)
}

// 加载更多数据
loadMoreData() {
    if (this.currentPage * this.pageSize >= this.musicData.length) {
        this.hasMoreData = false
        return
    }
    
    this.currentPage += 1
}

然后,在List中使用分页数据:

// 在List中使用分页数据
ForEach(this.paginatedData, (albumData: MusicType) => {
    ListItemGroup({ /* ... */ }) { /* ... */ }
})

// 在List底部添加加载更多按钮
if (this.hasMoreData) {
    ListItem() {
        Button('加载更多')
            .width('100%')
            .height(48)
            .onClick(() => this.loadMoreData())
    }
    .height(64)
}

九、代码结构与样式设置

功能模块 主要实现方法 关键属性和样式
自定义粘性头部 AlbumHeader构建器 isSticky参数, backdropBlur, shadow
头部交互增强 toggleAlbumDetails, AlbumDetails onClick事件, 详情视图
分组折叠与展开 toggleAlbumCollapse collapsedAlbums状态, 条件渲染
动画与过渡效果 transition, animation opacity, scale, translate, curve
列表项交互优化 swipeAction, LongPressGesture 滑动操作, 长按菜单
播放控制功能 playSong, togglePlay, playNextSong 播放状态管理, 进度控制
列表性能优化 cachedCount, 分页加载 懒加载, 虚拟化, 分页

十、总结

本教程详细讲解了HarmonyOS NEXT中粘性头部列表的进阶功能和优化技巧, 通过这些进阶功能,你可以构建一个功能丰富、用户体验出色的粘性头部列表应用。这些技巧不仅适用于音乐播放器应用,也可以应用到其他需要分组显示内容的场景,如联系人列表、设置页面、电商应用等。

收藏00

登录 后评论。没有帐号? 注册 一个。

全栈若城

  • 0回答
  • 4粉丝
  • 0关注
相关话题