前言
做鸿蒙有几个月的时间了,最近接到一个比较大的页面改造需求,页面内除了顶部主页信息介绍外,下面是由一个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)
}
}
代码直接贴上来了,希望可以帮助到有需要的朋友。