news 2025/12/27 11:14:38

使用Tabs选项卡组件快速搭建鸿蒙APP框架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用Tabs选项卡组件快速搭建鸿蒙APP框架

kUI提供了很多布局组件,其中Tabs选项卡组件可以用于快速搭建鸿蒙APP框架,本文通过案例研究Tabs构建鸿蒙原生应用框架的方法和步骤。

一、效果展示

1、效果展示

1

整个APP外层Tabs包含4个选项卡:首页、发现、消息、我的。在首页中,上滑列表会出现吸顶效果,分类可以左右滑动,当滑到最后一个分类时,与外层Tabs联动,滑到“发现”页面。首页中的分类标签可以用户自定义选择显示。

2、技术分析

主要使用Tabs选项卡搭建整个APP的框架,通过设置Tabs相关的属性和方法实现布局、滚动、吸顶、内外层嵌套联动等功能。

Tabs组件的页面组成包含两个部分,分别是TabContent和TabBar。TabContent是内容页,TabBar是导航页签栏,,根据不同的导航类型,布局会有区别,可以分为底部导航、顶部导航、侧边导航,其导航栏分别位于底部、顶部和侧边。

本例中通过嵌套Tabs实现,外层Tabs为底部导航、内层Tabs为顶部导航。

二、功能实现

1、准备工作

1.1 数据准备

在商业项目中,界面显示的数据是通过网络请求后端接口获得,本例重点放在Tabs组件的用法研究上,因此简化数据获取过程,直接将数据写入到json文件中。

将准备好的界面数据文件(tab标签和数据列表)拷贝到resources/rawfile目录下包含4个文件:default_all_tabs.json、default_all_tabs_en.json、default_content_items.json、default_content_items_en.json。

1.2 本地化

将界面文字

zh_CN/element:integer.json、string.json

en_US/element:integer.json、string.json

base/element:integer.json、string.json、color.json

1.3 素材

base/media:图片素材

1.4 通用类

ets目录新建common目录,新建constat目录用于存放常量,新建utils目录用于存放工具类。

constant目录下新建Constants.ets文件,记录用到的常量。

export class Constants {

/**

* Full screen width.

*/

static readonly FULL_WIDTH: string = '100%';

/**

* Full screen height.

*/

static readonly FULL_HEIGHT: string = '100%';

}

utils目录下新建StringUtil.ets文件,用于处理从文件中读取的数据。

import { util } from "@kit.ArkTS";

import { BusinessError } from "@kit.BasicServicesKit";

import { hilog } from "@kit.PerformanceAnalysisKit";

export default class StringUtil {

static async getStringFromRawFile(ctx: Context, source: string) {

try {

let getJson = await ctx.resourceManager.getRawFileContent(source);

let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });

let result = textDecoder.decodeToString(getJson);

return Promise.resolve(result);

} catch (error) {

let code = (error as BusinessError).code;

let message = (error as BusinessError).message;

hilog.error(0x0000, 'StringUtil', 'getStringSync failed,error code: %{code}s,message: %{message}s.', code,

message);

return Promise.reject(error);

}

}

}

2、整体框架

整体布局分为2部分,顶部搜索栏和其下的嵌套Tabs页面。为了提升可维护性,采用组件化编程思想。

2.1 搜索组件

在ets目录下新建view目录用于存放组件,新建搜索组件SearchBarComponent.ets

import { Constants } from "../common/constant/Constants";

@Component

