HarmonyOS NEXT实战:自定义封装多种样式导航栏组件
涉及知识点和装饰器
- @ComponentV2,@Local, @Builder,@BuilderParam,@Extend, @Require ,@Param,@Event等
- 第三方库:ZRouter ,如项目中本来就用了ZRouter路由库,案例中点返回按钮直接使用了 ZRouter.pop(),没有用到的话也支持自定义返回事件。
背景:
在项目开发进程中,导航栏的应用场景颇为繁多。以我的页面为例,其导航栏呈现为图标、文字与箭头相组合的样式;而设置页面的导航栏则是图标、文字、右侧文字以及小红点的搭配形式;至于公用顶部导航栏,又表现为左侧返回图标、中间文字、右侧图标与文字的布局。倘若针对每一处用到导航栏的地方均单独编写代码,那么代码的重复编写现象将极为严重。基于此,我们可采用自定义封装的方式构建公用组件。如此一来,不仅为项目后期的维护与拓展提供了极大的便利,同时也能够显著提升开发效率,让开发者有更多精力投入到更具价值的工作思考中,减少不必要的重复劳作时间消耗。
先上效果图
-
图一
-
图二
-
图三
实现 图一 效果图
- 1、首先需要定义好类型,比如 图片+文字+小红点+返回右键等。
@ObservedV2
export class TabHorizontalModel {title: string;index: number; //下标icon: string | Resource;hasIcon: boolean; //是否显示图@Trace rightTitle: string;hasRightTitle: boolean;@Trace hasNew: boolean; //是否显示红点hasRightIcon: boolean; //是否显示图constructor(title: string, index: number = -1, icon: string | Resource = '', hasIcon: boolean = false, rightTitle: string = '', hasRightTitle: boolean = false, hasNew: boolean = false,hasRightIcon: boolean = true) {this.icon = icon;this.hasIcon = hasIcon;this.title = title;this.rightTitle = rightTitle;this.hasRightTitle = hasRightTitle;this.hasNew = hasNew && rightTitle !== '';this.index = index;this.hasRightIcon = hasRightIcon;}
}
- 2、封装一个通用的 横向Tab 图片、文字、右边文字、小红点 组件
import { CommonConst } from "utils"
import { TabHorizontalModel } from "../model/TabHorizontalModel"/*** Author:J* Describe: 横向Tab 图片、文字、右边文字、小红点*/
@ComponentV2
export struct HorizontalTabItemComp {@Param @Require tabItem: TabHorizontalModel= new TabHorizontalModel('')@Param onItemClick?: () => void = undefinedbuild() {Row() {Image(this.tabItem.icon).width(24).margin({ right: 12 }).visibility(this.tabItem.hasIcon ? Visibility.Visible : Visibility.None)Text(this.tabItem.title).fontSize(16).fontColor($r('app.color.color_222222')).layoutWeight(1)if (this.tabItem.hasNew) {Badge({value: '',position: BadgePosition.Right,style: { badgeSize: 7, badgeColor: $r('app.color.color_FA2A2D') }}) {Text(this.tabItem.rightTitle).fontSize(16).fontColor($r('app.color.color_222222')).visibility(this.tabItem.hasRightTitle ? Visibility.Visible : Visibility.None).margin({ right: 20 })}} else {Text(this.tabItem.rightTitle).fontSize(16).fontColor($r('app.color.color_222222')).visibility(this.tabItem.hasRightTitle ? Visibility.Visible : Visibility.None)}Image($r('app.media.ic_arrow_right_gray_small')).width(24).margin({ left: 12 }).visibility(this.tabItem.hasRightIcon ? Visibility.Visible : Visibility.None)}.width(CommonConst.FULL_PARENT).height(44).backgroundColor($r('app.color.white')).onClick(() => {this.onItemClick?.()})}
}
- 3、使用案例
3.1 针对于一个,可以用下面的代码,但是对于一个页面有多个的话,要是一行行的写,虽然可以,但是不建议,而且也不优雅,所以需要用到ForEach来实现。
HorizontalTabItemComp({tabItem: new TabHorizontalModel("我的积分", 0, $r('app.media.ic_coin'), true),onItemClick: () => {ToastUtil.showToast('我的积分')}}).margin({ left: 12, right: 12 })
3.2 定义一组数据,塞到数组里
/** 横向Tab */
export const horizontalTabItemData: Array<TabHorizontalModel> = [new TabHorizontalModel("我的积分", 0, $r('app.media.ic_coin'), true, '666', true),new TabHorizontalModel("我的分享", 1, $r('app.media.ic_share_article'), true),new TabHorizontalModel("我的收藏", 2, $r('app.media.ic_collect'), true),new TabHorizontalModel("我的书签", 3, $r('app.media.ic_read_later'), true),new TabHorizontalModel("阅读历史", 4, $r('app.media.ic_read_record'), true),new TabHorizontalModel("开源项目", 5, $r('app.media.ic_github'), true),new TabHorizontalModel("关于作者", 6, $r('app.media.ic_about'), true, '请他喝杯咖啡~', true),
]
3.3 使用ForEach来实现
ForEach(horizontalTabItemData, (item: TabHorizontalModel, index: number) => {HorizontalTabItemComp({tabItem: item,onItemClick: () => {this.onItemClick(item)}}).margin({ left: 12, right: 12 })})/** 点击事件 */private onItemClick(item: TabHorizontalModel) {ToastUtil.showToast(item.title)if (item.index == 0) {} else if (item.index == 1) {} else if (item.index == 2) {} else if (item.index == 3) {} else if (item.index == 4) {} else if (item.index == 5) {} else if (item.index == 6) {}}
实现 图二 效果图
- 1、首先需要定义好需要的参数
/** 标题 */@Param title: ResourceStr = '';/** 返回按钮的点击事件 */@Param backClick?: (event?: ClickEvent) => void = undefined;/** 是否显示右侧按钮 */@Param isShowRight: boolean = false;/** 右侧标题 */@Param rightTitle: ResourceStr = '';/** 右侧图片 */@Param rightImage: ResourceStr = '';/** 右侧点击事件 */@Param rightClick?: (event?: ClickEvent) => void = undefined;
- 2、封装一个公用的自定义导航栏组件,内置了导航栏的返回按钮、标题、右侧按钮等,完整代码如下:
import { ZRouter } from '@hzw/zrouter';
import { CommonConst } from 'utils';/*** Author:J* Describe:自定义导航栏组件* 内置了导航栏的返回按钮、标题、右侧按钮等*/
@ComponentV2
export struct TitleBarComp {/** 标题 */@Param title: ResourceStr = '';/** 返回按钮的点击事件 */@Param backClick?: (event?: ClickEvent) => void = undefined;/** 是否显示右侧按钮 */@Param isShowRight: boolean = false;/** 右侧标题 */@Param rightTitle: ResourceStr = '';/** 右侧图片 */@Param rightImage: ResourceStr = '';/** 右侧点击事件 */@Param rightClick?: (event?: ClickEvent) => void = undefined;build() {Column() {Row() {Image($r('app.media.ic_arrow_left')).width(44).padding(8).onClick(() => {if (this.backClick) {this.backClick()} else {ZRouter.pop()}})Text(this.title).fontColor($r('app.color.color_222222')).fontSize(16).maxLines(1).fontWeight(FontWeight.Bold)Row() {if (this.rightTitle) {Text(this.rightTitle).fontColor($r('app.color.color_222222')).fontSize(16).margin({ right: 10 })} else {Image(this.rightImage ? this.rightImage : $r('app.media.ic_local_search')).width(44).padding(10)}}.onClick(this.rightClick).visibility(this.isShowRight ? Visibility.Visible : Visibility.Hidden)}.width(CommonConst.FULL_PARENT).height(44).justifyContent(FlexAlign.SpaceBetween).backgroundColor($r('app.color.white'))Divider().width(CommonConst.FULL_PARENT).color($r('app.color.color_F0F0F0'))}.width(CommonConst.FULL_PARENT).height(45)}
}
- 3、使用案例,包含了多种样式使用
NavDestination() {Column({space:8}) {Text('第一种样式').fontColor(Color.Red)TitleBarComp({ title: '设置' })Text('第二种样式,自定义返回事件').fontColor(Color.Red)TitleBarComp({title: '设置二', backClick: () => {ToastUtil.showToast('自定义返回事件')}})Text('第三种样式,右边有文字').fontColor(Color.Red)TitleBarComp({title: '设置三',isShowRight: true,rightTitle: '右边',rightClick: () => {ToastUtil.showToast('右边')}})Text('第四种,右边有图片').fontColor(Color.Red)TitleBarComp({title: '设置四',isShowRight: true,rightImage: $r('app.media.ic_share_article'),rightClick: () => {ToastUtil.showToast('右边')}})}.width(CommonConst.FULL_PARENT).height(CommonConst.FULL_PARENT).backgroundColor($r('app.color.white'))}.hideTitleBar(true)
实现 图三 效果图
- 背景:这个逻辑比较复杂,一步步优化实现,为啥还需要自定义,直接用官方自带的Tabs+TabContent就可以实现啊;如果只是针对于一行都是简单文字切换那还好,但是对于那种,左边、右边是图片+中间是文字用自带的就不行了,因为下面的内容的宽度是铺满屏幕的宽度的,所以需要自定义。
- 1、定义需要的参数,自定义左边视图,右边视图,内容,下划线,是否滑动等,具体可以看完整代码。
@Param currentTabIndex: number = 0;@Param tabContentArr: boolean[] = []; //存储页面状态private tabsController: TabsController = new TabsController();@Param tabs: Array<TabBarModel> = [];//左边视图@BuilderParam tabBarLeft: () => void = this.barLeft;//右边视图@BuilderParam tabBarRight: () => void = this.barRight;//内容@BuilderParam tabContentBuilder: ($$: TabBarModel) => void = this._TabContentBuilder;//是否显示下划线@Param isShowDivider: boolean = false;//是否滑动@Param scrollable: boolean = false;//顶部中间视图是否居中 true居中 false 默认 居左@Param isTabBarCenter: boolean = false;//选中字体颜色@Param selectFontColor: ResourceColor = $r('app.color.color_222222');//滑动条是否显示@Param isDividerVisible: boolean = true;//更新@Event changeFactory: (currentTabIndex: number, isShowDivider: boolean) => void = (currentTabIndex: number, isShowDivider: boolean) => {}
- 2、自定义顶部视图,List替换tabBar 配合Tabs 左视图–tabBar–右视图
Column() {//切换this.customTabBar()//下划线Divider().color($r('app.color.color_F0F0F0')).visibility(this.isShowDivider ? Visibility.Visible : Visibility.None)//TabContent中的tabBar居中显示,所以暂时不用tabBarTabs({ controller: this.tabsController, barPosition: BarPosition.Start }) {ForEach(this.tabs, (item: TabBarModel, index: number) => {TabContent() {//滑到哪个页面再加载,防止一块加载if (this.currentTabIndex === index || this.tabContentArr[index]) {this.tabContentBuilder(item)}}// .tabBar()}, (item: string) => item)}.layoutWeight(1).barHeight(0) //隐藏tabBar.scrollable(this.scrollable).onChange(index => {this.tabContentArr[index] = truethis.changeFactory(index,this.tabs[index].isShowDivider)})}.width(CommonConst.FULL_PARENT).backgroundColor($r('app.color.white'))
- 3、 List实现【标题+横线】选中效果
@BuildercustomTabBar() {Row() {//左边自定义this.tabBarLeft()//中间CustomTabBarComp({currentTabIndex: this.currentTabIndex,tabs: this.tabs,selectFontColor: this.selectFontColor,isTabBarCenter: this.isTabBarCenter,onTabClick: (index: number) => {this.tabsController.changeIndex(index)},isDividerVisible: this.isDividerVisible})//右边自定义this.tabBarRight()}.width(CommonConst.FULL_PARENT).height(44)}
- 4、标题+横线 List和TabContent.tabBar都可以用
@ComponentV2
export struct TabBarViewComp {@Param private index: number = 0@Param currentTabIndex: number = 0@Param tabs: Array<TabBarModel> = new Array<TabBarModel>()//选中字体颜色@Param selectFontColor: ResourceColor = $r('app.color.color_222222');@Param onTabClick: (index: number) => void = () => {};@Param isDividerVisible: boolean = true;build() {Column() {//右上角图片Image(this.tabs[this.index].rightSrc).height(11).margin({ left: 46 }).visibility(this.tabs[this.index].isShowRightSrc ? Visibility.Visible : Visibility.None)Text(this.tabs[this.index].name).fontSize(this.currentTabIndex == this.index ? 16 : 14).fontColor(this.currentTabIndex == this.index ? this.selectFontColor : $r('app.color.color_505050')).fontWeight(this.currentTabIndex == this.index ? FontWeight.Bold : FontWeight.Normal).margin({ top: this.tabs[this.index].isShowRightSrc ? 0 : 11 })Divider().width(16).height(4).backgroundColor($r('app.color.colorPrimary')).margin({ top: 4, bottom: 4 }).borderRadius(12).visibility(this.isDividerVisible && this.currentTabIndex == this.index ? Visibility.Visible : Visibility.Hidden)}.margin({ right: 15 }).onClick(() => {this.onTabClick(this.index)})}
}
- 5、完整代码如下
import { CommonConst } from "utils";
import { TabBarModel } from "../model/TabBarModel";/*** Author:J* Describe:自定义tabBar 左视图--tabBar--右视图** ListWithTabBarView({}) List替换tabBar 配合Tabs 左视图--tabBar--右视图* CustomTabBarComp({}) List实现【标题+横线】选中效果* TabBarViewComp({}) 标题+横线 List和TabContent.tabBar都可以用*/
@Preview
@ComponentV2
export struct ListWithTabBarView {@Param currentTabIndex: number = 0;@Param tabContentArr: boolean[] = []; //存储页面状态private tabsController: TabsController = new TabsController();@Param tabs: Array<TabBarModel> = [];//左边视图@BuilderParam tabBarLeft: () => void = this.barLeft;//右边视图@BuilderParam tabBarRight: () => void = this.barRight;//内容@BuilderParam tabContentBuilder: ($$: TabBarModel) => void = this._TabContentBuilder;//是否显示下划线@Param isShowDivider: boolean = false;//是否滑动@Param scrollable: boolean = false;//顶部中间视图是否居中 true居中 false 默认 居左@Param isTabBarCenter: boolean = false;//选中字体颜色@Param selectFontColor: ResourceColor = $r('app.color.color_222222');//滑动条是否显示@Param isDividerVisible: boolean = true;//更新@Event changeFactory: (currentTabIndex: number, isShowDivider: boolean) => void = (currentTabIndex: number, isShowDivider: boolean) => {}aboutToAppear() {for (let index = 0; index < this.tabs.length; index++) {this.tabContentArr.push(index == 0 ? true : false)}}build() {Column() {//切换this.customTabBar()//下划线Divider().color($r('app.color.color_F0F0F0')).visibility(this.isShowDivider ? Visibility.Visible : Visibility.None)//TabContent中的tabBar居中显示,所以暂时不用tabBarTabs({ controller: this.tabsController, barPosition: BarPosition.Start }) {ForEach(this.tabs, (item: TabBarModel, index: number) => {TabContent() {//滑到哪个页面再加载,防止一块加载if (this.currentTabIndex === index || this.tabContentArr[index]) {this.tabContentBuilder(item)}}// .tabBar()}, (item: string) => item)}.layoutWeight(1).barHeight(0) //隐藏tabBar.scrollable(this.scrollable).onChange(index => {this.tabContentArr[index] = truethis.changeFactory(index,this.tabs[index].isShowDivider)})}.width(CommonConst.FULL_PARENT).backgroundColor($r('app.color.white'))// .padding({ left: 12, right: 12 })}@Builder_TabContentBuilder($$: TabBarModel) {Text("tabContentBuilder:()=>{your @Builder View}")}@BuildercustomTabBar() {Row() {//左边自定义this.tabBarLeft()//中间CustomTabBarComp({currentTabIndex: this.currentTabIndex,tabs: this.tabs,selectFontColor: this.selectFontColor,isTabBarCenter: this.isTabBarCenter,onTabClick: (index: number) => {this.tabsController.changeIndex(index)},isDividerVisible: this.isDividerVisible})//右边自定义this.tabBarRight()}.width(CommonConst.FULL_PARENT).height(44)}@BuilderbarLeft() {}@BuilderbarRight() {}
}@ComponentV2
export struct CustomTabBarComp {@Param currentTabIndex: number = 0;@Param tabs: Array<TabBarModel> = new Array<TabBarModel>()//选中字体颜色@Param selectFontColor: ResourceColor = $r('app.color.color_222222');@Param onTabClick: (index: number) => void = () => {};@Param isTabBarCenter: boolean = false;@Param isDividerVisible: boolean = true;build() {Row() {List() {ForEach(this.tabs, (item: TabBarModel, index: number) => {ListItem() {TabBarViewComp({index: index,currentTabIndex: this.currentTabIndex,tabs: this.tabs,selectFontColor: this.selectFontColor,onTabClick: (index: number) => {this.onTabClick(index)},isDividerVisible: this.isDividerVisible})}})}// .width(Constants.FULL_PARENT).height(44).listDirection(Axis.Horizontal).alignListItem(ListItemAlign.Center).scrollBar(BarState.Off)// .margin({ right: 8 })}.layoutWeight(1).justifyContent(this.isTabBarCenter ? FlexAlign.Center : FlexAlign.Start)}
}@ComponentV2
export struct TabBarViewComp {@Param private index: number = 0@Param currentTabIndex: number = 0@Param tabs: Array<TabBarModel> = new Array<TabBarModel>()//选中字体颜色@Param selectFontColor: ResourceColor = $r('app.color.color_222222');@Param onTabClick: (index: number) => void = () => {};@Param isDividerVisible: boolean = true;build() {Column() {//右上角图片Image(this.tabs[this.index].rightSrc).height(11).margin({ left: 46 }).visibility(this.tabs[this.index].isShowRightSrc ? Visibility.Visible : Visibility.None)Text(this.tabs[this.index].name).fontSize(this.currentTabIndex == this.index ? 16 : 14).fontColor(this.currentTabIndex == this.index ? this.selectFontColor : $r('app.color.color_505050')).fontWeight(this.currentTabIndex == this.index ? FontWeight.Bold : FontWeight.Normal).margin({ top: this.tabs[this.index].isShowRightSrc ? 0 : 11 })Divider().width(16).height(4).backgroundColor($r('app.color.colorPrimary')).margin({ top: 4, bottom: 4 }).borderRadius(12).visibility(this.isDividerVisible && this.currentTabIndex == this.index ? Visibility.Visible : Visibility.Hidden)}.margin({ right: 15 }).onClick(() => {this.onTabClick(this.index)})}
}
- 6、使用案例如下,多种样式的使用:
import { ToastUtil } from '@pura/harmony-utils'
import { ListWithTabBarView, TabBarModel } from 'uicomponents'
import { CommonConst } from 'utils'
import { GetFourStyleTabData, GetOneStyleTabData, GetThreeStyleTabData, GetTwoStyleTabData } from './TestModel'@Preview
@ComponentV2
export struct TestTabBarView {@Local currentTabIndex: number = 0@Local isShowDivider: boolean = GetOneStyleTabData[0].isShowDivider@Local currentTabIndex2: number = 0@Local currentTabIndex3: number = 0@Local currentTabIndex4: number = 0build() {NavDestination() {Column() {Text('第一种样式').text(Color.Red)ListWithTabBarView({currentTabIndex: this.currentTabIndex,tabs: GetOneStyleTabData,tabBarLeft: this.tabBarLeft,tabBarRight: this.tabBarRight,tabContentBuilder: this.tabContentBuilder,isShowDivider: this.isShowDivider,changeFactory: (currentTabIndex, isShowDivider) => {this.currentTabIndex = currentTabIndexthis.isShowDivider = isShowDivider}}).height(80)Text('第二种样式').text(Color.Pink)ListWithTabBarView({currentTabIndex: this.currentTabIndex2,tabs: GetTwoStyleTabData,tabBarLeft: this.tabBarLeft2,tabBarRight: this.tabBarRight2,tabContentBuilder: this.tabContentBuilder,changeFactory: (currentTabIndex) => {this.currentTabIndex2 = currentTabIndex},isTabBarCenter: true}).height(80)Text('第三种样式').text(Color.Blue)ListWithTabBarView({currentTabIndex: this.currentTabIndex,tabs: GetThreeStyleTabData,tabBarLeft: this.tabBarLeft3,tabBarRight: this.tabBarRight3,tabContentBuilder: this.tabContentBuilder,isTabBarCenter: true,changeFactory: (currentTabIndex) => {this.currentTabIndex = currentTabIndex}}).height(80)Text('第四种样式').text(Color.Grey)ListWithTabBarView({currentTabIndex: this.currentTabIndex4,tabs: GetFourStyleTabData,tabBarLeft: this.tabBarLeft4,tabContentBuilder: (tab): void => this.tabContentBuilder(tab),isShowDivider: true,changeFactory: (currentTabIndex) => {this.currentTabIndex4 = currentTabIndex}}).layoutWeight(1).height(80)}.width(CommonConst.FULL_PARENT).height(CommonConst.FULL_PARENT)}}@BuildertabBarLeft() {Text().width(12)}@BuildertabBarRight() {Row({ space: 12 }) {Image($r('app.media.app_icon')).width(24).visibility(this.currentTabIndex == 0 || this.currentTabIndex == 1 ? Visibility.Visible : Visibility.Hidden).onClick(() => {ToastUtil.showToast('点了1')})Image($r('app.media.app_icon')).width(24).onClick(() => {ToastUtil.showToast('点了')})}.padding({ right: 12 })}@BuildertabContentBuilder($$: TabBarModel) {Text($$.id)}@BuildertabBarLeft2() {Image($r('app.media.ic_arrow_left')).width(24).margin({ left: 12, right: 6 }).onClick(() => {ToastUtil.showToast('返回键')// ZRouter.pop()})}@BuildertabBarRight2() {Image($r('app.media.app_icon')).width(24).margin({ right: 12 }).onClick(() => {ToastUtil.showToast('点了')})}@BuildertabBarLeft3() {Image($r('app.media.app_icon')).width(24).fillColor(Color.Black).margin({ left: 12, right: 20 }).onClick(() => {ToastUtil.showToast('设置')})}@BuildertabBarRight3() {Image(this.currentTabIndex == 1 ? $r('app.media.ic_next') : $r('app.media.ic_local_search')).width(24).margin({ right: 12 }).visibility(this.currentTabIndex != 2 ? Visibility.Visible : Visibility.Hidden).onClick(() => {ToastUtil.showToast(this.currentTabIndex == 1 ? '点了1' : '搜索')})}@BuildertabBarLeft4() {Text().width(12)}
}@Extend(Text)
function text(color: ResourceColor) {.height(44).width(CommonConst.FULL_PARENT).fontColor(Color.White).backgroundColor(color)
}
以往系列文章
- 《探索 HarmonyOS NEXT(5.0):开启构建模块化项目架构奇幻之旅 —— 模块化基础篇》
- 《探索 HarmonyOS NEXT(5.0):开启构建模块化项目架构奇幻之旅 —— 构建基础特性层》
- 《探索 HarmonyOS NEXT(5.0):开启构建模块化项目架构奇幻之旅 —— 构建公共能力层》
- 《探索 HarmonyOS NEXT(5.0):开启构建模块化项目架构奇幻之旅 —— Tabs底部导航栏》
- 《探索 HarmonyOS NEXT (5.0):开启构建模块化项目架构奇幻之旅 —— 动态路由 ZRouter:引领高效模块通信的智慧中枢》
- 《探索 HarmonyOS NEXT(5.0):开启构建模块化项目架构奇幻之旅 ——第三方库的使用:网络请求RCP、二次封装上下拉刷新、弹窗》
- HarmonyOS NEXT:模块化项目 ——修改应用图标+启动页等
- HarmonyOSNext模块化设计实践:打造简洁高效的登录注册页面
若本文对您稍有帮助,诚望您不吝点赞,多谢。
有兴趣的同学可以点击查看源码
- gitee:https://gitee.com/jiaojiaoone/explore-harmony-next/tree/case%2Fwanandroid/
- github:https://github.com/JasonYinH/ExploreHarmonyNext.git