HarmonyOS鸿蒙Next中根据swiper的偏移量来移动叠层图片,能不能像附件视频中的一样呢 目前会出现闪屏异常
HarmonyOS鸿蒙Next中根据swiper的偏移量来移动叠层图片,能不能像附件视频中的一样呢 目前会出现闪屏异常
import { display } from "@kit.ArkUI";
interface SwiperTabComponentModel {
id: number;
title?: Resource | string;
image?: Resource | string;
}
interface ImageTransform {
positionX: number;
translateX: number;
scale: number;
zIndex: number;
imageIndex: number;
}
@ComponentV2
struct TestSwiperBanner {
@Local currentIndex: number = 0
@Local screenWidth: number = 0
@Local imageTransforms: ImageTransform[] = []
@Local isSwiping: boolean = false
@Local lastPosition: number = 0
@Local hasReachedThreshold: boolean = false
private tabTopItem: SwiperTabComponentModel[] = [
{ id: 0, title: '推荐', image: $r('app.media.bg_entrance_custom_agent') },
{ id: 1, title: '视频', image: $r('app.media.bg_entrance_chatbot_agent') },
{ id: 2, title: '图片', image: $r('app.media.bg_entrance_avatar_agent') },
{ id: 3, title: '视频', image: $r('app.media.bg_entrance_custom_agent') },
{ id: 4, title: '图片', image: $r('app.media.bg_entrance_chatbot_agent') },
{ id: 5, title: '视频', image: $r('app.media.bg_entrance_custom_agent') },
{ id: 6, title: '图片', image: $r('app.media.bg_entrance_chatbot_agent') },
{ id: 7, title: '视频', image: $r('app.media.bg_entrance_custom_agent') }
]
aboutToAppear() {
this.updateAllTransforms(1, 0)
}
// 计算循环索引
getCircularIndex(index: number): number {
const total = this.tabTopItem.length
if (index < 0) {
return (index % total + total) % total
}
return index % total
}
// 检查是否达到滑动阈值(超过一半)
checkThresholdReached(position: number, direction: number): boolean {
const threshold = 0.5 // 50%阈值
if (direction === 1) { // 右滑
return position >= threshold
} else if (direction === -1) { // 左滑
return position <= -threshold
}
return false
}
// 根据位置计算变换
calculateTransformForPosition(positionOffset: number, centerIndex: number, progressOffset: number): ImageTransform {
// 计算相对于中心的实际偏移
const relativeOffset = positionOffset + progressOffset
// 根据相对偏移计算变换
let scale = 0.6
let translateX = 0
let zIndex = 0
let positionX = 160
// 使用分段线性插值实现平滑变换
if (relativeOffset <= -2) {
// 最左侧图片
scale = 0.6
translateX = -160
zIndex = 1
} else if (relativeOffset < -1) {
// 左侧第二个到第一个之间的过渡
const factor = (relativeOffset + 2) // -1.9 to -1.1 -> 0.1 to 0.9
scale = 0.6 + 0.2 * factor // 0.6-0.8
translateX = -160 + 80 * factor // -160 to -80
zIndex = factor > 0.5 ? 5 : 1
} else if (relativeOffset < 0) {
// 左侧第一个图片
const factor = (relativeOffset + 1) // -0.9 to -0.1 -> 0.1 to 0.9
scale = 0.8 + 0.2 * factor // 0.8-1.0
translateX = -80 + 80 * factor // -80 to 0
zIndex = 5
} else if (relativeOffset < 1) {
// 中间图片
const factor = relativeOffset // 0 to 0.9
scale = 1.0
translateX = 0 + 260 * factor // 0 to 260
zIndex = 10
positionX = 164
} else if (relativeOffset < 2) {
// 右侧第一个图片
scale = 1.0
translateX = 260
zIndex = 5
} else {
// 右侧第二个及更远
scale = 1.0
translateX = 260
zIndex = 1
}
return {
positionX: positionX,
translateX: translateX,
scale: scale,
zIndex: zIndex,
imageIndex: 0 // 这里先设为0,后面会重新计算
}
}
// 为每个位置分配唯一的图片索引
assignUniqueImageIndices(transforms: ImageTransform[], centerIndex: number, progressOffset: number, direction: number = 0): ImageTransform[] {
const total = this.tabTopItem.length
const usedIndices = new Set<number>()
const result: ImageTransform[] = []
// 计算5个位置:左侧2个,中间1个,右侧2个
const positions = [-2, -1, 0, 1, 2]
for (let i = 0; i < positions.length; i++) {
const positionOffset = positions[i]
const transform = transforms[i]
let idealIndex: number
// if (this.hasReachedThreshold && direction !== 0) {
if (true && direction !== 0) {
// 根据滑动方向调整索引
if (direction === -1) { // 左滑
idealIndex = this.getCircularIndex(Math.round(centerIndex + positionOffset + progressOffset - 1))
} else { // 右滑
idealIndex = this.getCircularIndex(Math.round(centerIndex + positionOffset + progressOffset + 1))
}
} else {
idealIndex = this.getCircularIndex(Math.round(centerIndex + positionOffset + progressOffset))
}
// 如果理想索引已被使用,寻找下一个可用的索引
let assignedIndex = idealIndex
let attempts = 0
while (usedIndices.has(assignedIndex) && attempts < total) {
assignedIndex = this.getCircularIndex(assignedIndex + 1)
attempts++
}
// 如果找不到可用索引,使用理想索引
if (attempts >= total) {
assignedIndex = idealIndex
}
usedIndices.add(assignedIndex)
const newTransform: ImageTransform = {
positionX: transform.positionX,
translateX: transform.translateX,
scale: transform.scale,
zIndex: transform.zIndex,
imageIndex: assignedIndex
}
result.push(newTransform)
}
return result
}
// 重新排列图片(滑动结束后调用)
rearrangeImages(direction: number) {
const newTransforms: ImageTransform[] = []
// 复制现有的transforms
for (let i = 0; i < this.imageTransforms.length; i++) {
newTransforms.push(this.imageTransforms[i])
}
// 根据滑动方向更新每个位置的图片索引
if (direction === -1) {
// 左滑: 6,7,0,1,2 -> 5,6,7,0,1
for (let i = 0; i < 5; i++) {
const currentImageIndex = newTransforms[i].imageIndex
const newImageIndex = this.getCircularIndex(currentImageIndex - 1)
newTransforms[i].imageIndex = newImageIndex
}
} else if (direction === 1) {
// 右滑: 6,7,0,1,2 -> 7,0,1,2,3
for (let i = 0; i < 5; i++) {
const currentImageIndex = newTransforms[i].imageIndex
const newImageIndex = this.getCircularIndex(currentImageIndex + 1)
newTransforms[i].imageIndex = newImageIndex
}
}
this.imageTransforms = newTransforms
}
// 更新所有图片的变换
updateAllTransforms(centerIndex: number, progressOffset: number, direction: number = 0) {
const newTransforms: ImageTransform[] = []
const positions = [-2, -1, 0, 1, 2]
for (let i = 0; i < positions.length; i++) {
const transform = this.calculateTransformForPosition(
positions[i],
centerIndex,
progressOffset
)
newTransforms.push(transform)
}
// 为每个位置分配唯一的图片索引
const uniqueTransforms = this.assignUniqueImageIndices(newTransforms, centerIndex, progressOffset, direction)
this.imageTransforms = uniqueTransforms
}
build() {
Column() {
Row({ space: 20 }) {
ForEach(this.tabTopItem, (item: SwiperTabComponentModel) => {
Column() {
Text(item.title)
.fontColor(this.currentIndex === item.id ? Color.Blue : Color.Gray)
.fontWeight(this.currentIndex === item.id ? FontWeight.Bold : FontWeight.Normal)
.fontSize(16)
.onClick(() => {
this.currentIndex = item.id
this.hasReachedThreshold = false
this.updateAllTransforms(item.id, 0)
})
Divider()
.width(60)
.strokeWidth(2)
.color(Color.Blue)
.opacity(this.currentIndex === item.id ? 1 : 0)
}
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 20, bottom: 30 })
Stack() {
// 按照zIndex从低到高的顺序渲染图片,确保层级正确
// zIndex=1的图片(最底层)
Image(this.tabTopItem[this.imageTransforms[0]?.imageIndex || 0].image)
.width('260')
.height('260')
.position({
x: this.imageTransforms[0]?.positionX || 160,
y: 0
})
.borderRadius({
topLeft: 10,
bottomLeft: 10,
topRight: 0,
bottomRight: 0
})
.translate({
x: this.imageTransforms[0]?.translateX || -160,
y: 0
})
.scale({
x: this.imageTransforms[0]?.scale || 0.6,
y: this.imageTransforms[0]?.scale || 0.6
})
.zIndex(this.imageTransforms[0]?.zIndex || 1)
// zIndex=5的图片(中间层) - 左侧第一个
Image(this.tabTopItem[this.imageTransforms[1]?.imageIndex || 0].image)
.width('260')
.height('260')
.position({
x: this.imageTransforms[1]?.positionX || 160,
y: 0
})
.borderRadius({
topLeft: 10,
bottomLeft: 10,
topRight: 0,
bottomRight: 0
})
.translate({
x: this.imageTransforms[1]?.translateX || -80,
y: 0
})
.scale({
x: this.imageTransforms[1]?.scale || 0.8,
y: this.imageTransforms[1]?.scale || 0.8
})
.zIndex(this.imageTransforms[1]?.zIndex || 5)
// zIndex=5的图片(中间层) - 右侧第一个
Image(this.tabTopItem[this.imageTransforms[3]?.imageIndex || 0].image)
.width('260')
.height('260')
.position({
x: this.imageTransforms[3]?.positionX || 164,
y: 0
})
.borderRadius({
topLeft: 10,
bottomLeft: 10,
topRight: 0,
bottomRight: 0
})
.translate({
x: this.imageTransforms[3]?.translateX || 260,
y: 0
})
.scale({
x: this.imageTransforms[3]?.scale || 1,
y: this.imageTransforms[3]?.scale || 1
})
.zIndex(this.imageTransforms[3]?.zIndex || 5)
// zIndex=1的图片(最底层) - 右侧第二个
Image(this.tabTopItem[this.imageTransforms[4]?.imageIndex || 0].image)
.width('260')
.height('260')
.position({
x: this.imageTransforms[4]?.positionX || 160,
y: 0
})
.borderRadius({
topLeft: 10,
bottomLeft: 10,
topRight: 0,
bottomRight: 0
})
.translate({
x: this.imageTransforms[4]?.translateX || 260,
y: 0
})
.scale({
x: this.imageTransforms[4]?.scale || 1,
y: this.imageTransforms[4]?.scale || 1
})
.zIndex(this.imageTransforms[4]?.zIndex || 1)
// zIndex=10的图片(最上层) - 中间图片
Image(this.tabTopItem[this.imageTransforms[2]?.imageIndex || 0].image)
.width('260')
.height('260')
.position({
x: this.imageTransforms[2]?.positionX || 164,
y: 0
})
.borderRadius({
topLeft: 10,
bottomLeft: 10,
topRight: 0,
bottomRight: 0
})
.translate({
x: this.imageTransforms[2]?.translateX || 0,
y: 0
})
.scale({
x: this.imageTransforms[2]?.scale || 1,
y: this.imageTransforms[2]?.scale || 1
})
.zIndex(this.imageTransforms[2]?.zIndex || 10)
Swiper() {
ForEach(this.tabTopItem, (item: SwiperTabComponentModel) => {
Image('')
.ImageExtend()
})
}
.width('100%')
.zIndex(20)
.onContentDidScroll((selectedIndex: number, index: number, position: number, mainAxisLength: number) => {
console.info("onContentDidScroll selectedIndex: " + selectedIndex + ", index: " + index + ", position: " +
position + ", mainAxisLength: " + mainAxisLength)
// 确定滑动方向
let direction = 0
if (position > this.lastPosition) {
direction = 1 // 右滑
} else if (position < this.lastPosition) {
direction = -1 // 左滑
}
this.lastPosition = position
// 检查是否达到阈值
const thresholdReached = this.checkThresholdReached(position, direction)
if (thresholdReached && !this.hasReachedThreshold) {
console.log("滑动超过阈值,开始更新图片")
this.hasReachedThreshold = true
}
// 处理滑动偏移
const progressOffset = position
// 更新当前索引
this.currentIndex = selectedIndex
// 更新所有图片的变换
this.updateAllTransforms(index, progressOffset, direction)
})
.onChange((index: number) => {
console.log("Swiper onChange, index: " + index)
this.currentIndex = index
})
.onAnimationEnd(() => {
console.log("Swiper onAnimationEnd, hasReachedThreshold: " + this.hasReachedThreshold)
// 如果达到了阈值,重新排列图片
if (this.hasReachedThreshold) {
// 根据最后的方向重新排列
const direction = this.lastPosition > 0 ? 1 : -1
this.rearrangeImages(direction)
this.hasReachedThreshold = false
this.lastPosition = 0
// 重置变换
this.updateAllTransforms(this.currentIndex, 0)
}
})
.index(this.currentIndex)
.height('260')
.loop(true)
.indicator(false)
}
.width('100%')
.alignContent(Alignment.TopStart)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor(Color.White)
}
}
export { TestSwiperBanner }
@Extend(Image)
function ImageExtend() {
.width('100%')
.height('100%')
.backgroundColor(Color.Transparent)
}
更多关于HarmonyOS鸿蒙Next中根据swiper的偏移量来移动叠层图片,能不能像附件视频中的一样呢 目前会出现闪屏异常的实战教程也可以访问 https://www.itying.com/category-93-b0.html
尊敬的开发者,您好!您的问题已受理,请您耐心等待,感谢您的理解与支持!
更多关于HarmonyOS鸿蒙Next中根据swiper的偏移量来移动叠层图片,能不能像附件视频中的一样呢 目前会出现闪屏异常的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
可以用 ForEach+Stack 组件实现
在HarmonyOS Next中,通过Swiper的onChange事件获取当前页索引与偏移量,结合Canvas或Image组件,使用属性动画(如animateTo)实时更新叠层图片位置,可模拟视频效果。闪屏通常因频繁UI重绘或异步更新导致。需确保在UI线程同步执行位置计算与渲染,避免帧丢失。可尝试使用显式动画或离屏渲染优化。
闪屏问题通常是由于在滑动过程中频繁更新状态导致UI重绘冲突引起的。从你的代码看,主要问题集中在onContentDidScroll回调中实时更新imageTransforms状态。
核心问题分析:
-
滚动事件触发过于频繁:
onContentDidScroll在滑动过程中会高频触发,每次触发都调用updateAllTransforms并赋值给[@Local](/user/Local)状态变量,导致UI频繁重排。 -
状态更新与动画不同步:Swiper自身的滑动动画与外部图片的变换动画可能存在时序冲突,造成视觉上的闪烁。
优化建议:
-
使用动画曲线替代实时计算: 将图片变换与Swiper的滑动进度解耦,通过Swiper的
onChange事件获取目标索引,然后使用显式动画(如animateTo)驱动叠层图片的变换。 -
简化状态更新逻辑: 避免在
onContentDidScroll中更新所有图片状态。可以改为只记录滑动进度,在onAnimationEnd或onChange中批量更新。 -
使用@State替代@Local: 对于需要驱动UI更新的响应式变量,使用
[@State](/user/State)装饰器,ArkUI框架会对状态变更进行更好的调度。
关键代码调整示例:
// 1. 将高频更新的进度分离为普通变量,不触发UI更新
private scrollProgress: number = 0;
// 2. 在onContentDidScroll中只记录进度,不更新状态
.onContentDidScroll((selectedIndex: number, index: number, position: number) => {
this.scrollProgress = position; // 仅记录,不触发UI更新
})
// 3. 在onChange或onAnimationEnd中使用动画批量更新
.onChange((index: number) => {
this.currentIndex = index;
// 使用animateTo平滑过渡
animateTo({
duration: 100,
curve: Curve.EaseOut
}, () => {
this.updateAllTransforms(index, 0);
});
})
- 优化图片渲染逻辑:
考虑使用
LazyForEach或优化图片组件结构,减少不必要的重绘。
通过以上调整,将高频的实时计算转换为基于动画曲线的平滑过渡,可以有效避免闪屏问题。同时建议简化assignUniqueImageIndices中的复杂索引计算逻辑,避免在滑动过程中频繁执行重排。

