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/news/850233.shtml

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

相关文章

Spark 性能调优——分布式计算

前言 分布式计算的精髓&#xff0c;在于如何把抽象的计算流图&#xff0c;转化为实实在在的分布式计算任务&#xff0c;然后以并行计算的方式交付执行。今天这一讲&#xff0c;我们就来聊一聊&#xff0c;Spark 是如何实现分布式计算的。分布式计算的实现&#xff0c;离不开两个…

2024 年最新 Python 基于百度智能云实现短语音识别详细教程

百度智能云语音识别 采用国际领先的流式端到端语音语言一体化建模算法&#xff0c;将语音快速准确识别为文字&#xff0c;支持手机应用语音交互、语音内容分析、机器人对话等场景。百度短语音识别可以将 60 秒以下的音频识别为文字。适用于语音对话、语音控制、语音输入等场景…

【kubernetes】k8s集群中的ingress(对外服务)规则详解

目录 一、Ingress 简介 1.1service的作用 1.2外部访问方案 (四种&#xff09;&#x1f339;&#x1f339;&#x1f339; 部署externalIPs 1.3Ingress 是什么 二、Ingress 组成&#x1f339;&#x1f339;&#x1f339; 三、Ingress 工作原理&#x1f431;&#x1f…

STM32F103C8T6基于HAL库完成uC/OS-III多任务程序

一、在STM32CubeMX中建立工程 配置RCC 配置SYS 配置PC13为GPIO_Output 配置USART1 生成代码 二、获取uC/OS-III源码 官网下载地址&#xff1a;Micrium Software and Documentation - Silicon Labs 网盘下载&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;lzjl 三、复…

【QT5】<应用> 小游戏:贪吃蛇

文章目录 一、项目要求 二、需求分析 三、实现效果 四、代码 一、项目要求 【1】主要实现&#xff1a;游戏界面存在一条蛇&#x1f40d;&#xff0c;使用键盘wsad或者↑↓←→键盘可以控制蛇的行走方向。同时界面中会随机出现食物&#xff0c;蛇可以吃食物&#xff0c;然后…

近期面试HW中级蓝问题(非常详细)零基础入门到精通,收藏这一篇就够了

01 — HW问题 1.sqlmap拿shell的原理&#xff0c;需要什么条件&#xff0c;–os-shell的原理 2.冰蝎的流量特征 3.哥斯拉的流量特征 4.如果判断一个web是s2写的 5.fastjson了解嘛&#xff1f;Log4j了解嘛&#xff1f;如何在流量中发现Log4j的攻击特征 6.HW前的准备工作…

微信小程序基础工作模板

1.轮播图 点击跳转官方文档 简单例子 <!-- 顶部轮播图 --> <swiper indicator-dots"true" class"banner" autoplay"true" interval"2000"><swiper-item><image src"../../images/轮播图1.jpg" >…

【JMeter接口测试工具】第二节.JMeter基本功能介绍(下)【入门篇】

文章目录 前言八、Jmeter常用逻辑控制器 8.1 如果&#xff08;if&#xff09;控制器 8.2 循环控制器 8.3 ForEach控制器九、Jmeter关联 9.1 正则表达式提取器 9.2 xpath提取器 9.3 JSON提取器十、跨越线程组传值 10.1 高并发 10.2 高频…

关于修改Python中pip默认安装路径的终极方法

别想了&#xff0c;终极方法就是手动复制&#xff0c;不过我可以给你参考一下手动复制的方法 关于手动移动pip安装包的方法 别想了&#xff0c;终极方法就是手动复制&#xff0c;不过我可以给你参考一下手动复制的方法一、首先确认一下pip默认安装路径二、再确认一下需要移动到…

信号:干扰类别及特征提取(二)

目录 第二部分&#xff1a;特征提取 一&#xff1a;瞬时特征参数 1.零中心归一化瞬时幅度之谱密度的最大值 2.非弱信号段零中心归一化瞬时幅度的标准偏差 3.零中心归一化瞬时幅度绝对值的标准偏差 4.零中心归一化非弱信号段瞬时频率的标准偏差 5.零中心归一化非弱信号段…

Codeforces Round 951 (Div. 2)

A - Guess the Maximum 直接暴力枚举 a i , a i 1 a_i,a_{i1} ai​,ai1​找最小的最大值 答案即为最小的最大值-1 code: #include<bits/stdc.h> #define endl \n #define fast() ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr) #define F first #…

1118 如需挪车请致电(测试点4)

solution 测试点4&#xff1a;直接给出数字&#xff0c;即零个运算符的情况 #include<iostream> #include<string> #include<map> #include<cmath> using namespace std; int main(){string s, ans "";map<string, int> mp {{&quo…

用HAL库改写江科大的stm32入门-输入捕获原理图示

原理与接线&#xff1a; &#xff08;输入捕获的结构&#xff09; cubeMx: PA11&#xff1a;

OpenCV学习(4.7) Canndy边缘检测

1.目标 在本章中&#xff0c;我们将了解 Canny 边缘检测的概念OpenCV 的功能&#xff1a; cv.Canny&#xff08;&#xff09; Canny边缘检测是一种经典的边缘检测算法&#xff0c;由John F. Canny在1986年提出。Canny算法的目标是找到图像中真正的边缘&#xff0c;同时尽可能…

【旅行】关于毕业旅行与长期旅行计划(城市、攻略、预算、交通、面基等)

【旅行】关于毕业旅行与长期旅行计划&#xff08;城市、攻略、预算、交通、面基等&#xff09; 文章目录 一、目的地与去哪儿玩1、可能2、人民币3、国家地理4、省份与城市5、环球旅行 二、攻略之怎么玩&#xff08;旅行预算、攻略&#xff09;1、旅行预算之交通、住宿、门票等2…

【Autopilot】没有自动添加本地管理员的问题处理

【问题】某公司选用了D记的笔记本电脑&#xff0c;约定出厂就预配置好Autopilot&#xff0c;当时向D记提供了三个信息&#xff1a; 1. M365的租户ID 2. 公司域名信息 3. Group Tag (某公司为跨国公司&#xff0c;通过Group Tag来区分国家&#xff0c;比如CHN-中国&#xff0c;L…

【python】OpenCV GUI——Mouse(14.1)

参考学习来自 文章目录 背景知识cv2.setMouseCallback 介绍小试牛刀 背景知识 GUI&#xff08;Graphical User Interface&#xff0c;图形用户界面&#xff09; 是一种允许用户通过图形元素&#xff08;如窗口、图标、菜单和按钮&#xff09;与电子设备进行交互的界面。与传统…

四种跨域解决方案

文章目录 1.引出跨域1.基本介绍2.具体演示1.启动之前学习过的springboot-furn项目2.浏览器直接访问 [localhost:8081/furns](http://localhost:8081/furns) 可以显示信息3.启动前端项目&#xff0c;取消请求拦截器&#xff0c;这样设置&#xff0c;就会出现跨域4.跨域原因 2.跨…

Intel VT-x怎么开启?如何解决VMware打开虚拟机报错问题?

许多小伙伴在安装完VMware不能打开虚拟机&#xff0c;每次打开都会出现一个“此主机支持 Intel VT-x&#xff0c;但 Intel VT-x 处于禁用状态”的报错&#xff0c;然后因此启动不了虚拟机。今天小编就带来如何解决这个报错的方法。 什么是Intel VT-x&#xff1f; 这是英特尔cp…

玩转ChatGPT:最全学术论文提示词分享【中】

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 本篇文章&#xff0c;我们继续为大家分享「最全学术论文提示词【中】」。上篇文章的内容请到文末链接处跳转&#x1f447;&#x1f3fb; 6.数据分析 prompt 1&#xff1a;分析[定量/定…