鸿蒙 自定义TabBar,仿AndroidTabLayout,底部指示条支持滑动缩放效果

前言

做鸿蒙有几个月的时间了,最近接到一个比较大的页面改造需求,页面内除了顶部主页信息介绍外,下面是由一个TabBar加上一个翻页容器构成的联动容器效果,类似于dou音的个人主页页面。
鸿蒙自带的Tab组件其效果过于生硬,底部指示条无法随着Tab的偏移而跟随移动,效果不是很理想;在网上也搜了好多的方案,或许是鸿蒙的知识库还没那么全面,未能找到任何一种能满足理想效果的实现方案,所以只能自己动手实现了。

吐槽

本想录屏上传一下最终实现效果,发现居然只能上传图片,TMD...

口述下效果:
1、选中的Tab底部会有一个指示条;
2、Tab左右滑动切换的过程中,指示条会跟着进行偏移滑动;
3、指示条在滑动偏移的过程中宽度还会变大在恢复到静止时的宽度;
总之和dou音首页顶部TabBar效果一致(多了个滑动过程指示条宽度变化的动效,不喜欢可以不加)

代码实现

自定义TabBar组件:

import { HashMap } from "@kit.ArkTS"

const INDICATOR_WIDTH = 25
const INDICATOR_HEIGHT = 3.5
const INDICATOR_BORDER_RADIUS = 1.5

@Component
export struct TabBar {
  // 当前选中的Tab索引
  @Link @Watch('onSelectIndexChange') selectIndex: number
  // Tab列表数据
  @Prop tabList: Array<string> = []
  // Tab的滑动偏移量
  @Prop @Watch('onTabOffsetChange') tabOffset: number = 0
  // Tab组件的高度
  @State tabHeight: number = 0
  // 指示条的宽度
  @State indicatorWidth: number = INDICATOR_WIDTH
  // 指示条在X轴的位置(左侧坐标)
  @State indicatorLeft: number = 0
  // 指示条选中的索引
  @State indicatorSelectIndex: number = 0
  // Tab条目的区域信息集合
  private tabItemAreas: HashMap<number, Area> = new HashMap()
  // Tab列表滚动器
  private scroller: Scroller = new Scroller()

  aboutToAppear(): void {
    this.indicatorSelectIndex = this.selectIndex
  }

  /**
   * 响应Tab选中的索引变化
   */
  onSelectIndexChange() {
    this.tabOffset = 0
    this.indicatorSelectIndex = this.selectIndex
    this.scroller.scrollToIndex(this.selectIndex, true, ScrollAlign.CENTER)
  }

  /**
   * 响应Tab的滑动偏移
   */
  onTabOffsetChange() {
    if (this.tabOffset > 0 && this.tabOffset < 1) { // 向左滑
      if (this.selectIndex === 0) {
        return
      }

      if (this.tabOffset > 0.5) { // 左边的频道漏出比例大
        this.indicatorWidth = INDICATOR_WIDTH + INDICATOR_WIDTH * (1 - this.tabOffset)
        this.indicatorSelectIndex = this.selectIndex - 1
      } else { // 右边的频道漏出比例大
        this.indicatorWidth = INDICATOR_WIDTH + INDICATOR_WIDTH * this.tabOffset
        this.indicatorSelectIndex = this.selectIndex
      }
      // 计算滑动过程中指示条的位置
      const preIndicatorLeft = this.calculateTabIndicatorLeft(this.selectIndex - 1)
      const curIndicatorLeft = this.calculateTabIndicatorLeft(this.selectIndex)
      this.indicatorLeft = preIndicatorLeft + (curIndicatorLeft - preIndicatorLeft) * (1 - this.tabOffset)
    } else if (this.tabOffset > -1 && this.tabOffset < 0) { // 向右滑
      if (this.selectIndex === this.tabList.length - 1) {
        return
      }

      if (this.tabOffset < -0.5) { // 右边的频道漏出比例大
        this.indicatorWidth = INDICATOR_WIDTH + INDICATOR_WIDTH * (1 + this.tabOffset)
        this.indicatorSelectIndex = this.selectIndex + 1
      } else { // 左边的频道漏出比例大
        this.indicatorWidth = INDICATOR_WIDTH + INDICATOR_WIDTH * (-this.tabOffset)
        this.indicatorSelectIndex = this.selectIndex
      }
      // 计算滑动过程中指示条的位置
      const nextIndicatorLeft = this.calculateTabIndicatorLeft(this.selectIndex + 1)
      const curIndicatorLeft = this.calculateTabIndicatorLeft(this.selectIndex)
      this.indicatorLeft = curIndicatorLeft + (nextIndicatorLeft - curIndicatorLeft) * (-this.tabOffset)
    } else {
      this.tabOffset = 0
    }
  }