export default struct SearchBarComponent {

@State changeValue: string = '';

build() {

Row() {

// 1、传统方法

// Stack() {

// TextInput({ placeholder: $r('app.string.search_placeholder') })

// .height(40)

// .width(Constants.FULL_WIDTH)

// .fontSize(16)

// .placeholderColor(Color.Grey)

// .placeholderFont({ size: 16, weight: FontWeight.Normal })

// .borderStyle(BorderStyle.Solid)

// .backgroundColor($r('app.color.search_bar_input_color'))

// .padding({ left: 35, right: 66 })

// .onChange((currentContent) => {

// this.changeValue = currentContent;

// })

// Row() {

// Image($r('app.media.ic_search')).width(20).height(20)

// Button($r('app.string.search'))

// .padding({ left: 20, right: 20 })

// .height(36)

// .fontColor($r('app.color.search_bar_button_color'))

// .fontSize(16)

// .backgroundColor($r('app.color.search_bar_input_color'))

//

// }.width(Constants.FULL_WIDTH)

// .hitTestBehavior(HitTestMode.None)

// .justifyContent(FlexAlign.SpaceBetween)

// .padding({ left: 10, right: 2 })

// }.alignContent(Alignment.Start)

// .width(Constants.FULL_WIDTH)

// 2、搜索组件

Search({placeholder:$r('app.string.search_placeholder')})

.searchButton('搜索')

}

.justifyContent(FlexAlign.SpaceBetween)

.padding(10)

.width(Constants.FULL_WIDTH)

.backgroundColor($r('app.color.out_tab_bar_background_color'))

.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])

}

}

在主界面引入,即可查看效果。修改Index.ets

import { Constants } from '../common/constant/Constants';

import SearchBarComponent from '../view/SearchBarComponent';

@Entry

@Component

struct Index {

build() {

Column() {

// 搜索栏

SearchBarComponent()

}

.height(Constants.FULL_HEIGHT)

.width(Constants.FULL_WIDTH)

.expandSafeArea([SafeAreaType.SYSTEM])

}

}

2.2 外层Tabs

通过界面分析,外层Tabs的每一个TabContent内容不同,可以抽取为组件。第一个TabContent抽取为组件InTabsComponent,后边的几个抽取为OtherTabContentComponent。

在view目录下新建组件:InTabsComponent.ets

@Component

export default struct InTabsComponent {

build() {

Text('内层Tabs')

}

}

在InTabsComponent中,先简单写点提示信息,待整体框架完成后,后续再继续完成内层的内容。

在view目录下新建组件:OtherTabComponent.ets

import { Constants } from "../common/constant/Constants";

@Component

export default struct OtherTabContentComponent {

@State bgColor: ResourceColor = $r('app.color.other_tab_content_default_color');

build() {

Column()

.width(Constants.FULL_WIDTH)

.height(Constants.FULL_HEIGHT)

.backgroundColor(this.bgColor)

}

}

在OtherTabComponent中,通过接收父组件传递的颜色参数来设置背景颜色,用以区分不同的Tab。

在view目录下,新建外层组件OutTabsComponent.ets

import { Constants } from "../common/constant/Constants";

import InTabsComponent from "./InTabsComponent";

import OtherTabContentComponent from "./OtherTabComponent";

@Component

export default struct OutTabsComponent {

@State currentIndex: number = 0;

private tabsController: TabsController = new TabsController();

@Builder

tabBuilder(index: number, name: string | Resource, icon: Resource) {

Column() {

SymbolGlyph(icon).fontColor([this.currentIndex === index

? $r('app.color.out_tab_bar_font_active_color')

: $r('app.color.out_tab_bar_font_inactive_color')])

.fontSize(25)

Text(name)

.margin({ top: 4 })

.fontSize(10)

.fontColor(this.currentIndex === index

? $r('app.color.out_tab_bar_font_active_color')

: $r('app.color.out_tab_bar_font_inactive_color'))

}

.justifyContent(FlexAlign.Center)

.height(Constants.FULL_HEIGHT)

.width(Constants.FULL_WIDTH)

.padding({ bottom: 60 })

}

build() {

Tabs({

barPosition: BarPosition.End,

index: this.currentIndex,

controller: this.tabsController,

}) {

TabContent() {

InTabsComponent()

}.tabBar(this.tabBuilder(0, $r('app.string.out_bar_text_home'), $r('sys.symbol.house')))

TabContent() {

OtherTabContentComponent({ bgColor: Color.Blue })

}

.tabBar(this.tabBuilder(1, $r('app.string.out_bar_text_discover'), $r('sys.symbol.map_badge_local')))

TabContent() {

OtherTabContentComponent({ bgColor: Color.Yellow })

}

.tabBar(this.tabBuilder(2, $r('app.string.out_bar_text_messages'), $r('sys.symbol.ellipsis_message')))

TabContent() {

OtherTabContentComponent({ bgColor: Color.Orange })

}

.tabBar(this.tabBuilder(3, $r('app.string.out_bar_text_profile'), $r('sys.symbol.person')))

}

.vertical(false)

.barMode(BarMode.Fixed)

.scrollable(true) // false to disable scroll to switch

// .edgeEffect(EdgeEffect.None) // disables edge springback

.onChange((index: number) => {

this.currentIndex = index;

})

.height(Constants.FULL_HEIGHT)

.width(Constants.FULL_WIDTH)

.backgroundColor($r('app.color.out_tab_bar_background_color'))

.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])

