美文网首页
鸿蒙HarmonyOS-ArkTS项目实战【包括UI(tabba

鸿蒙HarmonyOS-ArkTS项目实战【包括UI(tabba

作者: wuyukobe | 来源:发表于2024-09-27 17:38 被阅读0次
    华为模拟器截图-掌盟项目

    本文主要介绍一个华为鸿蒙系统(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 。【友情提示:可以通过将顶部内容比如轮播器和金刚区加入到ListItemGroup的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%')
      }
    }
    

    七、华为模拟器的使用

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

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

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


    【未完待续】

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

    相关文章

      网友评论

          本文标题:鸿蒙HarmonyOS-ArkTS项目实战【包括UI(tabba

          本文链接:https://www.haomeiwen.com/subject/rfnxrjtx.html