关注

HarmonyOS6 高级标签栏实战:滚动模式、自定义 TabBar 与侧边导航

一、整体结构:入口页的三合一布局

@Entry
@Component
struct AdvancedTabsDemo {
  @State currentSection: number = 0;
  private sections: string[] = ['滚动标签栏', '自定义 TabBar', '侧边导航'];

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        SymbolGlyph($r('sys.symbol.arrow_left'))
          .fontSize(22)
          .fontColor(['#1A1A1A'])
          .padding(8)
          .onClick(() => { router.back(); })

        Text('高级标签栏特性')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .fontColor('#1A1A1A')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        Row().width(38)  // 占位,保持标题居中
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .border({ width: { bottom: 0.5 }, color: '#E5E5E5' })

      // 功能切换按钮组(胶囊样式)
      Row({ space: 8 }) {
        ForEach(this.sections, (section: string, index: number) => {
          Text(section)
            .fontSize(13)
            .fontWeight(this.currentSection === index ? FontWeight.Medium : FontWeight.Normal)
            .fontColor(this.currentSection === index ? '#FFFFFF' : '#555555')
            .padding({ left: 14, right: 14, top: 7, bottom: 7 })
            .borderRadius(16)
            .backgroundColor(this.currentSection === index ? '#007AFF' : '#EEEEEE')
            .onClick(() => {
              animateTo({ duration: 200 }, () => {
                this.currentSection = index;
              });
            })
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 10, bottom: 10 })
      .backgroundColor('#FFFFFF')

      // 内容区,根据 currentSection 条件渲染对应组件
      if (this.currentSection === 0) {
        ScrollableTabBarDemo().layoutWeight(1)
      } else if (this.currentSection === 1) {
        FullyCustomTabBarDemo().layoutWeight(1)
      } else {
        VerticalTabBarDemo().layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

代码说明:

  • 整体采用 Column 竖向布局,分为三层:顶部导航栏(固定 56px)、切换按钮组(自适应高度)、内容区(layoutWeight(1) 占满剩余空间)。原先使用 Stack 叠加会导致内容区被顶部遮挡,改为 Column 后三层互不干扰。
  • 切换按钮组用 ForEach 遍历 sections 数组,选中项背景变为蓝色、文字变白,未选中项为浅灰色,通过三元运算符动态切换,不需要额外维护样式状态。
  • 内容区使用 if / else if / else 条件渲染,切换时旧组件销毁、新组件创建,每个子组件都加了 .layoutWeight(1) 确保高度充满父容器。

二、可滚动标签栏(BarMode.Scrollable)

2.1 Fixed 与 Scrollable 的区别

Fixed 模式(3-5 个标签,等宽平铺):
+------------------------------------------+
|   关注        推荐        热榜        我的   |
+------------------------------------------+

Scrollable 模式(5 个以上标签,可横向滑动):
+------------------------------------------+
| 关注  推荐  热榜  发现  直播  教育  ... ->  |
+------------------------------------------+

经验法则:3-5 个标签用 BarMode.Fixed,5 个以上用 BarMode.Scrollable

2.2 自定义 TabBar 样式(@Builder)

系统默认 TabBar 只能设置文字和基础颜色。要实现"选中时加粗 + 底部指示线"的效果,需要用 @Builder 替换默认样式:

@Builder
tabBarBuilder(title: string, index: number) {
  Column() {
    Text(title)
      .fontSize(15)
      .fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
      .fontColor(this.currentIndex === index ? '#007AFF' : '#555555')
      .padding({ left: 4, right: 4 })

    // 选中时显示蓝色指示线,未选中时透明
    Divider()
      .strokeWidth(2)
      .color('#007AFF')
      .opacity(this.currentIndex === index ? 1 : 0)
      .width(20)
      .margin({ top: 4 })
  }
  .height(44)
  .justifyContent(FlexAlign.Center)
  .padding({ left: 8, right: 8 })
}

代码说明:

  • @Builder 方法接收 titleindex 两个参数,通过与 this.currentIndex 比较判断当前是否选中,实现颜色和字重的动态变化。
  • Divideropacity 在选中时为 1,未选中时为 0,而非通过 visibility 控制,这样布局高度不变,切换时不会产生抖动。

2.3 完整实现

@Component
struct ScrollableTabBarDemo {
  private tabsController: TabsController = new TabsController();
  @State currentIndex: number = 0;
  private tabTitles: string[] = [
    '关注', '推荐', '热榜', '发现', '直播', '教育', '科技', '经济', '读书', '文化'
  ];
  private contentItems: string[][] = [
    ['好友动态 A', '好友动态 B', '好友动态 C', '好友动态 D', '好友动态 E'],
    ['推荐内容 1', '推荐内容 2', '推荐内容 3', '推荐内容 4', '推荐内容 5'],
    // ... 其余 8 组省略
  ];

  build() {
    Column() {
      Tabs({
        barPosition: BarPosition.Start,
        index: this.currentIndex,
        controller: this.tabsController
      }) {
        ForEach(this.tabTitles, (title: string, tabIndex: number) => {
          TabContent() {
            // 内容区:Scroll 包裹列表,支持上下滚动
            Scroll() {
              Column({ space: 12 }) {
                ForEach(this.contentItems[tabIndex], (item: string, i: number) => {
                  Row() {
                    Column() {
                      Text(item)
                        .fontSize(16)
                        .fontColor('#1A1A1A')
                        .fontWeight(FontWeight.Medium)

                      Text(`这是"${title}"频道的第 ${i + 1} 条内容`)
                        .fontSize(13)
                        .fontColor('#999999')
                        .margin({ top: 4 })
                    }
                    .alignItems(HorizontalAlign.Start)
                    .layoutWeight(1)
                  }
                  .width('100%')
                  .padding(16)
                  .backgroundColor('#FFFFFF')
                  .borderRadius(10)
                })
              }
              .padding({ left: 16, right: 16, top: 12, bottom: 24 })
              .width('100%')
            }
            .scrollable(ScrollDirection.Vertical)
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring)   // 滚动到边界时弹性回弹
            .width('100%')
            .height('100%')
          }
          .tabBar(this.tabBarBuilder(title, tabIndex))  // 使用自定义 Builder
        })
      }
      .barMode(BarMode.Scrollable)    // 标签栏可横向滚动
      .scrollable(true)               // 内容区支持手势左右滑动
      .animationDuration(300)
      .barHeight(44)
      .divider({ strokeWidth: 0.5, color: '#E5E5E5' })
      .onChange((index: number) => {
        this.currentIndex = index;    // 手势滑动时同步更新选中态
      })
      .width('100%')
      .layoutWeight(1)
      .backgroundColor('#F5F5F5')
    }
    .width('100%')
    .height('100%')
  }
}

代码说明:

  • barMode(BarMode.Scrollable) 控制标签栏可横向滚动,.scrollable(true) 控制内容区支持手势左右滑动,两个属性相互独立,都需要设置。
  • 内容区用 Scroll 包裹 Column,设置 ScrollDirection.Vertical 为纵向滚动,EdgeEffect.Spring 让滚动到底部时有弹性回弹效果,提升手感。
  • tabIndex 作为下标索引从 contentItems 二维数组取对应的数据,内容与标签一一对应。
  • onChange 回调负责在用户手势滑动时同步 currentIndex,使 @Builder 中的激活样式随之更新。

三、完全自定义底部 TabBar(仿微信风格)

3.1 核心思路:隐藏系统 TabBar + 自绘底部 Row

系统 Tabs 的内置 TabBar 在样式定制上有限制,当设计稿对底部导航有特殊要求时,常用的方案是:

  1. 设置 barHeight(0) 将系统 TabBar 高度归零,视觉上完全隐藏;
  2. Tabs 下方另外绘制一个自定义 Row 作为底部导航;
  3. 点击自定义 Row 的条目时,同步更新 currentIndex 并调用 tabsController.changeIndex()

3.2 各 Tab 的内容用 @Builder 分离

@Builder
chatContent() {
  Scroll() {
    Column({ space: 0 }) {
      ForEach(this.chatList, (name: string, i: number) => {
        Row({ space: 12 }) {
          // 头像占位块
          Column()
            .width(44).height(44)
            .borderRadius(6)
            .backgroundColor(i % 2 === 0 ? '#A8D8EA' : '#FFCC99')

          Column() {
            Row() {
              Text(name)
                .fontSize(16).fontColor('#1A1A1A').layoutWeight(1)
              Text('昨天')
                .fontSize(12).fontColor('#999999')
            }
            .width('100%')

            Text('这是最新的一条消息内容...')
              .fontSize(13).fontColor('#999999')
              .margin({ top: 4 })
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor('#FFFFFF')

        Divider().strokeWidth(0.5).color('#E5E5E5').margin({ left: 72 })
      })
    }
  }
  .scrollable(ScrollDirection.Vertical)
  .scrollBar(BarState.Off)
  .edgeEffect(EdgeEffect.Spring)
  .width('100%').height('100%')
  .backgroundColor('#F0F0F0')
}

代码说明:

  • 每个 Tab 的内容通过独立的 @Builder 方法定义,结构清晰,不会让 build() 过于冗长。
  • 分割线 Divider.margin({ left: 72 }) 缩进,与头像右侧对齐,形成标准的列表分割线样式。
  • maxLines(1) 配合 textOverflow: TextOverflow.Ellipsis 保证消息预览超长时自动截断显示省略号。

3.3 Tabs 主体与自定义底部导航

build() {
  Column() {
    // 顶部标题栏:标题随 currentIndex 变化
    Row() {
      Text(this.tabData[this.currentIndex])
        .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1A1A1A')
        .layoutWeight(1).textAlign(TextAlign.Center)

      SymbolGlyph($r('sys.symbol.plus_circle'))
        .fontSize(22).fontColor(['#1A1A1A']).padding({ right: 8 })
    }
    .width('100%').height(52)
    .backgroundColor('#FFFFFF')
    .border({ width: { bottom: 0.5 }, color: '#E5E5E5' })

    // Tabs:barHeight(0) 隐藏系统标签栏
    Tabs({
      barPosition: BarPosition.End,
      index: this.currentIndex,
      controller: this.tabsController
    }) {
      TabContent() { this.chatContent() }
      TabContent() { this.contactContent() }
      TabContent() { this.discoverContent() }
      TabContent() { this.profileContent() }
    }
    .scrollable(false)       // 禁用手势滑动,避免与自定义逻辑冲突
    .barHeight(0)            // 系统 TabBar 高度为 0
    .animationDuration(200)
    .layoutWeight(1)
    .onChange((index: number) => {
      this.currentIndex = index;
    })

    // 自定义底部 TabBar
    Row() {
      ForEach(this.tabData, (title: string, index: number) => {
        Column() {
          SymbolGlyph(
            this.currentIndex === index ? this.activeIcons[index] : this.icons[index]
          )
            .fontSize(26)
            .fontColor(this.currentIndex === index ? ['#07C160'] : ['#999999'])

          Text(title)
            .fontSize(10)
            .fontColor(this.currentIndex === index ? '#07C160' : '#999999')
            .margin({ top: 3 })
        }
        .layoutWeight(1)
        .height(56)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          animateTo({ duration: 200 }, () => {
            this.currentIndex = index;
            this.tabsController.changeIndex(index);  // 驱动内容区切换
          });
        })
      })
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .border({ width: { top: 0.5 }, color: '#E5E5E5' })
  }
  .width('100%').height('100%')
  .backgroundColor('#F0F0F0')
}

代码说明:

  • barHeight(0) 必须设置,否则系统 TabBar 会占据空间并出现在自定义 Bar 上方。
  • 点击自定义 Bar 时需要同时做两件事:this.currentIndex = index(更新状态变量,让图标和颜色刷新)和 tabsController.changeIndex(index)(驱动 Tabs 内容区实际切换)。缺少其中任一步,都会造成底部 Bar 与内容区不同步。
  • animateTo 将两个状态变更包在同一次动画事务中,保证底部图标和内容区在同一帧切换,避免视觉撕裂。
  • 顶部标题 Text(this.tabData[this.currentIndex]) 会随选中项自动更新,实现了类微信"当前 Tab 名称作为页面标题"的效果。

四、侧边导航栏(图标 + 文字 + 指示边框)

4.1 布局结构

左侧用自定义 Column 实现,宽度固定为 72px;右侧用 Tabs(隐藏 TabBar)承载内容,layoutWeight(1) 自适应剩余宽度。

4.2 侧边条目:@Builder sideTabItem

@Builder
sideTabItem(title: string, icon: Resource, index: number) {
  Column({ space: 4 }) {
    SymbolGlyph(icon)
      .fontSize(22)
      .fontColor(this.currentIndex === index ? ['#007AFF'] : ['#888888'])

    Text(title)
      .fontSize(12)
      .fontWeight(this.currentIndex === index ? FontWeight.Medium : FontWeight.Normal)
      .fontColor(this.currentIndex === index ? '#007AFF' : '#666666')
  }
  .width('100%')
  .height(64)
  .justifyContent(FlexAlign.Center)
  .backgroundColor(this.currentIndex === index ? '#E8F3FF' : Color.Transparent)
  .border({
    width: { left: 3 },
    color: this.currentIndex === index ? '#007AFF' : Color.Transparent   // 选中时左侧蓝色边框
  })
  .onClick(() => {
    animateTo({ duration: 250, curve: Curve.EaseOut }, () => {
      this.currentIndex = index;
      this.tabsController.changeIndex(index);
    });
  })
}

代码说明:

  • 选中效果由三层叠加实现:蓝色图标和文字 + 浅蓝背景 #E8F3FF + 左侧 3px 蓝色实线边框,视觉上形成"当前选中条目高亮并凸显"的效果。
  • .border({ width: { left: 3 }, color: ... }) 仅设置左边框,其余三边宽度为 0,精确控制只在左侧显示指示线。
  • animateTo 指定了 curve: Curve.EaseOut,使切换动画有"快进慢出"的质感。

4.3 完整 build 结构

build() {
  Row() {
    // 左侧自定义导航栏
    Column() {
      ForEach(this.sideTabs, (title: string, index: number) => {
        this.sideTabItem(title, this.tabIcons[index], index)
      })
    }
    .width(72)
    .height('100%')
    .backgroundColor('#F7F7F7')
    .border({ width: { right: 0.5 }, color: '#E5E5E5' })

    // 右侧内容区(Tabs 隐藏系统 TabBar)
    Tabs({
      barPosition: BarPosition.Start,
      index: this.currentIndex,
      controller: this.tabsController
    }) {
      ForEach(this.sideTabs, (title: string, tabIndex: number) => {
        TabContent() {
          Column() {
            // 内容标题行
            Row() {
              Text(title)
                .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#1A1A1A')
            }
            .width('100%')
            .padding({ left: 16, right: 16, top: 16, bottom: 12 })

            // 可滚动列表
            Scroll() {
              Column({ space: 0 }) {
                ForEach(this.tabContents[tabIndex], (item: string) => {
                  Row() {
                    Text(item)
                      .fontSize(15).fontColor('#1A1A1A').layoutWeight(1)
                    SymbolGlyph($r('sys.symbol.chevron_right'))
                      .fontSize(14).fontColor(['#C0C0C0'])
                  }
                  .width('100%')
                  .padding({ left: 16, right: 16, top: 14, bottom: 14 })
                  .backgroundColor('#FFFFFF')

                  Divider().strokeWidth(0.5).color('#E5E5E5').margin({ left: 16 })
                })
              }
            }
            .scrollable(ScrollDirection.Vertical)
            .scrollBar(BarState.Off)
            .edgeEffect(EdgeEffect.Spring)
            .layoutWeight(1)
            .width('100%')
          }
          .width('100%').height('100%')
          .backgroundColor('#F5F5F5')
        }
      })
    }
    .barHeight(0)
    .scrollable(false)
    .animationDuration(300)
    .layoutWeight(1)
    .onChange((index: number) => {
      this.currentIndex = index;
    })
  }
  .width('100%').height('100%')
}

代码说明:

  • 右侧 Tabs 通过 barHeight(0) 隐藏系统 TabBar,scrollable(false) 禁止手势横向滑动,所有切换都由左侧点击驱动,避免用户误操作。
  • 右侧内容区每个 TabContent 内部是"标题行 + Scroll 列表"的结构。Column 设置了 height('100%')ScrolllayoutWeight(1) 填充标题行以下的所有剩余高度,保证列表区域可以正常滚动。
  • tabContents 是二维数组,tabIndex 作为下标取出当前 Tab 的内容项,与左侧导航的数量一一对应。

五、三种场景对比

场景推荐方案关键属性
标签数量 5 个以上BarMode.Scrollable.barMode(BarMode.Scrollable) + .scrollable(true)
需要代码驱动切换TabsController.changeIndex同时更新 currentIndex 状态变量
高度定制底部导航barHeight(0) + 自绘 Row.scrollable(false) 禁止手势冲突
侧边导航自定义左侧 Column + TabsbarHeight(0) + layoutWeight(1)

六、TabsController 使用要点

三个示例都用到了 TabsController,总结四个核心注意点:

注意点说明
索引范围必须在 [0, TabContent 数量 - 1] 内,越界调用会被忽略
动画changeIndex 本身不带动画,需用 animateTo 包裹
状态同步调用 changeIndex 后必须同步更新 currentIndex,否则自定义 Bar 样式不刷新
控制器独立一个 TabsController 只能绑定一个 Tabs,不能多组件共用

总结

这三个标签栏进阶用法解决的其实是同一类问题:系统默认能力不够用时,怎么拿回控制权。可滚动标签栏让十个以上的分类也能整洁呈现,完全自定义底部 Bar 让设计稿中任何形态的导航都能落地,侧边导航则打开了从手机到平板的布局适配思路。理解这三种模式之后,市面上绝大多数 App 的导航结构都有了对应的实现方式。代码写多了会发现,灵活性往往来自"隐藏系统提供的,自己重新画一遍",HarmonyOS 的 Tabs 给了足够的扩展空间,用好这些接口,导航部分基本不会卡壳。

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_33681891/article/details/160382964

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--