.barHeight(120)

.barBackgroundBlurStyle(BlurStyle.COMPONENT_THICK)

.barOverlap(true)

}

}

在主界面中引入外层Tabs组件OutTabsComponent,修改主界面Index.ets

import OutTabsComponent from '../view/OutTabsComponent';

...

// 外层tabs

OutTabsComponent()

这样就实现了整体布局。

3、内层组件

分析内层组件布局结构,顶部是一张Banner图片,下边是一个Tabs组件。整个内层组件可以上下滚动,并且上滑要产生吸顶效果,因此外层组件应该使用Scroll滚动组件作为顶层父容器,里边滚动的内容使用List组件即可,List里边的内容也需要封装成组件。

3.1 Banner组件

接下来先封装顶部的Banner图片组件,在view目录下新建BannerComponent组件,BannerComponent.ets

import { Constants } from "../common/constant/Constants";

@Component

export default struct BannerComponent {

build() {

Column() {

Image($r('app.media.pic5'))

.width(Constants.FULL_WIDTH)

.height(186)

.borderRadius(16)

}

.margin({

left: 5,

right: 5,

top: 10,

bottom: 2

})

}

}

3.2 列表项组件

接下来封装列表项组件ContentItemComponent,

封装数据类ContentItemModel,在ets目录下新建model目录,新建ContentItemModel.ets

export default class ContentItemModel {

username: string | Resource = '';

publishTime: string | Resource = '';

rawTitle: string | Resource = '';

title: string | Resource = '';

imgUrl1: string | Resource = '';

imgUrl2: string | Resource = '';

imgUrl3: string | Resource = '';

imgUrl4: string | Resource = '';

}

封装数据类ContentItemViewModel,在ets目录下新建viewmodel目录,新建ContentItemViewModel.ets文件

import ContentItemModel from "../model/ContentItemModel";

@Observed

export default class ContentItemViewModel {

username: string | Resource = '';

publishTime: string | Resource = '';

rawTitle: string | Resource = '';

title: string | Resource = '';

imgUrl1: string | Resource = '';

imgUrl2: string | Resource = '';

imgUrl3: string | Resource = '';

imgUrl4: string | Resource = '';

updateContentItem(contentItemModel: ContentItemModel) {

this.username = contentItemModel.username;

this.publishTime = contentItemModel.publishTime;

this.rawTitle = contentItemModel.rawTitle;

this.title = contentItemModel.title;

this.imgUrl1 = contentItemModel.imgUrl1;

this.imgUrl2 = contentItemModel.imgUrl2;

this.imgUrl3 = contentItemModel.imgUrl3;

this.imgUrl4 = contentItemModel.imgUrl4;

}

}

在view目录新建ContentItemComponent.ets

import { Constants } from "../common/constant/Constants";

import ContentItemViewModel from "../viewmodel/ContentItemViewModel";

@Component

