当前位置: 首页 > news >正文

自定义 TabBar 实战:浮动标签栏与舵式标签栏

文章目录

    • 一、为什么需要自定义 TabBar?
    • 二、公共数据类型
    • 三、浮动标签栏
      • 3.1 视觉效果图
      • 3.2 FloatingTabBar 核心实现
      • 3.3 父页面:Stack 叠层组合
    • 四、舵式标签栏
      • 4.1 视觉结构
      • 4.2 SteeredTabBar 核心实现
      • 4.3 与 Swiper 联动实现手势滑动
    • 五、尺寸规范参考
    • 总结

一、为什么需要自定义 TabBar?

系统默认的 Tabs 组件能快速搭建标准底部导航,但遇到以下场景时无能为力:

场景系统 Tabs 是否支持
标准底部导航(图标+文字)支持,直接用BarPosition.End
带阴影和圆角的浮动标签栏不支持,需完全自定义
突出中间项的舵式标签栏不支持,需完全自定义
选中指示条动画有限,需配合自定义实现

二、公共数据类型

两种标签栏都依赖项目的TabItemTabBarTheme接口:

// entry/src/main/ets/types/TabBarTypes.etsexportinterfaceTabItem{id:string;// 唯一标识icon:Resource;// 未选中图标activeIcon:Resource;// 选中图标title:string;// 标签文字badge?:number;// 角标数量(可选)showDot?:boolean;// 是否显示红点(可选)}exportinterfaceTabBarTheme{backgroundColor:ResourceColor;activeColor:ResourceColor;inactiveColor:ResourceColor;height:number;iconSize:number;fontSize:number;}

代码说明:把数据结构和主题配置拆成独立接口,修改外观只需换一个主题对象,UI 代码不需要改动。

三、浮动标签栏

3.1 视觉效果图

3.2 FloatingTabBar 核心实现

@Componentstruct FloatingTabBar{@LinkcurrentIndex:number;// 双向绑定,子组件可直接修改父组件状态@Proptheme:TabBarTheme;@Proptabs:TabItem[];onTabChange?:(index:number)=>void;build(){Row(){ForEach(this.tabs,(tab:TabItem,index:number)=>{Column(){SymbolGlyph(this.currentIndex===index?tab.activeIcon:tab.icon).fontSize(24)// SymbolGlyph.fontColor 接受 ResourceColor[] 数组,必须用方括号包裹.fontColor(this.currentIndex===index?[this.theme.activeColor]:[this.theme.inactiveColor])Text(tab.title).fontSize(this.theme.fontSize)// Text.fontColor 直接传 ResourceColor,无需数组.fontColor(this.currentIndex===index?this.theme.activeColor:this.theme.inactiveColor).margin({top:2}).fontWeight(this.currentIndex===index?FontWeight.Medium:FontWeight.Normal)// 选中指示条:使用 if/else + transition 实现淡入动画if(this.currentIndex===index){Row().width(20).height(3).backgroundColor(this.theme.activeColor).borderRadius(1.5).margin({top:4}).transition({type:TransitionType.Insert,opacity:0}).animation({duration:200,curve:Curve.EaseInOut})}else{Row().width(20).height(3).margin({top:4})}}.width(64).height(this.theme.height).justifyContent(FlexAlign.Center).onClick(()=>{this.onTabChange?.(index);})})}.height(this.theme.height+16).padding({left:16,right:16,top:8,bottom:8}).margin({left:24,right:24,bottom:20})// 不贴边,四周留白.backgroundColor(this.theme.backgroundColor).borderRadius(28)// 大圆角.justifyContent(FlexAlign.SpaceEvenly).shadow({radius:20,color:'rgba(0, 0, 0, 0.08)',offsetX:0,offsetY:4// 向下偏移,模拟悬浮投影}).expandSafeArea([SafeAreaType.SYSTEM],[SafeAreaEdge.BOTTOM])}}

代码说明:

  • @Link双向绑定,父组件传递$currentIndex,子组件内部可直接读写,两侧数据同步;
  • SymbolGlyph.fontColor()参数类型是ResourceColor[],一定要写成数组形式;而Text.fontColor()参数是ResourceColor,不需要数组;
  • 指示条用if/else条件渲染配合transition,这样在元素插入时可以触发淡入动画;
  • .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])让标签栏延伸到系统导航条区域,避免被遮挡。

