鸿蒙6.0应用开发——Tabs滑动动画
Tabs组件在用户交互方面提供了丰富的特性,其中与滑动动作相关的交互尤为常见。下文将介绍几种与Tabs和滑动动作相关的特性。
文章目录
Tabs滑动
Tabs组件在用户交互方面提供了丰富的特性,其中与滑动动作相关的交互尤为常见。下文将介绍几种与Tabs和滑动动作相关的特性。
双层Tabs嵌套滑动
在应用开发中,开发者经常遇到多层Tabs嵌套使用的场景。如果父子Tabs组件均需滑动切换时,开发者需要对父子Tabs的滑动切换行为进行约束,以避免冲突。通常做法是,让滑动操作优先切换子Tabs页签,当子Tabs页签切换到最后一个后,再触发父Tabs的页签切换。

实现原理
可以通过PanGesture结合TabsController的changeIndex()方法实现双层Tabs的切换。具体操作为:开启子Tabs的滑动切换功能,同时关闭父Tabs的滑动切换。在子Tabs的第一个或者最后一个页面上添加PanGesture事件处理函数,用于判断滑动方向,并根据滑动方向使用TabsController的changeIndex()方法切换到父Tabs的相应页签。这样一来,子Tabs的中间页签滑动时,仅会触发子Tabs页签的切换,而最后一个页签的滑动则会通过changeIndex()方法间接触发父Tabs页签的切换。

