HarmonyOS 6.0 自定义TabBar——从基础到动效
Tabs组件自带TabBar但说实话默认样式基本不能直接上生产。圆角背景、指示器动画、图标颜色切换、Badge红点——这些都得自己画。自定义TabBar的核心思路是隐藏默认TabBar用自定义布局替代通过Link或Prop与Tabs联动。隐藏默认TabBarTabs({barPosition:BarPosition.End}){TabContent(){Text(首页)}TabContent(){Text(发现)}TabContent(){Text(我的)}}.barHeight(0)// 隐藏默认TabBarbarHeight(0)是最干净的方式。.barOpacity(0)也行但还是会占空间。自定义TabBar布局import{curves}fromkit.ArkUI;interfaceTabItem{title:stringicon:Resource selectedIcon:Resource}Componentstruct CustomTabBar{PropcurrentIndex:number0Linktabs:TabItem[]onTabClick:(index:number)void(){}build(){Row(){ForEach(this.tabs,(item:TabItem,index:number){Column(){Image(this.currentIndexindex?item.selectedIcon:item.icon).width(24).height(24).objectFit(ImageFit.Contain)Text(item.title).fontSize(12).fontColor(this.currentIndexindex?#007DFF:#999999).margin({top:4})}.layoutWeight(1).height(56).justifyContent(FlexAlign.Center).onClick(()this.onTabClick(index))},(item:TabItem,index:number)index.toString())}.width(100%).height(56).backgroundColor(Color.White).shadow({radius:8,color:#1F000000,offsetY:-1})}}onTabClick回调需要在父组件里切换Tabs的currentIndexComponentstruct IndexPage{StatecurrentIndex:number0StatetabItems:TabItem[][{title:首页,icon:$r(app.media.ic_home),selectedIcon:$r(app.media.ic_home_selected)},{title:发现,icon:$r(app.media.ic_discover),selectedIcon:$r(app.media.ic_discover_selected)},{title:我的,icon:$r(app.media.ic_me),selectedIcon:$r(app.media.ic_me_selected)}]build(){Column(){Tabs({index:this.currentIndex}){TabContent(){HomePage()}TabContent(){DiscoverPage()}TabContent(){MePage()}}.barHeight(0).scrollable(false).onChange((index:number){this.currentIndexindex})CustomTabBar({currentIndex:this.currentIndex,tabs:$tabItems,onTabClick:(index:number){this.currentIndexindex}})}}}要点Tabs的index绑定State变量自定义TabBar的点击修改这个变量Tabs的onChange同步回来。双向同步是关键。scrollable(false)禁止左右滑动切换——很多应用要求TabBar和滑动不联动。指示器动画在选中Tab下方加一条横线带弹簧动画Componentstruct AnimatedTabBar{PropcurrentIndex:number0LinktabTitles:string[]onTabClick:(index:number)void(){}build(){Stack({alignContent:Alignment.Bottom}){Row(){ForEach(this.tabTitles,(title:string,index:number){Column(){Text(title).fontSize(16).fontWeight(this.currentIndexindex?FontWeight.Bold:FontWeight.Normal).fontColor(this.currentIndexindex?#333333:#999999)}.layoutWeight(1).height(44).justifyContent(FlexAlign.Center).onClick(()this.onTabClick(index))},(title:string,index:number)index.toString())}.width(100%)// 指示器Row(){Divider().width(20).height(3).color(#007DFF).borderRadius(2)}.width(100%).justifyContent(FlexAlign.Start).margin({left:this.currentIndex*(100/this.tabTitles.length)%}).animation({duration:300,curve:Curve.EaseOut})}.height(44)}}margin的百分比计算基于Tab数量均分。animation修饰器让margin变化产生动画。但百分比动画的组合在某些场景不够精确更可靠的方式是用具体px值。用px的方式需要先获取每个Tab的宽度可以通过onAreaChange获取StatetabWidths:number[][]StateindicatorLeft:number0// 在每个Tab的Column上.onAreaChange((oldArea:Area,newArea:Area){letwidthNumber(newArea.width)this.tabWidths[index]widththis.updateIndicator()})updateIndicator(){letoffset:number0for(leti0;ithis.currentIndex;i){offsetthis.tabWidths[i]}letcurrentWidththis.tabWidths[this.currentIndex]this.indicatorLeftoffset(currentWidth-20)/2}中间凸起Tab很多应用中间Tab是凸起的发布按钮Row(){ForEach(this.tabItems,(item:TabItem,index:number){if(index2){// 中间凸起Column(){Image(item.selectedIcon).width(40).height(40)}.width(56).height(56).borderRadius(28).backgroundColor(#007DFF).margin({top:-28}).justifyContent(FlexAlign.Center).onClick(()this.onTabClick(index))}else{Column(){Image(this.currentIndexindex?item.selectedIcon:item.icon).width(24).height(24)Text(item.title).fontSize(12).fontColor(this.currentIndexindex?#007DFF:#999999)}.layoutWeight(1).height(56).justifyContent(FlexAlign.Center).onClick(()this.onTabClick(index))}},(item:TabItem,index:number)index.toString())}.width(100%).height(56).backgroundColor(Color.White)margin({ top: -28 })让中间按钮向上凸出半个高度。外层容器用Stack可以防止遮挡。Badge红点TabBar上未读消息红点import{Badge}fromkit.ArkUI;Badge({count:this.unreadCount,maxCount:99,position:BadgePosition.RightTop}){Image(this.currentIndexindex?item.selectedIcon:item.icon).width(24).height(24)}Badge组件包裹图标count为0时自动隐藏。maxCount超过显示99。颜色过渡动画选中/未选中的颜色切换加transition效果Text(item.title).fontSize(12).fontColor(this.currentIndexindex?#007DFF:#999999).animation({duration:200,curve:Curve.EaseInOut})animation修饰器加在fontColor所在组件上颜色变化就会动画过渡。但如果用的是Prop currentIndex的ternary判断颜色是直接切换的animation不会生效——因为ternary的结果是两个不同值直接替换。更好的方式是用属性动画Text(item.title).fontSize(12).fontColor(this.currentIndexindex?#007DFF:#999999).animation({duration:200,curve:Curve.EaseInOut})实际上在ArkUI中Prop变化触发UI刷新时如果组件属性有animation修饰属性值的变化会自动产生动画。ternary两端的颜色值变化确实可以产生过渡动画——前提是两个颜色在色相上相近系统可以做插值。完整联动模式Componentstruct TabHost{StatecurrentIndex:number0build(){Column(){Tabs({index:this.currentIndex}){TabContent(){/* 页面A */}TabContent(){/* 页面B */}TabContent(){/* 页面C */}}.barHeight(0).scrollable(false).onChange((index:number){this.currentIndexindex})CustomTabBar({currentIndex:this.currentIndex,onTabClick:(index:number){this.currentIndexindex}})}.height(100%)}}自定义TabBar点击 → 修改currentIndex → Tabs自动切换TabContent → Tabs的onChange同步currentIndex回CustomTabBar。这个循环保证滑动和点击两种操作都能正确同步。踩坑清单问题原因解决点击Tab没反应onTabClick没修改currentIndex回调里赋值this.currentIndex index滑动切换后TabBar没更新没监听Tabs.onChangeonChange里同步currentIndex指示器位置不准百分比计算偏差用onAreaChange获取实际宽度animation不生效修饰器位置不对animation必须紧跟需要动画的属性凸起按钮被裁剪外层overflow:hidden用Stack或设置clip(false)Badge不显示count为0或负数count必须大于0Tab切换闪烁scrollable手动index冲突设scrollable(false)禁滑动自定义TabBar不算复杂但联动逻辑容易出bug。核心就一条currentIndex是唯一真相源点击和滑动都修改它UI根据它渲染。