3.3 父页面:Stack 叠层组合

@Entry@Componentstruct FloatingTabsDemo{@StatecurrentIndex:number=0;@Statetheme:TabBarTheme=FloatingTheme;@Statetabs:TabItem[]=[];build(){Stack({alignContent:Alignment.Bottom}){// Stack 让标签栏叠在内容上方Column(){FloatingContentPage({...})}.width('100%').height('100%')FloatingTabBar({currentIndex:$currentIndex,// $ 传递双向绑定引用theme:this.theme,tabs:this.tabs,onTabChange:(index:number)=>{animateTo({duration:250,curve:Curve.EaseInOut},()=>{this.currentIndex=index;});}})}.width('100%').height('100%')}}

代码说明:Stack布局是浮动标签栏的关键,它让标签栏悬浮在内容上方,而不是和内容并排布局。animateTo包裹状态变更,为内容区切换添加过渡动画。


四、舵式标签栏

4.1 视觉结构

标签数量必须是奇数(3、5、7),保证中间项落在正中央。

4.2 SteeredTabBar 核心实现

// entry/src/main/ets/pages/demos/SteeredTabsDemo.ets@Componentstruct SteeredTabBar{@LinkcurrentIndex:number;@Statetabs:TabItem[]=[];// 动态计算中间索引,支持任意奇数个标签privateisCenterItem(index:number):boolean{returnindex===Math.floor(this.tabs.length/2);}build(){Row(){ForEach(this.tabs,(tab:TabItem,index:number)=>{if(this.isCenterItem(index)){// 中间突出项:圆形背景Column(){SymbolGlyph(this.currentIndex===index?tab.activeIcon:tab.icon).fontSize(28).fontColor(['#FFFFFF'])}.width(56).height(56).backgroundColor('#007AFF').borderRadius(28)// 宽高一半 = 正圆.justifyContent(FlexAlign.Center).shadow({radius:8,color:'rgba(0, 122, 255, 0.3)',// 阴影颜色与背景色呼应offsetX:0,offsetY:4}).onClick(()=>{this.currentIndex=index;})}else{// 普通项Column(){SymbolGlyph(this.currentIndex===index?tab.activeIcon:tab.icon).fontSize(24).fontColor(this.currentIndex===index?['#007AFF']:['#8E8E93'])Text(tab.title).fontSize(11).fontColor(this.currentIndex===index?'#007AFF':'#8E8E93').margin({top:2})}.width(60).height(50).justifyContent(FlexAlign.Center).onClick(()=>{this.currentIndex=index;})}})}.width('100%').height(70).padding({bottom:8}).justifyContent(FlexAlign.SpaceEvenly).alignItems(VerticalAlign.Bottom)// 底部对齐,中间圆形按钮自然向上凸出}}

代码说明:

  • isCenterItem()Math.floor(length / 2)动态计算中间索引,标签数量变化无需改代码;
  • 圆形通过borderRadius等于宽高的一半来实现,width(56)+borderRadius(28)即正圆;
  • alignItems(VerticalAlign.Bottom)让所有项底部对齐,中间项更高,视觉上自然向上凸起;
  • 中间项的阴影颜色用带透明度的蓝色,与背景色呼应,比黑色阴影更精致。

4.3 与 Swiper 联动实现手势滑动

本项目的舵式标签栏使用Swiper作为内容区,支持手势滑动:

Swiper(){ForEach(this.contents,(item:string[],index:number)=>{ContentPage({...})})}.index(this.currentIndex)// 与 currentIndex 绑定.loop(false).indicator(false)// 隐藏默认指示点,标签栏已起到指示作用.duration(300).onChange((index:number)=>{this.currentIndex=index;// 手势滑动时同步更新标签选中状态})

代码说明:SwiperSteeredTabBar共享同一个currentIndex,互相监听onChange,任一方的变化都会驱动另一方更新,无需手动同步。


五、尺寸规范参考

场景属性推荐值
浮动左右外边距20-24vp
浮动圆角半径24-28vp
浮动阴影模糊半径16-24vp
舵式中间项直径52-60vp
舵式标签数量3、5 或 7(奇数)

总结

浮动标签栏和舵式标签栏看起来效果很炫,但核心原理其实很简单:前者靠Stack叠层、圆角和shadow实现悬浮感,后者靠borderRadius等于宽高一半画出正圆突出中间项。两种样式背后的逻辑都是:把barHeight设为 0 隐藏系统默认标签栏,然后自己用组件拼出想要的 UI。希望这两个完整的例子能让你在拿到设计稿时,少走一些弯路,直接动手就能写出来。

http://www.cnnetsun.cn/news/2023317.html

相关文章:

  • Java原生镜像内存“黑盒”终于被打开!用jcmd+native-image-agent反向追踪对象驻留路径——7类不可达但不释放内存场景全曝光
  • 别再为OTA升级失败抓狂了!手把手教你排查涂鸦IoT平台MCU固件升级的6个常见坑
  • 企业AI转型最大的障碍是什么?
  • 微积分基本定理实战:5个常见积分上限函数求导案例解析
  • Retinex算法实战:从原理到Python代码的完整图像增强方案
  • Qwen、QwQ、Qwen2.5...阿里通义千问大模型家族命名全解析!秒懂模型选型秘籍!
  • 自动驾驶中的MPC:从状态空间建模到轨迹跟踪的完整流程解析
  • 手把手解决Grafana Loki的‘parse error‘问题(Chrom浏览器用户必看)
  • PyTorch训练报错‘CUDA kernel errors might be asynchronously reported’?手把手教你用CUDA_LAUNCH_BLOCKING定位真凶
  • STM32F405RG主频降到84MHz才稳定?聊聊MotorControl Workbench工程里那些硬件坑
  • 别再为Fastjson漏洞发愁了!1.2.68+版本开启SafeMode的三种实战姿势(附代码示例)
  • 生信分析避坑指南:用R处理韦恩图交集时,90%的人都会忽略的数据类型和文件保存问题
  • 别再死记硬背了!用PyTorch代码和Tensor手算,彻底搞懂BatchNorm、LayerNorm和GroupNorm的区别
  • 3种方法让普通鼠标秒变Mac神器:Mac Mouse Fix终极安装指南
  • RTKLIB数据处理全流程实战:从观测文件下载到RTKPOST解算出图
  • 别再死记硬背网络结构了!通过调试LeNet/AlexNet的PyTorch代码,真正搞懂CNN每一层在做什么
  • 告别混乱提示!用SE91消息类统一管理你的SAP Fiori/ABAP程序用户交互
  • 从零到一:用INA3221为你的树莓派或STM32项目添加专业级电源监控
  • 在Ubuntu 18.04上搞定Cadence IC617和MMSIM151:一份避开所有“坑”的完整安装记录
  • Claude Design:设计商品化
  • Revit插件开发进阶:如何设计一个专业且易用的Ribbon UI?聊聊按钮交互逻辑与用户体验
  • Spring Boot项目里,Jackson的convertValue还能这么玩?一个方法搞定多种对象转换
  • 用 OpenClaw 构建个人知识库:从几百字到 10 万字,让 AI 真正懂你
  • 从日志里揪出WebShell:手把手教你用D盾和河马分析Apache/Nginx访问日志(附排查脚本)
  • 医疗影像、卫星图、老照片修复都离不开它:一文搞懂OpenCV CLAHE的参数调优与避坑指南
  • CSS如何修复Safari下边框圆角溢出问题_利用background-clip属性修正
  • 荣耀机器人包揽半马前六,具身智能融资火热但商业化落地仍待解
  • Windows Subsystem for Android 完全指南:在 Windows 11 上畅享 Android 应用生态
  • 好写作AI:科研绘图的“学术导航仪”,专治“做了研究却画不出来”
  • Vant动态表单封装实战:从零构建可配置的VForm组件