• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&nbsp;\|&nbsp;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&nbsp;\|&nbsp;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:&nbsp;(index:&nbsp;number)&nbsp;=&gt;&nbsp;void) | Tab页签切换后触发的事件。<br>-&nbsp;index:当前显示的index索引,索引从0开始计算。<br/>触发该事件的条件:<br/>1、TabContent支持滑动时,组件触发滑动时触发。<br/>2、通过[控制器](#tabscontroller)API接口调用。<br/>3、通过[状态变量](../../../quick-start/arkts-state.md)构造的属性值进行修改。<br/>4、通过页签处点击触发。 |
108| onTabBarClick(event:&nbsp;(index:&nbsp;number)&nbsp;=&gt;&nbsp;void)<sup>10+</sup> | Tab页签点击后触发的事件。<br>-&nbsp;index:被点击的index索引,索引从0开始计算。<br/>触发该事件的条件:<br/>通过页签处点击触发。 |
109| onAnimationStart<sup>11+</sup>(handler: (index: number, targetIndex: number, event: [TabsAnimationEvent](ts-types.md#tabsanimationevent11)) => void) | 切换动画开始时触发该回调。<br/>-&nbsp;index:当前显示元素的索引。<br/>-&nbsp;targetIndex:切换动画目标元素的索引。<br/>-&nbsp;event:动画相关信息,包括主轴方向上当前显示元素和目标元素相对Tabs起始位置的位移,以及离手速度。<br/>**说明:** <br/>参数为动画开始前的index值(不是最终结束动画的index值)。 |
110| onAnimationEnd<sup>11+</sup>(handler: (index: number, event: [TabsAnimationEvent](ts-types.md#tabsanimationevent11)) => void) | 切换动画结束时触发该回调。<br/>-&nbsp;index:当前显示元素的索引。<br/>-&nbsp;event:动画相关信息,只返回主轴方向上当前显示元素相对于Tabs起始位置的位移。<br/>**说明:** <br/>当Tabs切换动效结束时触发,包括动画过程中手势中断。参数为动画结束后的index值。 |
111| onGestureSwipe<sup>11+</sup>(handler: (index: number, event: [TabsAnimationEvent](ts-types.md#tabsanimationevent11)) => void) | 在页面跟手滑动过程中,逐帧触发该回调。<br/>-&nbsp;index:当前显示元素的索引。<br/>-&nbsp;event:动画相关信息,只返回主轴方向上当前显示元素相对于Tabs起始位置的位移。 |
112| customContentTransition<sup>11+</sup>(delegate: (from: number, to: number) => [TabContentAnimatedTransition](ts-types.md#tabcontentanimatedtransition11) \| undefined) | 自定义Tabs页面切换动画。其中,from和to参数为返回给开发者使用的值,代表的含义如下:<br> -&nbsp;from:动画开始时,当前页面的index值。<br/>-&nbsp;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![tabs2](figures/tabs2.gif)
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![tabs3](figures/tabs3.gif)
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![tabs4](figures/tabs4.gif)
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![tabs5](figures/tabs5.gif)
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![tabs5](figures/tabs6.gif)
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![tabs5](figures/tabs7.gif)
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![tabs5](figures/tabs8.gif)
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![tabs10](figures/tabs10.gif)