150.[HarmonyOS NEXT 实战案例十一:List系列] 下拉刷新和上拉加载更多列表组件实战:打造高效新闻应用 进阶篇

2025-06-30 22:26:18
104次阅读
0个评论

[HarmonyOS NEXT 实战案例十一:List系列] 下拉刷新和上拉加载更多列表组件实战:打造高效新闻应用 进阶篇

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

效果演示

image.png

一、引言

在基础篇中,我们学习了如何使用HarmonyOS NEXT的ListRefresh组件实现具有下拉刷新和上拉加载更多功能的新闻列表。本篇教程将在此基础上,探讨更多进阶特性和优化技巧,帮助你打造一个功能更加丰富、用户体验更加流畅的新闻应用。

我们将重点关注以下几个方面:

  1. 刷新和加载动画效果增强
  2. 骨架屏加载状态
  3. 智能缓存与离线模式
  4. 列表性能优化
  5. 高级交互功能

二、刷新和加载动画效果增强

2.1 自定义下拉刷新动画

基础版本中,我们使用了简单的LoadingProgress组件来显示刷新状态。现在,我们可以实现一个更加生动的自定义刷新动画:

@Builder
EnhancedRefreshHeader(refreshStatus: RefreshStatus) {
    Column() {
        if (refreshStatus === RefreshStatus.Pulling) {
            // 下拉过程中的动画
            Row() {
                Image($r('app.media.arrowright'))
                    .width(24)
                    .height(24)
                    .margin({ right: 8 })
                    .rotate({ angle: this.getRotateAngle(refreshStatus) })
                
                Text('下拉刷新')
                    .fontSize(14)
                    .fontColor('#666666')
            }
        } else if (refreshStatus === RefreshStatus.CanRelease) {
            // 可释放状态的动画
            Row() {
                Image($r('app.media.arrowright'))
                    .width(24)
                    .height(24)
                    .margin({ right: 8 })
                    .rotate({ angle: 180 })
                
                Text('释放刷新')
                    .fontSize(14)
                    .fontColor('#007DFF')
            }
        } else if (refreshStatus === RefreshStatus.Refreshing) {
            // 刷新中的动画
            Row() {
                LoadingProgress()
                    .width(24)
                    .height(24)
                    .margin({ right: 8 })
                
                Text('正在刷新...')
                    .fontSize(14)
                    .fontColor('#666666')
            }
        } else if (refreshStatus === RefreshStatus.Done) {
            // 刷新完成的动画
            Row() {
                Image($r('app.media.ic_done'))
                    .width(24)
                    .height(24)
                    .margin({ right: 8 })
                
                Text('刷新成功')
                    .fontSize(14)
                    .fontColor('#4CAF50')
            }
        }
    }
    .width('100%')
    .height(60)
    .justifyContent(FlexAlign.Center)
}

// 根据下拉距离计算旋转角度
getRotateAngle(refreshStatus: RefreshStatus): number {
    if (refreshStatus === RefreshStatus.Pulling) {
        // 根据下拉距离计算0-180度的旋转角度
        return this.pullDistance / this.refreshOffset * 180
    }
    return 0
}

在这个增强版的刷新头部中,我们根据不同的刷新状态显示不同的动画效果:

  • 下拉过程中:箭头随着下拉距离逐渐旋转
  • 可释放状态:箭头完全旋转,提示用户可以释放
  • 刷新中:显示加载动画
  • 刷新完成:显示成功图标

2.2 加载更多动画优化

同样,我们可以优化加载更多的动画效果:

@Builder
EnhancedLoadMoreFooter() {
    Column() {
        if (this.isLoadingMore) {
            // 加载中动画
            Row() {
                LoadingProgress()
                    .width(24)
                    .height(24)
                    .margin({ right: 8 })
                
                Text('正在加载更多...')
                    .fontSize(14)
                    .fontColor('#666666')
            }
            .animation({
                duration: 300,
                curve: Curve.EaseInOut,
                iterations: 1,
                playMode: PlayMode.Normal
            })
        } else if (!this.hasMoreData) {
            // 没有更多数据
            Row() {
                Divider()
                    .width(60)
                    .height(1)
                    .color('#E5E5E5')
                
                Text('没有更多内容了')
                    .fontSize(14)
                    .fontColor('#999999')
                    .margin({ left: 16, right: 16 })
                
                Divider()
                    .width(60)
                    .height(1)
                    .color('#E5E5E5')
            }
        } else {
            // 上拉加载提示
            Row() {
                Image($r('app.media.ic_arrow_up'))
                    .width(16)
                    .height(16)
                    .margin({ right: 8 })
                    .fillColor('#666666')
                
                Text('上拉加载更多')
                    .fontSize(14)
                    .fontColor('#666666')
            }
        }
    }
    .width('100%')
    .height(60)
    .justifyContent(FlexAlign.Center)
}

