当前位置: 首页 > news >正文

HarmonyOS NEXT 应用开发实战(八、知乎日报List列表下拉刷新及上滑加载更多分页的实现)

在现代应用开发中,列表展示是非常普遍且重要的需求。无论是新闻、社交媒体,还是电商平台,用户通常都希望能够便捷地浏览大量信息。因此,如何高效地实现列表的加载和分页功能显得尤为重要。在这篇文章中,我们将以“知乎日报”的例子,介绍如何使用HarmonyOS NEXT框架实现一个支持下拉刷新和上滑加载更多的列表。

本文主要介绍 ArkUI 开发中最常用的场景下拉刷新,上拉加载,在本文中介绍的内容在实际开发过程当中会高频的使用,特此记录下来留作备忘。 下文以知乎日报的小项目为例子,详细介绍下List组件的下拉刷新和上滑加载更多的实现。

List组件的基本使用

List 组件用于展示一系列相似的内容,并支持垂直或水平滚动。它的优点在于可以高效地处理大量数据,自动优化性能,避免性能瓶颈。

在HarmonyOS中,List组件是用来展示一系列数据项的核心组件。在实现知乎日报时,我们需要展示新闻列表,并且支持分页加载。

// 基本结构
// List 组件通常包括 List 和 ListItem。ListItem 用于定义每一项的数据展示。List() {ForEach(dataArray, (item) => {ListItem() {// 这里可以定义每一项的布局和样式Text(item.title);}});
}
.divider({strokeWidth:2,color:'#F1F3F5'}) //设置分割线
.listDirection(Axis.Vertical)
//这里的alwaysEnabled:true参数重要,后面实现分页加载的关键,否则容易掉坑里
.edgeEffect(EdgeEffect.Spring, {alwaysEnabled:true}) 
.layoutWeight(1) //铺满剩余空间,这个也重要,否则容易掉坑,导致你的list无法铺满剩余空间

组件生命周期

组件的声明周期也很重要,我们需要在组件的生命周期中,初始化并获取知乎的最新新闻列表。这部分主要包括调用API接口获取数据,并进行数据的格式化处理。

aboutToAppear() {...getZhiHuNews(this.currentDate.replace(/-/g, '')).then((res) => {this.zhiNews = res.data.stories;...}).catch((err) => {...});
}

下拉刷新的实现

下拉刷新的实现相对简单。在 ArkTS 中,使用 List 组件进行下拉刷新是一种常见且简单的实现方式。通过将 List 组件嵌套在 Refresh 组件中,可以快速实现下拉刷新的功能。本文将详细介绍如何实现这一功能。

基本原理

下拉刷新功能的实现主要依赖于 Refresh 组件。通过设置 refreshing 属性来表示当前是否处于刷新状态,并可以通过 onRefreshing 事件处理程序来执行具体的业务逻辑。

基本实现步骤

1.定义状态管理

首先,需要定义一个状态变量来控制刷新状态。可以在组件中使用 @State 装饰器来声明一个布尔值,例如:

@State isRefreshing: boolean = false;

2.包裹 Refresh 组件

在 List 组件外层包裹一个 Refresh 组件,设置其属性以控制刷新状态、偏移量等。可以设置以下属性:

  • refreshing: 当前是否在刷新状态。
  • offset: 下拉的触发偏移量,通常设置为 120。
  • friction: 下拉的摩擦力,可以调整下拉的灵敏度。

