#夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局 原创 精华

Buty9147
发布于 2022-6-15 01:59
浏览
4收藏

用OpenHarmony eTS 实现一个Huawei app标准布局

本文正在参加星光计划3.0 – 夏日挑战赛

@TOC

1.介绍

Huawei 的app,我们都能看得出来是用心设计过的,值得学习。如果我们仔细去看Huawei 手机自带的app,我们会发现所有的app,无论是什么类型的app,其布局结构都是一种类似的结构,这说明这种布局结构的用途真的可以很广泛,而且体验很好…

像华为的应用市场app、联系人、浏览器、图库、智慧生活app、音乐app、我的华为、畅连 等等,你去看,全部是这种上中下的布局结构,顶部栏(top+middle)+内容展示区(content)+底部tab栏,那么,今天我们就一起来实现一个这样的布局。

2.效果展示

DAYU200真机
#夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局-鸿蒙开发者社区

视频地址
https://ost.51cto.com/show/13842

模拟器

#夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局-鸿蒙开发者社区

#夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局-鸿蒙开发者社区

3.代码讲解

3.1准备工作

1).添加一个用来保存image资源的ets文件,resource_const.ets

export function initImageMap(): Map<string, Resource> {
  let imageMap = new Map()
  
  //tappage
  //tab icon
  imageMap.set('tab_01', $r('app.media.ic_public_home'))
  imageMap.set('tab_02', $r('app.media.ic_public_appstore'))
  imageMap.set('tab_03', $r('app.media.ic_gallery_album_damage_video'))
  imageMap.set('tab_04', $r('app.media.ic_gallery_search_things'))
  imageMap.set('tab_05', $r('app.media.ic_user_portrait'))

  imageMap.set('tab_01_filled', $r('app.media.ic_public_home_filled'))
  imageMap.set('tab_02_filled', $r('app.media.ic_public_appstore_filled'))
  imageMap.set('tab_03_filled', $r('app.media.ic_gallery_album_damage_video_filled'))
  imageMap.set('tab_04_filled', $r('app.media.ic_gallery_search_things_filled'))
  imageMap.set('tab_05_filled', $r('app.media.ic_user_portrait_filled'))

  //tab color
  imageMap.set('tab_filled_color', $r('app.color.filled_color'))
  imageMap.set('tab_unfilled_color', $r('app.color.unfilled_color'))

  return imageMap

}

为什么要这么做,

一是因为我发现,有时候如果直接这样用,有时候图片就会错乱,显示的不是该图片。

二是用改起来方便。

Image($r('app.media.light_power'))  //这样直接用
  .width(40)
  .height(40)
  .alignSelf(ItemAlign.End)
  .margin({ right: '10%', bottom: '3%' })
  .onClick(() => {
    router.push({ uri: 'pages/index' })
  })

2).string.json资源文件中定义tab显示文本

  {
    "name": "tab_01",
    "value": "家居"
  }
,
  {
    "name": "tab_02",
    "value": "商城"
  }
,
  {
    "name": "tab_03",
    "value": "内容"
  }
,
  {
    "name": "tab_04",
    "value": "场景"
  }
,
  {
    "name": "tab_05",
    "value": "我的"
  }

接下来就是进入正题了,新建一个ets页面,tabpage.ets

3.2实现一个底部tab栏

1).导入需要用的组件

//日志组件
import { CommonLog  as logger } from '@ohos/ohos_clogger'

//用于数据展示模型
import { NoticeDataModel, initOneNoticeData } from "../model/NoticeDataModel"

//引入定义的常量、视图组件
import { initImageMap } from '../common/resource_const'

//资源管理,用于实现屏幕方向的获取
import resourceManager from '@ohos.resourceManager';

2).定义tab图标和文本颜色 状态变量

//tab icon
@State tab_01_icon: Resource = initImageMap().get('tab_01_filled')
@State tab_02_icon: Resource = initImageMap().get('tab_02')
@State tab_03_icon: Resource = initImageMap().get('tab_03')
@State tab_04_icon: Resource = initImageMap().get('tab_04')
@State tab_05_icon: Resource = initImageMap().get('tab_05')
//tab 文本颜色
@State tab_01_color: Resource = initImageMap().get('tab_filled_color')
@State tab_02_color: Resource = initImageMap().get('tab_unfilled_color')
@State tab_03_color: Resource = initImageMap().get('tab_unfilled_color')
@State tab_04_color: Resource = initImageMap().get('tab_unfilled_color')
@State tab_05_color: Resource = initImageMap().get('tab_unfilled_color')