这个增强版的加载尾部添加了:

  • 加载中状态的平滑过渡动画
  • 没有更多数据时的分割线装饰
  • 上拉加载提示的箭头图标

三、骨架屏加载状态

骨架屏是提升用户体验的重要技术,它在内容加载过程中显示内容的大致轮廓,减少用户等待的焦虑感。

3.1 新闻项骨架屏实现

@Builder
NewsItemSkeleton() {
    Row() {
        // 内容骨架
        Column() {
            // 标题骨架
            Column()
                .width('80%')
                .height(16)
                .backgroundColor('#F0F0F0')
                .borderRadius(4)
            
            Column()
                .width('60%')
                .height(16)
                .backgroundColor('#F0F0F0')
                .borderRadius(4)
                .margin({ top: 8 })
            
            // 来源和时间骨架
            Row() {
                Column()
                    .width(60)
                    .height(14)
                    .backgroundColor('#F0F0F0')
                    .borderRadius(4)
                
                Column()
                    .width(80)
                    .height(14)
                    .backgroundColor('#F0F0F0')
                    .borderRadius(4)
                    .margin({ left: 16 })
            }
            .margin({ top: 8 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(2)
        
        // 图片骨架
        Column()
            .width(100)
            .height(70)
            .backgroundColor('#F0F0F0')
            .borderRadius(8)
            .margin({ left: 16 })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
    .opacity(this.skeletonOpacity)
    .animation({
        duration: 1000,
        tempo: 3.0,
        iterations: -1,
        curve: Curve.Linear
    })
}

在这个骨架屏实现中:

  • 使用Column和Row组件模拟新闻项的布局结构
  • 使用backgroundColor设置骨架的颜色
  • 添加呼吸动画效果,通过opacity属性的动画实现

3.2 骨架屏与实际内容的切换

@State isLoading: boolean = true
@State skeletonOpacity: number = 0.6

// 在组件初始化时显示骨架屏
aboutToAppear() {
    // 模拟初始加载
    setTimeout(() => {
        this.isLoading = false
    }, 2000)
}

// 在列表中根据加载状态显示骨架屏或实际内容
build() {
    // ...
    List() {
        // ...
        if (this.isLoading) {
            // 显示骨架屏
            ForEach([1, 2, 3, 4, 5], (item) => {
                ListItem() {
                    this.NewsItemSkeleton()
                }
            })
        } else {
            // 显示实际内容
            ForEach(this.newsList, (news: NewsType) => {
                ListItem() {
                    // 新闻项内容
                }
            })
        }
        // ...
    }
    // ...
}

这段代码实现了:

  • 初始加载时显示骨架屏
  • 数据加载完成后切换到实际内容
  • 通过条件渲染控制显示内容

四、智能缓存与离线模式

4.1 数据持久化

在实际应用中,我们需要将新闻数据持久化,以便在应用重启或离线时仍能显示内容:

import dataPreferences from '@ohos.data.preferences';

// 保存新闻数据到本地存储
async saveNewsToStorage() {
    try {
        let preferences = await dataPreferences.getPreferences(this.context, 'NewsStorage');
        await preferences.put('newsList', JSON.stringify(this.newsList));
        await preferences.put('lastUpdateTime', new Date().toString());
        await preferences.flush();
    } catch (error) {
        console.error('Failed to save news data: ' + error);
    }
}

// 从本地存储加载新闻数据
async loadNewsFromStorage() {
    try {
        let preferences = await dataPreferences.getPreferences(this.context, 'NewsStorage');
        let newsListStr = await preferences.get('newsList', '');
        let lastUpdateTime = await preferences.get('lastUpdateTime', '');
        
        if (newsListStr) {
            this.newsList = JSON.parse(newsListStr.toString());
            this.isLoading = false;
            console.info('Loaded news from storage, last updated: ' + lastUpdateTime);
        }
    } catch (error) {
        console.error('Failed to load news data: ' + error);
    }
}

这段代码使用HarmonyOS的dataPreferencesAPI实现数据持久化:

  • saveNewsToStorage方法将新闻列表和最后更新时间保存到本地
  • loadNewsFromStorage方法从本地加载保存的新闻数据

4.2 离线模式实现

@State isOffline: boolean = false

// 检查网络状态
checkNetworkStatus() {
    // 实际应用中应使用网络API检查连接状态
    // 这里使用模拟实现
    this.isOffline = false; // 假设有网络连接
    
    // 如果离线,显示提示
    if (this.isOffline) {
        this.showOfflineNotification();
    }
}

// 显示离线提示
showOfflineNotification() {
    this.offlineNotificationVisible = true;
    setTimeout(() => {
        this.offlineNotificationVisible = false;
    }, 3000);
}

// 离线提示UI
@Builder
OfflineNotification() {
    if (this.offlineNotificationVisible) {
        Row() {
            Image($r('app.media.ic_offline'))
                .width(20)
                .height(20)
                .margin({ right: 8 })
            
            Text('当前处于离线模式,显示缓存内容')
                .fontSize(14)
                .fontColor('#FFFFFF')
        }
        .width('90%')
        .padding(12)
        .backgroundColor('#333333')
        .borderRadius(8)
        .position({ x: '5%', y: 10 })
        .opacity(0.9)
    }
}

这段代码实现了:

  • 检查网络状态的方法
  • 显示离线提示的逻辑
  • 离线提示的UI组件

五、列表性能优化

5.1 懒加载与虚拟列表

为了优化长列表的性能,我们可以使用懒加载和虚拟列表技术:

List() {
    // ...
    ForEach(this.newsList, (news: NewsType, index) => {
        ListItem() {
            // 新闻项内容
        }
        .lazyForEach(this.newsList, (news: NewsType, index) => {
            // 只有当列表项即将显示时才会创建
            return this.NewsItem(news);
        })
    })
    // ...
}
.lanes(1) // 设置为单列
.cachedCount(5) // 设置缓存数量
.divider({ // 设置分割线
    strokeWidth: 1,
    color: '#E5E5E5',
    startMargin: 16,
    endMargin: 16
})

这段代码使用了:

  • lazyForEach实现懒加载,只有当列表项即将显示时才会创建
  • cachedCount设置缓存的列表项数量,提高滚动性能

5.2 图片优化

Image(news.image)
    .width(100)
    .height(70)
    .borderRadius(8)
    .margin({ left: 16 })
    .objectFit(ImageFit.Cover)
    .alt($r('app.media.img_placeholder')) // 设置占位图
    .syncLoad(false) // 异步加载

图片优化技巧包括:

  • 使用alt属性设置占位图,在图片加载前显示
  • 使用syncLoad(false)设置异步加载,避免阻塞UI线程
  • 适当设置图片尺寸,避免加载过大的图片

六、高级交互功能

6.1 下拉刷新手势反馈

为了提升用户体验,我们可以在下拉刷新时添加触觉反馈:

import vibrator from '@ohos.vibrator';

// 在刷新状态变化时提供触觉反馈
onRefreshStatusChanged(status: RefreshStatus) {
    if (status === RefreshStatus.CanRelease) {
        // 当达到可释放状态时提供轻微振动
        vibrator.startVibration({
            type: 'time',
            duration: 10,
            count: 1
        });
    } else if (status === RefreshStatus.Refreshing) {
        // 开始刷新时提供振动
        vibrator.startVibration({
            type: 'time',
            duration: 20,
            count: 1
        });
    }
}

这段代码使用HarmonyOS的振动API在刷新状态变化时提供触觉反馈,增强用户的交互感知。

6.2 新闻项交互增强

@Builder
NewsItem(news: NewsType) {
    Row()
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor(news.isTop ? '#FFFBF0' : '#FFFFFF')
        .borderRadius(8)
        .margin({ left: 8, right: 8, top: 4, bottom: 4 })
        .shadow({ radius: 2, color: 'rgba(0, 0, 0, 0.1)', offsetX: 0, offsetY: 2 })
        .stateStyles({
            pressed: {
                backgroundColor: '#F5F5F5',
                scale: { x: 0.98, y: 0.98 }
            },
            normal: {
                backgroundColor: news.isTop ? '#FFFBF0' : '#FFFFFF',
                scale: { x: 1.0, y: 1.0 }
            }
        })
        .onClick(() => {
            this.onNewsItemClick(news);
        })
        .gesture(
            LongPressGesture()
                .onAction(() => {
                    this.showNewsOptions(news);
                })
        )
        .gesture(
            SwipeGesture({ direction: SwipeDirection.Horizontal })
                .onAction((event: GestureEvent) => {
                    if (event.direction === SwipeDirection.Right) {
                        this.markNewsAsRead(news);
                    } else if (event.direction === SwipeDirection.Left) {
                        this.showShareOptions(news);
                    }
                })
        )
    {
        // 新闻内容...
    }
}

这段代码为新闻项添加了多种交互功能:

  • 点击效果:使用stateStyles设置按下状态的样式变化
  • 长按手势:显示更多操作选项
  • 滑动手势:向右滑动标记为已读,向左滑动显示分享选项

6.3 新闻分类与搜索

// 新闻分类数据
private categories: string[] = ['全部', '科技', '财经', '体育', '娱乐', '健康', '教育']
@State currentCategory: number = 0

// 搜索关键词
@State searchKeyword: string = ''

// 根据分类和搜索关键词过滤新闻
getFilteredNews(): NewsType[] {
    let filteredList = this.newsList;
    
    // 按分类过滤
    if (this.currentCategory !== 0) { // 0表示'全部'
        const category = this.categories[this.currentCategory];
        filteredList = filteredList.filter(news => {
            // 根据新闻标题或内容判断分类
            return news.title.includes(category) || 
                   news.source.includes(category);
        });
    }
    
    // 按关键词搜索
    if (this.searchKeyword.trim() !== '') {
        const keyword = this.searchKeyword.toLowerCase();
        filteredList = filteredList.filter(news => {
            return news.title.toLowerCase().includes(keyword) || 
                   news.source.toLowerCase().includes(keyword);
        });
    }
    
    return filteredList;
}

// 分类选择器UI
@Builder
CategorySelector() {
    Scroll() {
        Row() {
            ForEach(this.categories, (category: string, index) => {
                Text(category)
                    .fontSize(16)
                    .fontColor(this.currentCategory === index ? '#007DFF' : '#333333')
                    .fontWeight(this.currentCategory === index ? FontWeight.Bold : FontWeight.Normal)
                    .padding({ left: 16, right: 16, top: 12, bottom: 12 })
                    .backgroundColor(this.currentCategory === index ? '#E6F2FF' : 'transparent')
                    .borderRadius(16)
                    .margin({ right: 8 })
                    .onClick(() => {
                        this.currentCategory = index;
                    })
            })
        }
    }
    .width('100%')
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
}

// 搜索框UI
@Builder
SearchBar() {
    Row() {
        Image($r('app.media.ic_search'))
            .width(20)
            .height(20)
            .margin({ right: 8 })
        
        TextInput({ placeholder: '搜索新闻', text: this.searchKeyword })
            .width('80%')
            .height(36)
            .onChange((value: string) => {
                this.searchKeyword = value;
            })
        
        if (this.searchKeyword.length > 0) {
            Image($r('app.media.ic_clear'))
                .width(20)
                .height(20)
                .margin({ left: 8 })
                .onClick(() => {
                    this.searchKeyword = '';
                })
        }
    }
    .width('100%')
    .height(48)
    .borderRadius(24)
    .backgroundColor('#F5F5F5')
    .padding({ left: 16, right: 16 })
}

这段代码实现了:

  • 新闻分类功能:用户可以选择不同分类查看相关新闻
  • 搜索功能:用户可以通过关键词搜索新闻
  • 分类和搜索的UI组件

七、完整代码结构

下面是进阶版新闻列表应用的完整功能和技术点总结:

功能模块 技术点 实现方式
刷新动画增强 状态感知动画 根据RefreshStatus显示不同动画
骨架屏 占位UI + 动画 使用基础组件模拟内容结构 + 呼吸动画
数据持久化 本地存储 dataPreferences API保存和加载数据
离线模式 网络状态检测 + 缓存 检查网络状态并显示离线提示
列表性能优化 懒加载 + 虚拟列表 lazyForEach + cachedCount属性
图片优化 异步加载 + 占位图 syncLoad + alt属性
触觉反馈 振动API vibrator模块在状态变化时提供反馈
新闻项交互 手势 + 状态样式 点击、长按、滑动手势 + stateStyles
分类与搜索 数据过滤 + UI组件 根据条件过滤数据 + 分类选择器和搜索框

八、总结

本教程详细讲解了如何在HarmonyOS NEXT中实现一个具有进阶特性的下拉刷新和上拉加载更多新闻列表应用, 通过这些进阶特性的实现,我们的新闻列表应用不仅具备了基本的下拉刷新和上拉加载更多功能,还拥有了更加流畅的用户体验和更丰富的交互方式。这些技术和思路不仅适用于新闻应用,也可以应用到其他需要列表展示和数据加载的场景中。

收藏00

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

全栈若城

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