下面是包裹的示例代码:

Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 100, builder: this.LoadingCustom() }) {List() {// 这里放置 List 组件的内容}
}

3.处理刷新逻辑 

通过 onRefreshing 方法来处理业务逻辑,例如加载新数据。可以在这个方法中进行数据请求并更新状态:

.onRefreshing(() => {this.isRefreshing = true; // 进入刷新状态// 模拟网络请求setTimeout(() => {// 这里执行数据获取逻辑,完成后关闭刷新状态this.isRefreshing = false; // 结束刷新状态}, 2000); // 例如,2秒后结束刷新
});

完整示例代码 

import { getZhiHuNews } from '../../common/api/zhihu';
import { BaseResponse,ErrorResp,ZhiNewsItem } from '../../common/bean/ApiTypes';
import { Log } from '../../utils/logutil'
import { formatDate2 } from '../../utils/time';
import { promptAction } from "@kit.ArkUI";@Component
export default class NewsList extends Component {@State isRefreshing: boolean = false;@State newsData = [{ id: 1, title: '新闻标题 1' },{ id: 2, title: '新闻标题 2' },{ id: 3, title: '新闻标题 3' },];// 自定义加载效果private LoadingCustom() {return Text("加载中..."); // 可以自定义加载组件}build() {return (Refresh({ refreshing: this.isRefreshing, offset: 120, friction: 100, builder: this.LoadingCustom() }) {List() {ForEach(this.newsData, (item) => {ListItem() {Text(item.title).fontSize(20).padding(10);}}, (item) => item.id);}.onRefreshing(() => {this.isRefreshing = true; // 开始刷新// 模拟数据请求setTimeout(() => {// 这里应该是更新数据逻辑this.isRefreshing = false; // 刷新结束}, 2000);});});}
}

上滑加载更多的实现

上滑加载更多的实现,这确实是个技巧,网上找到的方法大都太繁琐。比如有的是让在onReachEnd中去用分页加载更多,但是这个页面更加载就会进入,之后可能不再触发了。还有的实现方法竟让计算滚动条的x,y位置去比较,太麻烦了,官方也没有提供一个好用的方法。

这里分享一个简单的方法,但需要注意一些细节,算是避坑指南吧,否则容易掉坑里。比如为啥一直无法j触发加载更多啊,List无法铺满剩余空间了等问题。如果谁有更简单更好用的方法,欢迎交流和推荐。

简单的实现方法

1. 核心逻辑

我们可以通过监听滚动事件,判断是否触底来实现加载更多的功能。以下是实现步骤:

  • 状态管理:使用一个状态变量 isEnd 来指示是否已经到达底部。
  • Scroller 对象:用来处理滚动事件。
  • 事件监听:注册几个关键的事件监听器,以便在合适的时机触发数据加载。
2. 示例代码

以下是实现上滑加载更多的基本代码示例:

@Component
export default class NewsList extends Component {@State isEnd: boolean = false; // 控制是否触底private newsData = [ /* 初始化数据 */ ];private scroller: Scroller = new Scroller();build() {return (List() {ForEach(this.newsData, (item) => {ListItem() {Text(item.title).fontSize(20).padding(10);}}, (item) => item.id);}.onScrollStart(() => {this.isEnd = false; // 当开始滚动时,重置触底状态}).onScrollStop(() => {// 当停止滚动时,如果记录为触底,则加载更多if (this.isEnd) {//在这里处理分页,加载更多this.getMoreNews();}}).onReachEnd(() => {this.isEnd = true; // 一旦触底,设置状态为 trueconsole.log("onReachEnd");});}// 加载更多数据的逻辑private getMoreNews() {// 这里可以调用API获取更多的数据console.log("Loading more news...");// 假设请求完成后,更新 this.newsData 数据console.log('getMoreNews:')promptAction.showToast({ message: '加载数据中' })// 注意加载完数据把滚动条移至底部this.scroller.scrollEdge(Edge.Bottom);}
}
3.避坑指南 

这里有个坑,是什么呢?就是你的List铺不满的话,是不支持滑动的,也就无法触发onScrollStart和Stop等事件。如何解决呢?.edgeEffect(EdgeEffect.Spring, {alwaysEnabled:true})就起了关键作用,尤其是后面的alwaysEnabled:true很容易被漏掉。

日报新闻页完整实现

import {getSwiperList} from "../../common/api/home"
import { getZhiHuNews } from '../../common/api/zhihu';
import { BaseResponse,ErrorResp,ZhiNewsItem } from '../../common/bean/ApiTypes';
import { Log } from '../../utils/logutil'
import { formatDate2 } from '../../utils/time';
import { promptAction } from "@kit.ArkUI";class BasicDataSource<T> implements IDataSource {private listeners: DataChangeListener[] = [];private originDataArray: T[] = [];totalCount(): number {return this.originDataArray.length;}getData(index: number): T {return this.originDataArray[index];}registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {this.listeners.push(listener);}}unregisterDataChangeListener(listener: DataChangeListener): void {const pos = this.listeners.indexOf(listener);if (pos >= 0) {this.listeners.slice(pos, 1);}}// 通知LazyForEach组件需要重新重载所有子组件notifyDataReload(): void {this.listeners.forEach(listener => {listener.onDataReloaded();})}// 通知LazyForEach组件需要在index对应索引处添加子组件notifyDataAdd(index: number): void {this.listeners.forEach(listener => {listener.onDataAdd(index);})}
}class SwiperDataSource<T> extends BasicDataSource<T> {private dataArray: T[] = [];totalCount(): number {return this.dataArray.length;}getData(index: number): T {return this.dataArray[index];}// 在列表末尾添加数据并通知监听器pushData(data: T): void {this.dataArray.push(data);this.notifyDataAdd(this.dataArray.length - 1);}// 重载数据reloadData(): void {// 不会引起状态变化this.dataArray = [];// 必须通过DataChangeListener来更新this.notifyDataReload();}
}@Component
export default struct ZhiHu{@State message: string = 'Hello World';private swiperController: SwiperController = new SwiperController()private swiperData: SwiperDataSource<ZhiNewsItem> = new SwiperDataSource()@State zhiNews:ZhiNewsItem[] = []private currentDate= '' // 初始化为今天的日期private previousDate= '' // 上一天的日期pageStack: NavPathStack = new NavPathStack()@State isRefreshing: boolean = false// 判断滚动条是否触底@Stateprivate isEnd: boolean = false;// 使用Scroller对象scroller: Scroller = new Scroller();// 组件生命周期aboutToAppear() {Log.info('ZhiHu aboutToAppear');this.currentDate = formatDate2(new Date())getSwiperList().then((res) => {Log.debug(res.data.message)Log.debug("request","res.data.code:%{public}d",res.data.code)Log.debug("request","res.data.data[0]:%{public}s",res.data.data[0].id)Log.debug("request","res.data.data[0]:%{public}s",res.data.data[0].imageUrl)Log.debug("request","res.data.data[0]:%{public}s",res.data.data[0].title)}).catch((err:BaseResponse<ErrorResp>) => {Log.debug("request","err.data.code:%d",err.data.code)Log.debug("request",err.data.message)});//获取知乎新闻列表//this.finished = falsegetZhiHuNews(this.currentDate.replace(/-/g, '')).then((res) => {Log.debug(res.data.message)Log.debug("request","res.data.code:%{public}d",res.data.code)res.data.stories.map((item, index) => {item.isShowDivider = falseif (item.date !== this.previousDate) {this.previousDate = item.date;item.isShowDivider = true}});this.zhiNews = res.data.storiesfor (const itm of res.data.top_stories) {this.swiperData.pushData(itm)}//this.getMoreNews();}).catch((err:BaseResponse<ErrorResp>) => {Log.debug("request","err.data.code:%d",err.data.code)Log.debug("request",err.data.message)});}// 组件生命周期aboutToDisappear() {Log.info('ZhiHu aboutToDisappear');}// 触底之后触发下一页getMoreNews():void {console.log('getMoreNews:')promptAction.showToast({ message: '加载数据中' })const date_ = new Date(this.currentDate);date_.setDate(date_.getDate() - 1); // 日期减一//console.log(date_);let currentDate_ = formatDate2(date_);console.log('currentDate_:'+currentDate_);getZhiHuNews(currentDate_.replace(/-/g, '')).then((result) => {console.log("getZhiHuNews,result:");// 加载完数据把滚动条移至底部this.scroller.scrollEdge(Edge.Bottom);console.log(result.data.message);this.currentDate = formatDate2(date_);if (result.data.stories != null){result.data.stories.map((item, index) => {item.isShowDivider = falseif (item.date !== this.previousDate) {this.previousDate = item.date;item.isShowDivider = true}})this.zhiNews = this.zhiNews.concat(result.data.stories);}});}@Builderprivate LoadingCustom() {Stack() {Row() {LoadingProgress().width(30).color("#4095cb")}}.width('100%')}build() {Navigation(this.pageStack){Column({ space: 0 }) {// 内容项Swiper(this.swiperController) {LazyForEach(this.swiperData, (item: ZhiNewsItem) => {Stack({ alignContent: Alignment.Center }) {Image(item.image).width('100%').height(200).backgroundColor(0xAFEEEE).zIndex(1).onClick(() => {//this.pageStack.pushPathByName("PageOne", item)this.pageStack.pushDestinationByName("PageOne", { id:"9773231" }).catch((e:Error)=>{// 跳转失败,会返回错误码及错误信息console.log(`catch exception: ${JSON.stringify(e)}`)}).then(()=>{// 跳转成功});})// 显示轮播图标题Text(item.title).padding(5).margin({ top:60 }).width('100%').height(50).textAlign(TextAlign.Center).maxLines(2).textOverflow({overflow:TextOverflow.Clip}).fontSize(20).fontColor(Color.White).opacity(100) // 设置标题的透明度 不透明度设为100%,表示完全不透明.backgroundColor('#808080AA') // 背景颜色设为透明.zIndex(2)}}, (item: ZhiNewsItem) => item.id)}.cachedCount(2).index(1).autoPlay(true).interval(4000).loop(true).indicatorInteractive(true).duration(1000).itemSpace(0).curve(Curve.Linear).onChange((index: number) => {console.info(index.toString())}).onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {console.info("index: " + index)console.info("current offset: " + extraInfo.currentOffset)}).height(200) // 设置高度Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 100,builder: this.LoadingCustom() }) {// list组件List({ space: 1 }) {ForEach(this.zhiNews, (item:ZhiNewsItem) => {ListItem() {Column({ space: 0 }) {Row() {Column({ space: 15 }) {Text(item.title).fontSize(16).fontWeight(FontWeight.Bold).align(Alignment.Start).width('100%')Text(item.hint).align(Alignment.Start).width('100%')}.justifyContent(FlexAlign.Start).width('70%').padding(5)Image(item.image).objectFit(ImageFit.Cover).borderRadius(5).height(100).padding(2)}.size({ width: '100%', height: 100 })}.size({ width: '100%', height: 100 })}}, (itm:ZhiNewsItem) => itm.id)}.divider({strokeWidth:2,color:'#F1F3F5'}).listDirection(Axis.Vertical).edgeEffect(EdgeEffect.Spring, {alwaysEnabled:true})// 当画面能滚动说明没有触底.onScrollStart(() => {this.isEnd = false})// 判断当前是否停止滚动.onScrollStop(() => {// 如果停止滚动并且满足滚动条已经在底部进行数据的加载if (this.isEnd) {// 加载数据this.getMoreNews();}})// 当滚动条触底把 flag 设置成 true.onReachEnd(() => {this.isEnd = trueconsole.log("onReachEnd")}).onScrollIndex((firstIndex: number, lastIndex: number) => {console.info('first' + firstIndex)console.info('last' + lastIndex)//this.getmorePage()})//.backgroundColor('#ff940a31')}.onStateChange((refreshStatus: RefreshStatus) => {console.info('Refresh onStatueChange state is ' + refreshStatus)})// 设置触发刷新的下拉偏移量.refreshOffset(64)// 设置当下拉距离超过refreshOffset时是否触发刷新.pullToRefresh(true).onRefreshing(() => {setTimeout(() => {this.isRefreshing = false}, 1000)console.log('onRefreshing test')})}//.backgroundColor('#ff94610a')}.mode(NavigationMode.Stack).titleMode(NavigationTitleMode.Mini).title("知乎日报").hideBackButton(true)//.backgroundColor('#ff0a3894').width('100%').height('100%')}
}