  /**
   * 计算Tab指示条的X(左侧)坐标
   * @param tabIndex
   * @returns
   */
  private calculateTabIndicatorLeft(tabIndex: number): number {
    const tabArea = this.tabItemAreas.get(tabIndex)
    const tabX = Number(tabArea?.globalPosition?.x ?? 0) ?? 0
    const tabWidth = Number(tabArea?.width ?? 0) ?? 0
    return tabX + (tabWidth - INDICATOR_WIDTH) / 2
  }

  build() {
    Stack() {
      // Tab列表
      List({ scroller: this.scroller }) {
        ForEach(this.tabList, (item: string, index: number) => {
          ListItem() {
            Column() {
              Text(item)
                .fontSize(20)
                .fontColor(this.selectIndex === index ? '#000000' : '#333333')
                .fontWeight(this.selectIndex === index ? FontWeight.Bold : FontWeight.Normal)
              // Tab静止时显示的指示条
              Row()
                .width(INDICATOR_WIDTH)
                .height(INDICATOR_HEIGHT)
                .backgroundColor('#4984ef')
                .borderRadius(INDICATOR_BORDER_RADIUS)
                .margin({ top: 5 })
                .visibility(this.indicatorSelectIndex === index && this.tabOffset === 0
                  ? Visibility.Visible : Visibility.Hidden)
            }
          }
          .margin({ left: 15, right: 15, })
          .onAreaChange((oldValue: Area, newValue: Area) => {
            if (Number(newValue.height) > Number(this.tabHeight)) {
              this.tabHeight = Number(newValue.height)
            }
            this.tabItemAreas.set(index, newValue)
          })
          .onClick(() => {
            this.selectIndex = index
          })
        }, (item: string, index: number) => {
          return item
        })
      }
      .width('100%')
      .height(this.tabHeight === 0 ? 'auto' : this.tabHeight)
      .listDirection(Axis.Horizontal)
      .scrollBar(BarState.Off)

      // Tab滑动偏移时显示的指示条
      Row()
        .width(this.indicatorWidth)
        .height(INDICATOR_HEIGHT)
        .backgroundColor('#4984ef')
        .borderRadius(INDICATOR_BORDER_RADIUS)
        .margin({ left: this.indicatorLeft })
        .visibility(this.tabOffset !== 0 ? Visibility.Visible : Visibility.Hidden)
    }
    .alignContent(Alignment.BottomStart)
  }
}

页面引用:

import { TabBar } from '../components/TabBar'

@Entry
@Component
struct Index {
  @State tabList: Array<string> = ['One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten']
  @State selectIndex: number = 0
  @State tabOffset: number = 0

  build() {
    Column() {
      // 顶部bar
      TabBar({ selectIndex: this.selectIndex, tabList: this.tabList, tabOffset: this.tabOffset })
        .width('100%')
        .height('auto')
      // 翻页容器
      Swiper() {
        this.tabContentBuilder()
      }
      .width('100%')
      .height('100%')
      .index(this.selectIndex)
      .loop(false)
      .onChange((index: number) => {
        this.selectIndex = index
      })
      .onContentDidScroll((selectedIndex: number, index: number, position: number, mainAxisLength: number) => {
        if (selectedIndex === index) {
          this.tabOffset = position
          console.log("Index--onContentDidScroll--" + selectedIndex)
        }
      })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  private tabContentBuilder() {
    ForEach(this.tabList, (item: string, index: number) => {
      Stack() {
        Text(item + "--" + (index + 1))
          .fontSize(30)
          .fontColor(Color.Black)
          .fontWeight(FontWeight.Bold)
      }
    }, (item: string) => item)
  }
}


代码直接贴上来了,希望可以帮助到有需要的朋友。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容