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.rhkb.cn/news/345850.html

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

相关文章

盘点2024年5月Sui生态发展,了解Sui近期成长历程!

2024年5月是Sui的第一个生日月&#xff0c;Sui迎来了它的上线一周年纪念日。在过去的一年中Sui在技术进步与创新、生态系统的扩展、社区发展与合作伙伴关系以及重大项目和应用推出方面取得重要进展&#xff0c;展示了其作为下一代区块链平台的潜力。 以下是Sui的近期成长历程集…

MySQL的group by与count(), *字段使用问题

文章目录 问题group by到底做了什么举个例子简单来说为什么select字段&#xff0c;count()不能和*共同使用总结 问题 这是一段摘抄自MySQL官网的文字。其大致意思是MySQL拓展了group by的使用&#xff0c;MySQL允许选择没有出现在group by中的字段。换句话说&#xff0c;标准SQ…

暴雨推出X705显示器:23.8英寸100Hz IPS屏

IT资讯 6月 7 日消息&#xff0c;日前&#xff0c;暴雨发布了一款 23.8 英寸 IPS 显示器&#xff0c;直屏、支持 100Hz 刷新率。 据官方介绍&#xff0c;X705 显示器分辨率为 19201080&#xff0c;亮度为 300 尼特&#xff08;典型值&#xff09;&#xff0c;对比度 1000:1&…

Polar Web【中等】search

Polar Web【中等】search Contents Polar Web【中等】search思路&探索首页一般注入方式 EXP&效果Payload 总结 思路&探索 见到题目标题&#xff0c;预测可能有目录扫描或者输入框查询数据之类情况&#xff0c;具体细节在破解过程中才能清楚 打开站点&#xff0c;显…

【学习笔记】finalshell上传文件夹、上传文件失败或速度为0

出现标题所述的情况&#xff0c;大概率是finalshell上传文件的过程中的权限不够。 可参照&#xff1a;Finalshell上传文件失败或者进度总为百分之零解决方法 如果不成功&#xff0c;建议关闭客户端重试。 同时建议在设置finalshell的ssh连接时根据不同用户设置多个连接&#xf…

Postman环境变量以及设置token全局变量!

前言百度百科解释&#xff1a; 环境变量&#xff08;environment variables&#xff09;一般是指在操作系统中用来指定操作系统运行环境的一些参数&#xff0c;如&#xff1a;临时文件夹位置和系统文件夹位置等。 环境变量是在操作系统中一个具有特定名字的对象&#xff0c;它…

UE5中在地形中加入湖、河

系统水资产添加 前提步骤123 完成 前提 使用版本 UE5.0.3,使用插件为UE内置的Water和water Extras. 步骤 1 记得重启 2 增加地形&#xff0c;把<启用编辑图层>勾选 如果地形没有勾选上编辑图层&#xff0c;那么就会导致湖、河等水景象无法融入地形。 如果忘记勾选…

【NOI】C++程序结构入门之循环结构三——break、continue

文章目录 前言一、循环的流程控制1.1 导入1.2 循环的打破与跳过1.2.1 break 打破1.2.2 continue 跳过1.2.3 总结 二、例题讲解问题&#xff1a;1468. 小鱼的航程问题&#xff1a;1074 - 小青蛙回来了问题&#xff1a;1261. 韩信点兵问题&#xff1a;1254. 求车速问题&#xff1…

Linux:冯·诺依曼体系结构和操作系统

文章目录 冯诺依曼体系结构操作系统概念操作系统的作用定位机制操作系统如何管理硬件 冯诺依曼体系结构 我们常见的计算机&#xff0c;如笔记本。我们不常见的计算机&#xff0c;如服务器&#xff0c;大部分都遵守冯诺依曼体系。 截至目前&#xff0c;我们所认识的计算机&…

记录一次被谷歌封号后又解封的过程

先提前恭祝2024年所有参加高考的学子们都能金榜题名&#xff0c;会的全对&#xff0c;不会的蒙的全对&#xff01; 一、背景 众所周知&#xff0c;谷歌、ios应用市场对app的审查都是极其严格的&#xff0c;开发者稍有不慎就会被谷歌下架应用&#xff0c;乃至封号。我们公司是做…

