HarmonyOS鸿蒙Next中如何让两个Swiper完全同步,包括切换行为和动画?
HarmonyOS鸿蒙Next中如何让两个Swiper完全同步,包括切换行为和动画? 一个切换时另一个跟着切换,动画保持同步,可以做到吗?
【背景知识】
Swiper组件内包含了PanGesture拖动手势事件,用于滑动轮播子组件。disableSwipe属性设为true会取消内部的PanGesture事件监听。
- index设置当前在容器中显示的子组件的索引值。该属性支持$$双向绑定变量。
- onAnimationStart切换动画开始时触发该回调。
- changeIndex控制Swiper翻页至指定页面。
【解决方案】
1、Swiper的index属性支持$$双向绑定变量,可以将两个Swiper绑定同一个变量。关键代码如下:
@State showIndex: number = 0;
// ...
Swiper(this.swiperControllerA) {
// ...
}.index($$this.showIndex)
Swiper(this.swiperControllerB) {
// ...
}.index($$this.showIndex)
// ...
手指滑动一个Swiper切换结束后,另一个Swiper也会切换,但是非手指滑动的Swiper没有动画效果,且相当于一前一后切换,不是同步滑动。
2、Swiper切换子组件的动画开始时会触发onAnimationStart事件,可以在onAnimationStart回调中使用SwiperController的changeIndex方法切换另一个Swiper。
private swiperControllerA: SwiperController = new SwiperController();
private swiperControllerB: SwiperController = new SwiperController();
// ...
Swiper(this.swiperControllerA) {
// ...
}
.onAnimationStart((index: number, targetIndex: number) => {
this.swiperControllerB.changeIndex(targetIndex, true)
})
Swiper(this.swiperControllerB) {
// ...
}
.onAnimationStart((index: number, targetIndex: number) => {
this.swiperControllerA.changeIndex(targetIndex, true)
})
手指滑动一个Swiper切换过程中,另一个Swiper也会切换,两个Swiper具有动画效果,但是非手指滑动的Swiper会略微滞后。
3、如果想保持完全同步可以参考scroll + swiper 案例:
import display from '@ohos.display';
import componentUtils from '@ohos.arkui.componentUtils';
@Entry
@Component
struct swiperTab {
private displayInfo: display.Display | null = null;
private animationDuration: number = 300; //动画时间
@State swiperIndex: number = 0; //内容区当前Index
@State swiperWidth: number = 0; // vp 内容区宽度
@State indicatorWidth: number = 0; // vp 页签宽度
@State indicatorMarginLeft: number = 5; // vp 当前页签距离左边margin
@State indicatorIndex: number = 0; //当前页签Index
@State swipeRatio: number = 0; //判断是否翻页,页面滑动超过一半,tabBar切换到下一页。
private arr: string[] = ['关注', '推荐', '热点', '上海', '视频', '新时代', '新歌', '新碟', '新片'];
private textLength: number[] = [2, 2, 2, 2, 2, 3, 2, 2, 2] // 控制页签所在父容器宽度,避免造成下划线目标位置计算错误
private scroller: Scroller = new Scroller();
private swiperController: SwiperController = new SwiperController();
aboutToAppear(): void {
this.displayInfo = display.getDefaultDisplaySync(); //获取屏幕实例
}
// 获取屏幕宽度,单位vp
private getDisplayWidth(): number {
return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;
}
// 获取组件大小、位置、平移缩放旋转及仿射矩阵属性信息。
private getTextInfo(index: number): Record<string, number> {
let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById(index.toString());
try {
return { 'left': px2vp(modePosition.windowOffset.x), 'width': px2vp(modePosition.size.width) }
} catch (error) {
return { 'left': 0, 'width': 0 }
}
}
// 当前下划线动画
private getCurrentIndicatorInfo(index: number, event: SwiperAnimationEvent): Record<string, number> {
let nextIndex = index;
// 滑动范围限制,Swiper不可循环,Scroll保持不可循环
if (index > 0 && event.currentOffset > 0) {
nextIndex--; // 左滑
} else if (index < this.arr.length - 1 && event.currentOffset < 0) {
nextIndex++; // 右滑
}
// 获取当前tabbar的属性信息
let indexInfo = this.getTextInfo(index);
// 获取目标tabbar的属性信息
let nextIndexInfo = this.getTextInfo(nextIndex);
// 滑动页面超过一半时页面切换
this.swipeRatio = Math.abs(event.currentOffset / this.swiperWidth);
let currentIndex = this.swipeRatio > 0.5 ? nextIndex : index; // 页面滑动超过一半,tabBar切换到下一页。
let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * this.swipeRatio;
let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * this.swipeRatio;
this.indicatorIndex = currentIndex;
return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth };
}
private scrollIntoView(currentIndex: number): void {
const indexInfo = this.getTextInfo(currentIndex);
let tabPositionLeft = indexInfo.left;
let tabWidth = indexInfo.width;
// 获取屏幕宽度,单位vp
const screenWidth = this.getDisplayWidth();
const currentOffsetX: number = this.scroller.currentOffset().xOffset; //当前滚动的偏移量
this.scroller.scrollTo({
// 将tabbar定位在正中间
xOffset: currentOffsetX + tabPositionLeft - screenWidth / 2 + tabWidth / 2,
yOffset: 0,
animation: {
duration: this.animationDuration,
curve: Curve.Linear,
}
});
this.underlineScrollAuto(this.animationDuration, currentIndex);
}
private startAnimateTo(duration: number, marginLeft: number, width: number): void {
animateTo({
duration: duration, // 动画时长
curve: Curve.Linear, // 动画曲线
onFinish: () => {
console.info('play end')
}
}, () => {
this.indicatorMarginLeft = marginLeft;
this.indicatorWidth = width;
})
}
// 下划线动画
private underlineScrollAuto(duration: number, index: number): void {
let indexInfo = this.getTextInfo(index);
this.startAnimateTo(duration, indexInfo.left, indexInfo.width);
}
build() {
Column() {
// tabbar
Column() {
Scroll(this.scroller) {
Row() {
ForEach(this.arr, (item: string, index: number) => {
Column() {
Text(item)
.fontSize(16)
.borderRadius(5)
.fontColor(this.indicatorIndex === index ? Color.Red : Color.Black)
.fontWeight(this.indicatorIndex === index ? FontWeight.Bold : FontWeight.Normal)
.margin({ left: 5, right: 5 })
.id(index.toString())
.onAreaChange((oldValue: Area, newValue: Area) => {
if (this.indicatorIndex === index &&
(this.indicatorMarginLeft === 0 || this.indicatorWidth === 0)) {
if (newValue.globalPosition.x != undefined) {
let positionX = Number.parseFloat(newValue.globalPosition.x.toString());
this.indicatorMarginLeft = Number.isNaN(positionX) ? 0 : positionX;
}
let width = Number.parseFloat(newValue.width.toString());
this.indicatorWidth = Number.isNaN(width) ? 0 : width;
}
})
.onClick(() => {
this.indicatorIndex = index;
// 点击tabbar下划线动效
this.underlineScrollAuto(this.animationDuration, index);
this.scrollIntoView(index);
// 跟swiper进行联动
this.swiperIndex = index;
})
}
.width(this.textLength[index] * 28)
}, (item: string) => item)
}
.blendMode(BlendMode.SRC_IN, BlendApplyType.OFFSCREEN)
.backgroundColor(Color.Transparent)
.height(32)
}
// 设置tabbar文字两端显隐
.linearGradient({
angle: 90,
colors: [['rgba(0, 0, 0, 0)', 0], ['rgba(0, 0, 0, 1)', 0], ['rgba(0, 0, 0, 1)', 0.9], ['rgba(0, 0, 0, 0)', 1]]
})
.blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)
.width('100%')
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.None)
// 滚动事件回调, 返回滚动时水平、竖直方向偏移量
.onScroll((xOffset: number, yOffset: number) => {
this.indicatorMarginLeft -= xOffset;
})
// 滚动停止事件回调
.onScrollStop(() => {
this.underlineScrollAuto(0, this.indicatorIndex);
})
Column()
.width(this.indicatorWidth)
.height(2)
.borderRadius(2)
.backgroundColor(Color.Red)
.alignSelf(ItemAlign.Start)
.margin({ left: this.indicatorMarginLeft, top: 5 })
}
.width('100%')
.margin({ top: 15, bottom: 10 })
Swiper(this.swiperController) {
ForEach(this.arr, (item: string, index: number) => {
Column() {
Text(item)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.backgroundColor($r('sys.color.ohos_id_color_sub_background'))
.onAreaChange((oldValue: Area, newValue: Area) => {
let width = Number.parseFloat(newValue.width.toString());
this.swiperWidth = Number.isNaN(width) ? 0 : width;
})
}, (item: string) => item)
}
.onChange((index: number) => {
this.swiperIndex = index;
})
.cachedCount(2)
// 跟自定义tabbar进行联动效果
.index(this.swiperIndex)
.indicator(false)
.curve(Curve.Linear)
.loop(false)
.onAnimationStart((index: number, targetIndex: number, event: SwiperAnimationEvent) => {
// 切换动画开始时触发该回调。下划线跟着页面一起滑动,同时宽度渐变。
this.indicatorIndex = targetIndex;
this.underlineScrollAuto(this.animationDuration, targetIndex);
})
.onAnimationEnd((index: number, event: SwiperAnimationEvent) => {
// 切换动画结束时触发该回调。下划线动画停止。
let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width);
this.scrollIntoView(index);
})
.onGestureSwipe((index: number, event: SwiperAnimationEvent) => {
// 在页面跟手滑动过程中,逐帧触发该回调。
let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
this.indicatorIndex = currentIndicatorInfo.index; //当前页签index
this.indicatorMarginLeft = currentIndicatorInfo.left; //当前页签距离左边margin
this.indicatorWidth = currentIndicatorInfo.width; //当前页签宽度
})
}
.width('100%')
.height('100%')
}
}
更多关于HarmonyOS鸿蒙Next中如何让两个Swiper完全同步,包括切换行为和动画?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
用同一个变量进行控制
在HarmonyOS Next中,可通过绑定SwiperController实现两个Swiper完全同步。为两个Swiper组件设置相同的controller属性,通过监听onChange事件,使用swipeTo方法确保切换索引一致。动画同步需确保两个Swiper的duration和curve参数完全相同。
是的,可以通过绑定相同的 current
属性和 change
事件来实现两个 Swiper 的完全同步。具体步骤如下:
- 在数据模型中定义一个变量(如
currentIndex
)用于控制两个 Swiper 的当前索引。 - 为两个 Swiper 组件设置相同的
current
属性,绑定到currentIndex
。 - 为每个 Swiper 添加
change
事件监听,当任意一个 Swiper 切换时,更新currentIndex
以触发另一个 Swiper 同步切换。
示例代码(基于 ArkTS):
@State currentIndex: number = 0;
build() {
Column() {
Swiper() {
// Swiper1 内容
}
.current(this.currentIndex)
.onChange((index: number) => {
this.currentIndex = index;
})
Swiper() {
// Swiper2 内容
}
.current(this.currentIndex)
.onChange((index: number) => {
this.currentIndex = index;
})
}
}
这样,无论是手动滑动还是程序控制,两个 Swiper 的切换行为和动画都会保持同步。