export default struct ContentItemComponent {

@Prop contentItemViewModel: ContentItemViewModel;

build() {

Column() {

Row() {

Image(this.contentItemViewModel.imgUrl1)

.width(30)

.height(30)

.borderRadius(15)

Column() {

Text(this.contentItemViewModel.username)

.fontSize(15)

Text(this.contentItemViewModel.publishTime)

.fontSize(12)

.fontColor($r('app.color.content_item_text_color'))

}

.margin({ left: 10 })

.justifyContent(FlexAlign.Start)

.alignItems(HorizontalAlign.Start)

}

Column() {

Text(this.contentItemViewModel.title)

.fontSize(16)

.id('title')

.textAlign(TextAlign.Start)

}

.margin({top:10, bottom: 10})

Row() {

Image(this.contentItemViewModel.imgUrl2)

.width(115)

.height(115)

Image(this.contentItemViewModel.imgUrl3)

.width(115)

.height(115)

Image(this.contentItemViewModel.imgUrl4)

.width(115)

.height(115)

}

.width(Constants.FULL_WIDTH)

.justifyContent(FlexAlign.SpaceBetween)

}

.width(Constants.FULL_WIDTH)

.alignItems(HorizontalAlign.Start)

}

}

3.3 列表数据封装

在制作列表项组件时封装了每一项数据对应的类ContentItemModel,还需要封装一个类用于表示整个Tabs界面的数据。

在model目录下新建InTabsModel.ets

import { BusinessError } from '@kit.BasicServicesKit';

import { hilog } from '@kit.PerformanceAnalysisKit';

import ContentItemModel from './ContentItemModel';

import StringUtil from '../common/utils/StringUtil';

export default class InTabsModel {

contentItems: ContentItemModel[] = [];

async loadContentItems(ctx: Context) {

let filename = '';

try {

filename = await ctx.resourceManager.getStringValue($r('app.string.default_content_items_file').id);

} catch (error) {

let err = error as BusinessError;

hilog.error(0x0000, 'InTabsModel', `getStringValue failed, error code=${err.code}, message=${err.message}`);

}

let res = await StringUtil.getStringFromRawFile(ctx, filename);

this.contentItems = JSON.parse(res).map((item: ContentItemModel) => {

let img1 = item.imgUrl1 as string;

if (img1.indexOf('app.media') === 0) {

item.imgUrl1 = $r(img1);

}

let img2 = item.imgUrl2 as string;

if (img2.indexOf('app.media') === 0) {

item.imgUrl2 = $r(img2);

}

let img3 = item.imgUrl3 as string;

if (img3.indexOf('app.media') === 0) {

item.imgUrl3 = $r(img3);

}

let img4 = item.imgUrl4 as string;

if (img4.indexOf('app.media') === 0) {

item.imgUrl4 = $r(img4);

}

return item;

});

}

}

该类主要实现从本地文件中读取列表数据。

在viewmodel目录下新建文件InTabsViewModel.ets

import ContentItemViewModel from "./ContentItemViewModel";

import InTabsModel from "../model/InTabsModel";

@Observed

class ContentItemArray extends Array<ContentItemViewModel> {

}

@Observed

export default class InTabsViewModel {

private inTabsModel: InTabsModel = new InTabsModel();

contentItems: ContentItemArray = new ContentItemArray();

async loadContentData(ctx: Context) {

await this.inTabsModel.loadContentItems(ctx);

let tempItems: ContentItemArray = [];

for (let item of this.inTabsModel.contentItems) {

let contentItemViewModel = new ContentItemViewModel();

contentItemViewModel.updateContentItem(item);

tempItems.push(contentItemViewModel);

}

this.contentItems = tempItems;

}

}

3.4 Tab类封装

将每一个Tab抽象为TabItemModel类,以便于记录当前选中的选项卡。

在model目录下新建TabItemModel.ets

export default class TabItemModel {

id: number = 0;

name: string | Resource = '';

isChecked: boolean = true;

}

在viewmodel目录下新建TabItemViewModel.ets

import TabItemModel from "../model/TabItemModel";

@Observed

export default class TabItemViewModel {

id: number = 0;

name: string | Resource = '';

isChecked: boolean = true;

updateTab(tabItemModel: TabItemModel) {

this.id = tabItemModel.id;

this.name = tabItemModel.name;

this.isChecked = tabItemModel.isChecked;

}

}

3.5 标签分类封装

内层Tabs的标签TarBar也是直接从文件读取,内层标签初始加载时直接读取文件内容进行显示,后续还需要添加分类的选择和取消功能,实现自定义显示分类。

