1# Tabs 2 3通过页签进行内容视图切换的容器组件,每个页签对应一个内容视图。 4 5> **说明:** 6> 7> 该组件从API Version 7开始支持。后续版本如有新增内容,则采用上角标单独标记该内容的起始版本。 8> 9> 该组件从API Version 11开始默认支持安全区避让特性(默认值为:expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])),开发者可以重写该属性覆盖默认行为,API Version 11之前的版本需配合[expandSafeArea](ts-universal-attributes-expand-safe-area.md)属性实现安全区避让。 10 11 12## 子组件 13 14仅可包含子组件[TabContent](ts-container-tabcontent.md)。 15 16> **说明:** 17> 18> Tabs子组件的visibility属性设置为None,或者visibility属性设置为Hidden时,对应子组件不显示,但依然会在视窗内占位。 19 20 21## 接口 22 23Tabs(value?: {barPosition?: BarPosition, index?: number, controller?: TabsController}) 24 25**参数:** 26 27| 参数名 | 参数类型 | 必填 | 参数描述 | 28| ----------- | --------------------------------- | ---- | ---------------------------------------- | 29| barPosition | [BarPosition](#barposition枚举说明)| 否 | 设置Tabs的页签位置。<br/>默认值:BarPosition.Start | 30| index | number | 否 | 设置当前显示页签的索引。<br/>默认值:0<br/>**说明:** <br/>设置为小于0的值时按默认值显示。<br/>可选值为[0, TabContent子节点数量-1]。<br/>直接修改index跳页时,切换动效不生效。 使用TabController的changeindex时,默认生效切换动效,可以设置animationDuration为0关闭动画。<br />从API version 10开始,该参数支持[$$](../../../quick-start/arkts-two-way-sync.md)双向绑定变量。 | 31| controller | [TabsController](#tabscontroller) | 否 | 设置Tabs控制器。 | 32 33## BarPosition枚举说明 34 35| 名称 | 描述 | 36| ----- | ---------------------------------------- | 37| Start | vertical属性方法设置为true时,页签位于容器左侧;vertical属性方法设置为false时,页签位于容器顶部。 | 38| End | vertical属性方法设置为true时,页签位于容器右侧;vertical属性方法设置为false时,页签位于容器底部。 | 39 40 41## 属性 42 43除支持[通用属性](ts-universal-attributes-size.md)外,还支持以下属性: 44 45| 名称 | 参数类型 | 描述 | 46| -------------------------------- | ---------------------------------------- | ---------------------------------------- | 47| vertical | boolean | 设置为false是为横向Tabs,设置为true时为纵向Tabs。<br/>默认值:false | 48| scrollable | boolean | 设置为true时可以通过滑动页面进行页面切换,为false时不可滑动切换页面。<br/>默认值:true | 49| barMode | [BarMode](#barmode枚举说明),[ScrollableBarModeOptions](#scrollablebarmodeoptions10对象说明) | TabBar布局模式,BarMode为必选项,ScrollableBarModeOptions为可选项,具体描述见BarMode枚举说明、ScrollableBarModeOptions对象说明。从API version 10开始,支持ScrollableBarModeOptions参数。其中ScrollableBarModeOptions参数仅Scrollable模式下有效,非必填参数。<br/>默认值:BarMode.Fixed | 50| barWidth | number \| Length<sup>8+</sup> | TabBar的宽度值。<br/>默认值:<br/>未设置[SubTabBarStyle](ts-container-tabcontent.md#subtabbarstyle9)和[BottomTabBarStyle](ts-container-tabcontent.md#bottomtabbarstyle9)的TabBar且vertical属性为false时,默认值为Tabs的宽度。<br/>未设置[SubTabBarStyle](ts-container-tabcontent.md#subtabbarstyle9)和[BottomTabBarStyle](ts-container-tabcontent.md#bottomtabbarstyle9)的TabBar且vertical属性为true时,默认值为56vp。<br/>设置SubTabbarStyle样式且vertical属性为false时,默认值为Tabs的宽度。<br/>设置SubTabbarStyle样式且vertical属性为true时,默认值为56vp。<br/>设置BottomTabbarStyle样式且vertical属性为true时,默认值为96vp。<br/>设置BottomTabbarStyle样式且vertical属性为false时,默认值为Tabs的宽度。<br/>**说明:** <br/>设置为小于0或大于Tabs宽度值时,按默认值显示。 | 51| barHeight | number \| Length<sup>8+</sup> | TabBar的高度值。<br/>默认值:<br/>未设置带样式的TabBar且vertical属性为false时,默认值为56vp。<br/>未设置带样式的TabBar且vertical属性为true时,默认值为Tabs的高度。<br/>设置SubTabbarStyle样式且vertical属性为false时,默认值为56vp。<br/>设置SubTabbarStyle样式且vertical属性为true时,默认值为Tabs的高度。<br/>设置BottomTabbarStyle样式且vertical属性为true时,默认值为Tabs的高度。<br/>设置BottomTabbarStyle样式且vertical属性为false时,默认值为56vp。<br/>**说明:** <br/>设置为小于0或大于Tabs高度值时,按默认值显示。 | 52| animationDuration | number | 点击TabBar页签切换TabContent的动画时长。<br/>默认值:<br/>API version 10及以前,不设置该属性或设置为null时,默认值为0ms,即点击TabBar页签切换TabContent无动画。设置为小于0或undefined时,默认值为300ms。<br/>API version 11及以后,不设置该属性或设置为异常值,且设置TabBar为BottomTabBarStyle样式时,默认值为0ms。设置TabBar为其他样式时,默认值为300ms。<br/>**说明:**<br/>该参数不支持百分比设置。 | 53| divider<sup>10+</sup> | [DividerStyle](#dividerstyle10对象说明) \| null | 用于设置区分TabBar和TabContent的分割线样式设置分割线样式,默认不显示分割线。<br/> DividerStyle: 分割线的样式;<br/> null: 不显示分割线。 | 54| fadingEdge<sup>10+</sup> | boolean | 设置页签超过容器宽度时是否渐隐消失。<br />默认值:true <br/>**说明:** <br/>建议配合barBackgroundColor属性一起使用,如果barBackgroundColor属性没有定义,会默认显示页签末端为白色的渐隐效果。 | 55| barOverlap<sup>10+</sup> | boolean | 设置TabBar是否背后变模糊并叠加在TabContent之上。<br />默认值:false | 56| barBackgroundColor<sup>10+</sup> | [ResourceColor](ts-types.md#resourcecolor) | 设置TabBar的背景颜色。<br />默认值:透明 | 57| barBackgroundBlurStyle<sup>11+</sup> | [BlurStyle](ts-appendix-enums.md#blurstyle9) | 设置TabBar的背景模糊材质。<br />默认值:NONE | 58| barGridAlign<sup>10+</sup> | [BarGridColumnOptions](#bargridcolumnoptions10对象说明) | 以栅格化方式设置TabBar的可见区域。具体参见BarGridColumnOptions对象。仅水平模式下有效,[不适用于XS、XL和XXL设备](../../../ui/arkts-layout-development-grid-layout.md#栅格系统断点)。 | 59 60## DividerStyle<sup>10+</sup>对象说明 61 62| 名称 | 参数类型 | 必填 | 描述 | 63| ----------- | ---------------------------------------- | ---- | ---------------------------------------- | 64| strokeWidth | [Length](ts-types.md#length) | 是 | 分割线的线宽(不支持百分比设置)。<br/>默认值:0.0<br/>单位:vp | 65| color | [ResourceColor](ts-types.md#resourcecolor) | 否 | 分割线的颜色。<br/>默认值:#33182431 | 66| startMargin | [Length](ts-types.md#length) | 否 | 分割线与侧边栏顶端的距离(不支持百分比设置)。<br/>默认值:0.0<br/>单位:vp | 67| endMargin | [Length](ts-types.md#length) | 否 | 分割线与侧边栏底端的距离(不支持百分比设置)。<br/>默认值:0.0<br/>单位:vp | 68 69## BarGridColumnOptions<sup>10+</sup>对象说明 70 71| 名称 | 参数类型 | 必填 | 描述 | 72| ----------- | ---------------------------------------- | ---- | ---------------------------------------- | 73| margin | [Dimension](ts-types.md#dimension10) | 否 | 网格模式下的column边距(不支持百分比设置)。<br/>默认值:24.0<br/>单位:vp | 74| gutter | [Dimension](ts-types.md#dimension10) | 否 | 网格模式下的column间隔(不支持百分比设置)。<br/>默认值:24.0<br/>单位:vp | 75| sm | number | 否 | 小屏下,页签占用的columns数量,必须是非负偶数。小屏为大于等于320vp但小于600vp。<br/>默认值为-1,代表页签占用TabBar全部宽度。 | 76| md | number | 否 | 中屏下,页签占用的columns数量,必须是非负偶数。中屏为大于等于600vp但小于800vp。<br/>默认值为-1,代表页签占用TabBar全部宽度。 | 77| lg | number | 否 | 大屏下,页签占用的columns数量,必须是非负偶数。大屏为大于等于840vp但小于1024vp。<br/>默认值为-1,代表页签占用TabBar全部宽度。 | 78 79## ScrollableBarModeOptions<sup>10+</sup>对象说明 80 81| 名称 | 参数类型 | 必填 | 描述 | 82| ----------- | ---------------------------------------- | ---- | ---------------------------------------- | 83| margin | [Dimension](ts-types.md#dimension10) | 否 | Scrollable模式下的TabBar的左右边距(不支持百分比设置)。<br/>默认值:0.0<br/>单位:vp | 84| nonScrollableLayoutStyle | [LayoutStyle](#layoutstyle10枚举说明) | 否 | Scrollable模式下不滚动时的页签排布方式。<br/>默认值:LayoutStyle.ALWAYS_CENTER | 85 86## BarMode枚举说明 87 88| 名称 | 描述 | 89| ---------- | ---------------------------------------- | 90| Scrollable | 每一个TabBar均使用实际布局宽度,超过总长度(横向Tabs的barWidth,纵向Tabs的barHeight)后可滑动。 | 91| Fixed | 所有TabBar平均分配barWidth宽度(纵向时平均分配barHeight高度)。 | 92 93## LayoutStyle<sup>10+</sup>枚举说明 94 95| 名称 | 描述 | 96| ---------- | ---------------------------------------- | 97| ALWAYS_CENTER | 当页签内容超过TabBar宽度时,TabBar可滚动。<br/>当页签内容不超过TabBar宽度时,TabBar不可滚动,页签紧凑居中。| 98| ALWAYS_AVERAGE_SPLITE | 当页签内容超过TabBar宽度时,TabBar可滚动。<br/>当页签内容不超过TabBar宽度时,TabBar不可滚动,且所有页签平均分配TabBar宽度。<br/>仅水平模式下有效,否则视为LayoutStyle.ALWAYS_CENTER。| 99| SPACE_BETWEEN_OR_CENTER | 当页签内容超过TabBar宽度时,TabBar可滚动。<br/>当页签内容不超过TabBar宽度但超过TabBar宽度一半时,TabBar不可滚动,页签紧凑居中。<br/>当页签内容不超过TabBar宽度一半时,TabBar不可滚动,保证页签居中排列在TabBar宽度一半,且间距相同。| 100 101## 事件 102 103除支持[通用事件](ts-universal-events-click.md)外,还支持以下事件: 104 105| 名称 | 功能描述 | 106| ------------------------------------------------------------ | ------------------------------------------------------------ | 107| onChange(event: (index: number) => void) | Tab页签切换后触发的事件。<br>- index:当前显示的index索引,索引从0开始计算。<br/>触发该事件的条件:<br/>1、TabContent支持滑动时,组件触发滑动时触发。<br/>2、通过[控制器](#tabscontroller)API接口调用。<br/>3、通过[状态变量](../../../quick-start/arkts-state.md)构造的属性值进行修改。<br/>4、通过页签处点击触发。 | 108| onTabBarClick(event: (index: number) => void)<sup>10+</sup> | Tab页签点击后触发的事件。<br>- index:被点击的index索引,索引从0开始计算。<br/>触发该事件的条件:<br/>通过页签处点击触发。 | 109| onAnimationStart<sup>11+</sup>(handler: (index: number, targetIndex: number, event: [TabsAnimationEvent](ts-types.md#tabsanimationevent11)) => void) | 切换动画开始时触发该回调。<br/>- index:当前显示元素的索引。<br/>- targetIndex:切换动画目标元素的索引。<br/>- event:动画相关信息,包括主轴方向上当前显示元素和目标元素相对Tabs起始位置的位移,以及离手速度。<br/>**说明:** <br/>参数为动画开始前的index值(不是最终结束动画的index值)。 | 110| onAnimationEnd<sup>11+</sup>(handler: (index: number, event: [TabsAnimationEvent](ts-types.md#tabsanimationevent11)) => void) | 切换动画结束时触发该回调。<br/>- index:当前显示元素的索引。<br/>- event:动画相关信息,只返回主轴方向上当前显示元素相对于Tabs起始位置的位移。<br/>**说明:** <br/>当Tabs切换动效结束时触发,包括动画过程中手势中断。参数为动画结束后的index值。 | 111| onGestureSwipe<sup>11+</sup>(handler: (index: number, event: [TabsAnimationEvent](ts-types.md#tabsanimationevent11)) => void) | 在页面跟手滑动过程中,逐帧触发该回调。<br/>- index:当前显示元素的索引。<br/>- event:动画相关信息,只返回主轴方向上当前显示元素相对于Tabs起始位置的位移。 | 112| customContentTransition<sup>11+</sup>(delegate: (from: number, to: number) => [TabContentAnimatedTransition](ts-types.md#tabcontentanimatedtransition11) \| undefined) | 自定义Tabs页面切换动画。其中,from和to参数为返回给开发者使用的值,代表的含义如下:<br> - from:动画开始时,当前页面的index值。<br/>- to:动画开始时,目标页面的index值。<br> 使用说明:<br> 1、当使用自定义切换动画时,Tabs组件自带的默认切换动画会被禁用,同时,页面也无法跟手滑动。<br> 2、当设置为undefined时,表示不使用自定义切换动画,仍然使用组件自带的默认切换动画。<br> 3、当前自定义切换动画不支持打断。<br> 4、目前自定义切换动画只支持两种场景触发:点击页签和调用TabsController.changeIndex()接口。<br> 5、当使用自定义切换动画时,Tabs组件支持的事件中,除了onGestureSwipe,其他事件均支持。<br> 6、onChange和onAnimationEnd事件的触发时机需要特殊说明:如果在第一次自定义动画执行过程中,触发了第二次自定义动画,那么在开始第二次自定义动画时,就会触发第一次自定义动画的onChange和onAnimationEnd事件。<br> 7、当使用自定义动画时,参与动画的页面布局方式会改为Stack布局。如果开发者未主动设置相关页面的zIndex属性,那么所有页面的zIndex值是一样的,页面的渲染层级会按照在组件树上的顺序(即页面的index值顺序)确定。因此,开发者需要主动修改页面的zIndex属性,来控制页面的渲染层级。 <br/> | 113 114 115## TabsController 116 117Tabs组件的控制器,用于控制Tabs组件进行页签切换。不支持一个TabsController控制多个Tabs组件。 118 119### 导入对象 120 121```ts 122let controller: TabsController = new TabsController() 123``` 124 125### changeIndex 126 127changeIndex(value: number): void 128 129控制Tabs切换到指定页签。 130 131**参数:** 132 133| 参数名 | 参数类型 | 必填 | 参数描述 | 134| ----- | ------ | ---- | ---------------------------------------- | 135| value | number | 是 | 页签在Tabs里的索引值,索引值从0开始。<br/>**说明:** <br/>设置小于0或大于最大数量的值时,取默认值0。 | 136 137 138## 示例 139 140### 示例1 141 142本示例通过onChange实现切换时自定义tabBar和TabContent的联动。 143 144```ts 145// xxx.ets 146@Entry 147@Component 148struct TabsExample { 149 @State fontColor: string = '#182431' 150 @State selectedFontColor: string = '#007DFF' 151 @State currentIndex: number = 0 152 private controller: TabsController = new TabsController() 153 154 @Builder tabBuilder(index: number, name: string) { 155 Column() { 156 Text(name) 157 .fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor) 158 .fontSize(16) 159 .fontWeight(this.currentIndex === index ? 500 : 400) 160 .lineHeight(22) 161 .margin({ top: 17, bottom: 7 }) 162 Divider() 163 .strokeWidth(2) 164 .color('#007DFF') 165 .opacity(this.currentIndex === index ? 1 : 0) 166 }.width('100%') 167 } 168 169 build() { 170 Column() { 171 Tabs({ barPosition: BarPosition.Start, index: this.currentIndex, controller: this.controller }) { 172 TabContent() { 173 Column().width('100%').height('100%').backgroundColor('#00CB87') 174 }.tabBar(this.tabBuilder(0, 'green')) 175 176 TabContent() { 177 Column().width('100%').height('100%').backgroundColor('#007DFF') 178 }.tabBar(this.tabBuilder(1, 'blue')) 179 180 TabContent() { 181 Column().width('100%').height('100%').backgroundColor('#FFBF00') 182 }.tabBar(this.tabBuilder(2, 'yellow')) 183 184 TabContent() { 185 Column().width('100%').height('100%').backgroundColor('#E67C92') 186 }.tabBar(this.tabBuilder(3, 'pink')) 187 } 188 .vertical(false) 189 .barMode(BarMode.Fixed) 190 .barWidth(360) 191 .barHeight(56) 192 .animationDuration(400) 193 .onChange((index: number) => { 194 this.currentIndex = index 195 }) 196 .width(360) 197 .height(296) 198 .margin({ top: 52 }) 199 .backgroundColor('#F1F3F5') 200 }.width('100%') 201 } 202} 203``` 204 205 206 207### 示例2 208 209本示例通过divider实现了分割线各种属性的展示。 210 211```ts 212// xxx.ets 213@Entry 214@Component 215struct TabsDivider1 { 216 private controller1: TabsController = new TabsController() 217 @State dividerColor: string = 'red' 218 @State strokeWidth: number = 2 219 @State startMargin: number = 0 220 @State endMargin: number = 0 221 @State nullFlag: boolean = false 222 223 build() { 224 Column() { 225 Tabs({ controller: this.controller1 }) { 226 TabContent() { 227 Column().width('100%').height('100%').backgroundColor(Color.Pink) 228 }.tabBar('pink') 229 230 TabContent() { 231 Column().width('100%').height('100%').backgroundColor(Color.Yellow) 232 }.tabBar('yellow') 233 234 TabContent() { 235 Column().width('100%').height('100%').backgroundColor(Color.Blue) 236 }.tabBar('blue') 237 238 TabContent() { 239 Column().width('100%').height('100%').backgroundColor(Color.Green) 240 }.tabBar('green') 241 242 TabContent() { 243 Column().width('100%').height('100%').backgroundColor(Color.Red) 244 }.tabBar('red') 245 } 246 .vertical(true) 247 .scrollable(true) 248 .barMode(BarMode.Fixed) 249 .barWidth(70) 250 .barHeight(200) 251 .animationDuration(400) 252 .onChange((index: number) => { 253 console.info(index.toString()) 254 }) 255 .height('200vp') 256 .margin({ bottom: '12vp' }) 257 .divider(this.nullFlag ? null : { 258 strokeWidth: this.strokeWidth, 259 color: this.dividerColor, 260 startMargin: this.startMargin, 261 endMargin: this.endMargin 262 }) 263 264 Button('常规Divider').width('100%').margin({ bottom: '12vp' }) 265 .onClick(() => { 266 this.nullFlag = false; 267 this.strokeWidth = 2; 268 this.dividerColor = 'red'; 269 this.startMargin = 0; 270 this.endMargin = 0; 271 }) 272 Button('空Divider').width('100%').margin({ bottom: '12vp' }) 273 .onClick(() => { 274 this.nullFlag = true 275 }) 276 Button('颜色变为蓝色').width('100%').margin({ bottom: '12vp' }) 277 .onClick(() => { 278 this.dividerColor = 'blue' 279 }) 280 Button('宽度增加').width('100%').margin({ bottom: '12vp' }) 281 .onClick(() => { 282 this.strokeWidth += 2 283 }) 284 Button('宽度减小').width('100%').margin({ bottom: '12vp' }) 285 .onClick(() => { 286 if (this.strokeWidth > 2) { 287 this.strokeWidth -= 2 288 } 289 }) 290 Button('上边距增加').width('100%').margin({ bottom: '12vp' }) 291 .onClick(() => { 292 this.startMargin += 2 293 }) 294 Button('上边距减少').width('100%').margin({ bottom: '12vp' }) 295 .onClick(() => { 296 if (this.startMargin > 2) { 297 this.startMargin -= 2 298 } 299 }) 300 Button('下边距增加').width('100%').margin({ bottom: '12vp' }) 301 .onClick(() => { 302 this.endMargin += 2 303 }) 304 Button('下边距减少').width('100%').margin({ bottom: '12vp' }) 305 .onClick(() => { 306 if (this.endMargin > 2) { 307 this.endMargin -= 2 308 } 309 }) 310 }.padding({ top: '24vp', left: '24vp', right: '24vp' }) 311 } 312} 313``` 314 315 316 317### 示例3 318 319本示例通过fadingEdge实现了切换子页签渐隐和不渐隐。 320 321```ts 322// xxx.ets 323@Entry 324@Component 325struct TabsOpaque { 326 @State message: string = 'Hello World' 327 private controller: TabsController = new TabsController() 328 private controller1: TabsController = new TabsController() 329 @State selfFadingFade: boolean = true; 330 331 build() { 332 Column() { 333 Button('子页签设置渐隐').width('100%').margin({ bottom: '12vp' }) 334 .onClick((event?: ClickEvent) => { 335 this.selfFadingFade = true; 336 }) 337 Button('子页签设置不渐隐').width('100%').margin({ bottom: '12vp' }) 338 .onClick((event?: ClickEvent) => { 339 this.selfFadingFade = false; 340 }) 341 Tabs({ barPosition: BarPosition.End, controller: this.controller }) { 342 TabContent() { 343 Column().width('100%').height('100%').backgroundColor(Color.Pink) 344 }.tabBar('pink') 345 346 TabContent() { 347 Column().width('100%').height('100%').backgroundColor(Color.Yellow) 348 }.tabBar('yellow') 349 350 TabContent() { 351 Column().width('100%').height('100%').backgroundColor(Color.Blue) 352 }.tabBar('blue') 353 354 TabContent() { 355 Column().width('100%').height('100%').backgroundColor(Color.Green) 356 }.tabBar('green') 357 358 TabContent() { 359 Column().width('100%').height('100%').backgroundColor(Color.Green) 360 }.tabBar('green') 361 362 TabContent() { 363 Column().width('100%').height('100%').backgroundColor(Color.Green) 364 }.tabBar('green') 365 366 TabContent() { 367 Column().width('100%').height('100%').backgroundColor(Color.Green) 368 }.tabBar('green') 369 370 TabContent() { 371 Column().width('100%').height('100%').backgroundColor(Color.Green) 372 }.tabBar('green') 373 } 374 .vertical(false) 375 .scrollable(true) 376 .barMode(BarMode.Scrollable) 377 .barHeight(80) 378 .animationDuration(400) 379 .onChange((index: number) => { 380 console.info(index.toString()) 381 }) 382 .fadingEdge(this.selfFadingFade) 383 .height('30%') 384 .width('100%') 385 386 Tabs({ barPosition: BarPosition.Start, controller: this.controller1 }) { 387 TabContent() { 388 Column().width('100%').height('100%').backgroundColor(Color.Pink) 389 }.tabBar('pink') 390 391 TabContent() { 392 Column().width('100%').height('100%').backgroundColor(Color.Yellow) 393 }.tabBar('yellow') 394 395 TabContent() { 396 Column().width('100%').height('100%').backgroundColor(Color.Blue) 397 }.tabBar('blue') 398 399 TabContent() { 400 Column().width('100%').height('100%').backgroundColor(Color.Green) 401 }.tabBar('green') 402 403 TabContent() { 404 Column().width('100%').height('100%').backgroundColor(Color.Green) 405 }.tabBar('green') 406 407 TabContent() { 408 Column().width('100%').height('100%').backgroundColor(Color.Green) 409 }.tabBar('green') 410 } 411 .vertical(true) 412 .scrollable(true) 413 .barMode(BarMode.Scrollable) 414 .barHeight(200) 415 .barWidth(80) 416 .animationDuration(400) 417 .onChange((index: number) => { 418 console.info(index.toString()) 419 }) 420 .fadingEdge(this.selfFadingFade) 421 .height('30%') 422 .width('100%') 423 } 424 .padding({ top: '24vp', left: '24vp', right: '24vp' }) 425 } 426} 427``` 428 429 430 431### 示例4 432 433本示例通过barOverlap实现了TabBar是否背后变模糊并叠加在TabContent之上。 434 435```ts 436// xxx.ets 437@Entry 438@Component 439struct barBackgroundColorTest { 440 private controller: TabsController = new TabsController() 441 @State barOverlap: boolean = true; 442 @State barBackgroundColor: string = '#88888888'; 443 444 build() { 445 Column() { 446 Button("barOverlap变化").width('100%').margin({ bottom: '12vp' }) 447 .onClick((event?: ClickEvent) => { 448 if (this.barOverlap) { 449 this.barOverlap = false; 450 } else { 451 this.barOverlap = true; 452 } 453 }) 454 455 Tabs({ barPosition: BarPosition.Start, index: 0, controller: this.controller }) { 456 TabContent() { 457 Column() { 458 Text(`barOverlap ${this.barOverlap}`).fontSize(16).margin({ top: this.barOverlap ? '56vp' : 0 }) 459 Text(`barBackgroundColor ${this.barBackgroundColor}`).fontSize(16) 460 }.width('100%').width('100%').height('100%') 461 .backgroundColor(Color.Pink) 462 } 463 .tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), "1")) 464 465 TabContent() { 466 Column() { 467 Text(`barOverlap ${this.barOverlap}`).fontSize(16).margin({ top: this.barOverlap ? '56vp' : 0 }) 468 Text(`barBackgroundColor ${this.barBackgroundColor}`).fontSize(16) 469 }.width('100%').width('100%').height('100%') 470 .backgroundColor(Color.Yellow) 471 } 472 .tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), "2")) 473 474 TabContent() { 475 Column() { 476 Text(`barOverlap ${this.barOverlap}`).fontSize(16).margin({ top: this.barOverlap ? '56vp' : 0 }) 477 Text(`barBackgroundColor ${this.barBackgroundColor}`).fontSize(16) 478 }.width('100%').width('100%').height('100%') 479 .backgroundColor(Color.Green) 480 } 481 .tabBar(new BottomTabBarStyle($r('sys.media.ohos_app_icon'), "3")) 482 } 483 .vertical(false) 484 .barMode(BarMode.Fixed) 485 .height('60%') 486 .barOverlap(this.barOverlap) 487 .scrollable(true) 488 .animationDuration(10) 489 .barBackgroundColor(this.barBackgroundColor) 490 } 491 .height(500) 492 .padding({ top: '24vp', left: '24vp', right: '24vp' }) 493 } 494} 495``` 496 497 498 499### 示例5 500 501本示例通过barGridAlign实现了以栅格化方式设置TabBar的可见区域。 502 503```ts 504// xxx.ets 505@Entry 506@Component 507struct TabsExample5 { 508 private controller: TabsController = new TabsController() 509 @State gridMargin: number = 10 510 @State gridGutter: number = 10 511 @State sm: number = -2 512 @State clickedContent: string = ""; 513 514 build() { 515 Column() { 516 Row() { 517 Button("gridMargin+10 " + this.gridMargin) 518 .width('47%') 519 .height(50) 520 .margin({ top: 5 }) 521 .onClick((event?: ClickEvent) => { 522 this.gridMargin += 10 523 }) 524 .margin({ right: '6%', bottom: '12vp' }) 525 Button("gridMargin-10 " + this.gridMargin) 526 .width('47%') 527 .height(50) 528 .margin({ top: 5 }) 529 .onClick((event?: ClickEvent) => { 530 this.gridMargin -= 10 531 }) 532 .margin({ bottom: '12vp' }) 533 } 534 535 Row() { 536 Button("gridGutter+10 " + this.gridGutter) 537 .width('47%') 538 .height(50) 539 .margin({ top: 5 }) 540 .onClick((event?: ClickEvent) => { 541 this.gridGutter += 10 542 }) 543 .margin({ right: '6%', bottom: '12vp' }) 544 Button("gridGutter-10 " + this.gridGutter) 545 .width('47%') 546 .height(50) 547 .margin({ top: 5 }) 548 .onClick((event?: ClickEvent) => { 549 this.gridGutter -= 10 550 }) 551 .margin({ bottom: '12vp' }) 552 } 553 554 Row() { 555 Button("sm+2 " + this.sm) 556 .width('47%') 557 .height(50) 558 .margin({ top: 5 }) 559 .onClick((event?: ClickEvent) => { 560 this.sm += 2 561 }) 562 .margin({ right: '6%' }) 563 Button("sm-2 " + this.sm).width('47%').height(50).margin({ top: 5 }) 564 .onClick((event?: ClickEvent) => { 565 this.sm -= 2 566 }) 567 } 568 569 Text("点击内容:" + this.clickedContent).width('100%').height(200).margin({ top: 5 }) 570 571 572 Tabs({ barPosition: BarPosition.End, controller: this.controller }) { 573 TabContent() { 574 Column().width('100%').height('100%').backgroundColor(Color.Pink) 575 }.tabBar(BottomTabBarStyle.of($r("sys.media.ohos_app_icon"), "1")) 576 577 TabContent() { 578 Column().width('100%').height('100%').backgroundColor(Color.Green) 579 }.tabBar(BottomTabBarStyle.of($r("sys.media.ohos_app_icon"), "2")) 580 581 TabContent() { 582 Column().width('100%').height('100%').backgroundColor(Color.Blue) 583 }.tabBar(BottomTabBarStyle.of($r("sys.media.ohos_app_icon"), "3")) 584 } 585 .width('350vp') 586 .animationDuration(300) 587 .height('60%') 588 .barGridAlign({ sm: this.sm, margin: this.gridMargin, gutter: this.gridGutter }) 589 .backgroundColor(0xf1f3f5) 590 .onTabBarClick((index: number) => { 591 this.clickedContent += "now index " + index + " is clicked\n"; 592 }) 593 } 594 .width('100%') 595 .height(500) 596 .margin({ top: 5 }) 597 .padding('10vp') 598 } 599} 600``` 601 602 603 604### 示例6 605 606本示例实现了barMode的ScrollableBarModeOptions参数,该参数仅在Scrollable模式下有效。 607 608```ts 609// xxx.ets 610@Entry 611@Component 612struct TabsExample6 { 613 private controller: TabsController = new TabsController() 614 @State scrollMargin: number = 0 615 @State layoutStyle: LayoutStyle = LayoutStyle.ALWAYS_CENTER 616 @State text: string = "文本" 617 618 build() { 619 Column() { 620 Row() { 621 Button("scrollMargin+10 " + this.scrollMargin) 622 .width('47%') 623 .height(50) 624 .margin({ top: 5 }) 625 .onClick((event?: ClickEvent) => { 626 this.scrollMargin += 10 627 }) 628 .margin({ right: '6%', bottom: '12vp' }) 629 Button("scrollMargin-10 " + this.scrollMargin) 630 .width('47%') 631 .height(50) 632 .margin({ top: 5 }) 633 .onClick((event?: ClickEvent) => { 634 this.scrollMargin -= 10 635 }) 636 .margin({ bottom: '12vp' }) 637 } 638 639 Row() { 640 Button("文本增加 ") 641 .width('47%') 642 .height(50) 643 .margin({ top: 5 }) 644 .onClick((event?: ClickEvent) => { 645 this.text += '文本增加' 646 }) 647 .margin({ right: '6%', bottom: '12vp' }) 648 Button("文本重置") 649 .width('47%') 650 .height(50) 651 .margin({ top: 5 }) 652 .onClick((event?: ClickEvent) => { 653 this.text = "文本" 654 }) 655 .margin({ bottom: '12vp' }) 656 } 657 658 Row() { 659 Button("layoutStyle.ALWAYS_CENTER") 660 .width('100%') 661 .height(50) 662 .margin({ top: 5 }) 663 .fontSize(15) 664 .onClick((event?: ClickEvent) => { 665 this.layoutStyle = LayoutStyle.ALWAYS_CENTER; 666 }) 667 .margin({ bottom: '12vp' }) 668 } 669 670 Row() { 671 Button("layoutStyle.ALWAYS_AVERAGE_SPLIT") 672 .width('100%') 673 .height(50) 674 .margin({ top: 5 }) 675 .fontSize(15) 676 .onClick((event?: ClickEvent) => { 677 this.layoutStyle = LayoutStyle.ALWAYS_AVERAGE_SPLIT; 678 }) 679 .margin({ bottom: '12vp' }) 680 } 681 682 Row() { 683 Button("layoutStyle.SPACE_BETWEEN_OR_CENTER") 684 .width('100%') 685 .height(50) 686 .margin({ top: 5 }) 687 .fontSize(15) 688 .onClick((event?: ClickEvent) => { 689 this.layoutStyle = LayoutStyle.SPACE_BETWEEN_OR_CENTER; 690 }) 691 .margin({ bottom: '12vp' }) 692 } 693 694 Tabs({ barPosition: BarPosition.End, controller: this.controller }) { 695 TabContent() { 696 Column().width('100%').height('100%').backgroundColor(Color.Pink) 697 }.tabBar(SubTabBarStyle.of(this.text)) 698 699 TabContent() { 700 Column().width('100%').height('100%').backgroundColor(Color.Green) 701 }.tabBar(SubTabBarStyle.of(this.text)) 702 703 TabContent() { 704 Column().width('100%').height('100%').backgroundColor(Color.Blue) 705 }.tabBar(SubTabBarStyle.of(this.text)) 706 } 707 .animationDuration(300) 708 .height('60%') 709 .backgroundColor(0xf1f3f5) 710 .barMode(BarMode.Scrollable, { margin: this.scrollMargin, nonScrollableLayoutStyle: this.layoutStyle }) 711 } 712 .width('100%') 713 .height(500) 714 .margin({ top: 5 }) 715 .padding('24vp') 716 } 717} 718``` 719 720 721 722### 示例7 723 724本示例通过customContentTransition实现了自定义Tabs页面的切换动画。 725 726```ts 727// xxx.ets 728@Entry 729@Component 730struct TabsCustomAnimationExample { 731 @State useCustomAnimation: boolean = true 732 @State tabContent0Scale: number = 1.0 733 @State tabContent1Scale: number = 1.0 734 @State tabContent0Opacity: number = 1.0 735 @State tabContent1Opacity: number = 1.0 736 @State tabContent2Opacity: number = 1.0 737 tabsController: TabsController = new TabsController() 738 private firstTimeout: number = 3000 739 private secondTimeout: number = 5000 740 private first2secondDuration: number = 3000 741 private second2thirdDuration: number = 5000 742 private first2thirdDuration: number = 2000 743 private baseCustomAnimation: (from: number, to: number) => TabContentAnimatedTransition = (from: number, to: number) => { 744 if ((from === 0 && to === 1) || (from === 1 && to === 0)) { 745 // 缩放动画 746 let firstCustomTransition = { 747 timeout: this.firstTimeout, 748 transition: (proxy: TabContentTransitionProxy) => { 749 if (proxy.from === 0 && proxy.to === 1) { 750 this.tabContent0Scale = 1.0 751 this.tabContent1Scale = 0.5 752 } else { 753 this.tabContent0Scale = 0.5 754 this.tabContent1Scale = 1.0 755 } 756 757 animateTo({ 758 duration: this.first2secondDuration, 759 onFinish: () => { 760 proxy.finishTransition() 761 } 762 }, () => { 763 if (proxy.from === 0 && proxy.to === 1) { 764 this.tabContent0Scale = 0.5 765 this.tabContent1Scale = 1.0 766 } else { 767 this.tabContent0Scale = 1.0 768 this.tabContent1Scale = 0.5 769 } 770 }) 771 } 772 } as TabContentAnimatedTransition; 773 return firstCustomTransition; 774 } else { 775 // 透明度动画 776 let secondCustomTransition = { 777 timeout: this.secondTimeout, 778 transition: (proxy: TabContentTransitionProxy) => { 779 if ((proxy.from === 1 && proxy.to === 2) || (proxy.from === 2 && proxy.to === 1)) { 780 if (proxy.from === 1 && proxy.to === 2) { 781 this.tabContent1Opacity = 1.0 782 this.tabContent2Opacity = 0.5 783 } else { 784 this.tabContent1Opacity = 0.5 785 this.tabContent2Opacity = 1.0 786 } 787 animateTo({ 788 duration: this.second2thirdDuration, 789 onFinish: () => { 790 proxy.finishTransition() 791 } 792 }, () => { 793 if (proxy.from === 1 && proxy.to === 2) { 794 this.tabContent1Opacity = 0.5 795 this.tabContent2Opacity = 1.0 796 } else { 797 this.tabContent1Opacity = 1.0 798 this.tabContent2Opacity = 0.5 799 } 800 }) 801 } else if ((proxy.from === 0 && proxy.to === 2) || (proxy.from === 2 && proxy.to === 0)) { 802 if (proxy.from === 0 && proxy.to === 2) { 803 this.tabContent0Opacity = 1.0 804 this.tabContent2Opacity = 0.5 805 } else { 806 this.tabContent0Opacity = 0.5 807 this.tabContent2Opacity = 1.0 808 } 809 animateTo({ 810 duration: this.first2thirdDuration, 811 onFinish: () => { 812 proxy.finishTransition() 813 } 814 }, () => { 815 if (proxy.from === 0 && proxy.to === 2) { 816 this.tabContent0Opacity = 0.5 817 this.tabContent2Opacity = 1.0 818 } else { 819 this.tabContent0Opacity = 1.0 820 this.tabContent2Opacity = 0.5 821 } 822 }) 823 } 824 } 825 } as TabContentAnimatedTransition; 826 return secondCustomTransition; 827 } 828 } 829 830 build() { 831 Column() { 832 Tabs({ controller: this.tabsController }) { 833 TabContent() { 834 Text("Red") 835 } 836 .tabBar("Red") 837 .scale({ x: this.tabContent0Scale, y: this.tabContent0Scale }) 838 .backgroundColor(Color.Red) 839 .opacity(this.tabContent0Opacity) 840 .width(100) 841 .height(100) 842 843 TabContent() { 844 Text("Yellow") 845 } 846 .tabBar("Yellow") 847 .scale({ x: this.tabContent1Scale, y: this.tabContent1Scale }) 848 .backgroundColor(Color.Yellow) 849 .opacity(this.tabContent1Opacity) 850 .width(200) 851 .height(200) 852 853 TabContent() { 854 Text("Blue") 855 } 856 .tabBar("Blue") 857 .backgroundColor(Color.Blue) 858 .opacity(this.tabContent2Opacity) 859 .width(300) 860 .height(300) 861 862 } 863 .backgroundColor(0xf1f3f5) 864 .width('100%') 865 .height(500) 866 .margin({ top: 5 }) 867 .customContentTransition(this.useCustomAnimation ? this.baseCustomAnimation : undefined) 868 .barMode(BarMode.Scrollable) 869 .onChange((index: number) => { 870 console.info("onChange index: " + index) 871 }) 872 .onTabBarClick((index: number) => { 873 console.info("onTabBarClick index: " + index) 874 }) 875 } 876 } 877} 878``` 879 880 881 882### 示例9 883 884本示例通过onChange、onAnimationStart、onAnimationEnd、onGestureSwipe等接口实现了自定义TabBar的切换动画。 885 886```ts 887// xxx.ets 888@Entry 889@Component 890struct TabsExample { 891 @State currentIndex: number = 0 892 @State animationDuration: number = 300 893 @State indicatorLeftMargin: number = 0 894 @State indicatorWidth: number = 0 895 private tabsWidth: number = 0 896 897 @Builder 898 tabBuilder(index: number, name: string) { 899 Column() { 900 Text(name) 901 .fontSize(16) 902 .fontColor(this.currentIndex === index ? '#007DFF' : '#182431') 903 .fontWeight(this.currentIndex === index ? 500 : 400) 904 .id(index.toString()) 905 .onAreaChange((oldValue: Area,newValue: Area) => { 906 if (this.currentIndex === index && (this.indicatorLeftMargin === 0 || this.indicatorWidth === 0)){ 907 if (newValue.position.x != undefined) { 908 let positionX = Number.parseFloat(newValue.position.x.toString()) 909 this.indicatorLeftMargin = Number.isNaN(positionX) ? 0 : positionX 910 } 911 let width = Number.parseFloat(newValue.width.toString()) 912 this.indicatorWidth = Number.isNaN(width) ? 0 : width 913 } 914 }) 915 }.width('100%') 916 } 917 918 build() { 919 Stack({ alignContent: Alignment.TopStart }) { 920 Tabs({ barPosition: BarPosition.Start }) { 921 TabContent() { 922 Column().width('100%').height('100%').backgroundColor('#00CB87') 923 }.tabBar(this.tabBuilder(0, 'green')) 924 925 TabContent() { 926 Column().width('100%').height('100%').backgroundColor('#007DFF') 927 }.tabBar(this.tabBuilder(1, 'blue')) 928 929 TabContent() { 930 Column().width('100%').height('100%').backgroundColor('#FFBF00') 931 }.tabBar(this.tabBuilder(2, 'yellow')) 932 933 TabContent() { 934 Column().width('100%').height('100%').backgroundColor('#E67C92') 935 }.tabBar(this.tabBuilder(3, 'pink')) 936 } 937 .onAreaChange((oldValue: Area,newValue: Area)=> { 938 let width = Number.parseFloat(newValue.width.toString()) 939 this.tabsWidth = Number.isNaN(width) ? 0 : width 940 }) 941 .barWidth('100%') 942 .barHeight(56) 943 .width('100%') 944 .height(296) 945 .backgroundColor('#F1F3F5') 946 .animationDuration(this.animationDuration) 947 .onChange((index: number) => { 948 this.currentIndex = index // 监听索引index的变化,实现页签内容的切换。 949 }) 950 .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => { 951 // 切换动画开始时触发该回调。下划线跟着页面一起滑动,同时宽度渐变。 952 this.currentIndex = targetIndex 953 let targetIndexInfo = this.getTextInfo(targetIndex) 954 this.startAnimateTo(this.animationDuration, targetIndexInfo.left, targetIndexInfo.width) 955 }) 956 .onAnimationEnd((index: number,event: TabsAnimationEvent) => { 957 // 切换动画结束时触发该回调。下划线动画停止。 958 let currentIndicatorInfo = this.getCurrentIndicatorInfo(index,event) 959 this.startAnimateTo(0,currentIndicatorInfo.left,currentIndicatorInfo.width) 960 }) 961 .onGestureSwipe((index: number,event: TabsAnimationEvent) => { 962 // 在页面跟手滑动过程中,逐帧触发该回调。 963 let currentIndicatorInfo = this.getCurrentIndicatorInfo(index,event) 964 this.currentIndex = currentIndicatorInfo.index 965 this.indicatorLeftMargin = currentIndicatorInfo.left 966 this.indicatorWidth = currentIndicatorInfo.width 967 }) 968 969 Column() 970 .height(2) 971 .width(this.indicatorWidth) 972 .margin({ left: this.indicatorLeftMargin, top:48}) 973 .backgroundColor('#007DFF') 974 }.width('100%') 975 } 976 977 private getTextInfo(index: number): Record<string, number> { 978 let strJson = getInspectorByKey(index.toString()) 979 try { 980 let obj: Record<string, string> = JSON.parse(strJson) 981 let rectInfo: number[][] = JSON.parse('[' + obj.$rect + ']') 982 return { 'left': px2vp(rectInfo[0][0]), 'width': px2vp(rectInfo[1][0] - rectInfo[0][0]) } 983 } catch (error) { 984 return { 'left': 0, 'width': 0 } 985 } 986 } 987 988 private getCurrentIndicatorInfo(index: number, event: TabsAnimationEvent): Record<string, number> { 989 let nextIndex = index 990 if (index > 0 && event.currentOffset > 0) { 991 nextIndex-- 992 } else if (index < 3 && event.currentOffset < 0) { 993 nextIndex++ 994 } 995 let indexInfo = this.getTextInfo(index) 996 let nextIndexInfo = this.getTextInfo(nextIndex) 997 let swipeRatio = Math.abs(event.currentOffset / this.tabsWidth) 998 let currentIndex = swipeRatio > 0.5 ? nextIndex : index // 页面滑动超过一半,tabBar切换到下一页。 999 let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * swipeRatio 1000 let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * swipeRatio 1001 return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth } 1002 } 1003 1004 private startAnimateTo(duration: number, leftMargin: number, width: number) { 1005 animateTo({ 1006 duration: duration, // 动画时长 1007 curve: Curve.Linear, // 动画曲线 1008 iterations: 1, // 播放次数 1009 playMode: PlayMode.Normal, // 动画模式 1010 onFinish: () => { 1011 console.info('play end') 1012 } 1013 }, () => { 1014 this.indicatorLeftMargin = leftMargin 1015 this.indicatorWidth = width 1016 }) 1017 } 1018} 1019``` 1020 1021