3).接下来看看build() 的布局,

最外层用Column容器布局,然后是Flex布局,top栏,middle,Content都是一行一行的,所以用Row容器布局,先占个位。

底部的tab栏用Flex布局,每个tab用Column布局,Column是上下2层,一个image,一个文本。

5个tab,所以每个tab的width设为20%,为了更美观,给最外层的Column设置个背景图片 。

build() {
  Column() {
    //用Flex布局  
    Flex({ direction: FlexDirection.Column, wrap: FlexWrap.NoWrap }) {
        //top栏
        Row() {}.width('100%').height('80vp')
        //middle
        Row() {}.width('100%').height('150vp')
        //content
        Row() {}.width('100%').height('100%')
        //bottom tab
        Flex() {
          Column() {
            Image(this.tab_01_icon)
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .objectFit(ImageFit.Contain)
            Text($r('app.string.tab_01'))
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .fontColor(this.tab_01_color)
              .fontSize('15fp')
              .textAlign(TextAlign.Center)
          }.onClick(() => {
            this.current_tab_index = 1
            this.switchTab()
          })
          .width('20%')
          .height('100%')

          Column() {
            Image(this.tab_02_icon)
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .objectFit(ImageFit.Contain)
            Text($r('app.string.tab_02'))
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .fontColor(this.tab_02_color)
              .fontSize('15fp')
              .textAlign(TextAlign.Center)
          }
          .onClick(() => {
            this.current_tab_index = 2
            this.switchTab()
          })
          .width('20%')
          .height('100%')

          Column() {
            Image(this.tab_03_icon)
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .objectFit(ImageFit.Contain)
            Text($r('app.string.tab_03'))
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .fontColor(this.tab_03_color)
              .fontSize('15fp')
              .textAlign(TextAlign.Center)
          }
          .onClick(() => {
            this.current_tab_index = 3
            this.switchTab()
          })
          .width('20%')
          .height('100%')

          Column() {
            Image(this.tab_04_icon)
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .objectFit(ImageFit.Contain)
            Text($r('app.string.tab_04'))
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .fontColor(this.tab_04_color)
              .fontSize('15fp')
              .textAlign(TextAlign.Center)
          }
          .onClick(() => {
            this.current_tab_index = 4
            this.switchTab()
          })
          .width('20%')
          .height('100%')

          Column() {
            Image(this.tab_05_icon)
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .objectFit(ImageFit.Contain)
            Text($r('app.string.tab_05'))
              .width('100%')
              .height('50%')
              .flexShrink(0)
              .fontColor(this.tab_05_color)
              .fontSize('15fp')
              .textAlign(TextAlign.Center)
          }
          .onClick(() => {
            this.current_tab_index = 5
            this.switchTab()
          })
          .width('20%')
          .height('100%')
        }
        .width('100%')
        .height('90vp')
        .align(Alignment.Center)
        .flexShrink(0)
        .backgroundColor('#ffdbc9c9')
        .margin({ top: '5vp'})
        .padding({ top: '5vp', bottom: '5vp', left: '5vp', right: '5vp' })
        
    }
  }
  .width('100%')
  .height('100%')
  .alignItems(HorizontalAlign.End)
  //设置个背景图片  
  .backgroundImage($r('app.media.community_notice'), ImageRepeat.XY)
}

#夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局-鸿蒙开发者社区

4).实现点击tab 切换的效果

定义当前操作的tab索引

//当前操作的tab
current_tab_index = 1

5).定义切换tab函数switchTab()

设置当前点击tab的选中效果,同时其它tab取消选中效果。该方法还可以继续优化。

