鸿蒙HarmonyOS-ArkTS项目实战【包括UI(tabbar切换、轮播器、金刚区、列表滑动)、网络请求、数据封装、路由跳转、webview加载、下拉刷新refresh】

华为模拟器截图-掌盟项目

本文主要介绍一个华为鸿蒙系统(HarmonyOS)使用ArkTS语言开发的实战项目,需要有一定的ArkTS基础。

声明:项目中展示数据皆为抓包获取,仅用于项目练手,无商业行为。

一、项目结构

项目结构

1、图中1是项目源码

  • common中是通用代码,包括网络请求networking、webview;
  • entryability里面EntryAbility.ets 可以设置项目入口。比如将原入口 pages/index 改为自定义入口 pages/Tabbar/TabsPage:
    windowStage.loadContent('pages/Tabbar/TabsPage', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
  • Images 是项目要用到的本地图片,是自定义的目录。引用方式:
Image('Images/home/home_search@3x.png')

也可以放在系统图片目录下 src/main/resources/base/media/home_search.png,引用方式:

 Image($r('app.media.home_search'))
  • pages下是项目页面代码,其中home、mall和mine是三个tabbar的控制器。tabbar下的TabsPage.ets是项目自定义的入口。
  • 项目默认的入口页面是Index.ets,即入口为pages/index,如果要使用华为模拟器运行项目,一定要在EntryAbility.ets中修改入口配置为 pages/tabbar/TabsPage。

2、图中2是项目配置

  • entry/src/main/resources/base/element/string.json 中可以设置项目名称(包括中文名和英文名)
  • entry/src/main/resources/base/media 中可以设置项目图标、启动图

二、UI页面

1、tabbar/TabsPage.ets

一般tabbar都会作为项目的启动页面,TabsPage中使用的是Tabs控件,里面只可以使用子组件TabContent,并且子组件TabContent可以自定义每个tabBar,即 TabBuilder。

TabsPage页面源码:
import {QQHomeController} from "../home/Controller/QQHomeController"
import {QQMallController} from  "../mall/Controller/QQMallController"
import {QQMineController} from "../mine/Controller/QQMineController"

@Entry
@Component
struct TabsPage {
  @State currentIndex: number = 0;
  private tabsController: TabsController = new TabsController()

  @Builder TabBuilder(title:string, targetIndex:number, normalImg:string, selectedImg:string) {
    Column() {
      Image(this.currentIndex == targetIndex ? selectedImg : normalImg)
        .width(24)
        .height(24)
      Text(title)
        .fontSize(10)
        .margin({top:5})
        .fontColor(this.currentIndex == targetIndex ? '#0F1114' : '#565D66')
    }
    .backgroundColor('#ffffff')
    .width('100%')
    .height(60)
    .justifyContent(FlexAlign.Center)
    .onClick(()=>{
      this.currentIndex = targetIndex
      this.tabsController.changeIndex(this.currentIndex)
    })
  }

  build() {
    RelativeContainer() {
      Column() {
        Tabs({barPosition:BarPosition.End, controller:this.tabsController, index:0}) {
          TabContent() {
            QQHomeController()
          }.tabBar(this.TabBuilder('首页', 0, 'Images/tabbar/tabbar_home_normal.png','Images/tabbar/tabbar_home_select.png'))
          TabContent() {
            QQMallController()
          }.tabBar(this.TabBuilder('商城', 1, 'Images/tabbar/tabbar_mall_normal.png','Images/tabbar/tabbar_mall_select.png'))
          TabContent() {
            QQMineController()
          }.tabBar(this.TabBuilder('我的', 2, 'Images/tabbar/tabbar_mine_normal.png','Images/tabbar/tabbar_mine_select.png'))
        }
      }
    }
    .height('100%')
    .width('100%')
  }
}

2、QQHomeController

QQHomeController 作为首页,里面使用了一些常用的控件,比如:

  • TabSegmentPage 是自定义的 Segment 控制器,里面使用了控件Tabs(Tabs可以通过设置位置barPosition: BarPosition.Start放在顶部,底部和侧边栏来实现不同的效果)来实现的;
TabSegmentPage.tes 源码:
import { QQChannelModel } from '../../Model/QQChannelModel'

@Entry
@Component
export struct TabSegmentPage {
  @State itemWidth: number = 50
  @State currentIndex: number = 0
  @State channelArray:Array<QQChannelModel> = Array<QQChannelModel>()

  @Builder TabBuilder(index: number, name: string) {
    Column() {
      Text(name)
        .fontColor(this.currentIndex === index ? '#161616' : '#868c8d')
        .fontSize(this.currentIndex === index ? 18 : 16)
        .fontWeight(this.currentIndex === index ? 500 : 400)
        .lineHeight(18)
        .margin({top: 10, bottom:5})
      Divider()
        .width(16)
        .strokeWidth(2)
        .color('#202020')
        .opacity(this.currentIndex === index ? 1 : 0)
    }
    // .width('100%')
    .width(this.itemWidth)
    .height('100%')
  }

  build() {
    RelativeContainer() {
      Column() {
        Tabs({ barPosition: BarPosition.Start}) {
          if (this.channelArray.length > 0) {
            ForEach(this.channelArray, (item:QQChannelModel, index) => {
              TabContent() {

              }
              .tabBar(this.TabBuilder(index, item.name ?? ''))
              .margin(0)
            })
          }
        }
        // .width('100%')
        .height('100%')
        // .barWidth('100%')
        .barWidth(this.itemWidth*this.channelArray.length)
        .align(Alignment.Start)
        .vertical(false)
        .scrollable(false)
        .barPosition(BarPosition.Start)
        .barMode(BarMode.Fixed)
        .animationDuration(300)
        .onChange((index:number) => {
          this.currentIndex = index
          console.log('TabSegmentPage index = ',index)
        })
      }
    }
    .height('100%')
    .width('100%')
  }
}

QQHomeController 中使用方式:

  TabSegmentPage({
    channelArray:this.channelArray
  })
  .width(240)
  .height(44)
  .backgroundColor(Color.White)
  • Swiper 轮播器,使用起来极其方便,因为实现了第一张和最后一张的轮播效果;
      Swiper() {
        ForEach(this.bannerListArray, (item:QQBannerBody, index) => {
          Image(item.imgUrl)
            .onClick(() => {
              router.pushUrl({
                url:'common/webview/QQWebviewController',
                params:{
                  url:item.intent
                }
              })
            })
        })
      }
      .width('100%')
      .height(150)
      .loop(true)
      .autoPlay(true)
      .interval(3000)
      .indicator(Indicator.dot()
        .itemWidth(10)
        .itemHeight(2)
        .selectedItemWidth(15)
        .selectedItemHeight(2)
        .color('#918c8e')
        .selectedColor(Color.White))
  • Grid 和 GridItem 来实现金刚区效果;
      Grid() {
        ForEach(this.iconListArray, (item:QQIconBody, index) => {
          GridItem() {
            Column({space:10}) {
              Image(item.iconUrl)
                .width(55)
                .width(55)
              Text(item.name)
                .fontSize(10)
                .fontColor('#939999')
            }
            .onClick(() => {
              router.pushUrl({
                url:'common/webview/QQWebviewController',
                params:{
                  url:item.intent
                }
              })
            })
          }
          .width('20%')
          .height(80)
        })
      }
      .width('100%')
      .backgroundColor(Color.White)
  • List 和 ListItem 来实现列表,还可以通过加入ListItemGroup来实现分组效果。
          List() {
            ListItemGroup({
              header:this.CustomHeader
            })
            ForEach(this.infoListArray, (item:QQInfoListFeedsInfo, index) => {
              ListItem() {
                // 列表item
                QQInfoItemPage({
                  body:item.feedNews?.body,
                  footer:item.feedNews?.footer
                })
                  .backgroundColor(Color.White)
              }
              .width('100%')
              .height(100)
              .onClick(() => {
                router.pushUrl({
                  url:'common/webview/QQWebviewController',
                  params:{
                    url:''
                  }
                })
              })
            })
          }
          .width('100%')
          .height('100%')

在 ListItemGroup 中还可以添加 header 和 footer ?!居亚樘崾荆嚎梢酝üゲ磕谌荼热缏植テ骱徒鸶涨尤氲絃istItemGroup的header中来实现整体的滑动效果】

ListItemGroup({
  header:this.CustomHeader()
})
  • 页面的生命周期
  // 生命周期
  aboutToAppear() {
    console.log('1 aboutToAppear')
  }

  aboutToDisappear() {
    console.log('2 aboutToDisappear')
  }

  aboutToReuse() {
    console.log('3 aboutToReuse')
  }

  onPageShow() {
    console.log('4 onPageShow')
  }

  onPageHide() {
    console.log('5 onPageHide')
  }

  onDidBuild() {
    console.log('6 onDidBuild')
  }

三、网络请求

导入头文件 import http from '@ohos.net.http',
通过http封装post和get请求。

里面需要注意的几点:
  • 请求header的类型为 Record<string, string>
let header:Record<string, string> = {
  'Cookie': 'tgw_l7_route=0f7eb4ab2f1d32df0ff24f07ba0cf8db; clientType=10; accountType=255',
  'qimei': '4f1c8ff7-4677-4c0a-9ac3-831c9dd865df',
  'accept': '*/*',
  'accept-encoding': 'gzip, deflate, br',
  'Content-Type': 'application/json',
  'user-agent': 'QTL/9.2.5 (iPhone; IOS 18.0; Scale/3.00)',
  'connection': 'keep-alive',
  'gh-header': '1-2-105-925-0',
  'subchannel': '1',
  'accept-language': 'zh-Hans-CN;q=1, zh-Hant-MO;q=0.9, en-CN;q=0.8',
};
  • 预计请求生成数据可以设置三种STRING、OBJECT、ARRAY_BUFFER,默认为STRING
// STRING
expectDataType:http.HttpDataType.STRING
// OBJECT
expectDataType:http.HttpDataType.OBJECT
// ARRAY_BUFFER
expectDataType:http.HttpDataType.ARRAY_BUFFER
QQNetworkRequest.ets 中通过http封装post和get请求的源码为:
import http from '@ohos.net.http'
import { JSON } from '@kit.ArkTS';

let header:Record<string, string> = {
  'Cookie': 'tgw_l7_route=0f7eb4ab2f1d32df0ff24f07ba0cf8db; clientType=10; accountType=255',
  'qimei': '4f1c8ff7-4677-4c0a-9ac3-831c9dd865df',
  'accept': '*/*',
  'accept-encoding': 'gzip, deflate, br',
  'Content-Type': 'application/json',
  'user-agent': 'QTL/9.2.5 (iPhone; IOS 18.0; Scale/3.00)',
  'connection': 'keep-alive',
  'gh-header': '1-2-105-925-0',
  'subchannel': '1',
  'accept-language': 'zh-Hans-CN;q=1, zh-Hant-MO;q=0.9, en-CN;q=0.8',
};

// post
export function postRequest(url:string, param:Object, success:(str:string)=>void, fail:(error:Error)=>void) {
  let httpRequest = http.createHttp()
  let reponseResult = httpRequest.request(url, {
    method: http.RequestMethod.POST,
    readTimeout:60000,
    connectTimeout:60000,
    header: header,
    extraData: param,
    expectDataType:http.HttpDataType.STRING
  }, (error, data) => {
    if (!error) {
      success(data.result.toString())
      // data.result为HTTP响应内容,可根据业务需要进行解析
      console.info('Networking ====================================');
      console.info('Networking Url:' + url);
      console.info('Networking Result 类型:' + typeof data.result);
      // console.info('Result 类型:' + typeof JSON.stringify(data.result));
      console.info('Networking Result:' + data.result);
      console.info('Networking code:' + data.responseCode);
      // data.header为HTTP响应头,可根据业务需要进行解析
      console.info('Networking header:' + JSON.stringify(data.header));
      console.info('Networking cookies:' + data.cookies); // 8+
      console.info('Networking ====================================');
    } else {
      fail(error)
      console.info('Networking error:' + JSON.stringify(error));
      // 当该请求使用完毕时,调用destroy方法主动销毁。
      httpRequest.destroy();
    }
  })
}

// get
export function getRequest(url:string, param:Object, success:(str:string)=>void, fail:(error:Error)=>void) {
  let httpRequest = http.createHttp()
  let reponseResult = httpRequest.request(url, {
    method: http.RequestMethod.GET,
    readTimeout:60000,
    connectTimeout:60000,
    header: header,
    extraData: param,
    expectDataType:http.HttpDataType.STRING
  }, (error, data) => {
    if (!error) {
      success(data.result.toString())
      // data.result为HTTP响应内容,可根据业务需要进行解析
      console.info('Networking ====================================');
      console.info('Networking Url:' + url);
      console.info('Networking Result 类型:' + typeof data.result);
      // console.info('Result 类型:' + typeof JSON.stringify(data.result));
      console.info('Networking Result:' + data.result);
      console.info('Networking code:' + data.responseCode);
      // data.header为HTTP响应头,可根据业务需要进行解析
      console.info('Networking header:' + JSON.stringify(data.header));
      console.info('Networking cookies:' + data.cookies); // 8+
      console.info('Networking ====================================');
    } else {
      fail(error)
      console.info('error:' + JSON.stringify(error));
      // 当该请求使用完毕时,调用destroy方法主动销毁。
      httpRequest.destroy();
    }
  })
}

export function getFullUrl(url: string): string {
  return "https://mlol.qt.qq.com" + url
}

四、数据封装

对于网络请求获取的json数据,可以通过方法 JSON.parse(jsonStr) 来转换成对应的数据模型进行使用。

比如网络获取首页banner的json数据为:
{"code":0,"data":{"result":0,"next":"0","feedsInfo":[{"feedBase":{"layoutType":"300","contentType":"300","contentId":"plat_banner_plat","intent":"","position":0,"priority":10},"feedNews":{"body":[{"enableWholeBannerClick":true,"isAmsAd":false,"imgUrl":"https://mlol-75948.qpic.cn/common/90376f2042fa865cb6a08a3e4cb43c28.jpg","bigImgUrl":"","intent":"https://lol.qq.com/act/a20240926t1orianna/index.html?exchangeType=1\u0026autoRefreshCookie=1\u0026page=1\u0026qd=true\u0026zmGameId=tft\u0026e_code=508034","title":"全球总决赛限定T1小小奥莉安娜","taskName":"全球总决赛限定T1小小奥莉安娜","contentId":"mlol-46","isVideo":false,"vid":"","reservedDesc":"","packageName":"","universalLink":"","algorithmInfo":{"adid":"4518","actionID":"4518","fname":"全球总决赛限定T1小小奥莉安娜","bannerId":"9","ecode":"508034","from":"mlol","clickEventId":"61505","expoEventId":"61504","gamecode":"tft","url":"https://lol.qq.com/act/a20240926t1orianna/index.html?exchangeType=1\u0026autoRefreshCookie=1\u0026page=1\u0026qd=true\u0026zmGameId=tft"},"extend":{"roomid":""},"commonInfo":{"cover":"https://mlol-75948.qpic.cn/common/90376f2042fa865cb6a08a3e4cb43c28.jpg","type":"image"}},{"enableWholeBannerClick":true,"isAmsAd":false,"imgUrl":"https://mlol-75948.qpic.cn/common/5a111a691f58c17451ef00df16f9fd0c.jpg","bigImgUrl":"","intent":"https://jcc.qq.com/cp/a20240913eo8xcf/index.html?zmGameId=jgame\u0026exchangeType=1\u0026autoRefreshCookie=1\u0026e_code=508035","title":"符文大陆焕新回归 体验抽阿狸雕塑","taskName":"符文大陆焕新回归 体验抽阿狸雕塑","contentId":"mlol-45","isVideo":false,"vid":"","reservedDesc":"","packageName":"","universalLink":"","algorithmInfo":{"adid":"4519","actionID":"4519","fname":"符文大陆焕新回归 体验抽阿狸雕塑","bannerId":"9","ecode":"508035","from":"mlol","clickEventId":"61505","expoEventId":"61504","gamecode":"jgame","url":"https://jcc.qq.com/cp/a20240913eo8xcf/index.html?zmGameId=jgame\u0026exchangeType=1\u0026autoRefreshCookie=1"},"extend":{"roomid":""},"commonInfo":{"cover":"https://mlol-75948.qpic.cn/common/5a111a691f58c17451ef00df16f9fd0c.jpg","type":"image"}},{"enableWholeBannerClick":true,"isAmsAd":false,"imgUrl":"https://mlol-75948.qpic.cn/common/2caa90261d7336313157de60dbf5d9d8.jpg","bigImgUrl":"","intent":"https://lol.qq.com/act/a20240926worldspass/index.html?exchangeType=1\u0026autoRefreshCookie=1\u0026page=1\u0026qd=true\u0026zmGameId=lol\u0026e_code=508036","title":"全球总决赛2024通行证上线","taskName":"全球总决赛2024通行证上线","contentId":"mlol-44","isVideo":false,"vid":"","reservedDesc":"","packageName":"","universalLink":"","memo":"解锁通行证赢取至臻 魔域梦魇 泽丽","algorithmInfo":{"adid":"4520","actionID":"4520","fname":"全球总决赛2024通行证上线","bannerId":"9","ecode":"508036","from":"mlol","clickEventId":"61505","expoEventId":"61504","gamecode":"lol","url":"https://lol.qq.com/act/a20240926worldspass/index.html?exchangeType=1\u0026autoRefreshCookie=1\u0026page=1\u0026qd=true\u0026zmGameId=lol"},"extend":{"roomid":""},"commonInfo":{"cover":"https://mlol-75948.qpic.cn/common/2caa90261d7336313157de60dbf5d9d8.jpg","type":"image"}},{"enableWholeBannerClick":true,"isAmsAd":false,"imgUrl":"https://mlol-75948.qpic.cn/common/8af1b425eeb5c79dbd95558e3595c2b3.png","bigImgUrl":"","intent":"https://mlol.qt.qq.com/go/mlol_news/varcache_article?docid=8961997516292572552\u0026gameid=166\u0026webview=cc\u0026e_code=508037","title":"lolm英雄决斗场","taskName":"lolm英雄决斗场","contentId":"mlol-43","isVideo":false,"vid":"","reservedDesc":"","packageName":"","universalLink":"","algorithmInfo":{"adid":"4516","actionID":"4516","fname":"lolm英雄决斗场","bannerId":"9","ecode":"508037","from":"mlol","clickEventId":"61505","expoEventId":"61504","gamecode":"lgame","url":"https://mlol.qt.qq.com/go/mlol_news/varcache_article?docid=8961997516292572552\u0026gameid=166\u0026webview=cc"},"extend":{"roomid":""},"commonInfo":{"cover":"https://mlol-75948.qpic.cn/common/8af1b425eeb5c79dbd955
我们创建对应的数据模型类 QQBannerModel.ets

export class QQBannerModel {
  code?: number
  data?: QQBannerData
  msg?: string
  result?: number
}

export class QQBannerData {
  result?: number
  next?: string
  feedsInfo?: Array<QQBannerFeedsInfo>
  attach?: object
  scope?: string
  distance?: number
}

export class QQBannerFeedsInfo {
  feedBase?: QQBannerFeedBase
  feedNews?: QQBannerFeedNews
}

export class QQBannerFeedNews {
  body?: Array<QQBannerBody>
}

export class QQBannerBody {
  enableWholeBannerClick?: boolean
  isAmsAd?: boolean
  imgUrl?: string
  bigImgUrl?: string
  intent?: string
  title?: string
  taskName?: string
  contentId?: string
  isVideo?: boolean
  vid?: string
  reservedDesc?: string
  packageName?: string
  universalLink?: string
  algorithmInfo?: QQBannerAlgorithmInfo
  extend?: QQBannerExtend
  commonInfo?: QQBannerCommonInfo
}

export class QQBannerCommonInfo {
  cover?: string
  type?: string
}

export class QQBannerExtend {
  roomid?: string
}

export class QQBannerAlgorithmInfo {
  adid?: string
  actionID?: string
  fname?: string
  bannerId?: string
  ecode?: string
  from?: string
  clickEventId?: string
  expoEventId?: string
  gamecode?: string
  url?: string
}

export class QQBannerFeedBase {
  layoutType?: string
  contentType?: string
  contentId?: string
  intent?: string
  position?: number
  priority?: number
}
推荐一个ArkTS的json转model的一个在线工具,将json放在左边,一键转换。

可以将其中生成的数组类型修改下形式,比如 string[] 修改成 Array<sring>,使用起来更加方便。
鸿蒙json转对象

在网络请求获取json的方法中写入以下代码,就可以获取到具体的数据进行渲染了。
let model:QQBannerModel = JSON.parse(str)

网络请求示例:

  // 请求banner数据
  loadHomeRequestBannerData() {
    this.viewModel.loadHomeRequestBannerData((str) => {
      console.log('home Banner 数据',str)
      let model:QQBannerModel = JSON.parse(str)
      // 刷新数据
      let dataModel:QQBannerData = model.data as QQBannerData
      let feedsInfoArray = dataModel.feedsInfo as Array<QQBannerFeedsInfo>
      if (feedsInfoArray.length > 0) {
        let feedsInfo = feedsInfoArray[0]
        let feedNews = feedsInfo.feedNews
        this.bannerListArray = feedNews?.body as Array<QQBannerBody>
      }
      console.log('home Banner 数据 .data ',JSON.parse(str))
    }, (error) => {
      console.log('home Banner 数据 error ',error)
    })
  }

五、路由跳转

路由跳转就是一个页面跳转另一个页面了,并可以携带参数。

比如首页 QQHomeController 跳转 webview页面QQWebviewController,并携带参数url:

QQHomeController 中点击响应方法中写入路由跳转:

router.pushUrl({
  url:'common/webview/QQWebviewController',
  params:{
    url:'https://www.baidu.com'
  }
})

QQWebviewController 中返回按钮方法中写入路由返回方法:

router.back()

对于跳转携带的参数url,可以在QQWebviewController中的生命周期方法中,用以下方式进行接收:

  aboutToAppear(): void {
    const param = router.getParams() as Map<string, string>
    this.url = param['url']
  }

六、webview加载

使用Web控件进行加载url,并与对应的控制器WebviewController进行绑定。

具体QQWebviewController.ets源码:
import { webview } from '@kit.ArkWeb'
import { router } from '@kit.ArkUI'
import { JSON } from '@kit.ArkTS'

@Entry
@Component
struct QQWebviewController {
  @State url:string = ''
  controller:WebviewController = new webview.WebviewController()

  aboutToAppear(): void {
    console.log(JSON.stringify(router.getParams()))
    const param = router.getParams() as Map<string, string>
    console.log('wxqtodo url =', param['url'])
    this.url = param['url']
  }

  build() {
    RelativeContainer() {
      Column() {
        Row() {
          Button({type:ButtonType.Normal})
            .width(36)
            .height(36)
            .backgroundImage('Images/common/common_back_icon@3x.png')
            .backgroundColor(Color.Transparent)
            .backgroundImageSize(ImageSize.Contain)
            .margin({
              left:20
            })
            .onClick(() => {
              router.back()
            })
        }
        .width('100%')
        .height(44)
        .justifyContent(FlexAlign.Start)
        Web({
          src: this.url,
          controller: this.controller
        })
      }
    }
    .height('100%')
    .width('100%')
  }
}

七、三方库的接入- refresh下拉刷新和上拉加载

引入三方控件 refresh

ohpm install @abner/refresh

输出以下信息则表示引入成功:

└─[0] <git:(main ba34beb?) > ohpm install @abner/refresh
ohpm INFO: MetaDataFetcher fetching meta info of package '@abner/refresh' from https://ohpm.openharmony.cn/ohpm/
ohpm INFO: fetch meta info of package '@abner/refresh' success https://ohpm.openharmony.cn/ohpm/@abner/refresh
install completed in 0s 177ms

refresh代码使用

在需要刷新的page页面引入头文件:

import { ListView, RefreshController, RefreshLayoutStatusModel, LoadMoreLayoutStatusModel } from '@abner/refresh'

声明一个刷新的控制器:

// 刷新控件
controller: RefreshController = new RefreshController() //刷新控制器,声明全局变量
在要刷新的地方添加refresh自带的刷新控件 ListView(即将系统的滑动控件,比如列表控件List、网格控件Grid等替换成 refresh自带的列表ListView、 网格GridView、瀑布流StaggeredGridView等控件),同时去设置以下信息:
  • items:【数据源】如果是纯列表,可以将请求获取的列表数据赋值到items(比如items: this.sourceArray),并在下一个参数itemLayout中去获取每一个item的信息。如果滑动列表包括一些头header,比如 header中有banner要跟着一起滑动,这时候可以将items设置成[1],具体到* * * * itemLayout里面就可以使用全部自定义page了。
  • itemLayout:自定义每个item。如果items是列表数据源,则 itemLayout 中只需要自定义每个item信息即可。如果将items设置成[1],则itemLayout可以自定义整个List视图。
  • controller:即声明的刷新控制器 controller: RefreshController。
  • onRefresh:下拉刷新的回调方法。
  • onLoadMore:上拉加载的回调方法。
  • headerRefreshLayout:自定义的刷新header。不写即使用默认的刷新header。
  • footerLoadLayout:自定义刷新footer。不写即使用默认的刷新footer。
ListView({
  items: [1], //数据源 数组,任意类型
  itemLayout: (item, index) => this.itemLayout(item, index), //条目布局
  controller: this.controller, //控制器,负责关闭下拉和上拉
  isLazyData: false,//禁止懒加载,也就是使用ForEach进行数据加载
  headerRefreshLayout:(model: RefreshLayoutStatusModel) => this.refreshHeader(model),
  footerLoadLayout:(model: LoadMoreLayoutStatusModel) => this.loadFooter(model),
  onRefresh: () => {
    //下拉刷新
    this.loadRequestData()
  },
  onLoadMore: () => {
    //上拉加载
    this.loadRequestData()
  }
})
  .width('100%')
  .height('100%')

自定义的 itemLayout

@Builder
itemLayout(item: Object, index: number): void {
  // 列表
  List() {
    ListItemGroup({
      header:this.CustomHeader
    })
    ForEach(this.infoListArray, (item:QQInfoListFeedsInfo, index) => {
      ListItem() {
        // 列表item
        QQInfoItemPage({
          body:item.feedNews?.body,
          footer:item.feedNews?.footer
        })
          .backgroundColor(Color.White)
      }
      .width('100%')
      .height(100)
      .onClick(() => {
        router.pushUrl({
          url:'common/webview/QQWebviewController',
          params:{
            url:''
          }
        })
      })
    })
  }
  .width('100%')
  .height('100%')
}

自定义刷新header和footer

只需要在ListView 中实现自定义的header和footer即可。即 headerRefreshLayout 和 footerLoadLayout。

headerRefreshLayout:(model: RefreshLayoutStatusModel) => this.refreshHeader(model),
footerLoadLayout:(model: LoadMoreLayoutStatusModel) => this.loadFooter(model),

自定义的header和footer:

@Builder
refreshHeader(model: RefreshLayoutStatusModel): void {
  Text("refreshHeader 当前状态:" + model.status)
    .width("100%")
    .textAlign(TextAlign.Center)
    .height(80)
    .backgroundColor(Color.Pink)
}

@Builder
loadFooter(model: LoadMoreLayoutStatusModel) {
  Text("loadFooter 当前状态:" + model.status)
    .width("100%")
    .textAlign(TextAlign.Center)
    .height(80)
    .backgroundColor(Color.Pink)
}

在网络请求结束的地方加上结束刷新的方法:

// 结束刷新
this.controller.finishRefresh() //关闭下拉刷新,在数据请求回后进行关闭
this.controller.finishLoadMore() //关闭上拉加载,在数据请求回后进行关闭

具体refresh的接入和使用请参考文章:鸿蒙HarmonyOS三方库刷新控件refresh的接入和使用

八、华为模拟器的使用

正常在开发中运行某个页面,我们都是使用预览器,可以实时看某一个页面的效果,比较方便。但是预览器只能在pages页面进行使用。使用华为模拟器就可以一键运行整个项目,效果就比较好。

注意:使用模拟器需要提前进行申请并下载

具体的华为模拟器申请可以参考:华为模拟器申请
模拟器使用中遇见问题可以参考:鸿蒙系统HarmonyOS-ArkTS项目开发问题汇总


【未完待续】

项目实现效果参考:华为模拟器录屏
项目源码:HMApp_ArkTS

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容