diff --git a/OAT.xml b/OAT.xml
index 057e94e940bf656e2efd3be94fb2420058d760de..9e9403a282efa8d4b584e0e3d1b36ae9821afc93 100644
--- a/OAT.xml
+++ b/OAT.xml
@@ -156,6 +156,14 @@ Note:If the text contains special characters, please escape them according to th
+
+
+
+
+
+
+
+
diff --git a/code/UI/CustomAnimationTab/.gitignore b/code/UI/CustomAnimationTab/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d2ff20141ceed86d87c0ea5d99481973005bab2b
--- /dev/null
+++ b/code/UI/CustomAnimationTab/.gitignore
@@ -0,0 +1,12 @@
+/node_modules
+/oh_modules
+/local.properties
+/.idea
+**/build
+/.hvigor
+.cxx
+/.clangd
+/.clang-format
+/.clang-tidy
+**/.test
+/.appanalyzer
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/AppScope/app.json5 b/code/UI/CustomAnimationTab/AppScope/app.json5
new file mode 100644
index 0000000000000000000000000000000000000000..797a723231a17125115945aa1ab00f2ed2e1982b
--- /dev/null
+++ b/code/UI/CustomAnimationTab/AppScope/app.json5
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "app": {
+ "bundleName": "com.samples.customanimationtab",
+ "vendor": "example",
+ "versionCode": 1000000,
+ "versionName": "1.0.0",
+ "icon": "$media:app_icon",
+ "label": "$string:app_name"
+ }
+}
diff --git a/code/UI/CustomAnimationTab/AppScope/resources/base/element/string.json b/code/UI/CustomAnimationTab/AppScope/resources/base/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..ced7afc82b11aebdba0051c6153bf0fab69efa58
--- /dev/null
+++ b/code/UI/CustomAnimationTab/AppScope/resources/base/element/string.json
@@ -0,0 +1,8 @@
+{
+ "string": [
+ {
+ "name": "app_name",
+ "value": "CustomAnimationTab"
+ }
+ ]
+}
diff --git a/code/UI/CustomAnimationTab/AppScope/resources/base/media/app_icon.png b/code/UI/CustomAnimationTab/AppScope/resources/base/media/app_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..a39445dc87828b76fed6d2ec470dd455c45319e3
Binary files /dev/null and b/code/UI/CustomAnimationTab/AppScope/resources/base/media/app_icon.png differ
diff --git a/code/UI/CustomAnimationTab/README.md b/code/UI/CustomAnimationTab/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8a2a711d44c4693cdfb195861938d1873e5366ea
--- /dev/null
+++ b/code/UI/CustomAnimationTab/README.md
@@ -0,0 +1,565 @@
+# 自定义动效tab
+
+### 介绍
+本示例介绍使用List、Text等组件,以及animateTo等接口实现自定义Tab效果
+
+### 效果预览图
+
+
+
+**使用说明**
+
+1.选中页签,字体放大加粗且后面有背景条,起到强调作用。
+
+2.手势触摸tab内容滑动,背景条跟随手势一起滑动。抬手时,当tab内容滑动距离不足一半时,会自动回弹,而当tab内容滑动距离大于一半时,背景条则会移动到下一个页签。当背景条滑动到一定距离后开始滑动页签条,使背景条始终能够保持在可视范围内。
+
+3.点击页签,可以进行页签切换。
+
+4.滑动页签条,背景条也会随之一起滑动,然后滑动tab内容,页签条会滑动到原处,使背景条处于可视范围内,之后背景条开始跟随手势滑动。
+
+5.动画承接,背景条滑动过程中,触摸屏幕,背景条动画停止,松开手势,背景条继续滑动
+
+### 下载安装
+
+1.模块oh-package.json5文件中引入依赖
+```typescript
+"dependencies": {
+ "@ohos-cases/custom_animation_tab": "har包地址"
+}
+```
+2.ets文件import自定义视图实现Tab效果组件
+```typescript
+import {CustomAnimationTab} from '@ohos-cases/custom_animation_tab'
+```
+
+
+### 快速使用
+
+本节主要介绍了如何快速上手使用自定义视图实现Tab效果组件,包括构建Tab组件以及常见自定义参数的初始化。
+
+1.构建Tab
+
+在代码合适的位置使用CustomAnimationTab组件并传入对应的参数(animationAttribute必须设置,其余参数可以使用默认值),后续将分别介绍对应参数的初始化。
+```typescript
+/**
+ * 构建自定义Tab
+ * animationAttribute: 动效属性
+ * tabsInfo: tab数据源
+ * indicatorBarAttribute: 背景条属性
+ * tabBarAttribute: 页签条属性
+ * tabController: 自定义动效tab控制器
+ * scroller: 页签条滚动控制器
+ */
+CustomAnimationTab({
+ animationAttribute: this.animationAttribute,
+ tabsInfo: this.tabsInfo,
+ indicatorBarAttribute: this.indicatorBarAttribute,
+ tabBarAttribute: this.tabBarAttribute,
+ tabController: this.tabController,
+ scroller: this.scroller
+})
+```
+
+2.动效属性初始化
+
+动效属性基类为AnimationAttribute,其中封装了tab组件内部的动效属性。本示例中提供了开发自定义背景条颜色动效属性的代码。首先创建一个MyAnimationAttribute类,并继承AnimationAttribute基类,其中新增背景条颜色属性indicatorBarColor;之后创建MyAnimationAttribute状态变量对象animationAttribute,并将背景条颜色属性绑定到自定义背景条上,同时与button相关联,通过点击事件动态更改背景条颜色。(这里需要注意class对象属性级更新的正确使用)
+```typescript
+// 自定义动效属性,添加了背景条颜色变化
+@State animationAttribute: MyAnimationAttribute = new MyAnimationAttribute($r("app.color.custom_animation_tab_indicator_color"));
+```
+```typescript
+export class MyAnimationAttribute extends AnimationAttribute {
+ // 背景条颜色
+ indicatorBarColor: ResourceColor;
+
+ constructor(indicatorBarColor: ResourceColor) {
+ super();
+ this.indicatorBarColor = indicatorBarColor;
+ }
+}
+```
+```typescript
+@Builder
+indicatorBar($$: BaseInterface) {
+ Column()
+ .height($r("app.float.custom_animation_tab_indicator_height"))
+ .width($r("app.string.custom_animation_tab_one_hundred_percent"))
+ // 绑定自定义动效属性
+ .backgroundColor(this.animationAttribute.indicatorBarColor)
+ .borderRadius($r("app.float.custom_animation_tab_indicator_border_radius"))
+}
+
+// 更新自定义动效变量——背景条颜色
+Column() {
+ Button($r("app.string.custom_animation_tab_button_text"))
+ .height($r("app.string.custom_animation_tab_ninety_percent"))
+ .type(ButtonType.Capsule)
+ .onClick(() => {
+ if ((this.animationAttribute.indicatorBarColor as Resource).id ===
+ $r("app.color.custom_animation_tab_indicator_color").id) {
+ this.animationAttribute.indicatorBarColor = Color.Yellow;
+ } else if (this.animationAttribute.indicatorBarColor === Color.Yellow) {
+ this.animationAttribute.indicatorBarColor = $r("app.color.custom_animation_tab_indicator_color");
+ }
+ })
+}
+.justifyContent(FlexAlign.Center)
+.height($r("app.string.custom_animation_tab_ten_percent"))
+.width($r("app.string.custom_animation_tab_one_hundred_percent"))
+```
+
+3.数据初始化
+
+本小节主要介绍了如何初始化自定义Tab数据源。首先构建一个TabInfo数组,然后向其中传入对应的TabInfo对象,TabInfo对象主要需要传入三个属性——页签标题、tab页面内容视图以及页签组件。以base页面为例,首先创建一个@Builder函数,在该函数中填入struct组件,在struct组件中编写对应tab页面内容视图。然后,构建对应的页签样式tabBar,其中需要添加一个TabBarItemInterface类对象作为形参,其包括了一些必要属性,可以自定义样式修改,本示例中主要通过使用当前索引curIndex与页签索引index之间的比较来动态更改页签样式。
+```typescript
+// tab数据
+tabsInfo: TabInfo[] = [];
+
+this.tabsInfo = [
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_BASE_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_UI_TAB, wrapBuilder(uiBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_DYEFFECT_TAB, wrapBuilder(dyEffectBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_THIRTYPARTY_TAB, wrapBuilder(thirdPartyBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_NATIVE_TAB, wrapBuilder(nativeBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_OTHER_TAB, wrapBuilder(otherBuilder), wrapBuilder(tabBar))
+]
+
+// baseBuilder页面
+import LazyDataSource from './LazyDataSource';
+import { SkeletonLayout } from './SkeletonLayout';
+
+@Builder
+export function baseBuilder() {
+ BasePage();
+}
+
+@Component
+struct BasePage {
+ @State data: LazyDataSource = new LazyDataSource();
+
+ aboutToAppear(): void {
+ for (let i = 0; i < 100; i++) {
+ this.data.pushData(`${i}`);
+ }
+ }
+
+ build() {
+ Column() {
+ List() {
+ LazyForEach(this.data, (data: string) => {
+ ListItem() {
+ SkeletonLayout({isMine: false})
+ }
+ })
+ }
+ .cachedCount(1)
+ .width("100%")
+ .height("100%")
+ }
+ .width("100%")
+ .height("100%")
+ }
+}
+
+// 页签样式
+@Builder
+function tabBar($$: TabBarItemInterface) {
+ Text($$.title)
+ .fontSize($$.curIndex === $$.index ? $r("app.float.custom_animation_tab_list_select_font_size") : $r("app.float.custom_animation_tab_list_unselect_font_size"))
+ .fontColor($r("app.color.custom_animation_tab_list_font_color"))
+ .fontWeight($$.curIndex === $$.index ? FontWeight.Bold : FontWeight.Medium)
+ .textAlign(TextAlign.Center)
+}
+```
+
+4.背景条初始化
+
+背景条可以通过IndicatorBarAttribute类进行配置,也可以使用已有的背景条配置(目前支持两种: IndicatorBarAttribute.BACKGROUNDBAR和IndicatorBarAttribute.THINSTRIP)。本示例主要介绍了构建IndicatorBarAttribute类进行背景条配置,其中传入了背景条组件indicatorBar ,背景条宽度模式设置为内边距模式,左右边距设为20,上下边距设为10,同时设置背景条最大偏移为CustomAnimationTabConfigure.INDICATOR_MAX_LEFT以及背景条宽度扩展比例为CustomAnimationTabConfigure.DEFAULT_INDICATOR_EXPAND。
+```typescript
+indicatorBarAttribute: IndicatorBarAttribute = new IndicatorBarAttribute(this.indicatorBar, SizeMode.Padding, 20, 10,
+ CustomAnimationTabConfigure.INDICATOR_MAX_LEFT, CustomAnimationTabConfigure.DEFAULT_INDICATOR_EXPAND);
+
+@Builder
+indicatorBar($$: BaseInterface) {
+ Column()
+ .height($r("app.string.custom_animation_tab_one_hundred_percent"))
+ .width($r("app.string.custom_animation_tab_one_hundred_percent"))
+ .backgroundColor(this.animationAttribute.indicatorBarColor)
+ .borderRadius($r("app.float.custom_animation_tab_indicator_border_radius"))
+}
+```
+
+5.页签条初始化
+
+页签条属性通过TabBarAttribute类进行配置。本例中主要配置了各个页签的宽度大小以及页签条高度。
+```typescript
+tabBarAttribute: TabBarAttribute = new TabBarAttribute(CustomAnimationTabConfigure.LIST_ITEM_WIDTH, CustomAnimationTabConfigure.TABBAR_HEIGHT)
+```
+
+6.tab及页签条控制器初始化
+
+自定义Tab控制器以及页签条控制器分别通过CustomAnimationTabController和Scroller初始化,分别用于控制自定义Tab与页签条的行为。
+```typescript
+// tabController
+tabController: CustomAnimationTabController = new CustomAnimationTabController();
+// scroller
+scroller: Scroller = new Scroller();
+```
+
+### 属性(接口)说明
+
+CustomAnimationTab组件属性
+
+| 属性 | 类型 | 释义 | 默认值 |
+|:---------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------:|:---------:|
+| animationAttribute | AnimationAttribute | 封装了包括背景条长度、背景条高度以及背景条左边距在内的动效属性,使组件内部对应的属性可以动态变化 | undefined |
+| tabsInfo | TabInfo[] | tab数据源 | - |
+| indicatorBarAttribute | IndicatorBarAttribute | 背景条属性类,配置背景条组件,以及背景条相关的属性 | - |
+| tabBarAttribute | AnimationAttribute | 页签条属性类,配置页签条相关的属性 | - |
+| tabController | CustomAnimationController | tab控制器,用于控制tabs组件进行页签切换 | - |
+| scroller | Scroller | 页签条控制器,可以控制页签条的滚动 | - |
+| animationDuration | number | 页签切换时长,控制页签切换时背景条滑动以及tab页面之间切换的时长 | 240ms |
+| startIndex | number | 配置起始的页签索引 | 0 |
+| gestureAnimation | (index: number, targetIndex: number, elementsInfo: [number, number][], ratio: number) => void | 配置在tab页面上手势滑动时背景条的跟手动效 | - |
+| autoAnimation | (index: number, targetIndex: number, elementsInfo: [number, number][], ratio: number) => void | 配置在tab页面上离手后背景条的自动滑动动效 | - |
+| clickAnimation | (index: number, targetIndex: number, indexInfo: Record, targetIndexInfo: Record, elementsInfo: [number, number][]) => void | 配置点击页签时背景条的滑动动效 | - |
+| getScrollInfo | (center: number, width: number) => [number, number] | 获取页签对应的背景条左边距以及页签条偏移,通过该函数可以自行配置选中各个页签时背景条的左边距以及页签条的偏移情况 | - |
+
+TabInfo类属性
+
+| 属性 | 类型 | 释义 | 默认值 |
+|:---------------:|:-------------------------------------:|:--------------------:|:---------:|
+| title | string | 页签标题 | - |
+| contentbuilder | WrappedBuilder<[]> | tab页面内容视图 | - |
+| barBuilder | WrappedBuilder<[TabBarItemInterface]> | 页签组件(没有配置,则使用内部默认配置) | undefined |
+
+IndicatorBarAttribute类属性
+
+| 属性 | 类型 | 释义 | 默认值 |
+|:-------------------:|:------------------------------:|:----------------------------------------------------------------------------------------------:|:--------------------:|
+| indicatorBar | (index: BaseInterface) => void | 自定义背景条组件 | - |
+| sizeMode | SizeMode | 背景条宽度模式 | SizeMode.Normal |
+| innerWidth | number | 1. 尺寸模式为正常模式,表示背景条宽度,值为0时则与页签宽度保持一致; 2. 尺寸模式为内边距模式,表示背景条与页签项之间的左右边距 | 0 |
+| innerHeight | number | 1. 尺寸模式为正常模式,表示背景条高度,值为0时则与页签高度保持一致; 2. 尺寸模式为内边距模式,表示背景条与页签项之间的上下边距 | 0 |
+| maxIndicatorBarLeft | number | 背景条最大偏移(<0: 无上限, >=0: innerMaxIndicatorBarLeft),配置背景条最大的滑动距离,超过该距离后除非页签条滑动到了底部,否则滑动页签条,背景条不再滑动 | -1 |
+| indicatorExpand | number | 背景条宽度扩展比例,配置背景条在滑动过程中宽度扩展的比例 | 1 |
+| barAlign | VerticalAlign | 背景条垂直布局,配置背景条相对于页签的位置 | VerticalAlign.Center |
+
+TabBarAttribute类属性
+
+| 属性 | 类型 | 释义 | 默认值 |
+|:------------------:|:-------------:|:-------------------------------------------------------:|:-----------------:|
+| barItemWidth | Length | 各个页签项的宽度(没有设置且尺寸模式为正常模式时,与页签同宽;没有设置且尺寸模式为内边距模式时,与背景条同宽) | undefined |
+| barHeight | Length | 页签条高度(没有设置且尺寸模式为正常模式时,与首个页签同高;没有设置且尺寸模式为内边距模式时,与背景条同高) | undefined |
+| scrollable | boolean | 是否可以滚动页签条(false则所有页签等分屏幕宽度,barItemWidth失效) | true |
+| barEdgeEffect | EdgeEffect | 页签条边缘滑动效果,支持弹簧效果和阴影效果 | EdgeEffect.Spring |
+| barVertical | BarPosition | 页签条位置,处于tab内容的上方或下方 | BarPosition.Start |
+| barBackgroundColor | ResourceColor | 页签条背景颜色 | Color.Transparent |
+
+BaseInterface属性
+
+| 属性 | 类型 | 释义 | 默认值 |
+|:--------:|:------:|:---------:|:---:|
+| curIndex | number | 当前选中的页签索引 | - |
+
+TabBarItemInterface属性
+
+| 属性 | 类型 | 释义 | 默认值 |
+|:--------:|:------:|:---------:|:---:|
+| curIndex | number | 当前选中的页签索引 | - |
+| index | number | 页签本身索引 | - |
+| title | string | 页签标题 | - |
+
+SizeMode属性
+
+| 属性 | 类型 | 释义 | 默认值 |
+|:-------:|:--:|:--------------------------------------------------:|:---:|
+| Normal | - | 标准宽度模式,背景条尺寸通过背景条宽高属性显示设置 | - |
+| Padding | - | 内边距模式,背景条尺寸通过页签上下边距隐性设置 | - |
+
+### 实现思路
+本案例的功能实现主要可以分为点击页签的切换以及滑动tab的切换两个部分。在后续两小节将对以上两个部分进行详细介绍。以下是一些重要的变量名及其含义。
+- maxListOffset:页签条最大偏移距离
+- maxIndicatorBarLeft: 背景条最大偏移距离
+- AnimationAttribute.left:背景条位置
+
+#### 1.核心函数getScrollInfo
+
+由于页签动画效果被分为两种不同类型的滑动,因此需要实现一个函数以分别获取每个页签对应的背景条位置以及页签条滑动偏移。
+
+1.1 背景条最大滑动距离以及页签条最大滑动距离
+
+首先,需要了解两个概念:背景条最大滑动距离以及页签条最大滑动距离,如下图所示。
+
+(1)背景条最大偏移距离:背景条滑动到该处时不再向后滑动,此时页签条接管滑动。
+
+(2)页签条最大偏移距离:当页签条接管滑动以后,当滑动到末尾时,无法向后滑动,此时背景条再次接管滑动。
+
+
+1.2 三个阶段
+
+从上面的两个概念,可以看出滑动主要可以切分为三个阶段:1)背景条初始滑动阶段;2)页签条滑动阶段;3)背景条再次滑动阶段。
+
+1.3 代码实现
+
+通过以上两个小节的介绍,可以尝试实现代码。具体代码如下所示:
+```typescript
+/**
+ * 获取页签对应的背景条位置以及页签条偏移
+ * @param center - 页签中心点
+ * @param width - 页签条宽度
+ * @returns: [背景条左端位置, 页签条偏移]
+ */
+@Param getScrollInfo: (center: number, width: number) => [number, number] =
+ (center: number, width: number): [number, number] => {
+ // 获取背景条位置
+ let indicatorLeft: number = center - this.indicatorBarWith / 2;
+ // TODO: 知识点: 当背景条位置大于默认的背景条最大位置时,选取背景条最大位置作为背景条实际位置
+ let finalIndicatorLeft: number = this.maxIndicatorBarLeft >= 0 ? Math.min(indicatorLeft, this.maxIndicatorBarLeft) : indicatorLeft;
+ // TODO: 知识点: 背景条产生的多余距离作为页签条滑动距离
+ let listOffset: number = indicatorLeft - finalIndicatorLeft;
+ // TODO: 知识点: 当页签条偏移大于页签条可偏移量,选取页签条可偏移量作为页签条实际偏移
+ let finalListOffset: number = Math.min(listOffset, this.maxListOffset);
+ // TODO: 知识点: 页签条多余的偏移作为背景条后续的滑动距离
+ finalIndicatorLeft += listOffset - finalListOffset;
+ return [finalIndicatorLeft, finalListOffset];
+ };
+```
+具体思路:首先可以根据页签的位置信息获取对应背景条的初始位置。1)背景条初始位置就是背景条的实际位置;2)当背景条偏移大于背景条最大偏移距离时,进入第二阶段。这时候后续多余的背景条偏移需要作为页签条偏移,以实现页签条移动;3)当页签条偏移大于页签条最大偏移量时,进入第三阶段。此时多余的页签条偏移会作为背景条的偏移,使背景条继续向后滑动。
+
+#### 2.点击页签的切换
+
+- 首先在onChange回调中实现对应的动画效果,当事件为点击事件并且需要进行页签切换时才进入到对应的动画效果实现,其中首先通过获取index页签的中心位置计算背景条位置,以实现背景条移动到当前页签位置。然后,通过elementsInfo数组获取index页签对应的页签条偏移,从而对页签条进行滑动。而背景条的滑动则通过页签条的滑动回调函数onDidScroll来进行。
+
+```typescript
+// tab
+Swiper(this.swiperController) {
+ // 布局实现
+}
+.onChange((index: number) => {
+ // 点击事件且发生页签切换
+ if (this.listTouchState === 1 && index !== this.curIndex) {
+ let indexInfo: Record = this.getElementInfo(this.curIndex);
+ let targetIndexInfo: Record = this.getElementInfo(index);
+ this.clickAnimation(this.curIndex, index, indexInfo, targetIndexInfo, this.elementsInfo);
+ }
+ this.curIndex = index;
+ console.log(`curIndex: ${this.curIndex}`)
+})
+
+clickAnimation: (targetIndex: number, targetIndexInfo: Record, elementsInfo: IndicatorAnimationInfo[]) => void =
+ (targetIndex: number, targetIndexInfo: Record, elementsInfo: IndicatorAnimationInfo[]): void => {
+ // 根据targetIndex页签当前位置获取对应的背景条位置
+ this.animationAttribute.left = targetIndexInfo.center - this.elementsInfo[targetIndex].width / 2;
+ this.animationAttribute.indicatorBarWidth = this.elementsInfo[targetIndex].width;
+ this.animationAttribute.indicatorBarHeight = this.elementsInfo[targetIndex].height;
+ this.scroller!.scrollTo({xOffset: elementsInfo[targetIndex].offset, yOffset: 0, animation: {duration: this.animationDuration, curve: Curve.Linear}});
+ };
+```
+
+- 在页签点击事件中触发页签切换事件,后续就会触发tab的onChange事件实现切换动画。
+
+```typescript
+// 页签点击事件
+ListItem() {
+ // 布局实现
+}
+.onClick(() => {
+ this.listTouchState = 1;
+ this.tabController.changeIndex(index);
+})
+```
+
+#### 2.滑动Tab的切换
+
+滑动页签切换主要分为两个部分:一个是背景条的滑动,一个是页签条的滑动。
+
+2.1 手势跟踪
+
+```typescript
+Swiper(this.swiperController) {
+ // 布局实现
+}
+.onGestureSwipe((index: number, event: TabsAnimationEvent) => {
+ this.listTouchState = 0;
+ let curOffset: number = event.currentOffset;
+ let targetIndex: number = index;
+ this.isReachBorder = false;
+ // tab组件到达边界使背景条和页签条跳转到终点位置
+ // TODO: 知识点: 这里不能判断到边界直接退出,因为onGestureSwipe每一帧触发回调,当手势滑动较快,上一帧背景条没有到达边界
+ // TODO(接上): 知识点: 下一帧content超出边界,这时候背景条没有更新,退出将导致背景条停滞在上一帧位置无法更新。
+ if ((index === 0 && curOffset > 0) ||
+ (index === this.innerBarData.length - 1 && curOffset < 0)) {
+ this.isReachBorder = true;
+ curOffset = 0;
+ }
+
+ let ratio: number = Math.abs(curOffset / this.tabsWidth); // tab滑动比例
+ if (curOffset < 0) { // tab右滑
+ targetIndex = index + 1;
+ } else if (curOffset > 0) { // tab左滑
+ targetIndex = index - 1;
+ }
+ // 获取背景条位置及页签条偏移
+ // 获取背景条位置及页签条偏移
+ this.gestureAnimation(index, targetIndex, this.elementsInfo, ratio);
+})
+
+/**
+ * 手势滑动动效
+ * @param index - 起始页签索引
+ * @param targetIndex - 目标页签索引
+ * @param elementsInfo - 页签信息[背景条左端位置, 页签条偏移]
+ * @param ratio - 当前手势滑动比例
+ * @returns
+ */
+gestureAnimation: (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number) => void =
+ (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number): void => {
+ this.animationAttribute.left = elementsInfo[index].left + (elementsInfo[targetIndex].left - elementsInfo[index].left) * ratio;
+ this.scroller!.scrollTo({xOffset: elementsInfo[index].offset + (elementsInfo[targetIndex].offset - elementsInfo[index].offset) * ratio, yOffset: 0});
+ let indicatorSize: [number, number] = this.getIndicatorSize(ratio, index, targetIndex);
+ this.animationAttribute.indicatorBarWidth = indicatorSize[0];
+ this.animationAttribute.indicatorBarHeight = indicatorSize[1];
+ };
+```
+
+具体思路: 手势跟踪滑动主要存在两种情况:1)背景条到达边界;2)背景条未到达边界。首先判断tab是否滑动到边界,若滑动到边界,则目标页签等于当前页签。否则,则根据当前的偏移情况来判断目标页签相对于当前页签的位置。然后,分别获取当前页签以及目标页签对应的背景条位置以及页签条偏移作为背景条和页签条的起始状态和最终状态。之后,可以通过计算tab滑动比例,获取当前背景条位置以及页签条偏移,公式如下所示:
+
+
+2.2 动画效果
+
+```typescript
+Swiper(this.swiperController) {
+ // 布局实现
+}
+.onContentDidScroll((selectedIndex: number, index: number, position: number, mainAxisLength: number) => {
+ // 动画启动,选取当前index索引页签的属性来执行背景条和页签条滑动
+ if (this.isAnimationStart && index === this.innerCurrnetIndex) {
+ // 使用选中页签相对于Swiper主轴起始位置的移动比例判断滑动的目标页签targetIndex的位置
+ let targetIndex: number = position < 0 ? index + 1 : index - 1;
+ if (targetIndex >= this.innerBarData.length || targetIndex < 0) {
+ console.warn(`Error: targetIndex exceeds the limit range:
+ selectedIndex: ${selectedIndex}, curIndex: ${this.innerCurrnetIndex}, index: ${index},
+ targetIndex: ${targetIndex}, position: ${position}, mainAxisLength: ${mainAxisLength}`);
+ return;
+ }
+ let ratio: number = Math.abs(position);
+ // 通过页签比例计算当前页签条和背景条的位置
+ this.autoAnimation(index, targetIndex, this.elementsInfo, ratio);
+ }
+})
+.onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
+ if (this.isReachBorder) { // 若tab到达边界,则不继续执行动画
+ return;
+ }
+
+ this.isAnimationStart = true;
+ this.listTouchState = 0;
+})
+.onAnimationEnd(() => {
+ this.isAnimationStart = false;
+})
+
+/**
+ * 自动滑动动效
+ * @param index - 起始页签索引
+ * @param targetIndex - 目标页签索引
+ * @param elementsInfo - 页签动效信息[背景条左端位置, 页签条偏移]
+ * @param ratio - 当前tab滑动比例
+ * @returns
+ */
+autoAnimation: (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number) => void =
+ (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number): void => {
+ this.animationAttribute.left = elementsInfo[index].left + (elementsInfo[targetIndex].left - elementsInfo[index].left) * ratio;
+ this.scroller!.scrollTo({
+ xOffset: elementsInfo[index].offset + (elementsInfo[targetIndex].offset - elementsInfo[index].offset) * ratio,
+ yOffset: 0
+ });
+ let indicatorSize: [number, number] = this.getIndicatorSize(ratio, index, targetIndex);
+ this.animationAttribute.indicatorBarWidth = indicatorSize[0];
+ this.animationAttribute.indicatorBarHeight = indicatorSize[1];
+ };
+```
+
+具体思路:首先在动画开始时,在onAnimationStart回调中只进行动画开始状态的改变(i.e. this.isAnimationStart = true)。然后,在onContentDidScroll回调中进行绘制动画。具体来说,在每一次回调onContentDidScroll接口时通过起始页签index、目标页签targetIndex以及滑动比例来判断当前背景条位置以及页签条的偏移,如公式(1)所示。 因此,动画函数中最重要的就是判断index、targetIndex以及滑动比例。由于页签条的滑动等价于背景条滑动,因此只需要判断背景条的滑动情况就可以覆盖所有情况。如下图所示,这里主要存在以下三种情况的判断:1)背景条未回弹且滑动比例小于0.5;2)背景条未回弹且滑动比例大于等于0.5;3)背景条回弹。
+- 背景条未回弹且滑动比例小于0.5。这时候起始页签index应该等于curIndex,而目标页签targetIndex则可以根据滑动比例正负判断index+1(index-1)。当tab不断向左(右)滑动时,index页签滑动比例不断增加,背景条也不断向右(左)滑动。
+
+- 背景条回弹。这时候起始页签应该等于curIndex,而目标页签targetIndex则可以根据滑动比例正负判断index+1(index-1)。当tab回弹时,index页签滑动比例不断减少,背景条也不断向左(右)滑动,直至回弹到原位置。
+
+- 背景条未回弹且滑动比例大于等于0.5。这时候目标页签应该等于curIndex,起始页签index应该则可以根据滑动比例正负判断targetIndex+1(targetIndex-1)。但是,仔细观察可以发现,其实这种情况与背景条回弹情况基本一致。可以将其看作是黄色页签开始向左滑动,也可以将其看作是绿色页签开始进行回弹。因此,可以将其转化为绿色页签回弹,如后续第二张图所示。这时候起始页签应该等于curIndex,而目标页签targetIndex则可以根据滑动比例正负判断index+1(index-1)。当index页签内容回弹时,tab滑动比例不断减少,背景条也不断向右(左)滑动,直至回弹到原位置。
+
+
+
+### 高性能知识点
+
+本示例使用了[LazyForEach](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)进行数据懒加载,LazyForEach懒加载可以通过设置cachedCount属性来指定缓存数量,同时搭配[组件复用](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/quick-start/arkts-reusable.md)能力以实现性能优化。
+
+### 工程结构&模块类型
+
+```
+customAnimationTab // har类型
+|---common
+| |---CommonConstants.ets // 内置常量定义
+|---model
+| |---AnimationAttribute.ets // 动效属性
+| |---BaseInterface.test // 基础信息接口
+| |---ComponentFactory.ets // 组件工厂
+| |---CustomAniamtionTabController.ets // 自定义tab控制器
+| |---IndicatorBarAttribute.ets // 背景条属性
+| |---TabBarAttribute.ets // 页签条属性
+| |---TabBarItemInterface.ets // 页签信息接口
+| |---TabInfo.ets // tab项信息
+|---utils
+| |---CustomAnimationTab.ets // customAnimationTab组件
+|---view
+| |---BasePage.ets // tab页面内容及页签
+| |---CustomAnimationTabConfigure.ets // 用户配置
+| |---CustomAnimationTabView.ets // 样例页面
+| |---DyEffectPage.ets // tab页面内容及页签
+| |---LazyDataSource.ets // 懒加载数据
+| |---NativePage.ets // tab页面内容及页签
+| |---OtherPage.ets // tab页面内容及页签
+| |---SkeletonLayout.ets // 骨架页面
+| |---ThirdPartyPage.ets // tab页面内容及页签
+| |---UIPage.ets // tab页面内容及页签
+|---FeatureComponent.ets // AppRouter入口文件
+```
+
+### 参考资料
+
+[RelativeContainer](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-container-relativecontainer.md)
+
+[显式动画 (animateTo)](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-explicit-animation.md)
+
+[Swiper](https://docs.openharmony.cn/pages/v4.1/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-container-swiper.md)
+
+[List](https://docs.openharmony.cn/pages/v5.0/en/application-dev/reference/apis-arkui/arkui-ts/ts-container-list.md)
+
+### 相关权限
+
+不涉及。
+
+### 依赖
+
+不涉及。
+
+### 约束与限制
+
+1.本示例仅支持标准系统上运行。
+
+2.本示例为Stage模型,支持API13版本SDK,SDK版本号(API Version 13 Release)。
+
+3.本示例需要使用DevEco Studio版本号(DevEco Studio 5.0.0 Release)及以上版本才可编译运行。
+
+### 下载
+
+如需单独下载本工程,执行如下命令:
+
+```shell
+git init
+git config core.sparsecheckout true
+echo code/UI/CustomAnimationTab > .git/info/sparse-checkout
+git remote add origin https://gitee.com/openharmony/applications_app_samples.git
+git pull origin master
+```
+
diff --git a/code/UI/CustomAnimationTab/build-profile.json5 b/code/UI/CustomAnimationTab/build-profile.json5
new file mode 100644
index 0000000000000000000000000000000000000000..8aa45a9d53f2a847640056d33833d517e0482b04
--- /dev/null
+++ b/code/UI/CustomAnimationTab/build-profile.json5
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "app": {
+ "signingConfigs": [],
+ "products": [
+ {
+ "name": "default",
+ "signingConfig": "default",
+ compileSdkVersion: 13,
+ "compatibleSdkVersion": 13,
+ "runtimeOS": "OpenHarmony",
+ "buildOption": {
+ "strictMode": {
+ "caseSensitiveCheck": true,
+ "useNormalizedOHMUrl": true
+ }
+ }
+ }
+ ],
+ "buildModeSet": [
+ {
+ "name": "debug"
+ },
+ {
+ "name": "release"
+ }
+ ]
+ },
+ "modules": [
+ {
+ "name": "entry",
+ "srcPath": "./entry",
+ "targets": [
+ {
+ "name": "default",
+ "applyToProducts": [
+ "default"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "customanimationtab",
+ "srcPath": "./casesfeature/customanimationtab"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/.gitignore b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/.gitignore
@@ -0,0 +1,6 @@
+/node_modules
+/oh_modules
+/.preview
+/build
+/.cxx
+/.test
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/.ohpmignore b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/.ohpmignore
new file mode 100644
index 0000000000000000000000000000000000000000..a95ccc7de049e333a4511b6625d113bdc7aac7cc
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/.ohpmignore
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+src/main/ets/FeatureComponent
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/BuildProfile.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/BuildProfile.ets
new file mode 100644
index 0000000000000000000000000000000000000000..b64c9ceef9433286022b6163230dd574f061fe56
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/BuildProfile.ets
@@ -0,0 +1,17 @@
+/**
+ * Use these variables when you tailor your ArkTS code. They must be of the const type.
+ */
+export const HAR_VERSION = '1.0.3';
+export const BUILD_MODE_NAME = 'debug';
+export const DEBUG = true;
+export const TARGET_NAME = 'default';
+
+/**
+ * BuildProfile Class is used only for compatibility purposes.
+ */
+export default class BuildProfile {
+ static readonly HAR_VERSION = HAR_VERSION;
+ static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
+ static readonly DEBUG = DEBUG;
+ static readonly TARGET_NAME = TARGET_NAME;
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/CHANGELOG.md b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..aab9346a75d2c113f3e4743f9a7d98eb1f93991a
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/CHANGELOG.md
@@ -0,0 +1,20 @@
+# 版本记录
+
+## 1.0.3
+
+1.修复了背景条颜色无法修改的bug
+
+## 1.0.2
+
+1.修复了页签项宽度小于页签条长度时,背景条位置不正确的bug
+
+2.将页签条背景色属性集成到TabBarAttribute类中
+
+3.提供了一些默认属性值
+
+## 1.0.1
+1.新增页签条位置设置功能
+
+## 1.0.0
+
+1.发布1.0.0初版
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/Index.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/Index.ets
new file mode 100644
index 0000000000000000000000000000000000000000..76d343025ed1d1da0a0c21cf2b497643283642f6
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/Index.ets
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { CustomAnimationTab } from './src/main/ets/utils/CustomAnimationTab'
+export { AnimationAttribute } from './src/main/ets/model/AnimationAttribute'
+export { CustomAnimationTabController } from './src/main/ets/model/CustomAniamtionTabController'
+export { IndicatorBarAttribute } from './src/main/ets/model/IndicatorBarAttribute'
+export { TabBarAttribute } from './src/main/ets/model/TabBarAttribute'
+export { TabBarItemInterface } from './src/main/ets/model/TabBarItemInterface'
+export { TabInfo } from './src/main/ets/model/TabInfo'
+export { BaseInterface } from './src/main/ets/model/BaseInterface'
+export { CustomAnimationTabView } from './src/main/ets/view/CustomAnimationTabView'
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/LICENSE b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..86ec6ba2ac85e19c7cb69aeb39d894041304d500
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2024 ohos-cases
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/build-profile.json5 b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/build-profile.json5
new file mode 100644
index 0000000000000000000000000000000000000000..caeb5a51fc02c7e2dd419db8ca8b392e303ccb8d
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/build-profile.json5
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "apiType": "stageMode",
+ "buildOption": {
+ },
+ "buildOptionSet": [
+ {
+ "name": "release",
+ "arkOptions": {
+ "obfuscation": {
+ "ruleOptions": {
+ "enable": true,
+ "files": [
+ "./obfuscation-rules.txt"
+ ]
+ },
+ "consumerFiles": [
+ "./consumer-rules.txt"
+ ]
+ }
+ },
+ },
+ ],
+ "targets": [
+ {
+ "name": "default"
+ },
+ {
+ "name": "ohosTest"
+ }
+ ]
+}
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/consumer-rules.txt b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/consumer-rules.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/custom_animation_tabs.gif b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/custom_animation_tabs.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4d1d6eed1951dd337f2175929a19b74e0ee5a848
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/custom_animation_tabs.gif differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/hvigorfile.ts b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/hvigorfile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0785a511e1f806573e5208d382645d48719a0f06
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/hvigorfile.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { harTasks } from '@ohos/hvigor-ohos-plugin';
+
+export default {
+ system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
+ plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
+}
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/obfuscation-rules.txt b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/obfuscation-rules.txt
new file mode 100644
index 0000000000000000000000000000000000000000..69c4d6a8a5531548e4886fa766090c5c157a87d9
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/obfuscation-rules.txt
@@ -0,0 +1,18 @@
+# Define project specific obfuscation rules here.
+# You can include the obfuscation configuration files in the current module's build-profile.json5.
+#
+# For more details, see
+# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
+
+# Obfuscation options:
+# -disable-obfuscation: disable all obfuscations
+# -enable-property-obfuscation: obfuscate the property names
+# -enable-toplevel-obfuscation: obfuscate the names in the global scope
+# -compact: remove unnecessary blank spaces and all line feeds
+# -remove-log: remove all console.* statements
+# -print-namecache: print the name cache that contains the mapping from the old names to new names
+# -apply-namecache: reuse the given cache file
+
+# Keep options:
+# -keep-property-name: specifies property names that you want to keep
+# -keep-global-name: specifies names that you want to keep in the global scope
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/oh-package.json5 b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/oh-package.json5
new file mode 100644
index 0000000000000000000000000000000000000000..7499ca720c9768eddc33d53f6301310310810797
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/oh-package.json5
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "name" : "customanimationtab",
+ "version" : "1.0.3",
+ "description" : "CustomAnimationTab实现自定义动效Tab,背景条跟随手势移动、点击等交互产生动效",
+ "keywords" : [ "跟手滑动", "自定义动效", "点击跳转", "Harmony", "OpenHarmony" ],
+ "main" : "Index.ets",
+ "author" : "ohos-cases",
+ "license" : "Apache License 2.0",
+ "homepage" : "https://gitee.com/harmonyos-cases/cases",
+ "repository" : "https://gitee.com/harmonyos-cases/cases/tree/master/CommonAppDevelopment/feature/customanimationtab",
+ "dependencies" : { }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/common/CommonConstants.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/common/CommonConstants.ets
new file mode 100644
index 0000000000000000000000000000000000000000..daed46f075b25c24956938a628ebd48831e2e797
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/common/CommonConstants.ets
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 内置默认属性值
+ *
+ * @since 1.0
+ */
+export class CommonConstants {
+ // 默认背景条伸缩比例
+ static readonly DEFAULT_INDICATOR_EXPAND = 1.5;
+ // 默认动画时长
+ static readonly DEFAULT_ANIMATION_DURATION = 240;
+ // 默认页签项宽度(单位: px)
+ static readonly DEFAULT_LIST_ITEM_WIDTH = 90;
+ // 默认页签条到达边界后继续移动的偏移相对于手滑偏移的比例
+ static readonly DEFAULT_LIST_RELATIVE_RATIO = 0.25;
+ // 页签条放手回弹时间
+ static readonly DEFAULT_TAB_SPRING_DURATION = 180;
+ // 页签条高度
+ static readonly DEFAULT_BAR_HEIGHT = 60;
+ // 100%
+ static readonly FULL_PERCENT = '100%';
+ // 40%
+ static readonly FORTY_PERCENT = '40%';
+ // 默认页签内容选中字体大小
+ static readonly DEFAULT_TAB_BAR_SELECT_FONT_SIZE = 17;
+ // 默认页签内容未选中字体大小
+ static readonly DEFAULT_TAB_BAR_UNSELECT_FONT_SIZE = 16;
+ // 默认页签内容字体颜色
+ static readonly DEFAULT_TAB_BAR_FONT_COLOR = '0x000000';
+ // Background模式的背景条颜色
+ static readonly BACKGROUND_INDICATOR_COLOR = 0xC0C0C0;
+ // Background模式的背景条高度
+ static readonly BACKGROUND_INDICATOR_HEIGHT = '80%';
+ // Background模式的背景条边框圆角
+ static readonly BACKGROUND_INDICATOR_BORDER_RADIUS = '40%';
+ // Background模式的背景条对齐模式
+ static readonly BACKGROUND_INDICATOR_ALIGN = VerticalAlign.Center;
+ // thinStrip模式的背景条颜色
+ static readonly THINSTRIP_INDICATOR_COLOR = 0xFF4500;
+ // Background模式的背景条高度
+ static readonly THINSTRIP_INDICATOR_HEIGHT = 3;
+ // Background模式的背景条的边框圆角
+ static readonly THINSTRIP_INDICATOR_BORDER_RADIUS = '40%';
+ // Background模式的背景条对齐模式
+ static readonly THINSTRIP_INDICATOR_ALIGN = VerticalAlign.Bottom;
+ // tab标题
+ static readonly DEFAULT_TITLE1_TAB = '标题1';
+ // tab标题
+ static readonly DEFAULT_TITLE2_TAB = '标题2';
+ // tab标题
+ static readonly DEFAULT_TITLE3_TAB = '标题3';
+ // tab标题
+ static readonly DEFAULT_TITLE4_TAB = '标题4';
+ // tab内容字体大小
+ static readonly DEFAULT_TAB_CONTENT_FONT_SIZE = 40;
+ // 背景条宽度
+ static readonly DEFAULT_INDICATOR_WIDTH = 90;
+ // 背景条高度
+ static readonly DEFAULT_INDICATOR_HEIGHT = 40;
+ // 默认背景条边角半径
+ static readonly DEFAULT_INDICATOR_BORDER_RADIUS = 20;
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/AnimationAttribute.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/AnimationAttribute.ets
new file mode 100644
index 0000000000000000000000000000000000000000..a45f21d9542714ba0cf14a1642eedc2771784169
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/AnimationAttribute.ets
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 动效属性,提供了必需的动效属性,后续添加属性可通过继承该类实现
+ *
+ */
+export class AnimationAttribute {
+ // 背景条左边距,即:背景条左侧离tab组件左侧的距离
+ public left: number = 0;
+ // 背景条宽度
+ public indicatorBarWidth: number = 0;
+ // 背景条高度
+ public indicatorBarHeight: number = 0;
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/BaseInterface.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/BaseInterface.ets
new file mode 100644
index 0000000000000000000000000000000000000000..bcf2a8819e238acdfe3622b47521456c32b89a54
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/BaseInterface.ets
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 基础接口
+ *
+ */
+export interface BaseInterface {
+ // 当前选中的页签索引
+ curIndex: number;
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/ComponentFactory.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/ComponentFactory.ets
new file mode 100644
index 0000000000000000000000000000000000000000..00c96cd10a02d5bbde0e888a844d75b45ff6d09b
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/ComponentFactory.ets
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TabBarItemInterface } from './TabBarItemInterface';
+import { TabInfo } from './TabInfo';
+
+/**
+ * 组件工厂,可以注册TabInfo对象并使用
+ *
+ */
+export class ComponentFactory {
+ // tabInfo对象集合
+ private tabsInfo: Map;
+ // 注册标题集合
+ private keys: string[] = [];
+
+ /**
+ * 构造器
+ */
+ public constructor() {
+ this.tabsInfo = new Map();
+ }
+
+ /**
+ * 设置tab项内容
+ * @param name - tab项标题
+ * @param content - tab项内容
+ */
+ public set(name: string, tabInfo: TabInfo) {
+ this.tabsInfo.set(name, tabInfo);
+ }
+
+ /**
+ * 获取tab项内容
+ * @param name - tab项标题
+ * @returns: tab项内容
+ */
+ public getContent(name: string): WrappedBuilder<[ESObject]> | undefined {
+ return this.tabsInfo.get(name)?.contentbuilder;
+ }
+
+ /**
+ * 获取tabBar
+ * @param name - tab项标题
+ * @returns: tabBar
+ */
+ public getBar(name: string): WrappedBuilder<[TabBarItemInterface]> | undefined {
+ return this.tabsInfo.get(name)?.barBuilder;
+ }
+
+ /**
+ * 获取输入参数
+ * @param name - tab项标题
+ * @returns: 输入参数
+ */
+ public getParams(name: string): ESObject {
+ return this.tabsInfo.get(name)?.params;
+ }
+
+ /**
+ * 删除tab项
+ * @param name - tab项标题
+ */
+ public delete(name: string) {
+ this.keys = [];
+ this.tabsInfo.delete(name);
+ }
+
+ /**
+ * 获取注册标题集合
+ * @returns: 包含所有注册标题的数组
+ */
+ public toArray(): string[] {
+ if (this.keys.length > 0) {
+ return this.keys;
+ }
+ let array: string[] = [];
+ let keys: IterableIterator = this.tabsInfo.keys()
+ for (let keysElement of keys) {
+ array.push(keysElement);
+ }
+ return array;
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/CustomAniamtionTabController.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/CustomAniamtionTabController.ets
new file mode 100644
index 0000000000000000000000000000000000000000..0f36283d86d0ecccba8b113f5562de1ce03b0ef4
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/CustomAniamtionTabController.ets
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 自定义动效tab控制器,控制自定义动效Tab组件进行页签切换
+ *
+ */
+export class CustomAnimationTabController extends SwiperController {
+ // 状态监听器
+ private listener: (state: number) => void = (state: number) => {
+ };
+
+ public changeIndex(index: number, useAnimation?: boolean | undefined): void {
+ this.listener(1);
+ super.changeIndex(index, useAnimation);
+ }
+
+ public setListener(func: (state: number) => void) {
+ this.listener = func;
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/IndicatorAniamtionInfo.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/IndicatorAniamtionInfo.ets
new file mode 100644
index 0000000000000000000000000000000000000000..57fac076d5dc8c14ede43d07944d0b6baeb7642c
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/IndicatorAniamtionInfo.ets
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface IndicatorAnimationInfo {
+ // 背景条左边距
+ left: number;
+
+ // 页签条便宜
+ offset: number;
+
+ // 背景条高度
+ height: number;
+
+ // 背景条宽度
+ width: number;
+
+ // 是否初始化(true: 已初始化, false: 未初始化)
+ flag: boolean;
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/IndicatorBarAttribute.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/IndicatorBarAttribute.ets
new file mode 100644
index 0000000000000000000000000000000000000000..10add1825b67cfd4d7da0ecc1e031f9ff0209d7c
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/IndicatorBarAttribute.ets
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { BaseInterface } from './BaseInterface';
+import { CommonConstants } from '../common/CommonConstants';
+import { SizeMode } from './SizeMode';
+
+/**
+ * 自定义背景条属性
+ *
+ */
+export class IndicatorBarAttribute {
+ // background模式背景条
+ public static readonly BACKGROUNDBAR: IndicatorBarAttribute =
+ new IndicatorBarAttribute(backgroundBar, SizeMode.Padding, 20, 10, 0, 1, VerticalAlign.Center);
+ // thinstrip模式背景条
+ public static readonly THINSTRIP: IndicatorBarAttribute =
+ new IndicatorBarAttribute(thinStrip, SizeMode.Normal, 0, CommonConstants.THINSTRIP_INDICATOR_HEIGHT, 0, 1,
+ VerticalAlign.Bottom);
+ // 自定义背景条组件
+ private innerIndicatorBar: (index: BaseInterface) => void;
+ // 尺寸模式
+ private innerSizeMode: SizeMode;
+ // 1. 尺寸模式为正常模式,表示背景条宽度,值为0时与页签宽度保持一致
+ // 2. 尺寸模式为内边距模式,表示背景条与页签项之间的左右边距
+ private innerWidth: number;
+ // 1. 尺寸模式为正常模式,表示背景条高度,值为0时与页签高度保持一致
+ // 2. 尺寸模式为内边距模式,表示背景条与页签项之间的上下边距
+ private innerHeight: number
+ // 背景条最大偏移(<0: 无上限, >=0: innerMaxIndicatorBarLeft)
+ private innerMaxIndicatorBarLeft: number;
+ // 背景条宽度扩展比例
+ private innerIndicatorExpand: number;
+ // 背景条垂直布局
+ private innerBarAlign: VerticalAlign;
+
+ constructor(indicatorBar: (index: BaseInterface) => void, sizeMode: SizeMode = SizeMode.Normal,
+ indicatorWidth: number = 0, indicatorHeight: number = 0, maxIndicatorBarLeft: number = -1,
+ indicatorExpand: number = 1, barAlign: VerticalAlign = VerticalAlign.Center) {
+ this.innerIndicatorBar = indicatorBar;
+ this.innerSizeMode = sizeMode;
+ this.innerWidth = indicatorWidth;
+ this.innerHeight = indicatorHeight;
+ this.innerBarAlign = barAlign;
+ this.innerIndicatorExpand = indicatorExpand;
+ this.innerMaxIndicatorBarLeft = maxIndicatorBarLeft;
+ }
+
+ get indicatorBar(): (index: BaseInterface) => void {
+ return this.innerIndicatorBar;
+ }
+
+ set maxIndicatorBarLeft(left: number) {
+ this.innerMaxIndicatorBarLeft = left;
+ }
+
+ get maxIndicatorBarLeft(): number {
+ return this.innerMaxIndicatorBarLeft;
+ }
+
+ get barAlign(): VerticalAlign {
+ return this.innerBarAlign;
+ }
+
+ get indicatorExpand(): number {
+ return this.innerIndicatorExpand;
+ }
+
+ get sizeMode(): SizeMode {
+ return this.innerSizeMode;
+ }
+
+ get indicatorWidth(): number {
+ return this.innerWidth;
+ }
+
+ get indicatorHeight(): number {
+ return this.innerHeight;
+ }
+}
+
+@Builder
+function backgroundBar($$: BaseInterface) {
+ Column()
+ .height(CommonConstants.FULL_PERCENT)
+ .width(CommonConstants.FULL_PERCENT)
+ .backgroundColor(CommonConstants.BACKGROUND_INDICATOR_COLOR)
+ .borderRadius(CommonConstants.BACKGROUND_INDICATOR_BORDER_RADIUS)
+}
+
+@Builder
+function thinStrip($$: BaseInterface) {
+ Column()
+ .height(CommonConstants.FULL_PERCENT)
+ .width(CommonConstants.FORTY_PERCENT)
+ .backgroundColor(CommonConstants.THINSTRIP_INDICATOR_COLOR)
+ .borderRadius(CommonConstants.THINSTRIP_INDICATOR_BORDER_RADIUS)
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/SizeMode.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/SizeMode.ets
new file mode 100644
index 0000000000000000000000000000000000000000..41076023a1daf342eeff6da8226c13e6645b710e
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/SizeMode.ets
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 背景条宽度模式
+ */
+export enum SizeMode {
+ // 标准宽度模式
+ Normal,
+ // 内边距模式
+ Padding
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabBarAttribute.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabBarAttribute.ets
new file mode 100644
index 0000000000000000000000000000000000000000..fdd3e00ee418e75d46f2bacd36a948a3ff8123b0
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabBarAttribute.ets
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 自定义页签条属性
+ *
+ */
+export class TabBarAttribute {
+ // 页签项宽度
+ private innerBarItemWidth: Length | undefined;
+ // 页签条边缘滑动效果
+ private innerBarEdgeEffect: EdgeEffect;
+ // 页签条高度
+ private innerBarHeight: Length | undefined;
+ // 是否可以滚动页签条(false则所有页签均分屏幕宽度)
+ private innerScrollable: boolean;
+ // 页签条位置
+ private innerBarVertical: BarPosition;
+ // 页签条边距
+ private innerBarMargin: Margin | undefined;
+ // 页签条背景颜色
+ private innerBarBackgroundColor: ResourceColor;
+
+ constructor(barItemWidth: Length | undefined = undefined, barHeight: Length | undefined = undefined,
+ scrollable: boolean = true, barEdgeEffect: EdgeEffect = EdgeEffect.Spring,
+ barVertical: BarPosition = BarPosition.Start,
+ barMargin: Margin | undefined = undefined, barBackgroundColor: ResourceColor = Color.Transparent) {
+ this.innerBarItemWidth = barItemWidth;
+ this.innerBarHeight = barHeight;
+ this.innerScrollable = scrollable;
+ this.innerBarEdgeEffect = barEdgeEffect;
+ this.innerBarVertical = barVertical;
+ this.innerBarBackgroundColor = barBackgroundColor;
+ this.innerBarMargin = barMargin;
+ }
+
+ get barItemWidth(): Length | undefined {
+ return this.innerBarItemWidth;
+ }
+
+ get barHeight(): Length | undefined {
+ return this.innerBarHeight;
+ }
+
+ get scrollable(): boolean {
+ return this.innerScrollable;
+ }
+
+ get barEdgeEffect(): EdgeEffect {
+ return this.innerBarEdgeEffect;
+ }
+
+ get barVertical(): BarPosition {
+ return this.innerBarVertical;
+ }
+
+ get barBackgroundColor(): ResourceColor {
+ return this.innerBarBackgroundColor;
+ }
+
+ get barMargin(): Margin | undefined {
+ return this.innerBarMargin;
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabBarItemInterface.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabBarItemInterface.ets
new file mode 100644
index 0000000000000000000000000000000000000000..f9bc73c8750f7b631b5bf984e315c3a59ccd4c0b
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabBarItemInterface.ets
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { BaseInterface } from './BaseInterface';
+
+/**
+ * 页签信息接口
+ *
+ */
+export interface TabBarItemInterface extends BaseInterface {
+ // 页签自身索引
+ index: number;
+
+ // 页签标题
+ title: string;
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabInfo.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabInfo.ets
new file mode 100644
index 0000000000000000000000000000000000000000..727d1fcc79621c0b985b68aa5d20ed1bcfe4c41a
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/model/TabInfo.ets
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { TabBarItemInterface } from './TabBarItemInterface';
+
+/**
+ * 构建一个Tab项所需要的信息
+ *
+ */
+export class TabInfo {
+ // tab项标题(页签内容)
+ public title: string;
+ // tab项内容
+ public contentbuilder: WrappedBuilder<[ESObject]>;
+ // tabBar(页签样式)
+ public barBuilder?: WrappedBuilder<[TabBarItemInterface]>;
+ // tabBarIndex
+ public params?: ESObject
+
+ /**
+ * TabInfo构造器
+ * @param title - tab项标题
+ * @param contentBuilder - tab项内容
+ * @param barBuilder - tabBar
+ */
+ constructor(title: string, contentBuilder: WrappedBuilder<[ESObject]>,
+ barBuilder?: WrappedBuilder<[TabBarItemInterface]>, params?: ESObject) {
+ this.title = title;
+ this.contentbuilder = contentBuilder;
+ this.barBuilder = barBuilder;
+ this.params = params;
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/utils/CustomAnimationTab.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/utils/CustomAnimationTab.ets
new file mode 100644
index 0000000000000000000000000000000000000000..dd7b20fbcfd15217167b55f910e38d94c5009e29
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/utils/CustomAnimationTab.ets
@@ -0,0 +1,630 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentUtils, curves } from '@kit.ArkUI';
+import { CommonConstants } from '../common/CommonConstants';
+import { AnimationAttribute } from '../model/AnimationAttribute';
+import { BaseInterface } from '../model/BaseInterface';
+import { ComponentFactory } from '../model/ComponentFactory';
+import { CustomAnimationTabController } from '../model/CustomAniamtionTabController';
+import { IndicatorAnimationInfo } from '../model/IndicatorAniamtionInfo';
+import { IndicatorBarAttribute } from '../model/IndicatorBarAttribute';
+import { SizeMode } from '../model/SizeMode';
+import { TabBarAttribute } from '../model/TabBarAttribute';
+import { TabBarItemInterface } from '../model/TabBarItemInterface';
+import { TabInfo } from '../model/TabInfo';
+
+/**
+ * 功能描述:
+ * 1. 选中页签,字体放大加粗且后面有背景条,起到强调作用
+ * 2. 手势触摸tab内容滑动,背景条跟随手势一起滑动。抬手时,当tab内容滑动距离不足一半时,会自动回弹,而当tab内容滑动距离大于一半时,
+ * 背景条则会移动到下一个页签。当背景条滑动到一定距离后开始滑动页签条,使背景条始终能够保持在可视范围内
+ * 3. 点击页签,可以进行页签切换
+ * 4. 滑动页签条,背景条也会随之一起滑动,然后滑动tab内容,页签条会滑动到原处,使背景条处于可视范围内,之后背景条开始跟随手势滑动
+ * 5. 动画承接,背景条滑动过程中,触摸屏幕,背景条动画停止,松开手势,背景条继续滑动
+ *
+ * 实现原理:
+ * 1. 通过getScrollInfo函数获取每个页签被选中时背景条位置以及页签条偏移信息
+ * 2. 在Swiper的onChange回调中判断点击事件,并实现对应的点击页签动画效果
+ * 3. 在Swiper的onGestureSwipe回调中实现背景条跟手滑动效果
+ * 4. 在Swiper的onContentDidScroll回调中实现背景条自动滑动效果
+ *
+ * @param {AnimationAttribute} animationAttribute - 动效属性(必需)
+ * @param {TabInfo[]} [tabsInfo] - tab信息
+ * @param {IndicatorBarAttribute} [indicatorBarAttribute] - 背景条属性
+ * @param {TabBarAttribute} [tabBarAttribute] - 页签条属性
+ * @param {CustomAnimationTabController} [tabController] - tab控制器
+ * @param {Scroller} [scroller] - 页签条控制器
+ * @param {number} [animationDuration] - 页签切换时长
+ * @param {number} [startIndex] - 起始页签索引
+ * @param {(index: number, targetIndex: number, elementsInfo: [number, number][], ratio: number) => void}
+ * [gestureAnimation] - 手势滑动动效
+ * @param {(index: number, targetIndex: number, elementsInfo: [number, number][], ratio: number) => void}
+ * [autoAnimation] - 自动滑动动效
+ * @param {(index: number, targetIndex: number, indexInfo: Record, targetIndexInfo:
+ * Record, elementsInfo: [number, number][])} [clickAnimation] - 点击页签动效
+ * @param {(center: number, width: number) => [number, number]} [getScrollInfo] - 获取页签对应的背景条位置以及页签条偏移
+ */
+@Component
+export struct CustomAnimationTab {
+ // -------------------对外暴露变量-----------------------
+ // 动效属性
+ @Link animationAttribute: AnimationAttribute;
+ // tab信息
+ tabsInfo: TabInfo[] = [
+ new TabInfo(CommonConstants.DEFAULT_TITLE1_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CommonConstants.DEFAULT_TITLE2_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CommonConstants.DEFAULT_TITLE3_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CommonConstants.DEFAULT_TITLE4_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar))
+ ];
+ // 背景条属性
+ indicatorBarAttribute: IndicatorBarAttribute = new IndicatorBarAttribute(indicatorBar, SizeMode.Normal,
+ CommonConstants.DEFAULT_INDICATOR_WIDTH, CommonConstants.DEFAULT_INDICATOR_HEIGHT);
+ // 页签条属性
+ tabBarAttribute: TabBarAttribute =
+ new TabBarAttribute(CommonConstants.DEFAULT_LIST_ITEM_WIDTH, CommonConstants.DEFAULT_BAR_HEIGHT);
+ // tab控制器
+ tabController: CustomAnimationTabController = new CustomAnimationTabController();
+ // 页签条控制器
+ scroller: Scroller = new Scroller();
+ // 页签切换时长
+ animationDuration: number = CommonConstants.DEFAULT_ANIMATION_DURATION;
+ // 起始页签索引
+ startIndex: number = 0;
+ /**
+ * 手势滑动动效
+ * @param {number} index - 起始页签索引
+ * @param {number} targetIndex - 目标页签索引
+ * @param {[number, number][]} elementsInfo - 页签信息[背景条左端位置, 页签条偏移]
+ * @param {number} ratio - 当前手势滑动比例
+ * @returns
+ */
+ gestureAnimation: (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number) =>
+ void = (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number): void => {
+ this.animationAttribute.left =
+ elementsInfo[index].left + (elementsInfo[targetIndex].left - elementsInfo[index].left) * ratio;
+ this.scroller!.scrollTo({
+ xOffset: elementsInfo[index].offset + (elementsInfo[targetIndex].offset -
+ elementsInfo[index].offset) * ratio, yOffset: 0
+ });
+ let indicatorSize: [number, number] = this.getIndicatorSize(ratio, index, targetIndex);
+ this.animationAttribute.indicatorBarWidth = indicatorSize[0];
+ this.animationAttribute.indicatorBarHeight = indicatorSize[1];
+ };
+ /**
+ * 自动滑动动效
+ * @param {number} index - 起始页签索引
+ * @param {number} targetIndex - 目标页签索引
+ * @param {[number, number][]} elementsInfo - 页签动效信息[背景条左端位置, 页签条偏移]
+ * @param {number} ratio - 当前tab滑动比例
+ * @returns
+ */
+ autoAnimation: (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number) => void =
+ (index: number, targetIndex: number, elementsInfo: IndicatorAnimationInfo[], ratio: number): void => {
+ this.animationAttribute.left =
+ elementsInfo[index].left + (elementsInfo[targetIndex].left - elementsInfo[index].left) * ratio;
+ this.scroller!.scrollTo({
+ xOffset: elementsInfo[index].offset + (elementsInfo[targetIndex].offset - elementsInfo[index].offset) * ratio,
+ yOffset: 0
+ });
+ let indicatorSize: [number, number] = this.getIndicatorSize(ratio, index, targetIndex);
+ this.animationAttribute.indicatorBarWidth = indicatorSize[0];
+ this.animationAttribute.indicatorBarHeight = indicatorSize[1];
+ };
+ /**
+ * 点击页签动效
+ * @param {number} index - 当前页签索引
+ * @param {number} targetIndex - 目标页签索引
+ * @param {Record} indexInfo - 当前页签信息(center: 页签中心, width: 页签宽度)
+ * @param {Record} targetIndexInfo - 目标页签信息(center: 页签中心, width: 页签宽度)
+ * @param {[number, number][]} elementsInfo - 页签动效信息[背景条左端位置, 页签条偏移]
+ * @returns
+ */
+ clickAnimation: (targetIndex: number, targetIndexInfo: Record,
+ elementsInfo: IndicatorAnimationInfo[]) => void =
+ (targetIndex: number, targetIndexInfo: Record, elementsInfo: IndicatorAnimationInfo[]): void => {
+ // 根据targetIndex页签当前位置获取对应的背景条位置
+ this.animationAttribute.left = targetIndexInfo.center - this.elementsInfo[targetIndex].width / 2;
+ this.animationAttribute.indicatorBarWidth = this.elementsInfo[targetIndex].width;
+ this.animationAttribute.indicatorBarHeight = this.elementsInfo[targetIndex].height;
+ this.scroller!.scrollTo({
+ xOffset: elementsInfo[targetIndex].offset,
+ yOffset: 0,
+ animation: { duration: this.animationDuration, curve: Curve.Linear }
+ });
+ };
+ /**
+ * 获取页签对应的背景条位置以及页签条偏移
+ * @param {number} center - 页签中心点
+ * @param {number} width - 背景条宽度
+ * @returns {[number, number]} 背景条位置以及页签条偏移[背景条左端位置, 页签条偏移]
+ */
+ getScrollInfo: (center: number, width: number) => [number, number] =
+ (center: number, width: number): [number, number] => {
+ // 获取背景条位置
+ let indicatorLeft: number = center - width / 2;
+ // TODO: 知识点: 当背景条位置大于默认的背景条最大位置时,选取背景条最大位置作为背景条实际位置
+ let finalIndicatorLeft: number =
+ this.maxIndicatorBarLeft >= 0 ? Math.min(indicatorLeft, this.maxIndicatorBarLeft) : indicatorLeft;
+ // TODO: 知识点: 背景条产生的多余距离作为页签条滑动距离
+ let listOffset: number = indicatorLeft - finalIndicatorLeft;
+ // TODO: 知识点: 当页签条偏移大于页签条可偏移量,选取页签条可偏移量作为页签条实际偏移
+ let finalListOffset: number = Math.min(listOffset, Math.max(this.maxListOffset, 0));
+ // TODO: 知识点: 页签条多余的偏移作为背景条后续的滑动距离
+ finalIndicatorLeft += listOffset - finalListOffset;
+ return [finalIndicatorLeft, finalListOffset];
+ };
+ // --------------------私有属性----------------------------
+ @State curIndex: number = 0;
+ @State barTitles: string[] = [];
+ @State barHeight: Length | undefined = undefined; // 页签条高度
+ private componentUtils: ComponentUtils = this.getUIContext().getComponentUtils();
+ private tabsWidth: number = 0;
+ private listTouchState: number = 0; // 1:changIndex切换事件, 0:tab滑动切换事件
+ private maxListOffset: number = 0; // 页签条最大可偏移长度
+ private isReachBorder: boolean = true; // 判断tab是否到达边界
+ private elementsInfo: IndicatorAnimationInfo[] = []; // 页签对应的背景条位置、页签条偏移、背景条高度以及背景条宽度
+ private isAnimationStart: boolean = false;
+ @BuilderParam private indicatorBar: (index: BaseInterface) => void; // 自定义背景条
+ private maxIndicatorBarLeft: number = 0; // 背景条最大偏移(<0: 无上限, >=0: maxIndicatorBarLeft)
+ private indicatorBarAlign: VerticalAlign = VerticalAlign.Top; // 背景条垂直布局
+ private barEdgeEffect: EdgeEffect = EdgeEffect.Spring; // 页签条边缘滑动效果(目前仅支持EdgeEffect.Spring和EdgeEffect.None)
+ private scrollable: boolean = true; // 是否可以滚动页签条(等分所有页签宽度,barItemWidth失效)
+ private factory: ComponentFactory = new ComponentFactory();
+ private indicatorExpand: number = CommonConstants.DEFAULT_INDICATOR_EXPAND;
+ private sizeMode: SizeMode = SizeMode.Normal;
+ private vertical: boolean = false;
+ private barTitleSize: [number, number][] = [];
+ private isInit: boolean = false;
+ private barItemWidth: Length | undefined = undefined; // 页签宽度
+ private leftMargin: number = 0;
+ private indicatorHeight: number = 0;
+ private indicatorWidth: number = 0;
+
+ aboutToAppear(): void {
+ // 检查参数是否合法及初始化
+ this.checkNotLegal();
+
+ // 加载页面基本数据
+ console.log(`${this.tabsInfo.length}`)
+ this.tabsInfo!.forEach(info => {
+ this.factory.set(info.title, info);
+ });
+
+ // 私有变量初始化
+ this.curIndex = this.startIndex;
+ this.tabsWidth = 0;
+ this.listTouchState = 0;
+ this.isReachBorder = true;
+ this.elementsInfo = []
+ this.isAnimationStart = false;
+ this.barTitles = this.factory.toArray();
+ console.log(`${this.barTitles.length}`)
+ this.maxListOffset = 0;
+ this.indicatorBar = this.indicatorBarAttribute!.indicatorBar;
+ this.maxIndicatorBarLeft = this.indicatorBarAttribute!.maxIndicatorBarLeft;
+ this.indicatorBarAlign = this.indicatorBarAttribute!.barAlign;
+ this.barHeight = this.tabBarAttribute!.barHeight;
+ this.scrollable = this.tabBarAttribute!.scrollable;
+ this.barEdgeEffect = this.tabBarAttribute!.barEdgeEffect;
+ this.indicatorExpand = this.indicatorBarAttribute.indicatorExpand;
+ this.sizeMode = this.indicatorBarAttribute.sizeMode;
+ this.barItemWidth = this.tabBarAttribute.barItemWidth;
+ this.isInit = false;
+ this.indicatorHeight = this.indicatorBarAttribute.indicatorHeight;
+ this.indicatorWidth = this.indicatorBarAttribute.indicatorWidth;
+ if (!this.scrollable) {
+ this.barItemWidth = (100 / this.barTitles.length).toString() + '%';
+ }
+ for (let i = 0; i < this.barTitles.length; i++) {
+ this.elementsInfo[i] = {
+ left: 0,
+ offset: 0,
+ width: 0,
+ height: 0,
+ flag: false
+ }
+ }
+ this.tabController.setListener((state: number) => {
+ this.listTouchState = state;
+ })
+ }
+
+ /**
+ * 检查输入参数合法性
+ */
+ private checkNotLegal(): void {
+ if (this.tabsInfo === undefined || this.tabsInfo.length <= 0) {
+ this.tabsInfo = [
+ new TabInfo(CommonConstants.DEFAULT_TITLE1_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CommonConstants.DEFAULT_TITLE2_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CommonConstants.DEFAULT_TITLE3_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CommonConstants.DEFAULT_TITLE4_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar))
+ ];
+ }
+ if (this.indicatorBarAttribute === undefined) {
+ this.indicatorBarAttribute = new IndicatorBarAttribute(indicatorBar, SizeMode.Normal,
+ CommonConstants.DEFAULT_INDICATOR_WIDTH, CommonConstants.DEFAULT_INDICATOR_HEIGHT);
+ }
+ if (this.tabBarAttribute === undefined) {
+ this.tabBarAttribute =
+ new TabBarAttribute(CommonConstants.DEFAULT_LIST_ITEM_WIDTH, CommonConstants.DEFAULT_BAR_HEIGHT);
+ }
+ if (this.tabController === undefined) {
+ this.tabController = new CustomAnimationTabController();
+ }
+ if (this.scroller! === undefined) {
+ this.scroller = new Scroller();
+ }
+ if (this.animationDuration <= 0) {
+ console.error(`IllegalArgumentException(animationDuration: ${this.animationDuration}): animationDuration cannot be less than 0.`);
+ }
+ if (this.startIndex < 0 || this.startIndex >= this.tabsInfo.length) {
+ console.error(`IllegalArgumentException(startIndex: ${this.startIndex}): animationDuration must take a value between [0, ${this.factory.toArray()
+ .length}].`)
+ }
+ }
+
+ build() {
+ RelativeContainer() {
+ // tab
+ Swiper(this.tabController) {
+ ForEach(this.barTitles, (item: string, index: number) => {
+ this.factory.getContent(item)?.builder(this.factory.getParams(item));
+ // TODO: 知识点: InnerBarItem为自定义组件,使用默认的键值生成规则可能会导致JSON.stringify()无法字符串化自定义类
+ // TODO(接上): 知识点: 1.可以自定义键值生成规则;2.在自定义类中引入toString或者toJSON自定义字符串化形式。
+ }, (item: string, index: number) => index.toString())
+ }
+ .id('tabContent')
+ .loop(false)
+ .vertical(this.vertical)
+ .index(this.startIndex)
+ .duration(this.animationDuration)
+ // TODO: 知识点: 动画曲线配置需要谨慎使用,因为这会影响onContentDidScroll函数调用情况,导致最终position无法接近100%
+ // TODO: 知识点: 某些插值类型不受duration的控制,因此会导致duration无效
+ .curve(Curve.Ease)
+ .indicator(false)
+ .width(CommonConstants.FULL_PERCENT)
+ .alignRules(
+ {
+ bottom: this.tabBarAttribute!.barVertical === BarPosition.Start ?
+ { anchor: '__container__', align: VerticalAlign.Bottom } :
+ { anchor: 'tabItems', align: VerticalAlign.Top },
+ top: this.tabBarAttribute!.barVertical === BarPosition.Start ?
+ { anchor: 'tabItems', align: VerticalAlign.Bottom } :
+ { anchor: '__container__', align: VerticalAlign.Top }
+ }
+ )
+ .onChange((index: number) => {
+ if (this.listTouchState === 1 && index !== this.curIndex) {
+ let targetIndexInfo: Record = this.getElementInfo(index);
+ this.clickAnimation(index, targetIndexInfo, this.elementsInfo);
+ }
+ this.curIndex = index;
+ })
+ .onContentDidScroll((selectedIndex: number, index: number, position: number, mainAxisLength: number) => {
+ // 动画启动,选取当前index索引页签的属性来执行背景条和页签条滑动
+ if (this.isAnimationStart && index === this.curIndex) {
+ // 使用选中页签相对于Swiper主轴起始位置的移动比例判断滑动的目标页签targetIndex的位置
+ let targetIndex: number = position < 0 ? index + 1 : index - 1;
+ if (targetIndex >= this.barTitles.length || targetIndex < 0) {
+ console.warn(`Warning: targetIndex exceeds the limit range:
+ selectedIndex: ${selectedIndex}, curIndex: ${this.curIndex}, index: ${index},
+ targetIndex: ${targetIndex}, position: ${position}, mainAxisLength: ${mainAxisLength}`);
+ targetIndex = index; // 保证背景条在index页签位置
+ }
+ let ratio: number = Math.abs(position);
+ // 通过页签比例计算当前页签条和背景条的位置
+ this.autoAnimation(index, targetIndex, this.elementsInfo, ratio);
+ }
+ })
+ .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
+ if (this.isReachBorder) { // 若tab到达边界,则不继续执行动画
+ return;
+ }
+
+ this.isAnimationStart = true;
+ this.listTouchState = 0;
+ })
+ .onAnimationEnd(() => {
+ this.isAnimationStart = false;
+ })
+ .onGestureSwipe((index: number, event: TabsAnimationEvent) => {
+ this.listTouchState = 0;
+ let curOffset: number = event.currentOffset;
+ let targetIndex: number = index;
+ this.isReachBorder = false;
+ // tab组件到达边界使背景条和页签条跳转到终点位置
+ // TODO: 知识点: 这里不能判断到边界直接退出,因为onGestureSwipe每一帧触发回调,当手势滑动较快,上一帧背景条没有到达边界
+ // TODO(接上): 知识点: 下一帧content超出边界,这时候背景条没有更新,退出将导致背景条停滞在上一帧位置无法更新。
+ if ((index === 0 && curOffset > 0) ||
+ (index === this.barTitles.length - 1 && curOffset < 0)) {
+ this.isReachBorder = true;
+ curOffset = 0;
+ }
+
+ let ratio: number = Math.abs(curOffset / this.tabsWidth); // tab滑动比例
+ if (curOffset < 0) { // tab右滑
+ targetIndex = index + 1;
+ } else if (curOffset > 0) { // tab左滑
+ targetIndex = index - 1;
+ }
+ // 获取背景条位置及页签条偏移
+ this.gestureAnimation(index, targetIndex, this.elementsInfo, ratio);
+ })
+ .onAreaChange((oldValue: Area, newValue: Area) => {
+ let width: number = Number.parseFloat(newValue.width.toString());
+ this.tabsWidth = Number.isNaN(width) ? 0 : width;
+ })
+
+ Stack({ alignContent: Alignment.Start }) {
+ // 背景条
+ Row() {
+ Column() {
+ this.indicatorBar({ 'curIndex': this.curIndex });
+ }
+ .id('backgroundBar')
+ .height(this.animationAttribute.indicatorBarHeight)
+ .width(this.animationAttribute.indicatorBarWidth)
+ .margin({ left: this.animationAttribute.left })
+ }
+ .alignItems(this.indicatorBarAlign)
+ // TODO: 知识点: 通过clip保证超出容器的部分被截断
+ .clip(true)
+ .height(CommonConstants.FULL_PERCENT)
+ .width(CommonConstants.FULL_PERCENT)
+
+ // 页签条
+ // TODO: 知识点: 通过scroll将list内部所有item加载出来, 否则只能获取部分页签项的背景条位置和页签条偏移
+ Scroll(this.scroller!) {
+ List() {
+ ForEach(this.barTitles, (item: string, index: number) => {
+ ListItem() {
+ Column() {
+ if (this.factory.getBar(item) !== undefined) {
+ this.factory.getBar(item)?.builder({ curIndex: this.curIndex, index: index, title: item });
+ } else {
+ tabBar({ curIndex: this.curIndex, index: index, title: item });
+ }
+ }
+ .padding(this.sizeMode === SizeMode.Padding ?
+ {
+ left: this.indicatorWidth,
+ right: this.indicatorWidth,
+ top: this.indicatorHeight,
+ bottom: this.indicatorHeight
+ }
+ : 0)
+ // TODO: 知识点: 通过column与padding组合获取内边距模式下背景条的尺寸
+ .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions): void => {
+ if (this.barTitleSize[index] === undefined && newValue.width !== undefined &&
+ newValue.height !== undefined) {
+ let width: number = Number.parseFloat(newValue.width.toString());
+ let height: number = Number.parseFloat(newValue.height.toString());
+ this.barTitleSize[index] = [width, height];
+ if (this.barHeight === undefined) {
+ this.barHeight = height;
+ }
+ }
+ })
+ }
+ .id(index.toString())
+ .height(CommonConstants.FULL_PERCENT)
+ .width(this.barItemWidth)
+ .onClick(() => {
+ this.tabController!.changeIndex(index);
+ })
+ // TODO: 知识点: 利用onSizeChange在onAreaChange前调用的性质,初始化变量
+ .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
+ // TODO: 知识点: 通过list内部组件宽度总和-list宽度获取list最大可偏移量
+ if (newValue.width !== undefined && newValue.height !== undefined && !this.isInit) {
+ let width: number = Number.parseFloat(newValue.width.toString());
+ let height: number = Number.parseFloat(newValue.height.toString());
+ if (this.sizeMode === SizeMode.Normal) {
+ this.indicatorWidth = this.indicatorWidth === 0 ? width : this.indicatorWidth;
+ this.indicatorHeight = this.indicatorHeight === 0 ? height : this.indicatorHeight;
+ }
+ this.maxListOffset += width;
+ console.log(`maxListOffset: ${this.maxListOffset}`)
+ }
+ })
+ .onAreaChange((oldValue: Area, newValue: Area) => {
+ console.log(`onAreaChange item`)
+ if (newValue.position.x !== undefined && !this.elementsInfo[index].flag) {
+ let width: number = Number.parseFloat(newValue.width.toString());
+ let positionX: number = Number.parseFloat(newValue.position.x.toString());
+ // 内边距模式下背景条的尺寸
+ if (this.sizeMode === SizeMode.Padding) {
+ this.indicatorHeight = this.barTitleSize[index][1];
+ }
+ if (this.sizeMode === SizeMode.Padding) {
+ this.indicatorWidth = this.barTitleSize[index][0];
+ }
+ // 计算每一个页签对应的背景条位置与页签条偏移
+ // TODO: 知识点: 当页签宽度之和小于List大小时,需要加上差值的一半
+ let scrollInfo: [number, number] =
+ this.getScrollInfo(positionX + width / 2 - Math.min(0, this.maxListOffset) / 2,
+ this.indicatorWidth);
+ scrollInfo[0] += this.elementsInfo[index].left;
+ this.elementsInfo[index] = {
+ left: scrollInfo[0],
+ offset: scrollInfo[1],
+ height: this.indicatorHeight,
+ width: this.indicatorWidth,
+ flag: true
+ };
+ if (this.curIndex === index) {
+ this.animationAttribute.left = this.elementsInfo[index].left;
+ this.scroller!.scrollTo({ xOffset: this.elementsInfo[index].offset, yOffset: 0 });
+ this.animationAttribute.indicatorBarWidth = this.indicatorWidth;
+ this.animationAttribute.indicatorBarHeight = this.indicatorHeight;
+ }
+ this.isInit = true;
+ }
+ })
+ }, (item: string, index: number) => index.toString())
+ }
+ .alignListItem(ListItemAlign.Center)
+ .listDirection(Axis.Horizontal)
+ .scrollBar(BarState.Off)
+ .height(CommonConstants.FULL_PERCENT)
+ }
+ .margin(this.tabBarAttribute.barMargin)
+ .edgeEffect(this.barEdgeEffect)
+ .scrollable(ScrollDirection.Horizontal)
+ .scrollBar(BarState.Off)
+ .height(CommonConstants.FULL_PERCENT)
+ .width(CommonConstants.FULL_PERCENT)
+ .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
+ if (newValue.width !== undefined && !this.isInit) {
+ let width: number = Number.parseFloat(newValue.width.toString());
+ this.maxListOffset -= width;
+ console.log(`maxListOffset: ${this.maxListOffset}`)
+ }
+ })
+ .onAreaChange((oldValue: Area, newValue: Area) => {
+ this.leftMargin = newValue.position.x === undefined ? 0 : Number.parseFloat(newValue.position.x.toString());
+ for (let i = 0; i < this.barTitles.length; i++) {
+ this.elementsInfo[i].left += this.leftMargin;
+ }
+ })
+ .onTouch((event: TouchEvent) => {
+ if (event.type === TouchType.Move) {
+ this.listTouchState = 1;
+ }
+ })
+ .onDidScroll((scrollOffset: number, scrollState: ScrollState) => {
+ // changIndex事件切换, 背景条跟随页签条一起滑动
+ // TODO: 知识点: 使用scrollTo实现页签条动画,并通过状态变量赋值来执行背景条动画,可以使两者动画同时进行
+ // TODO(接上): 知识点: 具体可见line.111上添加动画this.startAnimateTo(index, this.animationDuration,
+ // TODO(接上): 知识点: this.elementsInfo[index][0], CommonConstants.DEFAULT_INDICATOR_WIDTH)对比效果
+ if (this.listTouchState === 1) {
+ this.animationAttribute.left -= scrollOffset;
+ }
+ })
+ }
+ .id('tabItems')
+ .height(this.barHeight)
+ .backgroundColor(this.tabBarAttribute.barBackgroundColor)
+ // TODO: 知识点: 通过赋值null使得对应的对其方式失效
+ .alignRules({
+ top: this.tabBarAttribute!.barVertical === BarPosition.Start ?
+ { anchor: '__container__', align: VerticalAlign.Top } :
+ undefined,
+ bottom: this.tabBarAttribute!.barVertical === BarPosition.Start ?
+ undefined :
+ { anchor: '__container__', align: VerticalAlign.Bottom },
+ right: { anchor: '__container__', align: HorizontalAlign.End },
+ left: { anchor: '__container__', align: HorizontalAlign.Start }
+ })
+ }
+ .width(CommonConstants.FULL_PERCENT)
+ .height(CommonConstants.FULL_PERCENT)
+ .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
+ // 保证背景条最大偏移不会超出屏幕
+ let width = newValue.width === undefined ? 0 : Number.parseFloat(newValue.width.toString());
+ this.maxIndicatorBarLeft = this.maxIndicatorBarLeft > width ? (width / 2) : this.maxIndicatorBarLeft;
+ })
+ }
+
+ /**
+ * 获取id为index组件的中心点信息
+ * @param {number} index - 组件id
+ * @returns {Record} index页签当前的中心位置以及其宽度
+ */
+ private getElementInfo(index: number): Record {
+ let rectangle = this.componentUtils.getRectangleById(index.toString());
+ let width: number = rectangle.size.width;
+ let center: number = rectangle.localOffset.x + width / 2;
+ // TODO: 知识点: 由于页签条使用offset进行偏移,因此localOffset.x等于页签在页签条中的位置,为了得到相对于tab容器的位置,需要减去一个页签条的偏移
+ // TODO: 知识点: 当页签宽度之和小于List大小时,需要加上差值的一半
+ return {
+ 'center': px2vp(center) - Math.min(0, this.maxListOffset) / 2 - this.scroller!.currentOffset().xOffset +
+ this.leftMargin,
+ 'width': width
+ };
+ }
+
+ /**
+ * 获取背景条宽度
+ * @param {number} ratio - tab偏移比例
+ * @param {number} fromIndex - 起始索引
+ * @param {number> toIndex - 终止索引
+ * @returns {[number, number]} [背景条宽度, 背景条高度]
+ */
+ private getIndicatorSize(ratio: number, fromIndex: number, toIndex: number): [number, number] {
+ ratio = Math.abs(ratio);
+ let width: number = 0;
+ let height: number = 0;
+ // 获取背景条扩展阶段的起始宽度和终止宽度
+ let fromWidth: number = this.elementsInfo[fromIndex].width;
+ let toWidth: number =
+ (this.elementsInfo[fromIndex].width + this.elementsInfo[toIndex].width) * this.indicatorExpand / 2;
+ let fromHeight: number = this.elementsInfo[fromIndex].height;
+ let toHeight: number = this.elementsInfo[toIndex].height;
+ let stageRatio: number = ratio * 2;
+ // 获取背景条缩短阶段的起始宽度和终止宽度
+ if (ratio >= 0.5) {
+ fromWidth = toWidth;
+ toWidth = this.elementsInfo[toIndex].width;
+ stageRatio = (ratio - 0.5) * 2;
+ }
+ // 获取当前ratio,背景条的实际宽度
+ width = (toWidth - fromWidth) * stageRatio + fromWidth;
+ height = (toHeight - fromHeight) * ratio + fromHeight;
+ return [width, height];
+ }
+}
+
+/**
+ * 默认tabContent样式
+ */
+@Builder
+function baseBuilder(params: ESObject) {
+ Column() {
+ Text('Text')
+ .fontSize(CommonConstants.DEFAULT_TAB_CONTENT_FONT_SIZE)
+ }
+ .justifyContent(FlexAlign.Center)
+ .height(CommonConstants.FULL_PERCENT)
+ .width(CommonConstants.FULL_PERCENT)
+}
+
+/**
+ * 默认tabBar样式
+ * @param {TabBarItemInterface} $$ - 返回的页签信息
+ */
+@Builder
+function tabBar($$: TabBarItemInterface) {
+ Text($$.title)
+ .fontSize($$.curIndex === $$.index ? CommonConstants.DEFAULT_TAB_BAR_SELECT_FONT_SIZE :
+ CommonConstants.DEFAULT_TAB_BAR_UNSELECT_FONT_SIZE)
+ .fontColor(CommonConstants.DEFAULT_TAB_BAR_FONT_COLOR)
+ .fontWeight($$.curIndex === $$.index ? FontWeight.Bold : FontWeight.Medium)
+ .textAlign(TextAlign.Center)
+}
+
+/**
+ * 默认背景条样式
+ * @param {BaseInterface} $$ - 返回的基本信息
+ */
+@Builder
+function indicatorBar($$: BaseInterface) {
+ Column()
+ .height(CommonConstants.FULL_PERCENT)
+ .width(CommonConstants.FULL_PERCENT)
+ .backgroundColor(Color.Red)
+ .borderRadius(CommonConstants.DEFAULT_INDICATOR_BORDER_RADIUS)
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/utils/Logger.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/utils/Logger.ets
new file mode 100644
index 0000000000000000000000000000000000000000..17d1f64c68d74a9f6b8f3326047b865397e5ddc2
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/utils/Logger.ets
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import hilog from '@ohos.hilog';
+
+/**
+ * 日志打印类
+ */
+class Logger {
+ private domain: number;
+ private prefix: string;
+ private format: string = '%{public}s, %{public}s';
+
+ constructor(prefix: string) {
+ this.prefix = prefix;
+ this.domain = 0xFF00;
+ this.format.toUpperCase();
+ }
+
+ debug(...args: string[]) {
+ hilog.debug(this.domain, this.prefix, this.format, args);
+ }
+
+ info(...args: string[]) {
+ hilog.info(this.domain, this.prefix, this.format, args);
+ }
+
+ warn(...args: string[]) {
+ hilog.warn(this.domain, this.prefix, this.format, args);
+ }
+
+ error(...args: string[]) {
+ hilog.error(this.domain, this.prefix, this.format, args);
+ }
+}
+
+export let logger = new Logger('[CommonAppDevelopment]')
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/BasePage.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/BasePage.ets
new file mode 100644
index 0000000000000000000000000000000000000000..b0c116a9a348a20add0f0c54890f8f5050d17098
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/BasePage.ets
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SkeletonLayout } from './SkeletonLayout';
+import LazyDataSource from './LazyDataSource';
+
+@Builder
+export function baseBuilder() {
+ BasePage();
+}
+
+@Component
+struct BasePage {
+ @State data: LazyDataSource = new LazyDataSource();
+
+ aboutToAppear(): void {
+ for (let i = 0; i < 100; i++) {
+ this.data.pushData(`${i}`);
+ }
+ }
+
+ build() {
+ Column() {
+ List() {
+ LazyForEach(this.data, (data: string) => {
+ ListItem() {
+ SkeletonLayout({isMine: false})
+ }
+ })
+ }
+ .cachedCount(1)
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/CustomAnimationTabConfigure.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/CustomAnimationTabConfigure.ets
new file mode 100644
index 0000000000000000000000000000000000000000..dc55a8225c1cd8d7e4fbf6d7c06b4246f1b421ad
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/CustomAnimationTabConfigure.ets
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 自定义属性
+ */
+export class CustomAnimationTabConfigure {
+ // 背景条最大偏移距离(单位: px)
+ public static readonly INDICATOR_MAX_LEFT = 90;
+ // 背景条宽度扩展比例
+ public static readonly DEFAULT_INDICATOR_EXPAND = 1;
+ // 页签宽度
+ public static readonly LIST_ITEM_WIDTH = 100;
+ // 背景条高度
+ public static readonly TABBAR_HEIGHT = 70;
+ // 背景条宽度
+ public static readonly INDICATOR_WIDTH = 100;
+ // tab标题
+ public static readonly DEFAULT_BASE_TAB = '全部';
+ // tab标题
+ public static readonly DEFAULT_DYEFFECT_TAB = '动效';
+ // tab标题
+ public static readonly DEFAULT_NATIVE_TAB = 'Native';
+ // tab标题
+ public static readonly DEFAULT_OTHER_TAB = '其它';
+ // tab标题
+ public static readonly DEFAULT_THIRTYPARTY_TAB = '三方库';
+ // tab标题
+ public static readonly DEFAULT_UI_TAB = 'UI布局';
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/CustomAnimationTabView.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/CustomAnimationTabView.ets
new file mode 100644
index 0000000000000000000000000000000000000000..317060b9a0c0672f4af6d8fb347045f036fe049c
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/CustomAnimationTabView.ets
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { BaseInterface } from '../model/BaseInterface';
+import { baseBuilder } from './BasePage';
+import { CustomAnimationTabController } from '../model/CustomAniamtionTabController';
+import { CustomAnimationTabConfigure } from './CustomAnimationTabConfigure'
+import { dyEffectBuilder } from './DyEffectPage';
+import { IndicatorBarAttribute } from '../model/IndicatorBarAttribute';
+import { nativeBuilder } from './NativePage';
+import { otherBuilder } from './OtherPage';
+import { thirdPartyBuilder } from './ThirdPartyPage';
+import { TabBarItemInterface } from '../model/TabBarItemInterface';
+import { TabBarAttribute } from '../model/TabBarAttribute';
+import { TabInfo } from '../model/TabInfo';
+import { uiBuilder } from './UIPage';
+import { MyAnimationAttribute } from './MyAnimationAttribute';
+import { SizeMode } from '../model/SizeMode';
+import ConfigurationConstant from '@ohos.app.ability.ConfigurationConstant';
+import { CustomAnimationTab } from '../utils/CustomAnimationTab';
+
+/**
+ * 功能说明: 本示例介绍使用List、Text等组件,以及animateTo等接口实现自定义Tab效果
+ *
+ * 推荐场景: 需要自定义动效的tab场景
+ *
+ * 核心组件:
+ * 1. CustomAnimationTab: 自定义动效tab构建组件
+ * 2. AnimationAttribute: 动效属性,可通过继承扩展动效属性
+ * 3. TabInfo: 设置TabBar的标题、TabContent以及TabBar样式的类
+ * 4. CustomAnimationTabController: 自定义动效Tab控制器,用于控制自定义动效Tab组件进行页签切换
+ * 5. IndicatorBarAttribute: 设置背景条属性
+ * 6. TabBarAttribute: 设置页签条属性
+ * 7. Scroller: 页签条滚动控制器
+ *
+ * 实现步骤:
+ * 1. 数据准备: 首先构建一个TabInfo数组,然后向其中传入对应的内容
+ * @example
+ * this.tabsInfo = [
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_BASE_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_UI_TAB, wrapBuilder(uiBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_DYEFFECT_TAB, wrapBuilder(dyEffectBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_THIRTYPARTY_TAB, wrapBuilder(thirdPartyBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_NATIVE_TAB, wrapBuilder(nativeBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_OTHER_TAB, wrapBuilder(otherBuilder), wrapBuilder(tabBar))
+ ]
+ * 2. 动效属性准备: 创建动效属性AnimationAttribute对象,可以通过继承添加额外动效属性
+ * @example
+ * @State animationAttribute: MyAnimationAttribute = new MyAnimationAttribute(CustomAnimationTabConfigure.
+ * INDICATOR_WIDTH, $r("app.color.custom_animation_tab_indicator_color"));
+ * 3. 背景条配置: 背景条可以自行new IndicatorBarAttribute配置,
+ * 也可以使用已有的背景条配置(目前支持两种: IndicatorBarAttribute.BACKGROUNDBAR和IndicatorBarAttribute.THINSTRIP)
+ * @example
+ * indicatorBarAttribute: IndicatorBarAttribute = new IndicatorBarAttribute(this.indicatorBar);
+ * 4. 页签条配置
+ * @example
+ * tabBarAttribute: TabBarAttribute = new TabBarAttribute(CustomAnimationTabConfigure.LIST_ITEM_WIDTH);
+ * 5. 自定义动效tab控制器配置
+ * @example
+ * tabController: CustomAnimationTabController = new CustomAnimationTabController();
+ * 6. 页签条滑动配置
+ * @example
+ * scroller: Scroller = new Scroller();
+ * 7. 构建自定义动效tab
+ * @example
+ * CustomAnimationTab({
+ animationAttribute: this.animationAttribute,
+ tabsInfo: this.tabsInfo,
+ indicatorBarAttribute: this.indicatorBarAttribute,
+ tabBarAttribute: this.tabBarAttribute,
+ tabController: this.tabController,
+ scroller: this.scroller
+ })
+ */
+@Builder
+function testBuilder() {
+ Column() {
+ }
+ .height('100%')
+ .width('100%')
+ .backgroundColor(Color.Gray)
+}
+
+@Component
+@Entry
+export struct CustomAnimationTabView {
+ // 自定义动效属性,添加了背景条颜色变化
+ @State animationAttribute: MyAnimationAttribute =
+ new MyAnimationAttribute($r('app.color.custom_animation_tab_indicator_color'));
+ // tab数据
+ tabsInfo: TabInfo[] = [];
+ // tabController
+ tabController: CustomAnimationTabController = new CustomAnimationTabController();
+ // indicatorBar
+ indicatorBarAttribute: IndicatorBarAttribute = new IndicatorBarAttribute(this.indicatorBar, SizeMode.Normal, 0, 0,
+ CustomAnimationTabConfigure.INDICATOR_MAX_LEFT, CustomAnimationTabConfigure.DEFAULT_INDICATOR_EXPAND);
+ // tabBar
+ tabBarAttribute: TabBarAttribute =
+ new TabBarAttribute(CustomAnimationTabConfigure.LIST_ITEM_WIDTH, CustomAnimationTabConfigure.TABBAR_HEIGHT,
+ true, EdgeEffect.Spring, BarPosition.Start);
+ // scroller
+ scroller: Scroller = new Scroller();
+
+ aboutToAppear(): void {
+ this.tabsInfo = [
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_BASE_TAB, wrapBuilder(baseBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_UI_TAB, wrapBuilder(uiBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_DYEFFECT_TAB, wrapBuilder(dyEffectBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_THIRTYPARTY_TAB, wrapBuilder(thirdPartyBuilder),
+ wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_NATIVE_TAB, wrapBuilder(nativeBuilder), wrapBuilder(tabBar)),
+ new TabInfo(CustomAnimationTabConfigure.DEFAULT_OTHER_TAB, wrapBuilder(otherBuilder), wrapBuilder(tabBar))
+ ]
+ }
+
+ build() {
+ RelativeContainer() {
+ this.body();
+ }
+ .height('100%')
+ .width('100%')
+ }
+
+ @Builder
+ body() {
+ Column() {
+ /**
+ * 构建自定义动效Tab
+ * indicatorBarAttribute: 背景条属性
+ * tabsInfo: tab数据源
+ * tabBarAttribute: 页签条属性
+ * animationAttribute: 动效属性
+ * tabController: 自定义动效tab控制器
+ * scroller: 页签条滚动控制器
+ */
+ CustomAnimationTab({
+ animationAttribute: this.animationAttribute,
+ tabsInfo: this.tabsInfo,
+ indicatorBarAttribute: this.indicatorBarAttribute,
+ tabBarAttribute: this.tabBarAttribute,
+ tabController: this.tabController,
+ scroller: this.scroller
+ })
+ .height($r('app.string.custom_animation_tab_ninety_percent'))
+ // 更新自定义动效变量——背景条颜色
+ Column() {
+ Button($r('app.string.custom_animation_tab_button_text'))
+ .height($r('app.string.custom_animation_tab_sixty_percent'))
+ .onClick(() => {
+ if ((this.animationAttribute.indicatorBarColor as Resource).id ===
+ $r('app.color.custom_animation_tab_indicator_color').id) {
+ this.animationAttribute.indicatorBarColor = Color.Yellow;
+ } else if (this.animationAttribute.indicatorBarColor === Color.Yellow) {
+ this.animationAttribute.indicatorBarColor = $r('app.color.custom_animation_tab_indicator_color');
+ }
+ })
+ }
+ .justifyContent(FlexAlign.Center)
+ .height($r('app.string.custom_animation_tab_ten_percent'))
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+
+ @Builder
+ indicatorBar($$: BaseInterface) {
+ Column()
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))// 绑定自定义动效属性
+ .backgroundColor(this.animationAttribute.indicatorBarColor)
+ .borderRadius($r('app.float.custom_animation_tab_indicator_border_radius'))
+ }
+}
+
+// tabBar样式
+@Builder
+function tabBar($$: TabBarItemInterface) {
+ Column() {
+ Image($r('app.media.return_home_fill'))
+ .height(20)
+ .width(20)
+ .objectFit(ImageFit.Contain)
+ Text($$.title)
+ .fontSize($$.curIndex === $$.index ? $r('app.float.custom_animation_tab_list_select_font_size') :
+ $r('app.float.custom_animation_tab_list_unselect_font_size'))
+ .fontColor($r('app.color.custom_animation_tab_list_font_color'))
+ .fontWeight($$.curIndex === $$.index ? FontWeight.Bold : FontWeight.Medium)
+ .textAlign(TextAlign.Center)
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/DyEffectPage.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/DyEffectPage.ets
new file mode 100644
index 0000000000000000000000000000000000000000..aa7c6a7f6bed8d9c8f0e2ece6749e6c7efb9bc7c
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/DyEffectPage.ets
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SkeletonLayout } from './SkeletonLayout';
+import LazyDataSource from './LazyDataSource';
+
+@Builder
+export function dyEffectBuilder() {
+ DyEffectPage();
+}
+
+@Component
+struct DyEffectPage {
+ data: LazyDataSource = new LazyDataSource();
+
+ aboutToAppear(): void {
+ for (let i = 0; i < 100; i++) {
+ this.data.pushData(`${i}`);
+ }
+ }
+
+ build() {
+ Column() {
+ List() {
+ LazyForEach(this.data, (data: string) => {
+ ListItem() {
+ SkeletonLayout({isMine: false})
+ }
+ })
+ }
+ .cachedCount(1)
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/LazyDataSource.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/LazyDataSource.ets
new file mode 100644
index 0000000000000000000000000000000000000000..acf2395ba4afb7d253f65585255bdc214645ad13
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/LazyDataSource.ets
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const TAG = '[BasicDataSource]';
+
+class BasicDataSource implements IDataSource {
+ private listeners: DataChangeListener[] = [];
+
+ public totalCount(): number {
+ return 0;
+ }
+
+ public getData(index: number): T | undefined {
+ return undefined;
+ }
+
+ registerDataChangeListener(listener: DataChangeListener): void {
+ if (this.listeners.indexOf(listener) < 0) {
+ this.listeners.push(listener);
+ }
+ }
+
+ unregisterDataChangeListener(listener: DataChangeListener): void {
+ const pos = this.listeners.indexOf(listener);
+ if (pos >= 0) {
+ this.listeners.splice(pos, 1);
+ }
+ }
+
+ notifyDataReload(): void {
+ this.listeners.forEach(listener => {
+ listener.onDataReloaded();
+ })
+ }
+
+ notifyDataAdd(index: number): void {
+ this.listeners.forEach(listener => {
+ listener.onDataAdd(index);
+ })
+ }
+
+ notifyDataChange(index: number): void {
+ this.listeners.forEach(listener => {
+ listener.onDataChange(index);
+ })
+ }
+
+ notifyDataDelete(index: number): void {
+ this.listeners.forEach(listener => {
+ listener.onDataDelete(index);
+ })
+ }
+
+ notifyDataMove(from: number, to: number): void {
+ this.listeners.forEach(listener => {
+ listener.onDataMove(from, to);
+ })
+ }
+}
+
+@Observed
+export default class LazyDataSource extends BasicDataSource {
+ dataArray: T[] = [];
+
+ public totalCount(): number {
+ return this.dataArray.length;
+ }
+
+ public getData(index: number): T {
+ return this.dataArray[index];
+ }
+
+ public addData(index: number, data: T): void {
+ this.dataArray.splice(index, 0, data);
+ this.notifyDataAdd(index);
+ }
+
+ public pushData(data: T): void {
+ this.dataArray.push(data);
+ this.notifyDataAdd(this.dataArray.length - 1);
+ }
+
+ public deleteData(index: number): void {
+ this.dataArray.splice(index, 1);
+ this.notifyDataDelete(index);
+ }
+
+ public clear(): void {
+ this.dataArray.splice(0, this.dataArray?.length);
+ }
+
+ public isEmpty(): boolean {
+ return this.dataArray.length === 0;
+ }
+}
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/MyAnimationAttribute.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/MyAnimationAttribute.ets
new file mode 100644
index 0000000000000000000000000000000000000000..d4236eef7887c1ee15a8d949bad700620dcf6344
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/MyAnimationAttribute.ets
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { AnimationAttribute } from '../model/AnimationAttribute';
+
+export class MyAnimationAttribute extends AnimationAttribute {
+ // 背景条颜色
+ public indicatorBarColor: ResourceColor;
+
+ constructor(indicatorBarColor: ResourceColor) {
+ super();
+ this.indicatorBarColor = indicatorBarColor;
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/NativePage.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/NativePage.ets
new file mode 100644
index 0000000000000000000000000000000000000000..533f10477ce7a3846aa81f5e45d26e9492344ed3
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/NativePage.ets
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SkeletonLayout } from './SkeletonLayout';
+import LazyDataSource from './LazyDataSource';
+
+@Builder
+export function nativeBuilder() {
+ NativePage();
+}
+
+@Component
+struct NativePage {
+ data: LazyDataSource = new LazyDataSource();
+
+ aboutToAppear(): void {
+ for (let i = 0; i < 100; i++) {
+ this.data.pushData(`${i}`);
+ }
+ }
+
+ build() {
+ Column() {
+ List() {
+ LazyForEach(this.data, (data: string) => {
+ ListItem() {
+ SkeletonLayout({isMine: false})
+ }
+ })
+ }
+ .cachedCount(1)
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/OtherPage.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/OtherPage.ets
new file mode 100644
index 0000000000000000000000000000000000000000..048485807bc756249de617367ef902203d51606f
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/OtherPage.ets
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SkeletonLayout } from './SkeletonLayout';
+import LazyDataSource from './LazyDataSource';
+
+@Builder
+export function otherBuilder() {
+ OtherPage();
+}
+
+@Component
+struct OtherPage {
+ data: LazyDataSource = new LazyDataSource();
+
+ aboutToAppear(): void {
+ for (let i = 0; i < 100; i++) {
+ this.data.pushData(`${i}`);
+ }
+ }
+
+ build() {
+ Column() {
+ List() {
+ LazyForEach(this.data, (data: string) => {
+ ListItem() {
+ SkeletonLayout({ isMine: false })
+ }
+ })
+ }
+ .cachedCount(1)
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/SkeletonLayout.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/SkeletonLayout.ets
new file mode 100644
index 0000000000000000000000000000000000000000..d3c8f0030b18417b3985b22a3f25103833b26467
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/SkeletonLayout.ets
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@Component
+@Reusable
+export struct SkeletonLayout {
+ isMine: boolean = false;
+
+ aboutToReuse(params: Record): void {
+ }
+
+ build() {
+ Row() {
+ Column() {
+ Column({ space: 4 }) {
+ // 标题
+ textArea($r('app.float.custom_animation_tab_ske_card_title_height'))
+ // 内容
+ textArea($r('app.float.custom_animation_tab_ske_card_text_height'))
+ }
+ .alignItems(HorizontalAlign.Start)
+ .layoutWeight(1)
+
+ // 判断是否自己作品来适配不同骨架布局
+ if (!this.isMine) {
+ Row() {
+ // 浏览量
+ actionArea()
+ // 点赞数
+ actionArea()
+ // 收藏量
+ actionArea()
+ }
+ .padding({ top: $r('app.float.custom_animation_tab_ske_md_padding_margin') })
+ .height($r('app.float.custom_animation_tab_ske_row_height'))
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .justifyContent(FlexAlign.SpaceBetween)
+ } else {
+ Row({ space: 12 }) {
+ // 摄影作品标签
+ Row()
+ .width($r('app.float.custom_animation_tab_ske_lg_topic_width'))
+ .topicStyle()
+ // 自然风光标签
+ Row()
+ .width($r('app.float.custom_animation_tab_ske_lg_topic_width'))
+ .topicStyle()
+ }
+ .padding({ top: $r('app.float.custom_animation_tab_ske_md_padding_margin') })
+ .height($r('app.float.custom_animation_tab_ske_row_height'))
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .justifyContent(FlexAlign.Start)
+ }
+ }
+ .layoutWeight(1)
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+
+ Row()
+ .backgroundColor($r('app.color.custom_animation_tab_skeleton_color_light'))
+ .borderRadius($r('app.float.custom_animation_tab_ske_small_border_radius'))
+ .width($r('app.float.custom_animation_tab_ske_image_width'))
+ .height($r('app.float.custom_animation_tab_ske_image_height'))
+ .margin({ left: $r('app.float.custom_animation_tab_ske_md_padding_margin') })
+ }
+ .padding($r('app.float.custom_animation_tab_ske_md_padding_margin'))
+ .borderRadius($r('app.float.custom_animation_tab_ske_large_border_radius'))
+ .backgroundColor(Color.White)
+ .alignItems(VerticalAlign.Top)
+ .justifyContent(FlexAlign.SpaceAround)
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.float.custom_animation_tab_ske_card_height'))
+ }
+}
+
+@Builder
+function actionArea() {
+ Row()
+ .width($r('app.float.custom_animation_tab_ske_action_area_width'))
+ .height($r('app.float.custom_animation_tab_ske_action_area_height'))
+ .backgroundColor($r('app.color.custom_animation_tab_skeleton_color_medium'))
+}
+
+@Styles
+function topicStyle() {
+ .height($r('app.float.custom_animation_tab_ske_topic_height'))
+ .backgroundColor($r('app.color.custom_animation_tab_skeleton_color_medium'))
+ .borderRadius($r('app.float.custom_animation_tab_ske_xs_border_radius'))
+}
+
+@Builder
+function textArea(height: Length) {
+ Row()
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height(height)
+ .backgroundColor($r('app.color.custom_animation_tab_skeleton_color_medium'))
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/ThirdPartyPage.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/ThirdPartyPage.ets
new file mode 100644
index 0000000000000000000000000000000000000000..f99d22f088f954c54d8088d85aeb473431e3b43a
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/ThirdPartyPage.ets
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SkeletonLayout } from './SkeletonLayout';
+import LazyDataSource from './LazyDataSource';
+
+@Builder
+export function thirdPartyBuilder() {
+ ThirdPartyPage();
+}
+
+@Component
+struct ThirdPartyPage {
+ data: LazyDataSource = new LazyDataSource();
+
+ aboutToAppear(): void {
+ for (let i = 0; i < 100; i++) {
+ this.data.pushData(`${i}`);
+ }
+ }
+
+ build() {
+ Column() {
+ List() {
+ LazyForEach(this.data, (data: string) => {
+ ListItem() {
+ SkeletonLayout({ isMine: false })
+ }
+ })
+ }
+ .cachedCount(1)
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/UIPage.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/UIPage.ets
new file mode 100644
index 0000000000000000000000000000000000000000..70ce4532caae3d485ff9cc52fb16df024f86cd60
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/ets/view/UIPage.ets
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { SkeletonLayout } from './SkeletonLayout';
+import LazyDataSource from './LazyDataSource';
+
+@Builder
+export function uiBuilder() {
+ UIPage();
+}
+
+@Component
+struct UIPage {
+ data: LazyDataSource = new LazyDataSource();
+
+ aboutToAppear(): void {
+ for (let i = 0; i < 100; i++) {
+ this.data.pushData(`${i}`);
+ }
+ }
+
+ build() {
+ Column() {
+ List() {
+ LazyForEach(this.data, (data: string) => {
+ ListItem() {
+ SkeletonLayout({isMine: false})
+ }
+ })
+ }
+ .cachedCount(1)
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+ .width($r('app.string.custom_animation_tab_one_hundred_percent'))
+ .height($r('app.string.custom_animation_tab_one_hundred_percent'))
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/module.json5 b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/module.json5
new file mode 100644
index 0000000000000000000000000000000000000000..d04f3ffe72ee9816922308e70ae4226c85ca273f
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/module.json5
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "module": {
+ "name": "customanimationtab",
+ "type": "har",
+ "deviceTypes": [
+ "default",
+ "tablet"
+ ]
+ }
+}
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/color.json b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/color.json
new file mode 100644
index 0000000000000000000000000000000000000000..3d3c3ec9596f348ac4907c46479e3e5d507f4e77
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/color.json
@@ -0,0 +1,44 @@
+{
+ "color": [
+ {
+ "name": "start_window_background",
+ "value": "#FFFFFF"
+ },
+ {
+ "name": "custom_animation_tab_indicator_color",
+ "value": "#C0C0C0"
+ },
+ {
+ "name": "custom_animation_tab_menu_button_color",
+ "value": "#DCDCDC"
+ },
+ {
+ "name": "custom_animation_tab_menu_button_inner_color",
+ "value": "#C0C0C0"
+ },
+ {
+ "name": "custom_animation_tab_background_color",
+ "value": "#F5F5F5"
+ },
+ {
+ "name": "custom_animation_tab_list_font_color",
+ "value": "#000000"
+ },
+ {
+ "name": "custom_animation_tab_skeleton_color_medium",
+ "value": "#FFF2F3F4"
+ },
+ {
+ "name": "custom_animation_tab_skeleton_color_light",
+ "value": "#FFECECEC"
+ },
+ {
+ "name": "custom_animation_tab_title_background_color",
+ "value": "#FF4500"
+ },
+ {
+ "name": "custom_animation_tab_search_background_color",
+ "value": "#FFFFFF"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/float.json b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/float.json
new file mode 100644
index 0000000000000000000000000000000000000000..5a4761598781061650097655f618637899934ea8
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/float.json
@@ -0,0 +1,179 @@
+{
+ "float": [
+ {
+ "name": "custom_animation_tab_list_item_width",
+ "value": "100"
+ },
+ {
+ "name": "custom_animation_tab_indicator_height",
+ "value": "40"
+ },
+ {
+ "name": "custom_animation_tab_indicator_border_radius",
+ "value": "20"
+ },
+ {
+ "name": "custom_animation_tab_inner_tabBar_height",
+ "value": "40"
+ },
+ {
+ "name": "custom_animation_tab_menu_button_size",
+ "value": "40"
+ },
+ {
+ "name": "custom_animation_tab_menu_button_inner_size",
+ "value": "30"
+ },
+ {
+ "name": "custom_animation_tab_list_select_font_size",
+ "value": "18"
+ },
+ {
+ "name": "custom_animation_tab_list_unselect_font_size",
+ "value": "17"
+ },
+ {
+ "name": "custom_animation_tab_inner_tab_divider_width",
+ "value": "2px"
+ },
+ { "name": "custom_animation_tab_ske_xs_font_size",
+ "value": "10fp"
+ },
+ {
+ "name": "custom_animation_tab_ske_small_text_size",
+ "value": "12fp"
+ },
+ {
+ "name": "custom_animation_tab_ske_normal_text_size",
+ "value": "14fp"
+ },
+ {
+ "name": "custom_animation_tab_ske_failure_img_size",
+ "value": "120vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_reload_btn_height",
+ "value": "32vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_reload_btn_width",
+ "value": "80vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_large_border_radius",
+ "value": "8vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_small_border_radius",
+ "value": "8vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_xs_border_radius",
+ "value": "4vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_lg_topic_width",
+ "value": "58vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_topic_height",
+ "value": "16vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_action_area_width",
+ "value": "58vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_action_area_height",
+ "value": "20vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_card_height",
+ "value": "102vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_card_title_height",
+ "value": "20vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_card_text_height",
+ "value": "20vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_image_height",
+ "value": "80vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_image_width",
+ "value": "128vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_type_width",
+ "value": "28vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_type_height",
+ "value": "16vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_sm_margin",
+ "value": "5vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_topic_width",
+ "value": "132vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_xxl_padding_margin",
+ "value": "24vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_xl_padding_margin",
+ "value": "20vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_md_padding_margin",
+ "value": "12vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_sm_padding_margin",
+ "value": "8vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_xs_padding_margin",
+ "value": "4vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_xxs_padding_margin",
+ "value": "2vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_icon_size",
+ "value": "18vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_navigation_height",
+ "value": "56vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_none_view_image_size",
+ "value": "120vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_view_count_icon_width",
+ "value": "36vp"
+ },
+ {
+ "name": "custom_animation_tab_ske_row_height",
+ "value": "40vp"
+ },
+ {
+ "name": "custom_animation_tab_search_height",
+ "value": "120px"
+ },
+ {
+ "name": "custom_animation_tab_ic_public_more",
+ "value": "18"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/string.json b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..e0bcac2f03fa4dd11858e8e6148ad7bcde9639a5
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/element/string.json
@@ -0,0 +1,32 @@
+{
+ "string": [
+ {
+ "name": "module_desc",
+ "value": "module description"
+ },
+ {
+ "name": "entryability_desc",
+ "value": "description"
+ },
+ {
+ "name": "custom_animation_tab_one_hundred_percent",
+ "value": "100%"
+ },
+ {
+ "name": "custom_animation_tab_ninety_percent",
+ "value": "90%"
+ },
+ {
+ "name": "custom_animation_tab_ten_percent",
+ "value": "10%"
+ },
+ {
+ "name": "custom_animation_tab_sixty_percent",
+ "value": "60%"
+ },
+ {
+ "name": "custom_animation_tab_button_text",
+ "value": "修改背景条颜色"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimation_ic_public_more.svg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimation_ic_public_more.svg
new file mode 100644
index 0000000000000000000000000000000000000000..88fd9e2a80e4c489790c3f835d2bedd7b9a9e330
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimation_ic_public_more.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtab_example2.jpg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtab_example2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..5b2e88c963321d4766801a38577bce91ea11b9c8
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtab_example2.jpg differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_eq.jpg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_eq.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..5f0ec41babe988e48eb31f95df6a44cebb85ebd2
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_eq.jpg differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example1.jpg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9c356cc0a1d4616287b0e844873974e3085f0117
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example1.jpg differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example2.jpg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..df395f03dcc7b2e00e51b96822fcc608886df0a8
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example2.jpg differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example3.jpg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example3.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3a4c14f12fc4804efc6d80025bed0e4dda89a087
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example3.jpg differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example4.jpg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example4.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..0e636dd74199f735e7c49590c9df22fa3577f49d
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example4.jpg differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example5.jpg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example5.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..1ca6f001f1d4ed1347a97ef128cf619e50139cdc
Binary files /dev/null and b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/customanimationtabs_example5.jpg differ
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/return_home_fill.svg b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/return_home_fill.svg
new file mode 100644
index 0000000000000000000000000000000000000000..529375de763bca415f5b7a536bb1e3c2b1ccef46
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/base/media/return_home_fill.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/en_US/element/string.json b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/en_US/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..f51a9c8461a55f6312ef950344e3145b7f82d607
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/en_US/element/string.json
@@ -0,0 +1,8 @@
+{
+ "string": [
+ {
+ "name": "page_show",
+ "value": "page from package"
+ }
+ ]
+}
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/zh_CN/element/string.json b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/zh_CN/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..f51a9c8461a55f6312ef950344e3145b7f82d607
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/main/resources/zh_CN/element/string.json
@@ -0,0 +1,8 @@
+{
+ "string": [
+ {
+ "name": "page_show",
+ "value": "page from package"
+ }
+ ]
+}
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/ets/test/Ability.test.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/ets/test/Ability.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..0f8ce9a2c012f8fe36114cef65216ef0b6254f41
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/ets/test/Ability.test.ets
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
+
+export default function abilityTest() {
+ describe('ActsAbilityTest', () => {
+ // Defines a test suite. Two parameters are supported: test suite name and test suite function.
+ beforeAll(() => {
+ // Presets an action, which is performed only once before all test cases of the test suite start.
+ // This API supports only one parameter: preset action function.
+ })
+ beforeEach(() => {
+ // Presets an action, which is performed before each unit test case starts.
+ // The number of execution times is the same as the number of test cases defined by **it**.
+ // This API supports only one parameter: preset action function.
+ })
+ afterEach(() => {
+ // Presets a clear action, which is performed after each unit test case ends.
+ // The number of execution times is the same as the number of test cases defined by **it**.
+ // This API supports only one parameter: clear action function.
+ })
+ afterAll(() => {
+ // Presets a clear action, which is performed after all test cases of the test suite end.
+ // This API supports only one parameter: clear action function.
+ })
+ it('assertContain', 0, () => {
+ // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
+ hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
+ let a = 'abc';
+ let b = 'b';
+ // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
+ expect(a).assertContain(b);
+ expect(a).assertEqual(a);
+ })
+ })
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/ets/test/List.test.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/ets/test/List.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..1eac52fcebe8958e19a7b8fed2e8f39c520a3e42
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/ets/test/List.test.ets
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import abilityTest from './Ability.test';
+
+export default function testsuite() {
+ abilityTest();
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/module.json5 b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/module.json5
new file mode 100644
index 0000000000000000000000000000000000000000..aff570d31c256a46d7dafd73737ee2173a7ce4f3
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/ohosTest/module.json5
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "module": {
+ "name": "customanimationtab_test",
+ "type": "feature",
+ "deviceTypes": [
+ "default",
+ "tablet"
+ ],
+ "deliveryWithInstall": true,
+ "installationFree": false
+ }
+}
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/test/List.test.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/test/List.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..bb5b5c3731e283dd507c847560ee59bde477bbc7
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/test/List.test.ets
@@ -0,0 +1,5 @@
+import localUnitTest from './LocalUnit.test';
+
+export default function testsuite() {
+ localUnitTest();
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/test/LocalUnit.test.ets b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/test/LocalUnit.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..165fc1615ee8618b4cb6a622f144a9a707eee99f
--- /dev/null
+++ b/code/UI/CustomAnimationTab/casesfeature/customanimationtab/src/test/LocalUnit.test.ets
@@ -0,0 +1,33 @@
+import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
+
+export default function localUnitTest() {
+ describe('localUnitTest', () => {
+ // Defines a test suite. Two parameters are supported: test suite name and test suite function.
+ beforeAll(() => {
+ // Presets an action, which is performed only once before all test cases of the test suite start.
+ // This API supports only one parameter: preset action function.
+ });
+ beforeEach(() => {
+ // Presets an action, which is performed before each unit test case starts.
+ // The number of execution times is the same as the number of test cases defined by **it**.
+ // This API supports only one parameter: preset action function.
+ });
+ afterEach(() => {
+ // Presets a clear action, which is performed after each unit test case ends.
+ // The number of execution times is the same as the number of test cases defined by **it**.
+ // This API supports only one parameter: clear action function.
+ });
+ afterAll(() => {
+ // Presets a clear action, which is performed after all test cases of the test suite end.
+ // This API supports only one parameter: clear action function.
+ });
+ it('assertContain', 0, () => {
+ // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
+ let a = 'abc';
+ let b = 'b';
+ // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
+ expect(a).assertContain(b);
+ expect(a).assertEqual(a);
+ });
+ });
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/code-linter.json5 b/code/UI/CustomAnimationTab/code-linter.json5
new file mode 100644
index 0000000000000000000000000000000000000000..28586467ee7a761c737d8654a73aed6fddbc3c71
--- /dev/null
+++ b/code/UI/CustomAnimationTab/code-linter.json5
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "files": [
+ "**/*.ets"
+ ],
+ "ignore": [
+ "**/src/ohosTest/**/*",
+ "**/src/test/**/*",
+ "**/src/mock/**/*",
+ "**/node_modules/**/*",
+ "**/oh_modules/**/*",
+ "**/build/**/*",
+ "**/.preview/**/*"
+ ],
+ "ruleSet": [
+ "plugin:@performance/recommended",
+ "plugin:@typescript-eslint/recommended"
+ ],
+ "rules": {
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/.gitignore b/code/UI/CustomAnimationTab/entry/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/.gitignore
@@ -0,0 +1,6 @@
+/node_modules
+/oh_modules
+/.preview
+/build
+/.cxx
+/.test
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/build-profile.json5 b/code/UI/CustomAnimationTab/entry/build-profile.json5
new file mode 100644
index 0000000000000000000000000000000000000000..b4d65d490ef6cbe22d933b9231555210f1555786
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/build-profile.json5
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "apiType": "stageMode",
+ "buildOption": {
+ },
+ "buildOptionSet": [
+ {
+ "name": "release",
+ "arkOptions": {
+ "obfuscation": {
+ "ruleOptions": {
+ "enable": false,
+ "files": [
+ "./obfuscation-rules.txt"
+ ]
+ }
+ }
+ }
+ },
+ ],
+ "targets": [
+ {
+ "name": "default"
+ },
+ {
+ "name": "ohosTest",
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/hvigorfile.ts b/code/UI/CustomAnimationTab/entry/hvigorfile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e4f43d54667f8327c367c8096bd08bb8c75aff54
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/hvigorfile.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { hapTasks } from '@ohos/hvigor-ohos-plugin';
+
+export default {
+ system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
+ plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
+}
diff --git a/code/UI/CustomAnimationTab/entry/obfuscation-rules.txt b/code/UI/CustomAnimationTab/entry/obfuscation-rules.txt
new file mode 100644
index 0000000000000000000000000000000000000000..272efb6ca3f240859091bbbfc7c5802d52793b0b
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/obfuscation-rules.txt
@@ -0,0 +1,23 @@
+# Define project specific obfuscation rules here.
+# You can include the obfuscation configuration files in the current module's build-profile.json5.
+#
+# For more details, see
+# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
+
+# Obfuscation options:
+# -disable-obfuscation: disable all obfuscations
+# -enable-property-obfuscation: obfuscate the property names
+# -enable-toplevel-obfuscation: obfuscate the names in the global scope
+# -compact: remove unnecessary blank spaces and all line feeds
+# -remove-log: remove all console.* statements
+# -print-namecache: print the name cache that contains the mapping from the old names to new names
+# -apply-namecache: reuse the given cache file
+
+# Keep options:
+# -keep-property-name: specifies property names that you want to keep
+# -keep-global-name: specifies names that you want to keep in the global scope
+
+-enable-property-obfuscation
+-enable-toplevel-obfuscation
+-enable-filename-obfuscation
+-enable-export-obfuscation
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/oh-package.json5 b/code/UI/CustomAnimationTab/entry/oh-package.json5
new file mode 100644
index 0000000000000000000000000000000000000000..1b5361b707fe956194c26cc78d40b2b67c2e723b
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/oh-package.json5
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "name": "entry",
+ "version": "1.0.0",
+ "description": "Please describe the basic information.",
+ "main": "",
+ "author": "",
+ "license": "",
+ "dependencies": {
+ "customanimationtab": "file:../casesfeature/customanimationtab"
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/ets/entryability/EntryAbility.ets b/code/UI/CustomAnimationTab/entry/src/main/ets/entryability/EntryAbility.ets
new file mode 100644
index 0000000000000000000000000000000000000000..3e77e4b9f7946d80aa77e6b2dcfe40b23ccf6178
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/ets/entryability/EntryAbility.ets
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { window } from '@kit.ArkUI';
+
+export default class EntryAbility extends UIAbility {
+ onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
+ this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
+ }
+
+ onDestroy(): void {
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
+ }
+
+ onWindowStageCreate(windowStage: window.WindowStage): void {
+ // Main window is created, set main page for this ability
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
+
+ windowStage.loadContent('pages/Index', (err) => {
+ if (err.code) {
+ hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
+ return;
+ }
+ hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
+ });
+ }
+
+ onWindowStageDestroy(): void {
+ // Main window is destroyed, release UI related resources
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
+ }
+
+ onForeground(): void {
+ // Ability has brought to foreground
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
+ }
+
+ onBackground(): void {
+ // Ability has back to background
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/code/UI/CustomAnimationTab/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets
new file mode 100644
index 0000000000000000000000000000000000000000..1504a74f09dfdcfae408be979f99369a2c5affab
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
+
+export default class EntryBackupAbility extends BackupExtensionAbility {
+ async onBackup() {
+ hilog.info(0x0000, 'testTag', 'onBackup ok');
+ }
+
+ async onRestore(bundleVersion: BundleVersion) {
+ hilog.info(0x0000, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion));
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/ets/pages/Index.ets b/code/UI/CustomAnimationTab/entry/src/main/ets/pages/Index.ets
new file mode 100644
index 0000000000000000000000000000000000000000..3ab01e7c9f3f7654d66ff852d1a7bac1c19addc1
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/ets/pages/Index.ets
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { CustomAnimationTabView } from 'customanimationtab';
+
+@Entry
+@Component
+struct Index {
+ @State message: string = 'Hello World';
+
+ build() {
+ RelativeContainer() {
+ /**
+ * 功能描述:本示例介绍使用List、Text等组件,以及animateTo等接口实现自定义Tab效果。
+ * 参数介绍:无
+ */
+ CustomAnimationTabView();
+
+ }
+ .height('100%')
+ .width('100%')
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/module.json5 b/code/UI/CustomAnimationTab/entry/src/main/module.json5
new file mode 100644
index 0000000000000000000000000000000000000000..dfbe47e6ac287d71e455c7f49cef83dd292447e6
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/module.json5
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "module": {
+ "name": "entry",
+ "type": "entry",
+ "description": "$string:module_desc",
+ "mainElement": "EntryAbility",
+ "deviceTypes": [
+ "default",
+ "tablet"
+ ],
+ "deliveryWithInstall": true,
+ "installationFree": false,
+ "pages": "$profile:main_pages",
+ "abilities": [
+ {
+ "name": "EntryAbility",
+ "srcEntry": "./ets/entryability/EntryAbility.ets",
+ "description": "$string:EntryAbility_desc",
+ "icon": "$media:layered_image",
+ "label": "$string:app_name",
+ "startWindowIcon": "$media:startIcon",
+ "startWindowBackground": "$color:start_window_background",
+ "exported": true,
+ "skills": [
+ {
+ "entities": [
+ "entity.system.home"
+ ],
+ "actions": [
+ "action.system.home"
+ ]
+ }
+ ]
+ }
+ ],
+ "extensionAbilities": [
+ {
+ "name": "EntryBackupAbility",
+ "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
+ "type": "backup",
+ "exported": false,
+ "metadata": [
+ {
+ "name": "ohos.extension.backup",
+ "resource": "$profile:backup_config"
+ }
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/element/color.json b/code/UI/CustomAnimationTab/entry/src/main/resources/base/element/color.json
new file mode 100644
index 0000000000000000000000000000000000000000..d66f9a7d4ac61fb8d215239ab3620b7bcd77bf33
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/resources/base/element/color.json
@@ -0,0 +1,8 @@
+{
+ "color": [
+ {
+ "name": "start_window_background",
+ "value": "#FFFFFF"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/element/string.json b/code/UI/CustomAnimationTab/entry/src/main/resources/base/element/string.json
new file mode 100644
index 0000000000000000000000000000000000000000..f94595515a99e0c828807e243494f57f09251930
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/resources/base/element/string.json
@@ -0,0 +1,16 @@
+{
+ "string": [
+ {
+ "name": "module_desc",
+ "value": "module description"
+ },
+ {
+ "name": "EntryAbility_desc",
+ "value": "description"
+ },
+ {
+ "name": "EntryAbility_label",
+ "value": "label"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/background.png b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/background.png
new file mode 100644
index 0000000000000000000000000000000000000000..f939c9fa8cc8914832e602198745f592a0dfa34d
Binary files /dev/null and b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/background.png differ
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/foreground.png b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/foreground.png
new file mode 100644
index 0000000000000000000000000000000000000000..4483ddad1f079e1089d685bd204ee1cfe1d01902
Binary files /dev/null and b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/foreground.png differ
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/layered_image.json b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/layered_image.json
new file mode 100644
index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/layered_image.json
@@ -0,0 +1,7 @@
+{
+ "layered-image":
+ {
+ "background" : "$media:background",
+ "foreground" : "$media:foreground"
+ }
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/startIcon.png b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/startIcon.png
new file mode 100644
index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b
Binary files /dev/null and b/code/UI/CustomAnimationTab/entry/src/main/resources/base/media/startIcon.png differ
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/profile/backup_config.json b/code/UI/CustomAnimationTab/entry/src/main/resources/base/profile/backup_config.json
new file mode 100644
index 0000000000000000000000000000000000000000..d742c2f96e7dd0f406f499941f3147345e998f95
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/resources/base/profile/backup_config.json
@@ -0,0 +1,3 @@
+{
+ "allowToBackupRestore": true
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/base/profile/main_pages.json b/code/UI/CustomAnimationTab/entry/src/main/resources/base/profile/main_pages.json
new file mode 100644
index 0000000000000000000000000000000000000000..1898d94f58d6128ab712be2c68acc7c98e9ab9ce
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/resources/base/profile/main_pages.json
@@ -0,0 +1,5 @@
+{
+ "src": [
+ "pages/Index"
+ ]
+}
diff --git a/code/UI/CustomAnimationTab/entry/src/main/resources/dark/element/color.json b/code/UI/CustomAnimationTab/entry/src/main/resources/dark/element/color.json
new file mode 100644
index 0000000000000000000000000000000000000000..438d5bc43bb23c59c210d586b96635a72da5b64a
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/main/resources/dark/element/color.json
@@ -0,0 +1,8 @@
+{
+ "color": [
+ {
+ "name": "start_window_background",
+ "value": "#000000"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/ohosTest/ets/test/Ability.test.ets b/code/UI/CustomAnimationTab/entry/src/ohosTest/ets/test/Ability.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..f8e941d5fa9f2720b9fffa05b26cfd9d68677e6f
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/ohosTest/ets/test/Ability.test.ets
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
+import { abilityDelegatorRegistry, Driver, ON, UiDirection } from '@kit.TestKit';
+import { Want } from '@kit.AbilityKit';
+import { Point } from '@ohos.UiTest';
+
+const DELAY_MS: number = 1000;
+const TAG: string = 'CustomAnimationTab_Test'
+let abilityDelegator = abilityDelegatorRegistry.getAbilityDelegator();
+
+export default function abilityTest() {
+ describe('ActsAbilityTest', () => {
+ // Defines a test suite. Two parameters are supported: test suite name and test suite function.
+ beforeAll(async (done: Function) => {
+ let want: Want = {
+ bundleName: 'com.samples.customanimationtab',
+ abilityName: 'EntryAbility'
+ }
+
+ abilityDelegator.startAbility(want, (err) => {
+ if (err) {
+ hilog.error(0xFF00, TAG, '%{public}s', `error: ${JSON.stringify(err)}`);
+ } else {
+ hilog.info(0xFF00, TAG, '%{public}s', 'success start ability');
+ }
+ });
+ await driver.delayMs(DELAY_MS);
+
+ done();
+ })
+
+ let driver = Driver.create();
+
+ /**
+ * 关联用例: 快速滑动,背景条自动左右滑动
+ * 执行步骤:
+ * 1. 快速滑动tab页
+ * 2. 通过对比背景条中心点与下一页签中心点是否相等进行验证
+ */
+ it("短距离脱手滑动", 0, async (done: Function) => {
+ await driver.fling(UiDirection.RIGHT, 4000);
+ await driver.delayMs(1000);
+ // 验证背景条滑动至下一页签
+ const afterTabBar = await driver.findComponent(ON.id("1"));
+ const afterTabCenter = await afterTabBar.getBoundsCenter();
+ const afterBackgroundBar = await driver.findComponent(ON.id("backgroundBar"));
+ const afterBackgroundCenter = await afterBackgroundBar.getBoundsCenter();
+ if (!isPointEqual(afterTabCenter, afterBackgroundCenter)) {
+ throw new Error();
+ }
+ done();
+ })
+
+ /**
+ * 关联用例: 滑动距离不足一般,背景条自动回弹
+ * 执行步骤:
+ * 1. 短距离缓慢滑动tab页并松手
+ * 2. 通过对比背景条中心点与当前页签中心点是否相等进行验证
+ */
+ it("短距离滑动", 0, async (done: Function) => {
+ await driver.swipe(1000, 1000, 600, 1000, 400);
+ await driver.delayMs(1000);
+ // 验证背景条回弹至当前页签
+ const afterTabBar = await driver.findComponent(ON.id("1"));
+ const afterTabCenter = await afterTabBar.getBoundsCenter();
+ const afterBackgroundBar = await driver.findComponent(ON.id("backgroundBar"));
+ const afterBackgroundCenter = await afterBackgroundBar.getBoundsCenter();
+ if (!isPointEqual(afterTabCenter, afterBackgroundCenter)) {
+ throw new Error();
+ }
+ done();
+ })
+
+ /**
+ * 关联用例: 滑动距离超过一半,背景条自动滑动至下一页签
+ * 执行步骤:
+ * 1. 长距离缓慢滑动tab页并松手
+ * 2. 通过对比背景条中心点与下一页签中心点是否相等进行验证
+ */
+ it("长距离滑动", 0, async (done: Function) => {
+ await driver.swipe(1000, 1000, 200, 1000, 400);
+ await driver.delayMs(1000);
+ // 验证背景条跳转至下个页签位置
+ const afterTabBar = await driver.findComponent(ON.id("2"));
+ const afterTabCenter = await afterTabBar.getBoundsCenter();
+ const afterBackgroundBar = await driver.findComponent(ON.id("backgroundBar"));
+ const afterBackgroundCenter = await afterBackgroundBar.getBoundsCenter();
+ hilog.info(0xFF00, TAG, '%{public}s',
+ `left: ${afterTabCenter.x}, right: ${afterTabCenter.y}, left: ${afterBackgroundCenter.x}, right: ${afterBackgroundCenter.y}`);
+ if (!isPointEqual(afterTabCenter, afterBackgroundCenter)) {
+ throw new Error();
+ }
+ done();
+ })
+
+ /**
+ * 关联用例: 背景条跟随页签条滑动
+ * 执行步骤:
+ * 1. 缓慢滑动页签条
+ * 2. 通过对比背景条滑动距离与任一页签滑动距离是否相等进行验证
+ */
+ it("页签条滑动", 0, async (done: Function) => {
+ // 验证背景条与页签条一起滑动
+ const beforeTabBar = await driver.findComponent(ON.id("4"));
+ const beforeTabCenter = await beforeTabBar.getBoundsCenter();
+ const beforeBackgroundBar = await driver.findComponent(ON.id("backgroundBar"));
+ const beforeBoundCenter = await beforeBackgroundBar.getBoundsCenter();
+ await driver.swipe(1000, 445, 900, 445, 200);
+ const afterTabBar = await driver.findComponent(ON.id("4"));
+ const afterTabCenter = await afterTabBar.getBoundsCenter();
+ const afterBackgroundBar = await driver.findComponent(ON.id("backgroundBar"));
+ const afterBoundCenter = await afterBackgroundBar.getBoundsCenter();
+ if (!equals(Math.abs(beforeBoundCenter.x - afterBoundCenter.x), Math.abs(beforeTabCenter.x - afterTabCenter.x),
+ 5)) {
+ throw new Error();
+ }
+ await driver.delayMs(1000);
+ done();
+ })
+
+
+ /**
+ * 关联用例: 点击页签,背景条跳转至对应页签
+ * 执行步骤:
+ * 1. 点击目标页签
+ * 2. 通过对比背景条中心点与目标页签中心点是否相等进行验证
+ */
+ it("点击页签", 0, async (done: Function) => {
+ const beforeTabBar = await driver.findComponent(ON.text("三方库"));
+ await beforeTabBar.click();
+ await driver.delayMs(1000);
+ // 验证背景条滑动至选中页签项
+ const afterTabBar = await driver.findComponent(ON.id('3'));
+ const afterTabCenter = await afterTabBar.getBoundsCenter();
+ const afterBackgroundBar = await driver.findComponent(ON.id("backgroundBar"));
+ const afterBackgroundCenter = await afterBackgroundBar.getBoundsCenter();
+ if (!isPointEqual(afterTabCenter, afterBackgroundCenter)) {
+ throw new Error();
+ }
+ done();
+ })
+ })
+}
+
+function isPointEqual(obj1: Point, obj2: Point, offset: number = 0): boolean {
+ // 给定5个像素作为误差
+ return equals(obj1.x + offset, obj2.x, 5) &&
+ equals(obj1.y + offset, obj2.y, 5);
+}
+
+function equals(obj1: number, obj2: number, epsilon: number = Number.EPSILON): boolean {
+ return Math.abs(obj1 - obj2) < epsilon;
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/ohosTest/ets/test/List.test.ets b/code/UI/CustomAnimationTab/entry/src/ohosTest/ets/test/List.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..1eac52fcebe8958e19a7b8fed2e8f39c520a3e42
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/ohosTest/ets/test/List.test.ets
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import abilityTest from './Ability.test';
+
+export default function testsuite() {
+ abilityTest();
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/ohosTest/module.json5 b/code/UI/CustomAnimationTab/entry/src/ohosTest/module.json5
new file mode 100644
index 0000000000000000000000000000000000000000..9983b2ba4e55e31a172f0328c82c9a75bfa00ded
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/ohosTest/module.json5
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "module": {
+ "name": "entry_test",
+ "type": "feature",
+ "deviceTypes": [
+ "default",
+ "tablet"
+ ],
+ "deliveryWithInstall": true,
+ "installationFree": false
+ }
+}
diff --git a/code/UI/CustomAnimationTab/entry/src/test/List.test.ets b/code/UI/CustomAnimationTab/entry/src/test/List.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..bb5b5c3731e283dd507c847560ee59bde477bbc7
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/test/List.test.ets
@@ -0,0 +1,5 @@
+import localUnitTest from './LocalUnit.test';
+
+export default function testsuite() {
+ localUnitTest();
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/entry/src/test/LocalUnit.test.ets b/code/UI/CustomAnimationTab/entry/src/test/LocalUnit.test.ets
new file mode 100644
index 0000000000000000000000000000000000000000..165fc1615ee8618b4cb6a622f144a9a707eee99f
--- /dev/null
+++ b/code/UI/CustomAnimationTab/entry/src/test/LocalUnit.test.ets
@@ -0,0 +1,33 @@
+import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
+
+export default function localUnitTest() {
+ describe('localUnitTest', () => {
+ // Defines a test suite. Two parameters are supported: test suite name and test suite function.
+ beforeAll(() => {
+ // Presets an action, which is performed only once before all test cases of the test suite start.
+ // This API supports only one parameter: preset action function.
+ });
+ beforeEach(() => {
+ // Presets an action, which is performed before each unit test case starts.
+ // The number of execution times is the same as the number of test cases defined by **it**.
+ // This API supports only one parameter: preset action function.
+ });
+ afterEach(() => {
+ // Presets a clear action, which is performed after each unit test case ends.
+ // The number of execution times is the same as the number of test cases defined by **it**.
+ // This API supports only one parameter: clear action function.
+ });
+ afterAll(() => {
+ // Presets a clear action, which is performed after all test cases of the test suite end.
+ // This API supports only one parameter: clear action function.
+ });
+ it('assertContain', 0, () => {
+ // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
+ let a = 'abc';
+ let b = 'b';
+ // Defines a variety of assertion methods, which are used to declare expected boolean conditions.
+ expect(a).assertContain(b);
+ expect(a).assertEqual(a);
+ });
+ });
+}
\ No newline at end of file
diff --git a/code/UI/CustomAnimationTab/hvigor/hvigor-config.json5 b/code/UI/CustomAnimationTab/hvigor/hvigor-config.json5
new file mode 100644
index 0000000000000000000000000000000000000000..a653156fd914ed32a77644b64b6a0da0cc63c153
--- /dev/null
+++ b/code/UI/CustomAnimationTab/hvigor/hvigor-config.json5
@@ -0,0 +1,22 @@
+{
+ "modelVersion": "5.0.1",
+ "dependencies": {
+ },
+ "execution": {
+ // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */
+ // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */
+ // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */
+ // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */
+ // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */
+ },
+ "logging": {
+ // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
+ },
+ "debugging": {
+ // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */
+ },
+ "nodeOptions": {
+ // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/
+ // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/
+ }
+}
diff --git a/code/UI/CustomAnimationTab/hvigorfile.ts b/code/UI/CustomAnimationTab/hvigorfile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2a5e543f190732c159beb574dfc9fa37bc94e156
--- /dev/null
+++ b/code/UI/CustomAnimationTab/hvigorfile.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { appTasks } from '@ohos/hvigor-ohos-plugin';
+
+export default {
+ system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
+ plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
+}
diff --git a/code/UI/CustomAnimationTab/oh-package.json5 b/code/UI/CustomAnimationTab/oh-package.json5
new file mode 100644
index 0000000000000000000000000000000000000000..eaef52736f22c14e04fca386ba01611ab32a7e4a
--- /dev/null
+++ b/code/UI/CustomAnimationTab/oh-package.json5
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2025 Huawei Device Co., Ltd.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+{
+ "modelVersion": "5.0.1",
+ "description": "Please describe the basic information.",
+ "dependencies": {
+ },
+ "devDependencies": {
+ "@ohos/hypium": "1.0.19",
+ "@ohos/hamock": "1.0.0"
+ }
+}
diff --git a/code/UI/CustomAnimationTab/ohosTest.md b/code/UI/CustomAnimationTab/ohosTest.md
new file mode 100644
index 0000000000000000000000000000000000000000..68c7aff8dcfc0661a11e0a124cb3425a87f1fe08
--- /dev/null
+++ b/code/UI/CustomAnimationTab/ohosTest.md
@@ -0,0 +1,12 @@
+# 多文件下载监听案例 测试用例归档
+
+## 用例表
+
+|测试功能|预置条件|输入|预期输出|是否自动|测试结果|
+|--------------------------------|--------------------------------|--------------------------------|--------------------------------|--------------------------------|--------------------------------|
+|快速滑动,背景条自动左右滑动| 1.进入案例页面 | 手势触摸tab内容左右滑动并立马松开手势 |背景条自动左右滑动|是|Pass|
+| 滑动距离不足一半,自动回弹 | 1.进入案例页面 | 手势触摸tab内容不松开向右滑动且滑动距离不足页面一半,然后松手 |背景条跟随手势滑动,松手后回弹|是|Pass|
+|滑动距离超过一半,自动滑动至下一页签| 1.进入案例页面 | 手势触摸tab内容不松开向右滑动且滑动距离超过页面一半,然后松手 |背景条跟随手势滑动,松手后滑动至下一页签|是|Pass|
+|背景条跳转至点击页签并滑动| 1.进入案例页面 | 点击页签 |背景条跳转至页签处并与页签一起滑动|是|Pass|
+|背景条跟随页签滑动|1.进入案例页面|滑动页签条| 背景条跟随页签条一起滑动 |是|Pass|
+|触摸暂定滑动动画,松开后继续原始动画|1.进入案例页面|手势触摸tab内容左右滑动并立马松开手势,在动画结束前再次触摸tab内容,之后松开手势|再次触摸tab内容时背景条停止滑动动画,松开手势后继续执行未完成的滑动动画|否|Pass|