在ArkUI中的Tabs,通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。其中内容是图TabContent作为Tabs的自组件,通过给TabContent设置tabBar属性来自定义导航栏样式。现在我们就根据UI设计的效果图来实现下图效果:
根据上图分析可知,要实现以上效果需要下面这几步:
1、设置tabBar背景颜色,以及点击选中背景样式;
2、自定义导航栏指示器;
3、设置指示器跟随内容视图一起滑动切换效果。
设置tabBar背景颜色以及点击选中背景样式
1、首先我们需要使用@Builder修饰方法来表示这是一个自定义组件;
2、根据用户点击的tab索引和当前索引来设置背景图片和背景颜色,这里需要注意的是设置背景颜色的时候,注意左上角和右上角是有圆角的,需要根据索引判断是否展示圆角。
3、由于选中样式是带圆角的梯形,所以这里是用来3个不同的梯形切图。
@Builder
tabBuilder(title: string, targetIndex: number, selectImage: ResourceStr) {
// 创建一个Column布局
Column() {
// 创建一个Text组件,显示标题
Text(title)
// 根据当前索引和目标索引判断字体颜色
.fontColor(this.currentIndex === targetIndex ? $r("app.color.text_one") : $r("app.color.text_two"))
// 设置字体大小为14
.fontSize(14)
// 根据当前索引和目标索引判断字体粗细
.fontWeight(this.currentIndex === targetIndex ? 600 : 400)
}
// 设置Column的宽度为100%
.width('100%')
// 设置Column的高度为100%
.height("100%")
// 设置Column的子组件垂直居中对齐
.justifyContent(FlexAlign.Center)
// 根据当前索引和目标索引判断是否设置背景图片
.backgroundImage(this.currentIndex == targetIndex ? selectImage : null)
// 设置Column的背景颜色
.backgroundColor($r("app.color.bg_data_color"))
// 根据目标索引判断是否需要设置顶部左右圆角
.borderRadius({ topLeft: targetIndex == 0 ? 8 : 0, topRight: targetIndex == 2 ? 8 : 0 })
// 设置背景图片填充方式为填充整个容器
.backgroundImageSize(ImageSize.FILL)
}
自定义导航栏指示器
由于指示器需要跟随内容视图一起滑动切换,所以指示器不能在单个tabBuilder中设置。
1、使用Column组件定义底部指示器,设置一个宽度为文字宽度,高度为3的蓝色指示器;
2、这里的指示器宽度可以动态设置成文字的宽度,也可以直接设置成文字某个固定宽度;
3、指示器距离左边的距离需要动态设置,配上动画,可以实现指示器跟随手指滑动。
Stack() {
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
this.tripPage()
}.tabBar(this.tabBuilder("房源", 0, $r("app.media.trip_data_start_bg")))
.align(Alignment.TopStart).margin({ top: 54 })
...
...
...
...
}
.backgroundColor($r("app.color.white"))
.borderRadius(8)
.barHeight(44)
.width("93.6%")
.height(380)
.onChange((index) => {
this.currentIndex = index
})
//自定义指示器,设置一个宽度为文字宽度,高度为3的蓝色指示器
Column()
.width(this.indicatorWidth)
.height(3)
.backgroundColor($r("app.color.main_color"))
.margin({ left: this.indicatorLeftMargin, top: 42 })
.borderRadius(1)
}
添加指示器动画
要实现指示器跟随手指滑动,切换不同的tab,需要为指示器添加动画,监听Tabs动画开始和动画结束,以及手势监听。
/**
* 启动动画至指定位置
*
* @param duration 动画时长
* @param leftMargin 动画结束后的左边距
* @param width 动画结束后的宽度
*/
private startAnimateTo(duration: number, leftMargin: number, width: number) {
// 设置动画开始标志为true
this.isStartAnimateTo = true
animateTo({
// 动画时长
duration: duration, // 动画时长
// 动画曲线
curve: Curve.Linear, // 动画曲线
// 播放次数
iterations: 1, // 播放次数
// 动画模式
playMode: PlayMode.Normal, // 动画模式
// 动画结束时的回调函数
onFinish: () => {
// 将动画开始标志设置为false
this.isStartAnimateTo = false
// 输出动画结束信息
console.info('play end')
}
}, () => {
// 设置指示器的左边距
this.indicatorLeftMargin = leftMargin
// 设置指示器的宽度
this.indicatorWidth = width
})
}
1、动画开始的监听
Tab切换动画开始时,动画返回的目标索引设置为当前索引,调用startAnimateTo方法,给指示器设置动画,动态设置指示器的左边距。
Tabs({ barPosition: BarPosition.Start }) {}
.onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
// 切换动画开始时触发该回调。指示器跟着页面一起滑动。
this.currentIndex = targetIndex
this.startAnimateTo(this.animationDuration, this.textInfos[targetIndex][0], this.textInfos[targetIndex][1])
})
2、动画结束的监听
tab切换动画结束时,回触发onAnimationEnd监听。
Tabs({ barPosition: BarPosition.Start }) {}
.onAnimationEnd((index: number, event: TabsAnimationEvent) => {
// 切换动画结束时触发该回调。指示器动画停止。
let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event)
this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width)
})
3、手势滑动监听
在页面跟手滑动过程中,逐帧触发该回调。
Tabs({ barPosition: BarPosition.Start }) {}
.onGestureSwipe((index: number, event: TabsAnimationEvent) => {
// 在页面跟手滑动过程中,逐帧触发该回调。
let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event)
//设置当前索引
this.currentIndex = currentIndicatorInfo.index
//设置指示器距离左边间距
this.indicatorLeftMargin = currentIndicatorInfo.left
//指示器宽度设置
this.indicatorWidth = currentIndicatorInfo.width
})
封装获取指示器信息方法,返回指示器的索引,左边距和指示器宽度,在手势滑动监听中调用该方法,可以动态获取指示器的左边距,配合动画,可以实现指示器跟随手势滑动。从而实现UI设计效果。
/**
* 获取当前指示器信息
*
* @param index 当前索引
* @param event Tabs动画事件
* @returns 包含指示器索引、左边距和宽度的对象
*/
private getCurrentIndicatorInfo(index: number, event: TabsAnimationEvent): Record<string, number> {
// 当前Tab的索引
let nextIndex = index
// 如果当前索引大于0且滑动偏移量大于0,表示向左滑动,将nextIndex减1
if (index > 0 && event.currentOffset > 0) {
nextIndex--
}
// 如果当前索引小于3且滑动偏移量小于0,表示向右滑动,将nextIndex加1
else if (index < 3 && event.currentOffset < 0) {
nextIndex++
}
// 获取当前索引对应的Tab信息
let indexInfo = this.textInfos[index]
// 获取nextIndex对应的Tab信息
let nextIndexInfo = this.textInfos[nextIndex]
// 计算滑动比例
let swipeRatio = Math.abs(event.currentOffset / this.tabsWidth)
// 如果滑动比例大于0.5,则将currentIndex设为nextIndex,表示切换到下一页的tabBar
// 页面滑动超过一半,tabBar切换到下一页。
let currentIndex = swipeRatio > 0.5 ? nextIndex : index
// 根据滑动比例计算当前Tab的左边距
let currentLeft = indexInfo[0] + (nextIndexInfo[0] - indexInfo[0]) * swipeRatio
// 根据滑动比例计算当前Tab的宽度
let currentWidth = indexInfo[1] + (nextIndexInfo[1] - indexInfo[1]) * swipeRatio
// 返回包含当前Tab索引、左边距和宽度的对象
return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth }
}