//切换Tab
switchTab() {

  if (this.current_tab_index == 1) {
    if (this.tab_01_icon.id != initImageMap().get('tab_01_filled').id) {

      logger.getInstance(this).debug(`===========${JSON.stringify(this.tab_01_icon)}`)
      logger.getInstance(this).debug(`===========${JSON.stringify(initImageMap().get('tab_01_filled'))}`)
      //当前选中
      this.tab_01_icon = initImageMap().get('tab_01_filled')
      this.tab_01_color = initImageMap().get('tab_filled_color')
      //重置其它
      this.tab_02_icon = initImageMap().get('tab_02')
      this.tab_02_color = initImageMap().get('tab_unfilled_color')

      this.tab_03_icon = initImageMap().get('tab_03')
      this.tab_03_color = initImageMap().get('tab_unfilled_color')

      this.tab_04_icon = initImageMap().get('tab_04')
      this.tab_04_color = initImageMap().get('tab_unfilled_color')

      this.tab_05_icon = initImageMap().get('tab_05')
      this.tab_05_color = initImageMap().get('tab_unfilled_color')
    }
  }
  if (this.current_tab_index == 2) {
    if (this.tab_02_icon.id != initImageMap().get('tab_02_filled').id) {

      logger.getInstance(this).debug(`===========${JSON.stringify(this.tab_02_icon)}`)
      logger.getInstance(this).debug(`===========${JSON.stringify(initImageMap().get('tab_02_filled'))}`)
      //当前选中
      this.tab_02_icon = initImageMap().get('tab_02_filled')
      this.tab_02_color = initImageMap().get('tab_filled_color')
      //重置其它
      this.tab_01_icon = initImageMap().get('tab_01')
      this.tab_01_color = initImageMap().get('tab_unfilled_color')

      this.tab_03_icon = initImageMap().get('tab_03')
      this.tab_03_color = initImageMap().get('tab_unfilled_color')

      this.tab_04_icon = initImageMap().get('tab_04')
      this.tab_04_color = initImageMap().get('tab_unfilled_color')

      this.tab_05_icon = initImageMap().get('tab_05')
      this.tab_05_color = initImageMap().get('tab_unfilled_color')
    }
  }
  if (this.current_tab_index == 3) {
    if (this.tab_03_icon.id != initImageMap().get('tab_03_filled').id) {

      logger.getInstance(this).debug(`===========${JSON.stringify(this.tab_03_icon)}`)
      logger.getInstance(this).debug(`===========${JSON.stringify(initImageMap().get('tab_03_filled'))}`)
      //当前选中
      this.tab_03_icon = initImageMap().get('tab_03_filled')
      this.tab_03_color = initImageMap().get('tab_filled_color')
      //重置其它
      this.tab_02_icon = initImageMap().get('tab_02')
      this.tab_02_color = initImageMap().get('tab_unfilled_color')

      this.tab_01_icon = initImageMap().get('tab_01')
      this.tab_01_color = initImageMap().get('tab_unfilled_color')

      this.tab_04_icon = initImageMap().get('tab_04')
      this.tab_04_color = initImageMap().get('tab_unfilled_color')

      this.tab_05_icon = initImageMap().get('tab_05')
      this.tab_05_color = initImageMap().get('tab_unfilled_color')
    }
  }
  if (this.current_tab_index == 4) {
    if (this.tab_04_icon.id != initImageMap().get('tab_04_filled').id) {

      logger.getInstance(this).debug(`===========${JSON.stringify(this.tab_04_icon)}`)
      logger.getInstance(this).debug(`===========${JSON.stringify(initImageMap().get('tab_04_filled'))}`)
      //当前选中
      this.tab_04_icon = initImageMap().get('tab_04_filled')
      this.tab_04_color = initImageMap().get('tab_filled_color')
      //重置其它
      this.tab_02_icon = initImageMap().get('tab_02')
      this.tab_02_color = initImageMap().get('tab_unfilled_color')

      this.tab_03_icon = initImageMap().get('tab_03')
      this.tab_03_color = initImageMap().get('tab_unfilled_color')

      this.tab_01_icon = initImageMap().get('tab_01')
      this.tab_01_color = initImageMap().get('tab_unfilled_color')

      this.tab_05_icon = initImageMap().get('tab_05')
      this.tab_05_color = initImageMap().get('tab_unfilled_color')
    }
  }
  if (this.current_tab_index == 5) {
    if (this.tab_05_icon.id != initImageMap().get('tab_05_filled').id) {

      logger.getInstance(this).debug(`===========${JSON.stringify(this.tab_05_icon)}`)
      logger.getInstance(this).debug(`===========${JSON.stringify(initImageMap().get('tab_05_filled'))}`)
      //当前选中
      this.tab_05_icon = initImageMap().get('tab_05_filled')
      this.tab_05_color = initImageMap().get('tab_filled_color')
      //重置其它
      this.tab_02_icon = initImageMap().get('tab_02')
      this.tab_02_color = initImageMap().get('tab_unfilled_color')

      this.tab_03_icon = initImageMap().get('tab_03')
      this.tab_03_color = initImageMap().get('tab_unfilled_color')

      this.tab_04_icon = initImageMap().get('tab_04')
      this.tab_04_color = initImageMap().get('tab_unfilled_color')

      this.tab_01_icon = initImageMap().get('tab_01')
      this.tab_01_color = initImageMap().get('tab_unfilled_color')
    }
  }

}