开发步骤
-
外层Tabs组件中定义TabsController属性,以及内层Tabs双向绑定的状态属性变量switchNext及其监听函数。当监听到需要切换页签时,利用TabsController切换到对应页签。因为本示例外层Tabs和内层Tabs封装到不同的自定义组件中了,所以需要@Link修饰的switchNext变量作为父子组件的交互媒介。
@Component export default struct OutTabsComponent { // ... @State @Watch('onchangeSwitchNext') switchNext: boolean = false; // ... onchangeSwitchNext() { if (this.switchNext) { this.switchNext = false; this.tabsController.changeIndex(1); } } // ... build() { Tabs({ // ... controller: this.tabsController, }) { TabContent() { InTabsComponent({ switchNext: this.switchNext }) }.tabBar(this.tabBuilder(0, $r('app.string.out_bar_text_home'), $r('sys.symbol.house'))) // ... } // ... } } -
内层Tabs组件在最后一个TabContent中注册滑动事件处理函数,监听向左滑动作,触发时修改switchNext变量值传递给外层Tabs组件触发切换。
@Component export default struct InTabsComponent { // ... @Link switchNext: boolean; // ... build() { // ... Tabs({ // ... }) { // bind selected tabs to ui ForEach(this.selectTabsViewModel.selectedTabs, (tab: TabItemViewModel, index: number) => { if (index === this.selectTabsViewModel.selectedTabs.length - 1) { TabContent() { // ... } .tabBar(this.tabBuilder(index, tab)) .gesture(PanGesture(new PanGestureOptions({ direction: PanDirection.Left })).onActionStart(() => { this.switchNext = true; })) // ... } else { // ... } }, (tab: TabItemViewModel, index: number) => index + '_' + JSON.stringify(tab)) } // ... } } -
注意滑动切换在自定义切换动画场景下失效,故需要注释掉切换动画函数注册。
Tabs({ barPosition: BarPosition.Start, controller: this.subsController, barModifier: this.tabBarModifier }) { // ... } // add animation function .customContentTransition(this.customContentTransition) // comment out to slide to switch
可滚动Tabs页签栏+更多按钮
可滚动页签栏通常设置在顶部或侧边导航栏,当内容分类较多,屏幕显示区域无法完全展示所有分类页签时,该页签栏允许用户通过滚动来访问隐藏的页签内容。

实现原理
通过将Tabs组件的barMode属性设置为BarMode.Scrollable,可以实现可滚动的页签栏。若要实现添加更多按钮的效果,可以通过Stack布局结合barModifier功能实现。具体做法是在Tabs组件的TabBar位置的末端上层利用Stack布局添加更多按钮,并且点击该按钮时可以弹出窗口,在弹窗中自定义需要显示的页签。
开发步骤
设置barMode属性为BarMode.Scrollable,并利用Stack布局在TabBar右上角添加更多按钮。
Stack({ alignContent: Alignment.TopEnd }) {
Row() {
Image($r('app.media.more'))
// ...
.onClick(() => {
this.showSelectTabsComponent = !this.showSelectTabsComponent;
})
}
// ...
.zIndex(1)
.bindSheet($$this.showSelectTabsComponent, this.sheetBuilder(), {
detents: [SheetSize.MEDIUM, SheetSize.MEDIUM, 500],
preferType: SheetType.BOTTOM,
title: { title: $r('app.string.bind_sheet_title') },
onWillDismiss: (dismissSheetAction: DismissSheetAction) => {
// update tab when closing modal box
this.selectTabsViewModel.updateSelectedTabs();
if (this.selectTabsViewModel.selectedTabs.length > 0) {
this.subsController.changeIndex(0);
}
dismissSheetAction.dismiss();
}
})
Column() {
Tabs({
// ...
}) {
// ...
}
// ...
.barMode(BarMode.Scrollable)
// ...
}
.width(Constants.FULL_WIDTH)
.height(Constants.FULL_HEIGHT)
.backgroundColor($r('app.color.out_tab_bar_background_color'))
}
禁用TabContent左右滑动
默认情况下,导航栏支持滑动切换。当存在多级导航栏嵌套或导航栏中的其他组件需要占用滑动动作时,为避免滑动响应冲突,开发者可选择禁用Tabs组件的滑动切换功能。通过将Tabs组件的scrollable属性设置为false,可以禁止通过滑动TabContent来切换页签。同样,若想禁用边缘回弹效果,可将edgeEffect的值设置为EdgeEffect.None。
示例代码:
build() {
Tabs({
// ...
}) {
// ...
}
// ...
.scrollable(true) // false to disable scroll to switch
// .edgeEffect(EdgeEffect.None) // disables edge springback
// ...
}
Tabs页签加载/更新
在使用Tabs组件进行开发时,特别是当Tabs组件作为二级导航使用时,业务需求往往需要对Tabs的标签页进行更精细的控制。下文将介绍几种定制标签页显示逻辑的场景。
显示指定页签与预加载
Tabs组件的TabContent默认在首次切换到该标签页时加载。如果TabContent中的内容或初始化逻辑较为复杂,加载速度较慢,则会影响标签页切换的流畅性,进而影响用户体验。此时,如果应用能在切换前预加载相应的标签页,将显著提升使用流畅度。

实现原理
通过TabController的preloadItem()方法可以预加载指定子节点。该方法参数为需要预加载的index数组,无参调用此方法时,会一次性加载所有指定的子节点。因此,为了性能考虑,建议分批加载子节点。代码示例这里做法是当切换到某页签时,预加载所选页签左右两侧的页签内容。
开发步骤
定义subsController属性,并在Tabs的onChange函数中调用preloadItem()预加载当前页签两侧页签。
@Component
export default struct InTabsComponent {
// ...
private subsController: TabsController = new TabsController();
// ...
build() {
// ...
Tabs({
// ...
controller: this.subsController,
// ...
}) {
// ...
}
// ...
.onChange((index: number) => {
this.focusIndex = index;
this.tabBarItemScroller.scrollToIndex(index, true, ScrollAlign.CENTER);
// preload the left and right item
let preloadItems: number[] = [];
if (index - 1 >= 0) {
preloadItems.push(index - 1);
}
if (index + 1 < this.selectTabsViewModel.selectedTabs.length) {
preloadItems.push(index + 1);
}
this.subsController.preloadItems(preloadItems);
})
// ...
}
}
切换到指定页签
Tabs组件除了自带的滑动切换和点击切换功能外,还提供了两种可编程方式来切换页签。第一种是通过调用TabsController的changeIndex()方法,切换到指定的index;第二种是定义一个由@State修饰的变量currentIndex,并将其绑定到Tabs,通过修改currentIndex的值来触发页签切换。
开发步骤
定义currentIndex变量和tabController属性,并绑定到Tabs。在按钮onClick函数中,调用tabController.changeIndex()或者直接修改currentIndex变量切换页签。
@Component
export default struct SwitchTabComponent {
// ...
@State currentIndex: number = 0;
private tabController: TabsController = new TabsController();
// ...
build() {
Column() {
Row() {
Button('Previous Tab')
.onClick(() => {
this.tabController.changeIndex((this.currentIndex + 3) % 4); // call tabController.changeIndex() to switch tab
})
// ...
Button('Next Tab')
.onClick(() => {
this.currentIndex = (this.currentIndex + 1) % 4; // change currentIndex to switch tab
})
// ...
}
Tabs({
controller: this.tabController,
index: $$this.currentIndex // use $$ for two-way data binding
}) {
// ...
}
}
}
}
增删Tabs页签
在日常的应用开发中,经常需要实现用户自定义选择频道的功能。通常,这些自定义选择的频道会通过Tabs组件来展示,因此需要动态地更新Tabs的页签。本示例设计了一对父子组件来演示这一功能。父组件负责显示页签及其内容,并在页签栏的最右侧设置一个“更多”按钮。点击此按钮会弹出一个窗口,供用户选择需要显示的页签。该弹窗内容由子组件提供,关闭弹窗后,父组件的页签将被更新。

实现原理
定义selectTabsViewModel对象,其中的数组allTabs表示所有可选择页签,数组selectedTabs表示选中的需要显示的页签,并通过@Link绑定到父组件InTabComponent和子组件SelectTabsComponent中。子组件SelectTabsComponent作为一个弹窗用于选择需要显示的页签。选择完成后,关闭弹窗并更新 selectTabsViewModel对象中的选中页签数组 selectedTabs,以触发父组件InTabComponent的页签更新。

开发步骤
-
定义SelectTabsViewModel类,包含所有可选择页签数组allTabs属性,和需要显示的页签数组selectedTabs属性,及更新显示页签数组的方法updateSelectedTabs()。
@Observed class TabItemArray extends Array<TabItemViewModel> { } @Observed export default class SelectTabsViewModel { allTabs: TabItemArray = new TabItemArray(); selectedTabs: TabItemArray = new TabItemArray(); // ... async loadTabs(ctx: Context) { // ... } // apply changes to the selected tabs updateSelectedTabs() { let tempTabs: TabItemViewModel[] = []; for (let tab of this.allTabs) { if (tab.isChecked) { tempTabs.push(tab); } } this.selectedTabs = tempTabs; } } -
在InTabsComponent中定义selectTabsViewModel属性,并且在aboutToAppear()方法中初始化。
@Component export default struct InTabsComponent { @State selectTabsViewModel: SelectTabsViewModel = new SelectTabsViewModel(); // ... async aboutToAppear() { // ... await this.selectTabsViewModel.loadTabs(this.ctx); // ... } // ... } -
利用ForEach组件将selectTabsViewModel.selectedTabs属性绑定到Tabs的页签上。
Tabs({ // ... }) { // bind selected tabs to ui ForEach(this.selectTabsViewModel.selectedTabs, (tab: TabItemViewModel, index: number) => { if (index === this.selectTabsViewModel.selectedTabs.length - 1) { TabContent() { // ... } .tabBar(this.tabBuilder(index, tab)) // ... } else { // ... } }, (tab: TabItemViewModel, index: number) => index + '_' + JSON.stringify(tab)) } -
在更多按钮的弹窗中初始化SelectTabsComponent,并将selectTabsViewModel属性作为双向绑定属性传入。在关闭弹窗处理函数中调用selectTabsViewModel.updateSelectedTabs()方法,更新需要显示的组件。
@Builder sheetBuilder() { //select tabs to show SelectTabsComponent({ selectTabsViewModel: this.selectTabsViewModel }) } build() { Scroll() { Column() { BannerComponent() Stack({ alignContent: Alignment.TopEnd }) { Row() { Image($r('app.media.more')) // ... .onClick(() => { this.showSelectTabsComponent = !this.showSelectTabsComponent; }) } // ... .zIndex(1) .bindSheet($$this.showSelectTabsComponent, this.sheetBuilder(), { detents: [SheetSize.MEDIUM, SheetSize.MEDIUM, 500], preferType: SheetType.BOTTOM, title: { title: $r('app.string.bind_sheet_title') }, onWillDismiss: (dismissSheetAction: DismissSheetAction) => { // update tab when closing modal box this.selectTabsViewModel.updateSelectedTabs(); if (this.selectTabsViewModel.selectedTabs.length > 0) { this.subsController.changeIndex(0); } dismissSheetAction.dismiss(); } }) // ... } } } // ... } -
在SelectTabsComponent中将selectTabsViewModel.allTabs属性渲染成toggle组件,并且注册toggle组件的切换处理函数onChange(),在其中修改该页签的选择状态isChecked属性,供更新显示页签方法selectTabsViewModel.updateSelectedTabs()使用。
@Component export default struct SelectTabsComponent { @State checkedChange: boolean = false; @Link selectTabsViewModel: SelectTabsViewModel; build() { Grid() { ForEach(this.selectTabsViewModel.allTabs, (tab: TabItemViewModel) => { GridItem() { Row() { Toggle({ type: ToggleType.Button, isOn: tab.isChecked }) { // ... } // ... .onChange((isOn: boolean) => { tab.isChecked = isOn; this.checkedChange = !this.checkedChange; }) } } }, (tab: TabItemViewModel, index: number) => index + '_' + JSON.stringify(tab)) } .columnsTemplate(('1fr 1fr 1fr 1fr') as string) .height(Constants.FULL_HEIGHT) } }
Tabs切换动效
TabContent切换动画
Tabs 自带的页签切换动画为平移动画。若开发者需实现更高级的动画效果,可通过Tabs提供的API实现自定义动画。

实现原理
使用customContentTransition()函数来自定义Tabs页面的切换动画。本场景采用属性动画实现,开发者可以定义由@State修饰的可动画属性,并在build()方法中将这些属性绑定到对应的页签上。这里,淡入淡出动画选用了TabContent的尺寸属性scale和透明度属性opacity作为生成动画属性。然后,在customContentTransition()函数中,设置动画的起始帧和结束帧对应的可动画属性值,系统将自动补全中间帧从而生成动画。

说明
- 使用自定义切换动画时,Tabs组件的默认切换动画将被禁用,且页面将无法通过手势滑动切换。
- 将customContentTransition设置为undefined表示不使用自定义切换动画,继续使用组件自带的默认切换动画。
- 当前自定义切换动画不支持中途打断。
- 目前,自定义切换动画仅支持以下两种触发场景:点击页签或通过调用TabsController.changeIndex()方法。
开发步骤
-
定义动画所需用到的属性数组。
@Component export default struct InTabsComponent { // ... @State scaleList: number[] = []; @State opacityList: number[] = []; // ... } -
将属性数组绑定到对应的页签上。
Tabs({ // ... }) { // bind selected tabs to ui ForEach(this.selectTabsViewModel.selectedTabs, (tab: TabItemViewModel, index: number) => { if (index === this.selectTabsViewModel.selectedTabs.length - 1) { TabContent() { // ... } // ... // bind animation properties .opacity(this.opacityList[index]) .scale({ x: this.scaleList[index], y: this.scaleList[index] }) } else { // ... } }, (tab: TabItemViewModel, index: number) => index + '_' + JSON.stringify(tab)) } -
定义Tabs的自定义转场函数。
@Component export default struct InTabsComponent { // ... @State scaleList: number[] = []; @State opacityList: number[] = []; // ... private animateDuration: number = 1000; private animateTimeout: number = 1000; private customContentTransition: (from: number, to: number) => TabContentAnimatedTransition = (from: number, to: number) => { let tabContentAnimatedTransition = { timeout: this.animateTimeout, transition: (proxy: TabContentTransitionProxy) => { // start frame this.scaleList[from] = 1.0; this.scaleList[to] = 0.5; this.opacityList[from] = 1.0; this.opacityList[to] = 0.5; this.getUIContext().animateTo({ duration: this.animateDuration, onFinish: () => { proxy.finishTransition(); } }, () => { // end frame this.scaleList[from] = 0.5; this.scaleList[to] = 1.0; this.opacityList[from] = 0.5; this.opacityList[to] = 1.0; }); } } as TabContentAnimatedTransition; return tabContentAnimatedTransition; }; // ... } -
将转场函数作为参数传递给Tabs的customContentTransition()方法。
Tabs({ barPosition: BarPosition.Start, controller: this.subsController, barModifier: this.tabBarModifier }) { // ... } // add animation function .customContentTransition(this.customContentTransition) // comment out to slide to switch
自定义Tabs页签切换联动
在自定义页签样式中,页签的选中和非选中状态显示样式不同时,页签的样式依赖于Tabs组件的切换动作。这种情况下,需要实现Tabs页签的联动,页签切换时,页签样式自动变更。

实现原理
可以通过onChange事件,在切换页签时自定义TabBar和TabContent的联动效果。具体做法是定义一个由@State修饰的变量currentIndex,用于标识当前显示的页签索引。然后,利用onChange()方法注册处理函数,并在处理函数中更新currentIndex,确保其与当前选择的页签的索引一致。在页签样式的实现中,通过判断currentIndex变量与各页签索引是否相等来决定显示的样式,同时currentIndex属性的变化会触发页签样式的更新。
开发步骤
定义currentIndex属性,tabBuilder方法,并在onChange函数中更新currentIndex属性值。
@Component
export default struct OutTabsComponent {
@State currentIndex: number = 0;
// ...
@Builder
tabBuilder(index: number, name: string | Resource, icon: Resource) {
Column() {
// set special styles if selected
SymbolGlyph(icon).fontColor([this.currentIndex === index
? $r('app.color.out_tab_bar_font_active_color')
: $r('app.color.out_tab_bar_font_inactive_color')])
.fontSize(25)
Text(name)
.margin({ top: 4 })
.fontSize(10)
.fontColor(this.currentIndex === index
? $r('app.color.out_tab_bar_font_active_color')
: $r('app.color.out_tab_bar_font_inactive_color'))
}
// ...
}
build() {
Tabs({
// ...
}) {
// ...
}
// ...
.onChange((index: number) => {
this.currentIndex = index;
})
// ...
}
}
更多推荐




所有评论(0)