回复
#HarmonyOS NEXT体验官# 自定义Tabs 原创
奥尼5354
发布于 2025-3-10 23:01
浏览
0收藏
背景
项目中Tabs的使用可以说是特别的频繁,但是官方提供的Tabs使用起来,存在tab选项卡切换动画滞后的问题。
0900086000300134184.20201216095126.86523331460016843504112994983392.png
原始动画无法满足产品的UI需求,因此,这篇文章将实现下面页面滑动,tab选项卡实时滑动的动画效果。
0900086000300134184.20201216095126.86523331460016843504112994983392.png
实现逻辑
需求讲解
- 需要实现固定宽度下,放下6个选项卡。
- 在没有选择时宽度均匀分配,选中时显示图标并且增加宽度。
- 实现下方内容区域滑动时,上面选项卡实时跳动。
- 实现动画效果,使整体操作更加流畅。
实现思路
1. 选项卡
- 选项卡使用Row布局组件+layoutWeight属性,来实现平均布局。通过选项卡的选择索引来实现是否选中判断。
- 选中时,layoutWeight值为1.5;没有选中时,layoutWeight值为1.
- 使用animation属性,只要layoutWeight值变化时,可以触发动画。
- 在外包裹的布局容器中,添加onAreaChange事件,用来计算整体Tab组件的宽度。
-
Row() { -
Text(name) -
.fontSize(16) -
.fontWeight(this.SelectedTabIndex == index ? FontWeight.Bold : FontWeight.Normal) -
.textAlign(TextAlign.Center) -
.animation({ duration: 300 }) -
Image($r('app.media.send')) -
.width(14) -
.height(14) -
.margin({ left: 2 }) -
.visibility(this.SelectedTabIndex == index ? Visibility.Visible : Visibility.None) -
.animation({ duration: 300 }) -
} -
.justifyContent(FlexAlign.Center) -
.layoutWeight(this.SelectedTabIndex == index ? 1.5 : 1) -
.animation({ duration: 300 })
2. 定位器
- 使用Rect定义背景的形状和颜色+Stack布局+position属性,实现定位器的移动。
- position属性中通过Left值的变化来实现Rect的移动。但是在swiper的滑动中会出现滑动一点然后松开的情况,因此,需要两个值同时在实现中间的移动过程。
-
Stack() { -
Rect() -
.height(30) -
.stroke(Color.Black) -
.radius(10) -
.width(this.FirstWidth) -
.fill("#bff9f2") -
.position({ -
left: this.IndicatorLeftOffset + this.IndicatorOffset, -
bottom: 0 -
}) -
.animation({ duration: 300, curve: Curve.LinearOutSlowIn }) -
} -
.width("100%") -
.alignRules({ -
center: { anchor: "Tabs", align: VerticalAlign.Center } -
})
3.主要内容区
- 使用Swiper组件加载对应的组件,这里需要注意的是,Demo没有考虑到内容比较多的优化方案,可以设置懒加载方案来实现性能的提升。
- onAnimationStart事件,实现监测控件是向左移动还是向右移动,并且修改IndicatorLeftOffset偏移值。
- onAnimationEnd事件,将中间移动过程值IndicatorOffset恢复成0。
- onGestureSwipe事件,监测组件的实时滑动,这个事件在onAnimationStart和onAnimationEnd事件之前执行,执行完后才会执行onAnimationStart事件。因此,这个方法需要实时修改定位器的偏移数值。
- 偏移数值是通过swiper的移动数值和整体宽度的比例方式进行计算,松手后的偏移方向,由onAnimationStart和onAnimationEnd事件来确定最终的距离
-
Swiper(this.SwiperController) { -
ForEach(this.TabNames, (name: string, index: number) => { -
Column() { -
Text(`${name} - ${index}`) -
.fontSize(24) -
.fontWeight(FontWeight.Bold) -
} -
.alignItems(HorizontalAlign.Center) -
.justifyContent(FlexAlign.Center) -
.height("100%") -
.width("100%") -
}) -
} -
.onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => { -
if (targetIndex > index) { -
this.IndicatorLeftOffset += this.OtherWidth; -
} else if (targetIndex < index) { -
this.IndicatorLeftOffset -= this.OtherWidth; -
} -
this.IndicatorOffset = 0 -
this.SelectedTabIndex = targetIndex -
}) -
.onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => { -
this.IndicatorOffset = 0 -
}) -
.onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => { -
let move: number = this.GetOffset(extraInfo.currentOffset); -
//这里需要限制边缘情况 -
if ((this.SelectedTabIndex == 0 && extraInfo.currentOffset > 0) || -
(this.SelectedTabIndex == this.TabNames.length - 1 && extraInfo.currentOffset < 0)) { -
return; -
} -
this.IndicatorOffset = extraInfo.currentOffset < 0 ? move : -move; -
}) -
.onAreaChange((oldValue: Area, newValue: Area) => { -
let width = newValue.width.valueOf() as number; -
this.SwiperWidth = width; -
}) -
.curve(Curve.LinearOutSlowIn) -
.loop(false) -
.indicator(false) -
.width("100%") -
.id("MainContext") -
.alignRules({ -
top: { anchor: "Tabs", align: VerticalAlign.Bottom }, -
bottom: { anchor: "__container__", align: VerticalAlign.Bottom } -
})
代码文件
- 里面涉及到资源的小图标,可以自己区定义的,文章就不提供了。
-
@Entry -
@ComponentV2 -
struct Index { -
/** -
* 标头名称集合 -
*/ -
@Local TabNames: string[] = ["飞机", "铁路", "自驾", "地铁", "公交", "骑行"] -
/** -
* Tab选择索引 -
*/ -
@Local SelectedTabIndex: number = 0 -
/** -
* 标点移动距离 -
*/ -
@Local IndicatorLeftOffset: number = 0 -
/** -
* 标点在swiper的带动下移动的距离 -
*/ -
@Local IndicatorOffset: number = 0 -
/** -
* 第一个宽度 -
*/ -
@Local FirstWidth: number = -1 -
/** -
* 其他的宽度 -
*/ -
@Local OtherWidth: number = -1 -
/** -
* Swiper控制器 -
*/ -
@Local SwiperController: SwiperController = new SwiperController() -
/** -
* Swiper容器宽度 -
*/ -
@Local SwiperWidth: number = 0 -
build() { -
RelativeContainer() { -
Stack() { -
Rect() -
.height(30) -
.stroke(Color.Black) -
.radius(10) -
.width(this.FirstWidth) -
.fill("#bff9f2") -
.position({ -
left: this.IndicatorLeftOffset + this.IndicatorOffset, -
bottom: 0 -
}) -
.animation({ duration: 300, curve: Curve.LinearOutSlowIn }) -
} -
.width("100%") -
.alignRules({ -
center: { anchor: "Tabs", align: VerticalAlign.Center } -
}) -
Row() { -
ForEach(this.TabNames, (name: string, index: number) => { -
Row() { -
Text(name) -
.fontSize(16) -
.fontWeight(this.SelectedTabIndex == index ? FontWeight.Bold : FontWeight.Normal) -
.textAlign(TextAlign.Center) -
.animation({ duration: 300 }) -
Image($r('app.media.send')) -
.width(14) -
.height(14) -
.margin({ left: 2 }) -
.visibility(this.SelectedTabIndex == index ? Visibility.Visible : Visibility.None) -
.animation({ duration: 300 }) -
} -
.justifyContent(FlexAlign.Center) -
.layoutWeight(this.SelectedTabIndex == index ? 1.5 : 1) -
.animation({ duration: 300 }) -
.onClick(() => { -
this.SelectedTabIndex = index; -
this.SwiperController.changeIndex(index, false); -
animateTo({ duration: 500, curve: Curve.LinearOutSlowIn }, () => { -
this.IndicatorLeftOffset = this.OtherWidth * index; -
}) -
}) -
}) -
} -
.width("100%") -
.height(30) -
.id("Tabs") -
.onAreaChange((oldValue: Area, newValue: Area) => { -
let tabWidth = newValue.width.valueOf() as number; -
this.FirstWidth = 1.5 * tabWidth / (this.TabNames.length + 0.5); -
this.OtherWidth = tabWidth / (this.TabNames.length + 0.5); -
}) -
Swiper(this.SwiperController) { -
ForEach(this.TabNames, (name: string, index: number) => { -
Column() { -
Text(`${name} - ${index}`) -
.fontSize(24) -
.fontWeight(FontWeight.Bold) -
} -
.alignItems(HorizontalAlign.Center) -
.justifyContent(FlexAlign.Center) -
.height("100%") -
.width("100%") -
}) -
} -
.onAnimationStart((index: number, targetIndex: number, extraInfo: SwiperAnimationEvent) => { -
if (targetIndex > index) { -
this.IndicatorLeftOffset += this.OtherWidth; -
} else if (targetIndex < index) { -
this.IndicatorLeftOffset -= this.OtherWidth; -
} -
this.IndicatorOffset = 0 -
this.SelectedTabIndex = targetIndex -
}) -
.onAnimationEnd((index: number, extraInfo: SwiperAnimationEvent) => { -
this.IndicatorOffset = 0 -
}) -
.onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => { -
let move: number = this.GetOffset(extraInfo.currentOffset); -
//这里需要限制边缘情况 -
if ((this.SelectedTabIndex == 0 && extraInfo.currentOffset > 0) || -
(this.SelectedTabIndex == this.TabNames.length - 1 && extraInfo.currentOffset < 0)) { -
return; -
} -
this.IndicatorOffset = extraInfo.currentOffset < 0 ? move : -move; -
}) -
.onAreaChange((oldValue: Area, newValue: Area) => { -
let width = newValue.width.valueOf() as number; -
this.SwiperWidth = width; -
}) -
.curve(Curve.LinearOutSlowIn) -
.loop(false) -
.indicator(false) -
.width("100%") -
.id("MainContext") -
.alignRules({ -
top: { anchor: "Tabs", align: VerticalAlign.Bottom }, -
bottom: { anchor: "__container__", align: VerticalAlign.Bottom } -
}) -
} -
.height('100%') -
.width('100%') -
.padding(10) -
} -
/** -
* 需要注意的点,当前方法仅计算偏移值,不带方向 -
* @param swiperOffset -
* @returns -
*/ -
GetOffset(swiperOffset: number): number { -
let swiperMoveRatio: number = Math.abs(swiperOffset / this.SwiperWidth); -
let tabMoveValue: number = swiperMoveRatio >= 1 ? this.OtherWidth : this.OtherWidth * swiperMoveRatio; -
return tabMoveValue; -
} -
}
总结
这里实现了新的Tab选项卡的定义。但是没有进行高度封装,想法是方便读者理解组件的使用逻辑,而不是直接提供给读者进行调用。希望这篇文章可以帮助到你~~
©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
赞
收藏
回复
相关推荐




















