HarmonyOS(二十三)——HTTP请求实战一个新闻列表

在前一篇文章,我们已经知道如何实现一个http请求的完整流程,今天就用官方列子实战一个简单的新闻列表。进一步掌握ArkTS的声明式开发范式,数据请求,常用系统组件以及touch事件的使用。

主要包含以下功能:

  1. 数据请求。
  2. 列表下拉刷新。
  3. 列表上拉加载。

看一下最终的效果。
在这里插入图片描述

1.实战分析准备

既然是实现一个新闻列表请求,那么肯定少不了服务端搭建以及前端代码实战,下面就从这俩个方面一一实战讲解。

2.服务端搭建

服务端搭建,这里就用node.js提供服务接口支持,方便大家更好的理解和修改接口数据。

  1. 搭建nodejs环境:本篇Codelab的服务端是基于nodejs实现的,需要安装nodejs,如果您本地已有nodejs环境可以跳过此步骤。

    • 检查本地是否安装nodejs:打开命令行工具(如Windows系统的cmd和Mac电脑的Terminal,这里以Windows为例),输入node -v,如果可以看到版本信息,说明已经安装nodejs。
      在这里插入图片描述
    • 如果本地没有nodejs环境,您可以去nodejs官网上下载所需版本进行安装配置。
    • 配置完环境变量后,重新打开命令行工具,输入node -v,如果可以看到版本信息,说明已安装成功。
    • 运行服务端代码: 去下载华为官方提供的HttpServerOfNews服务端代码到本地,在项目的根目录,下打开命令行工具,输入npm install 安装服务端依赖包,安装成功后输入npm start点击回车。看到“服务器启动成功!”则表示服务端已经在正常运行。
    • 连接服务器地址:打开命令行工具,输入ipconfig命令查看本地ip,将本地ip地址复制到src/main/ets/common/constant/CommonConstants.ets文件下的23行,注意只替换ip地址部分,不要修改端口号,保存好ip之后即可运行代码进行测试。

3. 前端实战前准备分析工作

前端实现非常简单,可以按照以下几个步骤实现。

  1. 点击应用进入主页面,页面使用tabBar展示新闻分类,tabContent展示新闻列表,新闻分类和新闻列表通过请求nodejs服务端获取。
  2. 点击页签或左右滑动页面,切换标签并展示对应新闻类型的数据。
  3. 新闻列表页面,滑动到新闻列表首项数据,接着往下滑动会触发下拉刷新操作,页面更新初始4条新闻数据,滑动到新闻列表最后一项数据,往上拉会触发上拉加载操作,新闻列表会在后面加载4条新闻数据。

逻辑思路清晰了,下面我们开始实战前的准备配置。既然是实现网络请求,那么首先就需要配置网络权限。

  • 配置网络权限
    在进行网络请求前,您需要在module.json5文件中申明网络访问权限。
{"module" : {"requestPermissions":[{"name": "ohos.permission.INTERNET"}]}
}

网络权限配置完成,就可以开始愉快的coding了

4.构建主界面

  1. 用tabBar展示新闻分类
    在TabBar.ets文件中的aboutToAppear()方法里获取新闻分类。代码如下:
import NewsList from '../view/newslist';
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import NewsViewModel, { NewsTypeBean } from '../viewmodel/NewsViewModel';/*** The tabBar component.*/
@Component
export default struct TabBar {@State tabBarArray: NewsTypeBean[] = NewsViewModel.getDefaultTypeList();@State currentIndex: number = 0;@State currentPage: number = 1;@Builder TabBuilder(index: number) {Column() {Text(this.tabBarArray[index].name).height(Const.FULL_HEIGHT).padding({ left: Const.TabBars_HORIZONTAL_PADDING, right: Const.TabBars_HORIZONTAL_PADDING }).fontSize(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_SIZE : Const.TabBars_UN_SELECT_TEXT_FONT_SIZE).fontWeight(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_WEIGHT : Const.TabBars_UN_SELECT_TEXT_FONT_WEIGHT).fontColor($r('app.color.fontColor_text3'))}}aboutToAppear() {// Request news category.NewsViewModel.getNewsTypeList().then((typeList: NewsTypeBean[]) => {this.tabBarArray = typeList;}).catch((typeList: NewsTypeBean[]) => {this.tabBarArray = typeList;});}build() {Tabs() {ForEach(this.tabBarArray, (tabsItem: NewsTypeBean) => {TabContent() {Column() {NewsList({ currentIndex: $currentIndex })}}.tabBar(this.TabBuilder(tabsItem.id))}, (item: NewsTypeBean) => JSON.stringify(item));}.barHeight(Const.TabBars_BAR_HEIGHT).barMode(BarMode.Scrollable).barWidth(Const.TabBars_BAR_WIDTH).onChange((index: number) => {this.currentIndex = index;this.currentPage = 1;}).vertical(false)}
}
  1. tabContent展示新闻列表
    在NewsList.ets文件中的aboutToAppear()方法里获取新闻数据,将数据加载到新闻列表页面ListLayout布局中。而数据列表是高度相似可以服用的一个item, 它由标题,描述信息,日期,以及若干图片组成, 因此,可以简单抽取并复用一个NewsItem, 完整代码如下所示:

NewsItem代码如下:

import { CommonConstant, CommonConstant as Const } from '../common/constant/CommonConstant';
import { NewsData, NewsFile } from '../viewmodel/NewsViewModel';/*** The news list item component.*/
@Component
export default struct NewsItem {private newsData: NewsData = new NewsData();build() {Column() {Row() {Image($r('app.media.news')).width(Const.NewsTitle_IMAGE_WIDTH).height($r('app.float.news_title_image_height')).objectFit(ImageFit.Fill)Text(this.newsData.title).fontSize(Const.NewsTitle_TEXT_FONT_SIZE).fontColor($r('app.color.fontColor_text')).width(Const.NewsTitle_TEXT_WIDTH).maxLines(1).margin({ left: Const.NewsTitle_TEXT_MARGIN_LEFT }).textOverflow({ overflow: TextOverflow.Ellipsis }).fontWeight(Const.NewsTitle_TEXT_FONT_WEIGHT)}.alignItems(VerticalAlign.Center).height($r('app.float.news_title_row_height')).margin({top: $r('app.float.news_title_row_margin_top'),left: Const.NewsTitle_IMAGE_MARGIN_LEFT})Text(this.newsData.content).fontSize(Const.NewsContent_FONT_SIZE).fontColor($r('app.color.fontColor_text')).height(Const.NewsContent_HEIGHT).width(Const.NewsContent_WIDTH).maxLines(Const.NewsContent_MAX_LINES).margin({ left: Const.NewsContent_MARGIN_LEFT, top: Const.NewsContent_MARGIN_TOP }).textOverflow({ overflow: TextOverflow.Ellipsis })Grid() {ForEach(this.newsData.imagesUrl, (itemImg: NewsFile) => {GridItem() {Image(Const.SERVER + itemImg.url).objectFit(ImageFit.Cover).borderRadius(Const.NewsGrid_IMAGE_BORDER_RADIUS)}}, (itemImg: NewsFile, index?: number) => JSON.stringify(itemImg) + index)}.columnsTemplate(CommonConstant.GRID_COLUMN_TEMPLATES.repeat(this.newsData.imagesUrl.length)).columnsGap(Const.NewsGrid_COLUMNS_GAP).rowsTemplate(Const.NewsGrid_ROWS_TEMPLATE).width(Const.NewsGrid_WIDTH).height(Const.NewsGrid_HEIGHT).margin({ left: Const.NewsGrid_MARGIN_LEFT, top: Const.NewsGrid_MARGIN_TOP,right: Const.NewsGrid_MARGIN_RIGHT })Text(this.newsData.source).fontSize(Const.NewsSource_FONT_SIZE).fontColor($r('app.color.fontColor_text2')).height(Const.NewsSource_HEIGHT).width(Const.NewsSource_WIDTH).maxLines(Const.NewsSource_MAX_LINES).margin({ left: Const.NewsSource_MARGIN_LEFT, top: Const.NewsSource_MARGIN_TOP }).textOverflow({ overflow: TextOverflow.None })}.alignItems(HorizontalAlign.Start)}
}

新闻列表NewsList代码如下:

import promptAction from '@ohos.promptAction';
import { CommonConstant, CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import NewsItem from './NewsItem';
import LoadMoreLayout from './LoadMoreLayout';
import RefreshLayout from './RefreshLayout';
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout';
import { listTouchEvent } from '../common/utils/PullDownRefresh';
import NewsViewModel, { CustomRefreshLoadLayoutClass, NewsData } from '../viewmodel/NewsViewModel';
import NoMoreLayout from './NoMoreLayout';
import NewsModel from '../viewmodel/NewsModel';/*** The news list component.*/
@Component
export default struct NewsList {@State newsModel: NewsModel = new NewsModel();@Watch('changeCategory') @Link currentIndex: number;changeCategory() {this.newsModel.currentPage = 1;NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST).then((data: NewsData[]) => {this.newsModel.pageState = PageState.Success;if (data.length === this.newsModel.pageSize) {this.newsModel.currentPage++;this.newsModel.hasMore = true;} else {this.newsModel.hasMore = false;}this.newsModel.newsData = data;}).catch((err: string | Resource) => {promptAction.showToast({message: err,duration: Const.ANIMATION_DURATION});this.newsModel.pageState = PageState.Fail;});}aboutToAppear() {// Request news data.this.changeCategory();}build() {Column() {if (this.newsModel.pageState === PageState.Success) {this.ListLayout()} else if (this.newsModel.pageState === PageState.Loading) {this.LoadingLayout()} else {this.FailLayout()}}.width(Const.FULL_WIDTH).height(Const.FULL_HEIGHT).justifyContent(FlexAlign.Center).onTouch((event: TouchEvent | undefined) => {if (event) {if (this.newsModel.pageState === PageState.Success) {listTouchEvent(this.newsModel, event);}}})}@Builder LoadingLayout() {CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass(true,$r('app.media.ic_pull_up_load'), $r('app.string.pull_up_load_text'), this.newsModel.pullDownRefreshHeight) })}@Builder ListLayout() {List() {ListItem() {RefreshLayout({refreshLayoutClass: new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullDown, this.newsModel.pullDownRefreshImage,this.newsModel.pullDownRefreshText, this.newsModel.pullDownRefreshHeight)})}ForEach(this.newsModel.newsData, (item: NewsData) => {ListItem() {NewsItem({ newsData: item })}.height($r('app.float.news_list_height')).backgroundColor($r('app.color.white')).margin({ top: $r('app.float.news_list_margin_top') }).borderRadius(Const.NewsListConstant_ITEM_BORDER_RADIUS)}, (item: NewsData, index?: number) => JSON.stringify(item) + index)ListItem() {if (this.newsModel.hasMore) {LoadMoreLayout({loadMoreLayoutClass: new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullUpLoad, this.newsModel.pullUpLoadImage,this.newsModel.pullUpLoadText, this.newsModel.pullUpLoadHeight)})} else {NoMoreLayout()}}}.width(Const.NewsListConstant_LIST_WIDTH).height(Const.FULL_HEIGHT).margin({ left: Const.NewsListConstant_LIST_MARGIN_LEFT, right: Const.NewsListConstant_LIST_MARGIN_RIGHT }).backgroundColor($r('app.color.listColor')).divider({color: $r('app.color.dividerColor'),strokeWidth: Const.NewsListConstant_LIST_DIVIDER_STROKE_WIDTH,endMargin: Const.NewsListConstant_LIST_MARGIN_RIGHT})// Remove the rebound effect..edgeEffect(EdgeEffect.None).offset({ x: 0, y: `${this.newsModel.offsetY}${CommonConstant.LIST_OFFSET_UNIT}` }).onScrollIndex((start: number, end: number) => {// Listen to the first index of the current list.this.newsModel.startIndex = start;this.newsModel.endIndex = end;})}@Builder FailLayout() {Image($r('app.media.none')).height(Const.NewsListConstant_NONE_IMAGE_SIZE).width(Const.NewsListConstant_NONE_IMAGE_SIZE)Text($r('app.string.page_none_msg')).opacity(Const.NewsListConstant_NONE_TEXT_opacity).fontSize(Const.NewsListConstant_NONE_TEXT_size).fontColor($r('app.color.fontColor_text3')).margin({ top: Const.NewsListConstant_NONE_TEXT_margin })}
}
  1. 实现下拉刷新
    前面我们完成了一个新闻列表页面用于显示新闻信息所要用到的所有组件,但是,通常用户手机的大小是有限制的,为了用户能更好,更实时,更全面的获取新闻,我们通常还要实现下拉刷新和上拉加载功能。

创建一个下拉刷新布局CustomLayout,动态传入刷新图片和刷新文字描述。

// CustomRefreshLoadLayout.ets
build() {Row() {// 下拉刷新图片Image(this.customRefreshLoadClass.imageSrc)...// 下拉刷新文字Text(this.customRefreshLoadClass.textValue)...}...
}

将下拉刷新的布局添加到NewsList.ets文件中新闻列表布局ListLayout里面,监听ListLayout组件的onTouch事件实现下拉刷新

// NewsList.ets
build() {Column() {if (this.newsModel.pageState === PageState.Success) {this.ListLayout()}...}....onTouch((event: TouchEvent | undefined) => {if (event) {if (this.newsModel.pageState === PageState.Success) {listTouchEvent(this.newsModel, event);}}})
}
...
@Builder ListLayout() {List() {ListItem() {RefreshLayout({refreshLayoutClass: new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullDown, this.newsModel.pullDownRefreshImage,this.newsModel.pullDownRefreshText, this.newsModel.pullDownRefreshHeight)})...}}...
}

完整下啦刷新代码如下:

import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout';/*** The refresh layout component.*/
@Component
export default struct RefreshLayout {@ObjectLink refreshLayoutClass: CustomRefreshLoadLayoutClass;build() {Column() {if (this.refreshLayoutClass.isVisible) {CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.refreshLayoutClass.isVisible, this.refreshLayoutClass.imageSrc, this.refreshLayoutClass.textValue,this.refreshLayoutClass.heightValue) })}}}
}
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';/*** Custom layout to show refresh or load.*/
@Component
export default struct CustomLayout {@ObjectLink customRefreshLoadClass: CustomRefreshLoadLayoutClass;build() {Row() {Image(this.customRefreshLoadClass.imageSrc).width(Const.RefreshLayout_IMAGE_WIDTH).height(Const.RefreshLayout_IMAGE_HEIGHT)Text(this.customRefreshLoadClass.textValue).margin({left: Const.RefreshLayout_TEXT_MARGIN_LEFT,bottom: Const.RefreshLayout_TEXT_MARGIN_BOTTOM}).fontSize(Const.RefreshLayout_TEXT_FONT_SIZE).textAlign(TextAlign.Center)}.clip(true).width(Const.FULL_WIDTH).justifyContent(FlexAlign.Center).height(this.customRefreshLoadClass.heightValue)}
}
import promptAction from '@ohos.promptAction';
import { touchMoveLoadMore, touchUpLoadMore } from './PullUpLoadMore';
import {CommonConstant as Const,RefreshState
} from '../constant/CommonConstant';
import NewsViewModel, { NewsData } from '../../viewmodel/NewsViewModel';
import NewsModel from '../../viewmodel/NewsModel';export function listTouchEvent(newsModel: NewsModel, event: TouchEvent) {switch (event.type) {case TouchType.Down:newsModel.downY = event.touches[0].y;newsModel.lastMoveY = event.touches[0].y;break;case TouchType.Move:if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {return;}let isDownPull = event.touches[0].y - newsModel.lastMoveY > 0;if (((isDownPull === true) || (newsModel.isPullRefreshOperation === true)) && (newsModel.isCanLoadMore === false)){// Finger movement, processing pull-down refresh.touchMovePullRefresh(newsModel, event);} else {// Finger movement, processing load more.touchMoveLoadMore(newsModel, event);}newsModel.lastMoveY = event.touches[0].y;break;case TouchType.Cancel:break;case TouchType.Up:if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {return;}if ((newsModel.isPullRefreshOperation === true)) {// Lift your finger and pull down to refresh.touchUpPullRefresh(newsModel);} else {// Fingers up, handle loading more.touchUpLoadMore(newsModel);}break;default:break;}
}export function touchMovePullRefresh(newsModel: NewsModel, event: TouchEvent) {if (newsModel.startIndex === 0) {newsModel.isPullRefreshOperation = true;let height = vp2px(newsModel.pullDownRefreshHeight);newsModel.offsetY = event.touches[0].y - newsModel.downY;// The sliding offset is greater than the pull-down refresh layout height, and the refresh condition is met.if (newsModel.offsetY >= height) {pullRefreshState(newsModel, RefreshState.Release);newsModel.offsetY = height + newsModel.offsetY * Const.Y_OFF_SET_COEFFICIENT;} else {pullRefreshState(newsModel, RefreshState.DropDown);}if (newsModel.offsetY < 0) {newsModel.offsetY = 0;newsModel.isPullRefreshOperation = false;}}
}export function touchUpPullRefresh(newsModel: NewsModel) {if (newsModel.isCanRefresh === true) {newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);pullRefreshState(newsModel, RefreshState.Refreshing);newsModel.currentPage = 1;setTimeout(() => {let self: NewsModel = newsModel;NewsViewModel.getNewsList(newsModel.currentPage, newsModel.pageSize, Const.GET_NEWS_LIST).then((data:NewsData[]) => {if (data.length === newsModel.pageSize) {self.hasMore = true;self.currentPage++;} else {self.hasMore = false;}self.newsData = data;closeRefresh(self, true);}).catch((err: string | Resource) => {promptAction.showToast({ message: err });closeRefresh(self, false);});}, Const.DELAY_TIME);} else {closeRefresh(newsModel, false);}
}export function pullRefreshState(newsModel: NewsModel, state: number) {switch (state) {case RefreshState.DropDown:newsModel.pullDownRefreshText = $r('app.string.pull_down_refresh_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_down_refresh');newsModel.isCanRefresh = false;newsModel.isRefreshing = false;newsModel.isVisiblePullDown = true;break;case RefreshState.Release:newsModel.pullDownRefreshText = $r('app.string.release_refresh_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = false;break;case RefreshState.Refreshing:newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);newsModel.pullDownRefreshText = $r('app.string.refreshing_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_load');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;case RefreshState.Success:newsModel.pullDownRefreshText = $r('app.string.refresh_success_text');newsModel.pullDownRefreshImage = $r('app.media.ic_succeed_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;case RefreshState.Fail:newsModel.pullDownRefreshText = $r('app.string.refresh_fail_text');newsModel.pullDownRefreshImage = $r('app.media.ic_fail_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;default:break;}
}export function closeRefresh(newsModel: NewsModel, isRefreshSuccess: boolean) {let self = newsModel;setTimeout(() => {let delay = Const.RefreshConstant_DELAY_PULL_DOWN_REFRESH;if (self.isCanRefresh === true) {pullRefreshState(newsModel, isRefreshSuccess ? RefreshState.Success : RefreshState.Fail);delay = Const.RefreshConstant_DELAY_SHRINK_ANIMATION_TIME;}animateTo({duration: Const.RefreshConstant_CLOSE_PULL_DOWN_REFRESH_TIME,delay: delay,onFinish: () => {pullRefreshState(newsModel, RefreshState.DropDown);self.isVisiblePullDown = false;self.isPullRefreshOperation = false;}}, () => {self.offsetY = 0;})}, self.isCanRefresh ? Const.DELAY_ANIMATION_DURATION : 0);
}
  1. 实现上拉加载更多
import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout';/*** The load more layout component.*/
@Component
export default struct LoadMoreLayout {@ObjectLink loadMoreLayoutClass: CustomRefreshLoadLayoutClass;build() {Column() {if (this.loadMoreLayoutClass.isVisible) {CustomRefreshLoadLayout({customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.loadMoreLayoutClass.isVisible,this.loadMoreLayoutClass.imageSrc, this.loadMoreLayoutClass.textValue, this.loadMoreLayoutClass.heightValue)})} else {CustomRefreshLoadLayout({customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.loadMoreLayoutClass.isVisible,this.loadMoreLayoutClass.imageSrc, this.loadMoreLayoutClass.textValue, 0)})}}}
}
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';/*** Custom layout to show refresh or load.*/
@Component
export default struct CustomLayout {@ObjectLink customRefreshLoadClass: CustomRefreshLoadLayoutClass;build() {Row() {Image(this.customRefreshLoadClass.imageSrc).width(Const.RefreshLayout_IMAGE_WIDTH).height(Const.RefreshLayout_IMAGE_HEIGHT)Text(this.customRefreshLoadClass.textValue).margin({left: Const.RefreshLayout_TEXT_MARGIN_LEFT,bottom: Const.RefreshLayout_TEXT_MARGIN_BOTTOM}).fontSize(Const.RefreshLayout_TEXT_FONT_SIZE).textAlign(TextAlign.Center)}.clip(true).width(Const.FULL_WIDTH).justifyContent(FlexAlign.Center).height(this.customRefreshLoadClass.heightValue)}
}
import promptAction from '@ohos.promptAction';
import { touchMoveLoadMore, touchUpLoadMore } from './PullUpLoadMore';
import {CommonConstant as Const,RefreshState
} from '../constant/CommonConstant';
import NewsViewModel, { NewsData } from '../../viewmodel/NewsViewModel';
import NewsModel from '../../viewmodel/NewsModel';export function listTouchEvent(newsModel: NewsModel, event: TouchEvent) {switch (event.type) {case TouchType.Down:newsModel.downY = event.touches[0].y;newsModel.lastMoveY = event.touches[0].y;break;case TouchType.Move:if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {return;}let isDownPull = event.touches[0].y - newsModel.lastMoveY > 0;if (((isDownPull === true) || (newsModel.isPullRefreshOperation === true)) && (newsModel.isCanLoadMore === false)){// Finger movement, processing pull-down refresh.touchMovePullRefresh(newsModel, event);} else {// Finger movement, processing load more.touchMoveLoadMore(newsModel, event);}newsModel.lastMoveY = event.touches[0].y;break;case TouchType.Cancel:break;case TouchType.Up:if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {return;}if ((newsModel.isPullRefreshOperation === true)) {// Lift your finger and pull down to refresh.touchUpPullRefresh(newsModel);} else {// Fingers up, handle loading more.touchUpLoadMore(newsModel);}break;default:break;}
}export function touchMovePullRefresh(newsModel: NewsModel, event: TouchEvent) {if (newsModel.startIndex === 0) {newsModel.isPullRefreshOperation = true;let height = vp2px(newsModel.pullDownRefreshHeight);newsModel.offsetY = event.touches[0].y - newsModel.downY;// The sliding offset is greater than the pull-down refresh layout height, and the refresh condition is met.if (newsModel.offsetY >= height) {pullRefreshState(newsModel, RefreshState.Release);newsModel.offsetY = height + newsModel.offsetY * Const.Y_OFF_SET_COEFFICIENT;} else {pullRefreshState(newsModel, RefreshState.DropDown);}if (newsModel.offsetY < 0) {newsModel.offsetY = 0;newsModel.isPullRefreshOperation = false;}}
}export function touchUpPullRefresh(newsModel: NewsModel) {if (newsModel.isCanRefresh === true) {newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);pullRefreshState(newsModel, RefreshState.Refreshing);newsModel.currentPage = 1;setTimeout(() => {let self: NewsModel = newsModel;NewsViewModel.getNewsList(newsModel.currentPage, newsModel.pageSize, Const.GET_NEWS_LIST).then((data:NewsData[]) => {if (data.length === newsModel.pageSize) {self.hasMore = true;self.currentPage++;} else {self.hasMore = false;}self.newsData = data;closeRefresh(self, true);}).catch((err: string | Resource) => {promptAction.showToast({ message: err });closeRefresh(self, false);});}, Const.DELAY_TIME);} else {closeRefresh(newsModel, false);}
}export function pullRefreshState(newsModel: NewsModel, state: number) {switch (state) {case RefreshState.DropDown:newsModel.pullDownRefreshText = $r('app.string.pull_down_refresh_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_down_refresh');newsModel.isCanRefresh = false;newsModel.isRefreshing = false;newsModel.isVisiblePullDown = true;break;case RefreshState.Release:newsModel.pullDownRefreshText = $r('app.string.release_refresh_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = false;break;case RefreshState.Refreshing:newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);newsModel.pullDownRefreshText = $r('app.string.refreshing_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_load');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;case RefreshState.Success:newsModel.pullDownRefreshText = $r('app.string.refresh_success_text');newsModel.pullDownRefreshImage = $r('app.media.ic_succeed_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;case RefreshState.Fail:newsModel.pullDownRefreshText = $r('app.string.refresh_fail_text');newsModel.pullDownRefreshImage = $r('app.media.ic_fail_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;default:break;}
}export function closeRefresh(newsModel: NewsModel, isRefreshSuccess: boolean) {let self = newsModel;setTimeout(() => {let delay = Const.RefreshConstant_DELAY_PULL_DOWN_REFRESH;if (self.isCanRefresh === true) {pullRefreshState(newsModel, isRefreshSuccess ? RefreshState.Success : RefreshState.Fail);delay = Const.RefreshConstant_DELAY_SHRINK_ANIMATION_TIME;}animateTo({duration: Const.RefreshConstant_CLOSE_PULL_DOWN_REFRESH_TIME,delay: delay,onFinish: () => {pullRefreshState(newsModel, RefreshState.DropDown);self.isVisiblePullDown = false;self.isPullRefreshOperation = false;}}, () => {self.offsetY = 0;})}, self.isCanRefresh ? Const.DELAY_ANIMATION_DURATION : 0);
}
  1. 新闻数据请求
    前面俩步我们已经实现了一个新闻列表页面展示所需要的各个组件。下面就是来到真正的请求数据环节。由于前一篇认识HTTP请求之从网络获取数据已经详细介绍过,这里不做过多描述,直接封装一个通用http 请求工具类实现数据请求:

导入http模块,封装httpRequestGet方法,调用者传入url地址发起网络数据请求。

import http from '@ohos.net.http';
import { ResponseResult } from '../../viewmodel/NewsViewModel';
import { CommonConstant as Const, ContentType } from '../constant/CommonConstant';/*** Initiates an HTTP request to a given URL.** @param url URL for initiating an HTTP request.* @param params Params for initiating an HTTP request.*/
export function httpRequestGet(url: string): Promise<ResponseResult> {let httpRequest = http.createHttp();let responseResult = httpRequest.request(url, {method: http.RequestMethod.GET,readTimeout: Const.HTTP_READ_TIMEOUT,header: {'Content-Type': ContentType.JSON},connectTimeout: Const.HTTP_READ_TIMEOUT,extraData: {}});let serverData: ResponseResult = new ResponseResult();// Processes the data and returns.return responseResult.then((value: http.HttpResponse) => {if (value.responseCode === Const.HTTP_CODE_200) {// Obtains the returned data.let result = `${value.result}`;let resultJson: ResponseResult = JSON.parse(result);if (resultJson.code === Const.SERVER_CODE_SUCCESS) {serverData.data = resultJson.data;}serverData.code = resultJson.code;serverData.msg = resultJson.msg;} else {serverData.msg = `${$r('app.string.http_error_message')}&${value.responseCode}`;}return serverData;}).catch(() => {serverData.msg = $r('app.string.http_error_message');return serverData;})
}
  1. NewsViewModel实现,获取服务端新闻数据列表
    在NewsViewModel.ets文件中封装getNewsList方法,调用httpRequestGet方法请求服务端,用Promise异步保存返回的新闻数据列表。
// NewsViewModel.ets
// 获取服务端新闻数据列表
getNewsList(currentPage: number, pageSize: number, path: string): Promise<NewsData[]> {return new Promise(async (resolve: Function, reject: Function) => {let url = `${Const.SERVER}/${path}`;url += '?currentPage=' + currentPage + '&pageSize=' + pageSize;httpRequestGet(url).then((data: ResponseResult) => {if (data.code === Const.SERVER_CODE_SUCCESS) {resolve(data.data);} else {Logger.error('getNewsList failed', JSON.stringify(data));reject($r('app.string.page_none_msg'));}}).catch((err: Error) => {Logger.error('getNewsList failed', JSON.stringify(err));reject($r('app.string.http_error_message'));});});
}

完整NewsViewModel.ets代码如下:

import { CommonConstant as Const } from '../common/constant/CommonConstant';
import { httpRequestGet } from '../common/utils/HttpUtil';
import Logger from '../common/utils/Logger';class NewsViewModel {/*** Get news type list from server.** @return NewsTypeBean[] newsTypeList*/getNewsTypeList(): Promise<NewsTypeBean[]> {return new Promise((resolve: Function) => {let url = `${Const.SERVER}/${Const.GET_NEWS_TYPE}`;httpRequestGet(url).then((data: ResponseResult) => {if (data.code === Const.SERVER_CODE_SUCCESS) {resolve(data.data);} else {resolve(Const.TabBars_DEFAULT_NEWS_TYPES);}}).catch(() => {resolve(Const.TabBars_DEFAULT_NEWS_TYPES);});});}/*** Get default news type list.** @return NewsTypeBean[] newsTypeList*/getDefaultTypeList(): NewsTypeBean[] {return Const.TabBars_DEFAULT_NEWS_TYPES;}/*** Get news type list from server.** @return NewsData[] newsDataList*/getNewsList(currentPage: number, pageSize: number, path: string): Promise<NewsData[]> {return new Promise(async (resolve: Function, reject: Function) => {let url = `${Const.SERVER}/${path}`;url += '?currentPage=' + currentPage + '&pageSize=' + pageSize;httpRequestGet(url).then((data: ResponseResult) => {if (data.code === Const.SERVER_CODE_SUCCESS) {resolve(data.data);} else {Logger.error('getNewsList failed', JSON.stringify(data));reject($r('app.string.page_none_msg'));}}).catch((err: Error) => {Logger.error('getNewsList failed', JSON.stringify(err));reject($r('app.string.http_error_message'));});});}
}let newsViewModel = new NewsViewModel();export default newsViewModel as NewsViewModel;/*** News list item info.*/
export class NewsData {/*** News list item title.*/title: string = '';/*** News list item content.*/content: string = '';/*** News list item imagesUrl.*/imagesUrl: Array<NewsFile> = [new NewsFile()];/*** News list item source.*/source: string = '';
}/*** News image list item info.*/
export class NewsFile {/*** News image list item id.*/id: number = 0;/*** News image list item url.*/url: string = '';/*** News image list item type.*/type: number = 0;/*** News image list item newsId.*/newsId: number = 0;
}/*** Custom refresh load layout data.*/
@Observed
export class CustomRefreshLoadLayoutClass {/*** Custom refresh load layout isVisible.*/isVisible: boolean;/*** Custom refresh load layout imageSrc.*/imageSrc: Resource;/*** Custom refresh load layout textValue.*/textValue: Resource;/*** Custom refresh load layout heightValue.*/heightValue: number;constructor(isVisible: boolean, imageSrc: Resource, textValue: Resource, heightValue: number) {this.isVisible = isVisible;this.imageSrc = imageSrc;this.textValue = textValue;this.heightValue = heightValue;}
}export class NewsTypeBean {id: number = 0;name: ResourceStr = '';
}export class ResponseResult {/*** Code returned by the network request: success, fail.*/code: string;/*** Message returned by the network request.*/msg: string | Resource;/*** Data returned by the network request.*/data: string | Object | ArrayBuffer;constructor() {this.code = '';this.msg = '';this.data = '';}
}

新闻列表数据详情数据结构以及页面分页刷行

import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import { NewsData } from './NewsViewModel';export default class NewsModel {newsData: Array<NewsData> = [];currentPage: number = 1;pageSize: number = Const.PAGE_SIZE;pullDownRefreshText: Resource = $r('app.string.pull_down_refresh_text');pullDownRefreshImage: Resource = $r('app.media.ic_pull_down_refresh');pullDownRefreshHeight: number = Const.CUSTOM_LAYOUT_HEIGHT;isVisiblePullDown: boolean = false;pullUpLoadText: Resource = $r('app.string.pull_up_load_text');pullUpLoadImage: Resource = $r('app.media.ic_pull_up_load');pullUpLoadHeight: number = Const.CUSTOM_LAYOUT_HEIGHT;isVisiblePullUpLoad: boolean = false;offsetY: number = 0;pageState: number = PageState.Loading;hasMore: boolean = true;startIndex = 0;endIndex = 0;downY = 0;lastMoveY = 0;isRefreshing: boolean = false;isCanRefresh = false;isPullRefreshOperation = false;isLoading: boolean = false;isCanLoadMore: boolean = false;
}
  1. 难点以及注意事项

1. 在onTouch事件中,listTouchEvent方法判断触摸事件是否满足下拉条件。

2. 在touchMovePullRefresh方法中,我们将对下拉的偏移量与下拉刷新布局的高度进行对比,如 果大于布局高度并且在新闻列表的顶部,则表示达到刷新条件。

3. 在pullRefreshState方法中我们会对下拉刷新布局中的状态图片和描述进行改变,当手指松开,才执行刷新操作。

// PullDownRefresh.ets
export function listTouchEvent(newsModel: NewsModel, event: TouchEvent) {switch (event.type) {...case TouchType.Move:if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {return;}let isDownPull = event.touches[0].y - newsModel.lastMoveY > 0;if (((isDownPull === true) || (newsModel.isPullRefreshOperation === true)) && (newsModel.isCanLoadMore === false)){// 手指移动,处理下拉刷新touchMovePullRefresh(newsModel, event);}...break;}
}
export function touchMovePullRefresh(newsModel: NewsModel, event: TouchEvent) {if (newsModel.startIndex === 0) {newsModel.isPullRefreshOperation = true;let height = vp2px(newsModel.pullDownRefreshHeight);newsModel.offsetY = event.touches[0].y - newsModel.downY;// 滑动偏移量大于下拉刷新布局高度,满足刷新条件。if (newsModel.offsetY >= height) {pullRefreshState(newsModel, RefreshState.Release);newsModel.offsetY = height + newsModel.offsetY * Const.Y_OFF_SET_COEFFICIENT;} else {pullRefreshState(newsModel, RefreshState.DropDown);}if (newsModel.offsetY < 0) {newsModel.offsetY = 0;newsModel.isPullRefreshOperation = false;}}
}
export function pullRefreshState(newsModel: NewsModel, state: number) {switch (state) {...case RefreshState.Release:newsModel.pullDownRefreshText = $r('app.string.release_refresh_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = false;break;case RefreshState.Refreshing:newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);newsModel.pullDownRefreshText = $r('app.string.refreshing_text');newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_load');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;case RefreshState.Success:newsModel.pullDownRefreshText = $r('app.string.refresh_success_text');newsModel.pullDownRefreshImage = $r('app.media.ic_succeed_refresh');newsModel.isCanRefresh = true;newsModel.isRefreshing = true;break;...default:break;}
}

上拉加载也是通过touch事件来实现的,此处不再赘叙。

5.总结

  1. 实现网络请求,我们需要在module.json5文件中申明网络访问权限。
  2. 鸿蒙实现网络请求,需要导入@ohos.net.http,并且每一个httpRequest对应一个HTTP请求任务,不可复用。
  3. 刷新和上拉加载都是借助触摸事件onTouch来实现的,关于触摸事件onTouch的使用可以参考官方文档《触摸事件onTouch》,后期会专门出一篇文章讲解触摸事件的使用及其使用场景。
  4. 灵活掌握各个组件的特性和使用场景,可以帮助我们快速完成开发,比如此次新闻列表页,我们用到了如下三个系统组件:
    • List组件:列表包含一系列相同宽度的列表项,用于显示完整的新闻列表,是最大的容器。
    • Tabs:通过页签进行内容视图切换,可以切换不同类型的新闻。
    • TabContent:仅在Tabs中使用,对应一个切换页签的内容视图。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/22955.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

1103. 分糖果 II Rust等差求和+一元二次方程求根(击败100% Rust用户)

题目内容 排排坐&#xff0c;分糖果。 我们买了一些糖果 candies&#xff0c;打算把它们分给排好队的 n num_people 个小朋友。 给第一个小朋友 1 颗糖果&#xff0c;第二个小朋友 2 颗&#xff0c;依此类推&#xff0c;直到给最后一个小朋友 n 颗糖果。 然后&#xff0c;…

C语言中指针的说明

什么是指针&#xff1f; 在C语言当中&#xff0c;我们可以将指针理解为内存当中存储的地址&#xff0c;就像生活当中&#xff0c;一个小区里面&#xff0c;在小区里面有很单元&#xff0c;每一栋单元&#xff0c;单元内的房间有着不同的房间号&#xff0c;我们可以同过几栋几单…

Docker 进入指定容器内部(以Mysql为例)

文章目录 一、启动容器二、查看容器是否启动三、进入容器内部 一、启动容器 这个就不多说了 直接docker run… 二、查看容器是否启动 查看正在运行的容器 docker ps查看所有的容器 docker ps -a结果如下图所示&#xff1a; 三、进入容器内部 通过CONTAINER ID进入到容器…

FFA-Net:用于单图像去雾的特征融合注意力网络

摘要 论文链接&#xff1a;https://arxiv.org/pdf/1911.07559v2 在这篇论文中&#xff0c;我们提出了一种端到端的特征融合注意力网络&#xff08;FFA-Net&#xff09;来直接恢复无雾图像。FFA-Net架构由三个关键组件组成&#xff1a; 一种新颖的特征注意力&#xff08;FA&…

C语言笔记第10篇:内存函数

上一篇的字符串函数只是针对字符串的函数&#xff0c;而内存函数是针对内存块的&#xff0c;不在乎内存中存储的数据&#xff01;这就是字符串函数和内存函数的区别。 准备好爆米花&#xff0c;正片开始 1、memcpy的使用和模拟实现 memcpy库函数的功能&#xff1a;任意类型数…

Element ui图片上传

前言 对于广大小白来说&#xff0c;图片上传简直是上传难&#xff0c;难于上青天&#xff01;废话不多说&#xff0c;步入正题&#xff0c;您就瞧好吧&#xff01; 步骤一&#xff1a;前端使用element ui组件&#xff08;upload上传&#xff09; 我个人喜欢使用第二个组件&a…

ingress规则

一 k8s 对外服务之 Ingress LB ingress 1 Ingress 简介 service的作用体现在两个方面 ? ① 对集群内部&#xff0c;它不断跟踪pod的变化&#xff0c;更新endpoint中对应pod的对象&#xff0c;提供了ip不断变化的pod的服务发现机制&#xff1b; ② 对集群外部&#xff0c…

Docker部署青龙面板

青龙面板 文章目录 青龙面板介绍资源列表基础环境一、安装Docker二、安装Docker-Compose三、安装青龙面板3.1、拉取青龙&#xff08;whyour/qinglong&#xff09;镜像3.2、编写docker-compose文件3.3、检查语法启动容器 四、访问青龙面板五、映射本地部署的青龙面板至公网5.1、…

Day06 创建首页ListBox列表数据

​ 完成当前章节后,最终效果图如下 ​​​​ 一.首页汇总方块鼠标悬停阴影效果设计 首先,在上一章节首页设计 的时候,就已经知道当前主界面的汇总方块是使用 Border 来实现的,那么想要实现鼠标悬停时设置阴影的效果,就要在 Border 中进行重写样式。 需要使用 触发器 来实…

【动手学深度学习】卷积神经网络CNN的研究详情

目录 &#x1f30a;1. 研究目的 &#x1f30a;2. 研究准备 &#x1f30a;3. 研究内容 &#x1f30d;3.1 卷积神经网络 &#x1f30d;3.2 练习 &#x1f30a;4. 研究体会 &#x1f30a;1. 研究目的 特征提取和模式识别&#xff1a;CNN 在计算机视觉领域被广泛用于提取图像…

Locality-aware subgraphs for inductive link prediction in knowledge graphs

Locality-aware subgraphs for inductive link prediction in knowledge graphs a b s t r a c t 最近的知识图&#xff08;KG&#xff09;归纳推理方法将链接预测问题转化为图分类任务。 他们首先根据目标实体的 k 跳邻域提取每个目标链接周围的子图&#xff0c;使用图神经网…

Spark SQL - 操作数据帧

本教程将通过一个具体的案例来演示如何在Spark SQL中操作数据帧。我们将从获取学生数据帧开始&#xff0c;包括两种方法&#xff1a;一是由数据集转换而来&#xff0c;二是直接读取文件生成数据帧。然后&#xff0c;我们将对数据帧进行各种操作&#xff0c;如投影、过滤、统计和…

Linux系统进行DNS域名解析

文章目录 一、DNS1.1 DNS概述1.2 DNS的通俗解释1.3 域名的体系结构1.4 DNS解析的参数 1.5 DNS域名解析的过程 二、如何实现DNS(内网)2.1 DNS正向解析2.2 DNS反向解析2.3 主从DNS 一、DNS 1.1 DNS概述 DNS &#xff1a;Domain Name System&#xff0c;是域名系统的简称&#x…

Flutter Bloc之简单记录

目录 0.库安装 1.插件和自动生成 2.状态的配置 1.初始化中&#xff1a; 2.赋值完成后&#xff1a; 3.如果出错&#xff1a; 3.事件的配置 1.定义一个读取事件 2.定义一个更改事件 4.Bloc的设置 5.Bloc的使用 1.BlocProvider 2.内部调用 参考文章进行类的配置 0.库…

NSS题目练习7

[MoeCTF 2022]baby_file 打开看见一串源代码&#xff0c;需要get传参传入file 题目提示php伪协议 用dirsearch扫描发现flag.php 用php伪协议查看&#xff0c;回显一串base64编码 解码后得到flag [鹤城杯 2021]Middle magic 读取这两个文件 一个php正则表达式 补充&#xff1a…

解锁ArrayBlockingQueue奥秘:深入源码的精彩之旅

1.简介 ArrayBlockingQueue 是 BlockingQueue 接口的一个实现类&#xff0c;它基于数组实现了一个有界阻塞队列。创建 ArrayBlockingQueue 实例时需要指定队列的容量&#xff0c;队列的大小是固定的&#xff0c;无法动态增长。 主要特点包括&#xff1a; 有界性&#xff1a;A…

STM32——hal_SPI_(介绍)

SPI&#xff08;串行外围设备接口&#xff09;是一种高速的、全双工、同步的通信协议&#xff0c;通常用于短距离通信&#xff0c;尤其是在嵌入式系统中与各种外围设备进行通信。SPI接口由摩托罗拉公司推出&#xff0c;由于其简单和灵活的特性&#xff0c;它被广泛用于多种应用…

2023年计算机图形学课程知识总结

去年就该写的&#xff0c;但是去年这个时候太忙了。 就写来自己看看。留个记录留个念 文章目录 1. 图形&#xff0c;图像的定义2. 点阵、矢量3. 走样&#xff0c;反走样4. 字符裁剪精度&#xff08;1&#xff09; 串精度&#xff08;2&#xff09; 字符精度&#xff08;3&…

SpringBoot打war包并配置外部Tomcat运行

简介 由于其他原因&#xff0c;我们需要使用SpringBoot打成war包放在外部的Tomcat中运行,本文就以一个案例来说明从SpringBoot打war包到Tomcat配置并运行的全流程经过 环境 SpringBoot 2.6.15 Tomcat 8.5.100 JDK 1.8.0_281 Windows 正文 一、SpringBoot配置打war包 第一步&a…

J-Lin烧录

1、J-linK介绍 J-Link是由德国SEGGER公司推出的&#xff0c;主要用于支持仿真ARM内核芯片的JTAG仿真器。它支持JTAG和SWD两种模式&#xff0c;可以配合多种集成开发环境&#xff08;如IAR EWAR, ADS, KEIL, WINARM, RealView等&#xff09;使用&#xff0c;支持ARM7/ARM9/ARM…