mobaxterm怎么ssh连接

要使用 MobaXterm 进行 SSH 连接&#xff0c;请按照以下步骤操作&#xff1a; 1、首先&#xff0c;确保已经安装了 MobaXterm 软件。 你可以在官方网站&#xff08;https://mobaxterm.mobatek.net/&#xff09;上下载并安装它。 2、打开 MobaXterm 软件后&#xff0c;你会看…

《大道平渊》· 拾壹 —— 商业一定是个故事:讲好故事,员工奋发,顾客买单。

《大道平渊》 拾壹 "大家都在喝&#xff0c;你喝不喝&#xff1f;" 商业一定是个故事&#xff0c;人民群众需要故事。 比如可口可乐的各种故事。 可口可乐公司也只是被营销大师们&#xff0c; 作为一种故事载体&#xff0c;发挥他们的本领。 营销大师们开发故事…

杨校老师项目之基于SpringBoot的理发店的预约管理系统

原系统是SSMJSP页面构成&#xff0c;先被修改为SpringBoot JSP页面 自助下载渠道: https://download.csdn.net/download/kese7952/89417001&#xff0c;或 点我下载 理发师信息&#xff1a; 理发师详细信息 公告信息 员工登录&#xff1a; 管理员登录

Mysql8安装教程与配置(超详细图文)

MySQL 8.0 是 MySQL 数据库的一个重大更新版本&#xff0c;它引入了许多新特性和改进&#xff0c;旨在提高性能、安全性和易用性。 1.下载MySQL 安装包 注&#xff1a;本文使用的是压缩版进行安装。 &#xff08;1&#xff09;从网盘下载安装文件 点击此处直接下载 &#…

CSS学习|css三种导入方式、基本选择器、层次选择器、结构伪类选择器、属性选择器、字体样式、文本样式

第一个css程序 css程序都是在style标签中书写 打开该网页&#xff0c;可以看到h1标签中的我是标题被渲染成了红色 可以在同级目录下创建一个css目录&#xff0c;专门存放css文件&#xff0c;可以和html分开编写 然后在html页面中&#xff0c;利用link标签以及css文件地址&…

2024年6月8日 (周六) 叶子游戏新闻

万能嗅探: 实测 网页打开 某视频号、某音、某红薯、某站&#xff0c;可以做到无水印的视频和封面下载功能哦&#xff0c;具体玩法大家自行发挥吧。 《丝之歌》粉丝又要失望&#xff1a;大概率不会亮相Xbox发布会即将于后天举行的 Xbox 发布会预计将会有许多令人兴奋的消息。早些…

使用Ollama+OpenWebUI部署和使用Phi-3微软AI大模型完整指南

&#x1f3e1;作者主页&#xff1a; 点击&#xff01; &#x1f916;AI大模型部署与应用专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年6月6日23点50分 &#x1f004;️文章质量&#xff1a;96分 欢迎来到Phi-3模型的奇妙世界&#xff01;Phi-3是由微软…

Vue学习|Vue快速入门、常用指令、生命周期、Ajax、Axios

什么是Vue? Vue 是一套前端框架&#xff0c;免除原生JavaScript中的DOM操作&#xff0c;简化书写 基于MVVM(Model-View-ViewModel)思想&#xff0c;实现数据的双向绑定&#xff0c;将编程的关注点放在数据上。官网:https://v2.cn.vuejs.org/ Vue快速入门 打开页面&#xff0…

Cinema 4D 2024 软件安装教程、附安装包下载

Cinema 4D 2024 Cinema 4D&#xff08;C4D&#xff09;是一款由Maxon开发的三维建模、动画和渲染软件&#xff0c;广泛用于电影制作、广告、游戏开发、视觉效果等领域。Cinema 4D允许用户创建复杂的三维模型&#xff0c;包括角色、场景、物体等。它提供了多种建模工具&#x…

调研管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;基础数据管理&#xff0c;教师类型管理&#xff0c;课程类型管理&#xff0c;公告类型管理 前台账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;论坛&#…