用OpenHarmony eTS 實作一個Huawei app标準布局
本文正在參加星光計劃3.0 -- 夏日挑戰賽
(目錄)
1.介紹
Huawei 的app,我們都能看得出來是用心設計過的,值得學習。如果我們仔細去看Huawei 手機自帶的app,我們會發現所有的app,無論是什麼類型的app,其布局結構都是一種類似的結構,這說明這種布局結構的用途真的可以很廣泛,而且體驗很好...
像華為的應用市場app、聯系人、浏覽器、圖庫、智慧生活app、音樂app、我的華為、暢連 等等,你去看,全部是這種上中下的布局結構,頂部欄(top+middle)+内容展示區(content)+底部tab欄,那麼,今天我們就一起來實作一個這樣的布局。
2.效果展示
DAYU200真機
視訊位址
https://ost.51cto.com/show/13842
模拟器

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)
}
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清單 |
---|---|---|
| | |
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.完整代碼
附件:https://ost.51cto.com/resource/2076