1# 自定义动效tab 2 3### 介绍 4本示例介绍使用List、Text等组件,以及animateTo等接口实现自定义Tab效果 5 6### 效果预览图 7 8 9 10**使用说明** 11 121.选中页签,字体放大加粗且后面有背景条,起到强调作用。 13 142.手势触摸tab内容滑动,背景条跟随手势一起滑动。抬手时,当tab内容滑动距离不足一半时,会自动回弹,而当tab内容滑动距离大于一半时,背景条则会移动到下一个页签。当背景条滑动到一定距离后开始滑动页签条,使背景条始终能够保持在可视范围内。 15 163.点击页签,可以进行页签切换。 17 184.滑动页签条,背景条也会随之一起滑动,然后滑动tab内容,页签条会滑动到原处,使背景条处于可视范围内,之后背景条开始跟随手势滑动。 19 205.动画承接,背景条滑动过程中,触摸屏幕,背景条动画停止,松开手势,背景条继续滑动 21 22### 下载安装 23 241.模块oh-package.json5文件中引入依赖 25```typescript 26"dependencies": { 27 "customanimationtab": "har包地址" 28} 29``` 302.ets文件import自定义视图实现Tab效果组件 31```typescript 32import {CustomAnimationTabView} from 'customanimationtab' 33``` 34 35 36### 快速使用 37 38本节主要介绍了如何快速上手使用自定义视图实现Tab效果组件,包括构建Tab组件以及常见自定义参数的初始化。 39 401.构建Tab 41 42在代码合适的位置使用CustomAnimationTab组件并传入对应的参数(animationAttribute必须设置,其余参数可以使用默认值),后续将分别介绍对应参数的初始化。 43```typescript 44/** 45 * 构建自定义Tab 46 * animationAttribute: 动效属性 47 * tabsInfo: tab数据源 48 * indicatorBarAttribute: 背景条属性 49 * tabBarAttribute: 页签条属性 50 * tabController: 自定义动效tab控制器 51 * scroller: 页签条滚动控制器 52 */ 53CustomAnimationTab({ 54 animationAttribute: this.animationAttribute, 55 tabsInfo: this.tabsInfo, 56 indicatorBarAttribute: this.indicatorBarAttribute, 57 tabBarAttribute: this.tabBarAttribute, 58 tabController: this.tabController, 59 scroller: this.scroller 60}) 61``` 62 632.动效属性初始化 64 65动效属性基类为AnimationAttribute,其中封装了tab组件内部的动效属性。本示例中提供了开发自定义背景条颜色动效属性的代码。首先创建一个MyAnimationAttribute类,并继承AnimationAttribute基类,其中新增背景条颜色属性indicatorBarColor;之后创建MyAnimationAttribute状态变量对象animationAttribute,并将背景条颜色属性绑定到自定义背景条上,同时与button相关联,通过点击事件动态更改背景条颜色。(这里需要注意class对象属性级更新的正确使用) 66```typescript 67// 自定义动效属性,添加了背景条颜色变化 68@State animationAttribute: MyAnimationAttribute = new MyAnimationAttribute($r('app.color.custom_animation_tab_indicator_color')); 69``` 70```typescript 71export class MyAnimationAttribute extends AnimationAttribute { 72 // 背景条颜色 73 indicatorBarColor: ResourceColor; 74 75 constructor(indicatorBarColor: ResourceColor) { 76 super(); 77 this.indicatorBarColor = indicatorBarColor; 78 } 79} 80``` 81```typescript 82@Builder 83indicatorBar($$: BaseInterface) { 84 Column() 85 .height($r('app.float.custom_animation_tab_indicator_height')) 86 .width($r('app.string.custom_animation_tab_one_hundred_percent')) 87 // 绑定自定义动效属性 88 .backgroundColor(this.animationAttribute.indicatorBarColor) 89 .borderRadius($r('app.float.custom_animation_tab_indicator_border_radius')) 90} 91 92// 更新自定义动效变量——背景条颜色 93Column() { 94 Button($r('app.string.custom_animation_tab_button_text')) 95 .height($r('app.string.custom_animation_tab_ninety_percent')) 96 .type(ButtonType.Capsule) 97 .onClick(() => { 98 if ((this.animationAttribute.indicatorBarColor as Resource).id === 99 $r('app.color.custom_animation_tab_indicator_color").id) { 100 this.animationAttribute.indicatorBarColor = Color.Yellow; 101 } else if (this.animationAttribute.indicatorBarColor === Color.Yellow) { 102 this.animationAttribute.indicatorBarColor = $r('app.color.custom_animation_tab_indicator_color"); 103 } 104 }) 105} 106.justifyContent(FlexAlign.Center) 107.height($r('app.string.custom_animation_tab_ten_percent')) 108.width($r('app.string.custom_animation_tab_one_hundred_percent')) 109``` 110 1113.数据初始化 112 113本小节主要介绍了如何初始化自定义Tab数据源。首先构建一个TabInfo数组,然后向其中传入对应的TabInfo对象,TabInfo对象主要需要传入三个属性——页签标题、tab页面内容视图以及页签组件。以base页面为例,首先创建一个@Builder函数,在该函数中填入struct组件,在struct组件中编写对应tab页面内容视图。然后,构建对应的页签样式tabBar,其中需要添加一个TabBarItemInterface类对象作为形参,其包括了一些必要属性,可以自定义样式修改,本示例中主要通过使用当前索引curIndex与页签索引index之间的比较来动态更改页签样式。 114```typescript 115// tab数据 116tabsInfo: TabInfo[] = []; 117 118this.tabsInfo = [ 119 new TabInfo(CustomAnimationTabConfigure.DEFAULT_BASE_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)), 120 new TabInfo(CustomAnimationTabConfigure.DEFAULT_UI_TAB, wrapBuilder(uiBuilder), wrapBuilder(tabBar)), 121 new TabInfo(CustomAnimationTabConfigure.DEFAULT_DYEFFECT_TAB, wrapBuilder(dyEffectBuilder), wrapBuilder(tabBar)), 122 new TabInfo(CustomAnimationTabConfigure.DEFAULT_THIRTYPARTY_TAB, wrapBuilder(thirdPartyBuilder), wrapBuilder(tabBar)), 123 new TabInfo(CustomAnimationTabConfigure.DEFAULT_NATIVE_TAB, wrapBuilder(nativeBuilder), wrapBuilder(tabBar)), 124 new TabInfo(CustomAnimationTabConfigure.DEFAULT_OTHER_TAB, wrapBuilder(otherBuilder), wrapBuilder(tabBar)) 125] 126 127// baseBuilder页面 128import LazyDataSource from './LazyDataSource'; 129import { SkeletonLayout } from './SkeletonLayout'; 130 131@Builder 132export function baseBuilder() { 133 BasePage(); 134} 135 136@Component 137struct BasePage { 138 @State data: LazyDataSource<string> = new LazyDataSource<string>(); 139 140 aboutToAppear(): void { 141 for (let i = 0; i < 100; i++) { 142 this.data.pushData(`${i}`); 143 } 144 } 145 146 build() { 147 Column() { 148 List() { 149 LazyForEach(this.data, (data: string) => { 150 ListItem() { 151 SkeletonLayout({isMine: false}) 152 } 153 }) 154 } 155 .cachedCount(1) 156 .width('100%') 157 .height('100%') 158 } 159 .width('100%') 160 .height('100%') 161 } 162} 163 164// 页签样式 165@Builder 166function tabBar($$: TabBarItemInterface) { 167 Text($$.title) 168 .fontSize($$.curIndex === $$.index ? $r('app.float.custom_animation_tab_list_select_font_size") : $r('app.float.custom_animation_tab_list_unselect_font_size')) 169 .fontColor($r('app.color.custom_animation_tab_list_font_color')) 170 .fontWeight($$.curIndex === $$.index ? FontWeight.Bold : FontWeight.Medium) 171 .textAlign(TextAlign.Center) 172} 173``` 174 1754.背景条初始化 176 177背景条可以通过IndicatorBarAttribute类进行配置,也可以使用已有的背景条配置(目前支持两种: IndicatorBarAttribute.BACKGROUNDBAR和IndicatorBarAttribute.THINSTRIP)。本示例主要介绍了构建IndicatorBarAttribute类进行背景条配置,其中传入了背景条组件indicatorBar ,背景条宽度模式设置为内边距模式,左右边距设为20,上下边距设为10,同时设置背景条最大偏移为CustomAnimationTabConfigure.INDICATOR_MAX_LEFT以及背景条宽度扩展比例为CustomAnimationTabConfigure.DEFAULT_INDICATOR_EXPAND。 178```typescript 179indicatorBarAttribute: IndicatorBarAttribute = new IndicatorBarAttribute(this.indicatorBar, SizeMode.Padding, 20, 10, 180 CustomAnimationTabConfigure.INDICATOR_MAX_LEFT, CustomAnimationTabConfigure.DEFAULT_INDICATOR_EXPAND); 181 182@Builder 183indicatorBar($$: BaseInterface) { 184 Column() 185 .height($r('app.string.custom_animation_tab_one_hundred_percent')) 186 .width($r('app.string.custom_animation_tab_one_hundred_percent')) 187 .backgroundColor(this.animationAttribute.indicatorBarColor) 188 .borderRadius($r('app.float.custom_animation_tab_indicator_border_radius')) 189} 190``` 191 1925.页签条初始化 193 194页签条属性通过TabBarAttribute类进行配置。本例中主要配置了各个页签的宽度大小以及页签条高度。 195```typescript 196tabBarAttribute: TabBarAttribute = new TabBarAttribute(CustomAnimationTabConfigure.LIST_ITEM_WIDTH, CustomAnimationTabConfigure.TABBAR_HEIGHT) 197``` 198 1996.tab及页签条控制器初始化 200 201自定义Tab控制器以及页签条控制器分别通过CustomAnimationTabController和Scroller初始化,分别用于控制自定义Tab与页签条的行为。 202```typescript 203// tabController 204tabController: CustomAnimationTabController = new CustomAnimationTabController(); 205// scroller 206scroller: Scroller = new Scroller(); 207``` 208 209### 属性(接口)说明 210 211CustomAnimationTab组件属性 212 213| 属性 | 类型 | 释义 | 默认值 | 214|:---------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------:|:---------:| 215| animationAttribute | AnimationAttribute | 封装了包括背景条长度、背景条高度以及背景条左边距在内的动效属性,使组件内部对应的属性可以动态变化 | undefined | 216| tabsInfo | TabInfo[] | tab数据源 | - | 217| indicatorBarAttribute | IndicatorBarAttribute | 背景条属性类,配置背景条组件,以及背景条相关的属性 | - | 218| tabBarAttribute | AnimationAttribute | 页签条属性类,配置页签条相关的属性 | - | 219| tabController | CustomAnimationController | tab控制器,用于控制tabs组件进行页签切换 | - | 220| scroller | Scroller | 页签条控制器,可以控制页签条的滚动 | - | 221| animationDuration | number | 页签切换时长,控制页签切换时背景条滑动以及tab页面之间切换的时长 | 240ms | 222| startIndex | number | 配置起始的页签索引 | 0 | 223| gestureAnimation | (index: number, targetIndex: number, elementsInfo: [number, number][], ratio: number) => void | 配置在tab页面上手势滑动时背景条的跟手动效 | - | 224| autoAnimation | (index: number, targetIndex: number, elementsInfo: [number, number][], ratio: number) => void | 配置在tab页面上离手后背景条的自动滑动动效 | - | 225| clickAnimation | (index: number, targetIndex: number, indexInfo: Record<string, number>, targetIndexInfo: Record<string, number>, elementsInfo: [number, number][]) => void | 配置点击页签时背景条的滑动动效 | - | 226| getScrollInfo | (center: number, width: number) => [number, number] | 获取页签对应的背景条左边距以及页签条偏移,通过该函数可以自行配置选中各个页签时背景条的左边距以及页签条的偏移情况 | - | 227 228TabInfo类属性 229 230| 属性 | 类型 | 释义 | 默认值 | 231|:---------------:|:-------------------------------------:|:--------------------:|:---------:| 232| title | string | 页签标题 | - | 233| contentbuilder | WrappedBuilder<[]> | tab页面内容视图 | - | 234| barBuilder | WrappedBuilder<[TabBarItemInterface]> | 页签组件(没有配置,则使用内部默认配置) | undefined | 235 236IndicatorBarAttribute类属性 237 238| 属性 | 类型 | 释义 | 默认值 | 239|:-------------------:|:------------------------------:|:----------------------------------------------------------------------------------------------:|:--------------------:| 240| indicatorBar | (index: BaseInterface) => void | 自定义背景条组件 | - | 241| sizeMode | SizeMode | 背景条宽度模式 | SizeMode.Normal | 242| innerWidth | number | 1. 尺寸模式为正常模式,表示背景条宽度,值为0时则与页签宽度保持一致;<br/>2. 尺寸模式为内边距模式,表示背景条与页签项之间的左右边距 | 0 | 243| innerHeight | number | 1. 尺寸模式为正常模式,表示背景条高度,值为0时则与页签高度保持一致;<br/>2. 尺寸模式为内边距模式,表示背景条与页签项之间的上下边距 | 0 | 244| maxIndicatorBarLeft | number | 背景条最大偏移(<0: 无上限, >=0: innerMaxIndicatorBarLeft),配置背景条最大的滑动距离,超过该距离后除非页签条滑动到了底部,否则滑动页签条,背景条不再滑动 | -1 | 245| indicatorExpand | number | 背景条宽度扩展比例,配置背景条在滑动过程中宽度扩展的比例 | 1 | 246| barAlign | VerticalAlign | 背景条垂直布局,配置背景条相对于页签的位置 | VerticalAlign.Center | 247 248TabBarAttribute类属性 249 250| 属性 | 类型 | 释义 | 默认值 | 251|:------------------:|:-------------:|:-------------------------------------------------------:|:-----------------:| 252| barItemWidth | Length | 各个页签项的宽度(没有设置且尺寸模式为正常模式时,与页签同宽;没有设置且尺寸模式为内边距模式时,与背景条同宽) | undefined | 253| barHeight | Length | 页签条高度(没有设置且尺寸模式为正常模式时,与首个页签同高;没有设置且尺寸模式为内边距模式时,与背景条同高) | undefined | 254| scrollable | boolean | 是否可以滚动页签条(false则所有页签等分屏幕宽度,barItemWidth失效) | true | 255| barEdgeEffect | EdgeEffect | 页签条边缘滑动效果,支持弹簧效果和阴影效果 | EdgeEffect.Spring | 256| barVertical | BarPosition | 页签条位置,处于tab内容的上方或下方 | BarPosition.Start | 257| barBackgroundColor | ResourceColor | 页签条背景颜色 | Color.Transparent | 258 259BaseInterface属性 260 261| 属性 | 类型 | 释义 | 默认值 | 262|:--------:|:------:|:---------:|:---:| 263| curIndex | number | 当前选中的页签索引 | - | 264 265TabBarItemInterface属性 266 267| 属性 | 类型 | 释义 | 默认值 | 268|:--------:|:------:|:---------:|:---:| 269| curIndex | number | 当前选中的页签索引 | - | 270| index | number | 页签本身索引 | - | 271| title | string | 页签标题 | - | 272 273SizeMode属性 274 275| 属性 | 类型 | 释义 | 默认值 | 276|:-------:|:--:|:--------------------------------------------------:|:---:| 277| Normal | - | 标准宽度模式,背景条尺寸通过背景条宽高属性显示设置 | - | 278| Padding | - | 内边距模式,背景条尺寸通过页签上下边距隐性设置 | - | 279 280### 实现思路 281本案例的功能主要分为两个部分:一是点击页签的切换,二是滑动tab的切换。在后续两小节将对以上两个部分进行详细介绍。以下是一些重要的变量名及其含义。 282- maxListOffset:页签条最大偏移距离 283- maxIndicatorBarLeft: 背景条最大偏移距离 284- AnimationAttribute.left:背景条位置 285 286#### 1.核心函数getScrollInfo 287 288由于本案例页签动画效果分为两种不同类型的滑动,因此需要实现一个函数以分别获取每个页签对应的背景条位置以及页签条滑动偏移。 289 2901.1 背景条最大滑动距离以及页签条最大滑动距离 291 292(1)背景条最大偏移距离:背景条滑动到该处时不再向后滑动,此时页签条接管滑动。 293 294(2)页签条最大偏移距离:当页签条接管滑动以后,当滑动到末尾时,无法向后滑动,此时背景条再次接管滑动。 295 296 2971.2 三个阶段 298 299从上面的两个概念,可以看出滑动主要可以分为三个阶段:1)背景条初始滑动阶段;2)页签条滑动阶段;3)背景条再次滑动阶段。 300 3011.3 代码实现 302 303结合以上两个小节的介绍,具体代码如下所示: 304```typescript 305/** 306 * 获取页签对应的背景条位置以及页签条偏移 307 * @param center - 页签中心点 308 * @param width - 页签条宽度 309 * @returns: [背景条左端位置, 页签条偏移] 310 */ 311@Param getScrollInfo: (center: number, width: number) => [number, number] = 312 (center: number, width: number): [number, number] => { 313 // 获取背景条位置 314 let indicatorLeft: number = center - this.indicatorBarWith / 2; 315 // TODO: 知识点: 当背景条位置大于默认的背景条最大位置时,选取背景条最大位置作为背景条实际位置 316 let finalIndicatorLeft: number = this.maxIndicatorBarLeft >= 0 ? Math.min(indicatorLeft, this.maxIndicatorBarLeft) : indicatorLeft; 317 // TODO: 知识点: 背景条产生的多余距离作为页签条滑动距离 318 let listOffset: number = indicatorLeft - finalIndicatorLeft; 319 // TODO: 知识点: 当页签条偏移大于页签条可偏移量,选取页签条可偏移量作为页签条实际偏移 320 let finalListOffset: number = Math.min(listOffset, this.maxListOffset); 321 // TODO: 知识点: 页签条多余的偏移作为背景条后续的滑动距离 322 finalIndicatorLeft += listOffset - finalListOffset; 323 return [finalIndicatorLeft, finalListOffset]; 324 }; 325``` 326具体思路:首先根据页签的位置信息获取对应背景条的初始位置。 3271)第一阶段:背景条初始位置就是背景条的实际位置。 3282)第二阶段:当背景条偏移大于背景条最大偏移距离时,进入第二阶段。这时候后续多余的背景条偏移需要作为页签条偏移,以实现页签条移动。 3293)第三阶段:当页签条偏移大于页签条最大偏移量时,进入第三阶段。此时多余的页签条偏移会作为背景条的偏移,使背景条继续向后滑动。 330 331#### 2.点击页签的切换 332 333- 首先在onChange回调中实现对应的动画效果,当事件为点击事件并且需要进行页签切换时才进入到对应的动画效果实现,其中首先通过获取index页签的中心位置计算背景条位置,以实现背景条移动到当前页签位置。然后,通过elementsInfo数组获取index页签对应的页签条偏移,从而对页签条进行滑动。而背景条的滑动则通过页签条的滑动回调函数onDidScroll来进行。 334 335```typescript 336// tab 337Swiper(this.swiperController) { 338 // 布局实现 339} 340.onChange((index: number) => { 341 // 点击事件且发生页签切换 342 if (this.listTouchState === 1 && index !== this.curIndex) { 343 let indexInfo: Record<string, number> = this.getElementInfo(this.curIndex); 344 let targetIndexInfo: Record<string, number> = this.getElementInfo(index); 345 this.clickAnimation(this.curIndex, index, indexInfo, targetIndexInfo, this.elementsInfo); 346 } 347 this.curIndex = index; 348 console.log(`curIndex: ${this.curIndex}`); 349}) 350 351clickAnimation: (targetIndex: number, targetIndexInfo: Record<string, number>, elementsInfo: IndicatorAnimationInfo[]) => void = 352 (targetIndex: number, targetIndexInfo: Record<string, number>, elementsInfo: IndicatorAnimationInfo[]): void => { 353 // 根据targetIndex页签当前位置获取对应的背景条位置 354 this.animationAttribute.left = targetIndexInfo.center - this.elementsInfo[targetIndex].width / 2; 355 this.animationAttribute.indicatorBarWidth = this.elementsInfo[targetIndex].width; 356 this.animationAttribute.indicatorBarHeight = this.elementsInfo[targetIndex].height; 357 this.scroller!.scrollTo({xOffset: elementsInfo[targetIndex].offset, yOffset: 0, animation: {duration: this.animationDuration, curve: Curve.Linear}}); 358 }; 359``` 360 361- 在页签点击事件中触发页签切换事件,后续就会触发tab的onChange事件实现切换动画。 362 363```typescript 364// 页签点击事件 365ListItem() { 366 // 布局实现 367} 368.onClick(() => { 369 this.listTouchState = 1; 370 this.tabController.changeIndex(index); 371}) 372``` 373 374#### 2.滑动Tab的切换 375 376滑动页签切换主要分为两个部分:一个是背景条的滑动,一个是页签条的滑动。 377 3782.1 手势跟踪 379 380```typescript 381Swiper(this.swiperController) { 382 // 布局实现 383} 384.onGestureSwipe((index: number, event: TabsAnimationEvent) => { 385 this.listTouchState = 0; 386 let curOffset: number = event.currentOffset; 387 let targetIndex: number = index; 388 this.isReachBorder = false; 389 // tab组件到达边界使背景条和页签条跳转到终点位置 390 // TODO: 知识点: 这里不能判断到边界直接退出,因为onGestureSwipe每一帧触发回调,当手势滑动较快,上一帧背景条没有到达边界 391 // TODO(接上): 知识点: 下一帧content超出边界,这时候背景条没有更新,退出将导致背景条停滞在上一帧位置无法更新。 392 if ((index === 0 && curOffset > 0) || 393 (index === this.innerBarData.length - 1 && curOffset < 0)) { 394 this.isReachBorder = true; 395 curOffset = 0; 396 } 397 398 let ratio: number = Math.abs(curOffset / this.tabsWidth); // tab滑动比例 399 if (curOffset < 0) { // tab右滑 400 targetIndex = index + 1; 401 } else if (curOffset > 0) { // tab左滑 402 targetIndex = index - 1; 403 } 404 // 获取背景条位置及页签条偏移 405 // 获取背景条位置及页签条偏移 406 this.gestureAnimation(index, targetIndex, this.elementsInfo, ratio); 407}) 408 409/** 410 * 手势滑动动效 411 * @param index - 起始页签索引 412 * @param targetIndex - 目标页签索引 413 * @param elementsInfo - 页签信息[背景条左端位置, 页签条偏移] 414 * @param ratio - 当前手势滑动比例 415 * @returns 416 */ 417gestureAnimation: (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number) => void = 418 (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number): void => { 419 this.animationAttribute.left = elementsInfo[index].left + (elementsInfo[targetIndex].left - elementsInfo[index].left) * ratio; 420 this.scroller!.scrollTo({xOffset: elementsInfo[index].offset + (elementsInfo[targetIndex].offset - elementsInfo[index].offset) * ratio, yOffset: 0}); 421 let indicatorSize: [number, number] = this.getIndicatorSize(ratio, index, targetIndex); 422 this.animationAttribute.indicatorBarWidth = indicatorSize[0]; 423 this.animationAttribute.indicatorBarHeight = indicatorSize[1]; 424 }; 425``` 426 427具体思路: 手势跟踪滑动主要存在两种情况:1)背景条到达边界;2)背景条未到达边界。首先判断tab是否滑动到边界,若滑动到边界,则目标页签等于当前页签。否则,则根据当前的偏移情况来判断目标页签相对于当前页签的位置。然后,分别获取当前页签以及目标页签对应的背景条位置以及页签条偏移作为背景条和页签条的起始状态和最终状态。之后,可以通过计算tab滑动比例,获取当前背景条位置以及页签条偏移,公式如下所示: 428 429 4302.2 动画效果 431 432```typescript 433Swiper(this.swiperController) { 434 // 布局实现 435} 436.onContentDidScroll((selectedIndex: number, index: number, position: number, mainAxisLength: number) => { 437 // 动画启动,选取当前index索引页签的属性来执行背景条和页签条滑动 438 if (this.isAnimationStart && index === this.innerCurrnetIndex) { 439 // 使用选中页签相对于Swiper主轴起始位置的移动比例判断滑动的目标页签targetIndex的位置 440 let targetIndex: number = position < 0 ? index + 1 : index - 1; 441 if (targetIndex >= this.innerBarData.length || targetIndex < 0) { 442 console.warn(`Error: targetIndex exceeds the limit range: 443 selectedIndex: ${selectedIndex}, curIndex: ${this.innerCurrnetIndex}, index: ${index}, 444 targetIndex: ${targetIndex}, position: ${position}, mainAxisLength: ${mainAxisLength}`); 445 return; 446 } 447 let ratio: number = Math.abs(position); 448 // 通过页签比例计算当前页签条和背景条的位置 449 this.autoAnimation(index, targetIndex, this.elementsInfo, ratio); 450 } 451}) 452.onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => { 453 if (this.isReachBorder) { // 若tab到达边界,则不继续执行动画 454 return; 455 } 456 457 this.isAnimationStart = true; 458 this.listTouchState = 0; 459}) 460.onAnimationEnd(() => { 461 this.isAnimationStart = false; 462}) 463 464/** 465 * 自动滑动动效 466 * @param index - 起始页签索引 467 * @param targetIndex - 目标页签索引 468 * @param elementsInfo - 页签动效信息[背景条左端位置, 页签条偏移] 469 * @param ratio - 当前tab滑动比例 470 * @returns 471 */ 472autoAnimation: (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number) => void = 473 (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number): void => { 474 this.animationAttribute.left = elementsInfo[index].left + (elementsInfo[targetIndex].left - elementsInfo[index].left) * ratio; 475 this.scroller!.scrollTo({ 476 xOffset: elementsInfo[index].offset + (elementsInfo[targetIndex].offset - elementsInfo[index].offset) * ratio, 477 yOffset: 0 478 }); 479 let indicatorSize: [number, number] = this.getIndicatorSize(ratio, index, targetIndex); 480 this.animationAttribute.indicatorBarWidth = indicatorSize[0]; 481 this.animationAttribute.indicatorBarHeight = indicatorSize[1]; 482 }; 483``` 484 485具体思路:首先在动画开始时,在onAnimationStart回调中只进行动画开始状态的改变(i.e. this.isAnimationStart = true)。然后,在onContentDidScroll回调中进行绘制动画。具体来说,在每一次回调onContentDidScroll接口时通过起始页签index、目标页签targetIndex以及滑动比例来判断当前背景条位置以及页签条的偏移,如公式(1)所示。 因此,动画函数中最重要的就是判断index、targetIndex以及滑动比例。由于页签条的滑动等价于背景条滑动,因此只需要判断背景条的滑动情况就可以覆盖所有情况。如下图所示,这里主要存在以下三种情况的判断:1)背景条未回弹且滑动比例小于0.5;2)背景条未回弹且滑动比例大于等于0.5;3)背景条回弹。 486- 背景条未回弹且滑动比例小于0.5。这时候起始页签index应该等于curIndex,而目标页签targetIndex则可以根据滑动比例正负判断index+1(index-1)。当tab不断向左(右)滑动时,index页签滑动比例不断增加,背景条也不断向右(左)滑动。 487 488- 背景条回弹。这时候起始页签应该等于curIndex,而目标页签targetIndex则可以根据滑动比例正负判断index+1(index-1)。当tab回弹时,index页签滑动比例不断减少,背景条也不断向左(右)滑动,直至回弹到原位置。 489 490- 背景条未回弹且滑动比例大于等于0.5。这时候目标页签应该等于curIndex,起始页签index应该则可以根据滑动比例正负判断targetIndex+1(targetIndex-1)。但是,仔细观察可以发现,其实这种情况与背景条回弹情况基本一致。可以将其看作是黄色页签开始向左滑动,也可以将其看作是绿色页签开始进行回弹。因此,可以将其转化为绿色页签回弹,如后续第二张图所示。这时候起始页签应该等于curIndex,而目标页签targetIndex则可以根据滑动比例正负判断index+1(index-1)。当index页签内容回弹时,tab滑动比例不断减少,背景条也不断向右(左)滑动,直至回弹到原位置。 491 492 493 494### 高性能知识点 495 496本示例使用了[LazyForEach](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)进行数据懒加载,LazyForEach懒加载可以通过设置cachedCount属性来指定缓存数量,同时搭配[组件复用](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/performance/component-recycle.md)能力以达到性能最优效果。 497 498### 工程结构&模块类型 499 500``` 501customAnimationTabs // har类型 502|---common 503| |---CommonConstants.ets // 内置常量定义 504|---model 505| |---AnimationAttribute.ets // 动效属性 506| |---BaseInterface.test // 基础信息接口 507| |---ComponentFactory.ets // 组件工厂 508| |---CustomAniamtionTabController.ets // 自定义tab控制器 509| |---IndicatorBarAttribute.ets // 背景条属性 510| |---TabBarAttribute.ets // 页签条属性 511| |---TabBarItemInterface.ets // 页签信息接口 512| |---TabInfo.ets // tab项信息 513|---utils 514| |---CustomAnimationTab.ets // customAnimationTab组件 515|---view 516| |---BasePage.ets // tab页面内容及页签 517| |---CustomAnimationTabConfigure.ets // 用户配置 518| |---CustomAnimationTabView.ets // 样例页面 519| |---DyEffectPage.ets // tab页面内容及页签 520| |---LazyDataSource.ets // 懒加载数据 521| |---NativePage.ets // tab页面内容及页签 522| |---OtherPage.ets // tab页面内容及页签 523| |---SkeletonLayout.ets // 骨架页面 524| |---ThirdPartyPage.ets // tab页面内容及页签 525| |---UIPage.ets // tab页面内容及页签 526|---FeatureComponent.ets // AppRouter入口文件 527``` 528 529### 参考资料 530 531[RelativeContainer](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/arkts-layout-development-relative-layout.md) 532 533[显式动画 (animateTo)](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-explicit-animation.md) 534 535[轮播图 (Swiper)](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-container-swiper.md) 536 537[列表 (List)](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-container-list.md) 538 539### 相关权限 540不涉及 541 542### 约束与限制 5431.本示例仅支持在标准系统上运行,支持设备:RK3568。 544 5452.本示例为Stage模型,支持API12版本SDK,SDK版本号(API Version 12 Release)。 546 5473.本示例需要使用DevEco Studio 5.0.0 Release 才可编译运行。 548 549### 下载 550如需单独下载本工程,执行如下命令: 551 552``` 553git init 554git config core.sparsecheckout true 555echo code/UI/CustomAnimationTab/ > .git/info/sparse-checkout 556git remote add origin https://gitee.com/openharmony/applications_app_samples.git 557git pull origin master 558``` 559 560