6).在点击tab时设置当前操作的tab索引并调用switchTab() 函数

      .onClick(() => {
        this.current_tab_index = 5
        this.switchTab()
      })

3.3实现一个顶部工具栏

因为我们想实现一个,向上滑动时,隐藏middle栏的内容,在top栏显示缩减版的middle信息。

所以定义一个show_top_title 变量,用于控制top title的显隐,定义一个show_mid_title状态变量控制middle title的显隐。

//控制组件显隐
@State show_top_title: boolean = false
@State show_mid_title: boolean = true

top栏包含一个文本(初始时不显示),一个搜索按钮,一个添加按钮,我们希望top栏的操作按钮能靠右边,所以注意设置

.alignItems(HorizontalAlign.End)

//top栏
Row() {
  Column() {
    if (this.show_top_title) {
      Text('步二神探的家')
        .height('100%')
        .width('100%')
        .fontSize(24)
        .fontWeight(FontWeight.Bolder)
        .fontWeight('#CCFFF')
    }
  }
  .width('60%')
  .height('100%')
  .padding({ left: 10 })

  Column() {
    Image($r('app.media.ic_public_search'))
      .width('50vp')
      .height('100%')
      .borderRadius(30)
      .margin({ right: 10 })
      .objectFit(ImageFit.ScaleDown)
    //.backgroundColor('#bbdd11')
  }
  .width('20%')
  .height('100%')
  //.backgroundColor('#bbdd11')
  .alignItems(HorizontalAlign.End)
  .onClick(() => {
    logger.getInstance(this).debug(`you click '🔍' button`)
  })

  Column() {
    Image($r('app.media.ic_public_add'))
      .width('50vp')
      .height('100%')
      .borderRadius(30)
      .margin({ right: 10 })
      .objectFit(ImageFit.ScaleDown)
    //.backgroundColor('#bbdd11')
  }
  .width('20%')
  .height('100%')
  //.backgroundColor('#bbdd11')
  .alignItems(HorizontalAlign.End)
  .onClick(() => {
    logger.getInstance(this).debug(`you click '+' button`)
  })
}
.width('100%')
.height('80vp')
.align(Alignment.End)
.backgroundColor('#ffdbc9c9')

3.4实现一个Grid网格展示

1).模拟数据列表,该数据来源 NoticeDataModel 的模拟数据,数据结构是一个通知,包括标题和内容,仅用于演示。

import { NoticeDataModel, initOneNoticeData } from "../model/NoticeDataModel"
//数据列表
@State notice_list: NoticeDataModel[] = [
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData(),
  initOneNoticeData()
]

2).content 布局使用Grid实现一个网格效果,使用ForEach进行遍历notice_list的数据,

ForEach的使用要注意,最后的部分, item => item.id) , 一般用唯一性的字段赋值,可以提高更改后重新渲染的性能。

.columnsTemplate(‘1fr 1fr’) 可以控制 列格式,显示为2列 ,可以是1列,3列