总结

通过以上的实现,我们成功构建了一个支持下拉刷新和上滑加载更多功能的知乎日报应用。使用HarmonyOS NEXT的List组件及自定义数据源,可以有效地管理列表数据,并提供良好的用户体验。希望大家在实际开发中能够灵活应用这些技巧,构建出更优秀的应用!

写在最后

最后,推荐下笔者的业余开源app影视项目“爱影家”,推荐分享给与我一样喜欢免费观影的朋友。【注】:该项目仅限于学习研究使用!请勿用于其他用途!

开源地址:爱影家app开源项目介绍及源码

https://gitee.com/yyz116/imovie

其他资源

文档中心

OpenHarmony三方库中心仓--abner/refresh(V1.3.7)

【鸿蒙实战开发】数据的下拉刷新与上拉加载_鸿蒙下拉刷新-CSDN博客


http://www.mrgr.cn/news/59906.html

相关文章:

  • 词向量——预训练词嵌入
  • Nodejs使用pkg打包为可执行文件
  • Amazon Linux 2023 安装 Docker
  • 【Qt】系统相关——多线程、Qt多线程介绍、常用函数、线程安全、网络、UDP Socket、TCP Socket
  • 计算机网络-CSMA/CD协议笔记及“争用期”的理解
  • HLS协议之nginx-hls-多码率测试环境搭建
  • 【笔记】Diffusion Model 扩散过程(熵增过程:从有序变为无序):在原始分布上逐步的加高斯噪声,加到最后这个分布就变成一个各项独立的高斯分布
  • 常用 Web 框架
  • 我的电脑问题
  • 使用openssl验证https配置的ssl证书是否可以正常访问
  • Mybatis-plus-扩展功能
  • linux中级(NFS服务器)
  • Linux TCP CC状态机
  • Puppeteer 与浏览器版本兼容性:自动化测试的最佳实践
  • uniapp实现与webview之间的相互通讯
  • Vue项目GET请求正常,POST请求却失效?揭秘Mock服务背后的故事
  • 创建WBS项目管理过程
  • 不小心drop column了一个列,真的凉凉了吗?
  • linux驱动-引入pinctrl子系统
  • 离散化步骤
  • 群控系统服务端开发模式-应用开发-业务架构逻辑开发BaseAPI
  • 企业信息化与数字化 - 信息化是基础,数字化是未来
  • 动态规划-子序列问题——1218.最长定差子序列
  • VS Code 代码提示 重叠 显示不全
  • 小白投资理财 - 看懂 K 线形态下
  • C++的相关习题(2)