/*
* Copyright (c) 2024 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<string, number>, targetIndexInfo: Record<string, number>, 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<string, number>} indexInfo - 当前页签信息(center: 页签中心, width: 页签宽度)
* @param {Record<string, number>} targetIndexInfo - 目标页签信息(center: 页签中心, width: 页签宽度)
* @param {[number, number][]} elementsInfo - 页签动效信息[背景条左端位置, 页签条偏移]
* @returns
*/
clickAnimation: (targetIndex: number, targetIndexInfo: Record<string, number>,
elementsInfo: IndicatorAnimationInfo[]) => void =
(targetIndex: number, targetIndexInfo: Record<string, number>, 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 }
}
// this.vertical ?
// {
// 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}
// } :
// {
// right: this.tabBarAttribute!.barVertical === BarPosition.Start ?
// {anchor: "__container__", align: HorizontalAlign.End} :
// {anchor: "tabItems", align: HorizontalAlign.Start},
// left: this.tabBarAttribute!.barVertical === BarPosition.Start ?
// {anchor: "tabItems", align: HorizontalAlign.End} :
// {anchor: "__container__", align: HorizontalAlign.Start}
// }
)
.onChange((index: number) => {
if (this.listTouchState === 1 && index !== this.curIndex) {
let targetIndexInfo: Record<string, number> = 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.listTouchState = 1;
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
};
// console.log(`index: ${index}, left: ${this.elementsInfo[index].left}, offset: ${this.elementsInfo[index].offset},
// height: ${this.elementsInfo[index].height}, width: ${this.elementsInfo[index].width}`)
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, 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<string, number>} index页签当前的中心位置以及其宽度
*/
private getElementInfo(index: number): Record<string, number> {
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)
}