//content
Row() {
  Grid() {
    ForEach(this.notice_list, item => {
      GridItem() {
        Column() {
          Text(item.notice_title)
            .fontSize(18)
            .width('100%')
          Text(item.notice_content)
            .fontSize(15)
            .width('100%')
            .padding({ top: 5 })
            .fontColor('#6D7278')
        }
        .width('100%')
        .height(160)
        .borderRadius(15)
        .padding({ top: 10, left: 10 })
        .backgroundColor(0xF9CF93)
      }
    }, item => item.id)
  }
  //列格式,显示为2列  
  .columnsTemplate('1fr 1fr')
  .columnsGap(20)
  .rowsGap(20)
  .margin({ top: 10 })
  .onScrollIndex((first: number) => {
    logger.getInstance(this).debug(`${first.toString()}`)
    if (first == 4) {
      this.show_top_title = true;
      this.show_mid_title = false;
    }
    if (first == 2) {
      this.show_top_title = false;
      this.show_mid_title = true;
    }
  })
}
.width('100%')
.height('100%')
.padding({ left: '5vp', right: '5vp' })
.alignItems(VerticalAlign.Top)
//.backgroundColor('#ffc1dfe0')
1列 2列 3列表
#夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局-鸿蒙开发者社区 #夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局-鸿蒙开发者社区 #夏日挑战赛# 用OpenHarmony eTS 实现一个Huawei app标准布局-鸿蒙开发者社区

3.当向上滑动content时,隐藏middle的内容,同时显示top栏中的文本内容。

onScrollIndex回调函数

.onScrollIndex((first: number) => {
  logger.getInstance(this).debug(`${first.toString()}`)
  if (first == 4) {
    this.show_top_title = true;
    this.show_mid_title = false;
  }
  if (first == 2) {
    this.show_top_title = false;
    this.show_mid_title = true;
  }
})

4.思考总结

4.1图标下载

1.华为提供的HarmonyOS图标库

2.阿里巴巴矢量图标库

4.2实现屏幕方向的获取

通过resourceManager获取getConfiguration,然后config.direction获取屏幕方向,这部分代码在HarmonyOS可以正常获取,但在OpenHarmony中还无法使用。

import resourceManager from '@ohos.resourceManager';
resourceManager.getResourceManager('com.example.lanls')
  .then(mgr => {

    logger.getInstance(this).debug(`=====${JSON.stringify(mgr)}`)
    mgr.getConfiguration()
      .then(config => {
        logger.getInstance(this).debug(`${JSON.stringify(config)}`)
        //DIRECTION_VERTICAL = 0,
        this.is_landscape = config.direction.valueOf() == 1 ? true : false
      })
      .catch(error => {
        logger.getInstance(this).error("getstring promise " + error);
      });
  })
  .catch(error => {
    logger.getInstance(this).error("error occurs" + error);
  });
DIRECTION_VERTICAL = 0,
DIRECTION_HORIZONTAL = 1

5.完整代码

附件可以直接下载

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
tabpage.zip 2.62K 98次下载
已于2022-6-17 18:00:01修改
7
收藏 4
回复
举报
7条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

楼主用心的分享也很值得学习。

1
回复
2022-6-15 10:25:28
Buty9147
Buty9147 回复了 红叶亦知秋
楼主用心的分享也很值得学习。

以后做app就用这个布局了!(^O^)y

回复
2022-6-16 08:08:40
狼哥Army
狼哥Army

获取方向,是否可以用媒体查询代替?

回复
2022-6-16 08:17:41
Buty9147
Buty9147 回复了 狼哥Army
获取方向,是否可以用媒体查询代替?

没能理解你意思

回复
2022-6-16 18:01:51
狼哥Army
狼哥Army 回复了 Buty9147
没能理解你意思

就是获取设备是竖屏,还是横屏,用媒体查询来匹配

回复
2022-6-17 00:29:24
Buty9147
Buty9147 回复了 狼哥Army
就是获取设备是竖屏,还是横屏,用媒体查询来匹配
已于2022-6-17 10:42:25修改
1
回复
2022-6-17 10:41:43
麒麟Berlin
麒麟Berlin

底部导航栏完全可以封装一个通用的,根据需求创建tab个数,动态计算每个tab的宽

回复
2022-9-26 17:50:46
回复
    相关推荐