本小节先封装相关类,在model目录下新建SelectTabsModel类,用于存取文件中的标签分类,SelectTabsModel.ets

import { BusinessError } from '@kit.BasicServicesKit';

import { hilog } from '@kit.PerformanceAnalysisKit';

import TabItemModel from './TabItemModel';

import StringUtil from '../common/utils/StringUtil';

export default class SelectTabsModel {

allTabs: TabItemModel[] = [];

async loadAllTabs(ctx: Context) {

let filename = '';

try {

filename = await ctx.resourceManager.getStringValue($r('app.string.default_all_tabs_file').id);

} catch (error) {

let err = error as BusinessError;

hilog.error(0x0000, 'SelectTabsModel', `getStringValue failed, error code=${err.code}, message=${err.message}`);

}

let result = await StringUtil.getStringFromRawFile(ctx, filename);

this.allTabs = JSON.parse(result);

}

}

在viewmodel目录下新建SelectTabsViewModel.ets

import TabItemViewModel from "./TabItemViewModel";

import SelectTabsModel from "../model/SelectTabsModel";

@Observed

class TabItemArray extends Array<TabItemViewModel> {

}

@Observed

export default class SelectTabsViewModel {

allTabs: TabItemArray = new TabItemArray();

selectedTabs: TabItemArray = new TabItemArray();

private selectTabsModel: SelectTabsModel = new SelectTabsModel();

async loadTabs(ctx: Context) {

await this.selectTabsModel.loadAllTabs(ctx);

let tempTabs: TabItemViewModel[] = [];

for (let tab of this.selectTabsModel.allTabs) {

let tabItemViewModel = new TabItemViewModel();

tabItemViewModel.updateTab(tab);

tempTabs.push(tabItemViewModel);

}

this.allTabs = tempTabs;

this.updateSelectedTabs();

}

updateSelectedTabs() {

let tempTabs: TabItemViewModel[] = [];

for (let tab of this.allTabs) {

if (tab.isChecked) {

tempTabs.push(tab);

}

}

this.selectedTabs = tempTabs;

}

}

3.6 内层组件

修改InTabsComponent.ets

import { Constants } from "../common/constant/Constants";

import BannerComponent from "./BannerComponent";

import { CommonModifier } from "@kit.ArkUI";

import ContentItemComponent from "./ContentItemComponent";

import ContentItemViewModel from "../viewmodel/ContentItemViewModel";

import TabItemViewModel from "../viewmodel/TabItemViewModel";

import InTabsViewModel from "../viewmodel/InTabsViewModel";

import { EnvironmentCallback, Configuration, AbilityConstant } from "@kit.AbilityKit";

import SelectTabsViewModel from "../viewmodel/SelectTabsViewModel";

@Component

