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

4 回复

尊敬的开发者,您好!您的问题已受理,请您耐心等待,感谢您的理解与支持!

更多关于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状态。

核心问题分析:

  1. 滚动事件触发过于频繁onContentDidScroll在滑动过程中会高频触发,每次触发都调用updateAllTransforms并赋值给[@Local](/user/Local)状态变量,导致UI频繁重排。

  2. 状态更新与动画不同步:Swiper自身的滑动动画与外部图片的变换动画可能存在时序冲突,造成视觉上的闪烁。

优化建议:

  1. 使用动画曲线替代实时计算: 将图片变换与Swiper的滑动进度解耦,通过Swiper的onChange事件获取目标索引,然后使用显式动画(如animateTo)驱动叠层图片的变换。

  2. 简化状态更新逻辑: 避免在onContentDidScroll中更新所有图片状态。可以改为只记录滑动进度,在onAnimationEndonChange中批量更新。

  3. 使用@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);
  });
})
  1. 优化图片渲染逻辑: 考虑使用LazyForEach或优化图片组件结构,减少不必要的重绘。

通过以上调整,将高频的实时计算转换为基于动画曲线的平滑过渡,可以有效避免闪屏问题。同时建议简化assignUniqueImageIndices中的复杂索引计算逻辑,避免在滑动过程中频繁执行重排。

回到顶部