export default struct InTabsComponent {

@State selectTabsViewModel: SelectTabsViewModel = new SelectTabsViewModel();

@State inTabsViewModel: InTabsViewModel = new InTabsViewModel();

@State tabBarModifier: CommonModifier = new CommonModifier();

@State focusIndex: number = 0;

@State showSelectTabsComponent: boolean = false;

@State selectTabsComponentZIndex: number = -1;

private ctx: Context = this.getUIContext().getHostContext() as Context;

private subsController: TabsController = new TabsController();

private tabBarItemScroller: Scroller = new Scroller();

subscribeSystemLanguageUpdate() {

let systemLanguage: string | undefined;

let inTabsViewModel = this.inTabsViewModel;

let selectTabsViewModel = this.selectTabsViewModel;

let applicationContext = this.ctx.getApplicationContext();

let environmentCallback: EnvironmentCallback = {

async onConfigurationUpdated(newConfig: Configuration) {

if (systemLanguage !== newConfig.language) {

await inTabsViewModel.loadContentData(applicationContext);

await selectTabsViewModel.loadTabs(applicationContext);

systemLanguage = newConfig.language;

}

},

onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => {

// do nothing

}

};

applicationContext.on('environment', environmentCallback);

}

async aboutToAppear() {

await this.inTabsViewModel.loadContentData(this.ctx);

await this.selectTabsViewModel.loadTabs(this.ctx);

this.tabBarModifier.margin({ right: 56 }).align(Alignment.Start);

this.subscribeSystemLanguageUpdate();

}

@Builder

tabBuilder(index: number, tab: TabItemViewModel) {

Row() {

Text(tab.name)

.fontSize(14)

.fontWeight(this.focusIndex === index ? FontWeight.Medium : FontWeight.Regular)

.fontColor(this.focusIndex === index ? Color.White : $r('app.color.in_tab_bar_text_normal_color'))

}

.justifyContent(FlexAlign.Center)

.backgroundColor(this.focusIndex === index

? $r('app.color.in_tab_bar_background_active_color')

: $r('app.color.in_tab_bar_background_inactive_color'))

.borderRadius(20)

.height(40)

.margin({ left: 4, right: 4 })

.padding({ left: 18, right: 18 })

.onClick(() => {

this.focusIndex = index;

this.subsController.changeIndex(index);

this.tabBarItemScroller.scrollToIndex(index, true, ScrollAlign.CENTER);

})

}

build() {

Scroll() {

Column() {

BannerComponent()

Stack({ alignContent: Alignment.TopEnd }) {

Row() {

Image($r('app.media.more'))

.width(20)

.height(20)

.margin({ left: 10 })

.onClick(() => {

// todo:弹层选择分类

})

}

.margin({ top: 8, bottom: 8, right: 5 })

.backgroundColor($r('app.color.in_tab_bar_background_inactive_color'))

.width(40)

.height(40)

.borderRadius(20)

.zIndex(1)

Column() {

Tabs({

barPosition: BarPosition.Start,

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/17 22:12:43

谢juncen经验分享

学弟学妹们你们好&#xff0c;我是信管2002班的谢juncen&#xff0c;去年考上了我们本校的管理科学与工程专业的研究生&#xff0c;我的初试成绩是326分&#xff0c;政治68&#xff0c;英语61&#xff0c;数学86&#xff0c;专业课111。以下是我给学弟学妹们的几点建议&#xf…

作者头像 李华
网站建设 2025/12/17 22:12:10

【光照】[PBR][漫反射]实现方法对比

URP BRDF漫反射方法对比方法名称 数学公式 特点 性能消耗 适用场景Lambert $L_d k_d * max(0, NL)$ 经典模型&#xff0c;能量不守恒 ★☆☆ 移动端低配Half-Lambert $L_d k_d * (0.5*(NL)0.5)^2$ 增强暗部细节 ★★☆ 卡通渲染Disney Diffuse 复杂能量守恒公式 物理准确&…

作者头像 李华
网站建设 2025/12/17 22:11:53

社团招新海报制作:核心要素与设计逻辑

社团招新海报是社团与新生建立连接的第一媒介&#xff0c;其设计质量直接影响新生对社团的初始认知与参与意愿。不同于商业海报的品牌曝光诉求&#xff0c;社团招新海报需在短时间内完成“吸引注意力—传递核心价值—引导行动”的闭环&#xff0c;因此需围绕“目标定位、视觉层…

作者头像 李华
网站建设 2025/12/17 22:10:48

H3C防火墙Web登录实验

H3C防火墙Web登录实验 文章目录H3C防火墙Web登录实验一、背景二、实验拓扑图三、实验环境规划四、实验需求五、实验步骤第一步&#xff1a;在本机PC上创建微软环回适配器第二步&#xff1a;修改本机环回适配器的IP地址第三步&#xff1a;配置FW1&#xff0c;给FW1设置IP地址第四…

作者头像 李华
网站建设 2025/12/25 14:44:40

告别 “告警风暴” 与被动救火!8 大数据库监控维度提前规避 80% 故障

在数字化转型纵深推进的今天&#xff0c;数据库已成为支撑业务运转的核心基础设施。无论是电商平台的订单交易、金融机构的账务处理&#xff0c;还是政务系统的数据存储&#xff0c;数据库的稳定性与性能直接决定业务连续性和用户体验。权威数据显示&#xff0c;75%的严重业务中…

作者头像 李华