HarmonyOS鸿蒙Next中Swiper组件内的图片组件使用geometryTransition动效异常,该如何解决?

HarmonyOS鸿蒙Next中Swiper组件内的图片组件使用geometryTransition动效异常,该如何解决? 在实现音乐播放器的一镜到底效果时,我希望在打开关闭播放页面时,底部播放条上的封面能有一镜到底效果,但在设置showPlay = true时,打开播放页,一镜到底动效只有后一半,播放条上的封面似乎有个向下移动的效果;设置showPlay = false时,播放页关闭,一镜到底动效正常,这是为什么?应该怎么解决?

如果没有Swiper动效是正常的。

//Index.ets
Column() {
  if (this.showPlay === false) {
    Row() {
      Row() {
        Image(this.baseURL + this.getCoverUrl + this.auth + `&id=${this.nowPlayingSong?.id}&size=${this.coverSize}`)
          .alt($rawfile('nocover.png'))
          .objectFit(ImageFit.Contain)
          .width(60)
          .aspectRatio(1)
          .borderRadius(5)
          .geometryTransition("cover")
        Column() {
          Marquee({
            start: true,
            step: 3,
            src: this.nowPlayingSong?.title
          })
            .fontSize(18)
            .fontColor($r('app.color.font'))
          Marquee({
            start: true,
            step: 3,
            src: this.nowPlayingSong?.artist + " - " + this.nowPlayingSong?.album
          })
            .fontSize(15)
            .fontColor($r('app.color.font_secondary'))
            .margin({ top: 5 })
        }
        .margin({ left: 10, right: 10 })
        .alignItems(HorizontalAlign.Start)
        .width('100%')
        .layoutWeight(1)
      }
      .width('100%')
      .layoutWeight(1)
      .justifyContent(FlexAlign.Start)

      Row() {
        Button({ type: ButtonType.Circle }) {
          Image($r('app.media.prev'))
            .width(30)
            .height(30)
        }
        .width(35)
        .height(35)
        .backgroundColor(Color.Transparent)
        .onClick(async () => {
          await this.avPlayer!.reset();
          if (this.nowPlayingIndex === 0) {
            this.nowPlayingIndex = this.nowPlayingList.length - 1
          } else {
            this.nowPlayingIndex -= 1
          }
          this.nowPlayingSong = this.nowPlayingList[this.nowPlayingIndex]
          this.nowPlayingLyrics =
            await getLyrics(this.baseURL, this.username, this.password, this.nowPlayingSong.id)
          if (this.nowPlayingLyrics !== undefined) {
            this.groupLyrics()
          }
          this.avPlayer!.url =
            this.baseURL + '/rest/stream' + this.auth + `&id=${this.nowPlayingSong.id}&format=raw`;
        })

        Stack() {
          Progress({
            value: this.nowPlayedTime / this.nowPlayingSong?.duration! * 100,
            total: 100,
            type: ProgressType.Ring
          })
            .width(45)
            .height(45)
            .color($r('app.color.progress'))
            .backgroundColor($r('app.color.progress_background'))
            .style({
              strokeWidth: 3
            })

          Button({ type: ButtonType.Circle }) {
            Image(this.isPlaying === true ? $r('app.media.play') : $r('app.media.pause'))
              .width(25)
              .height(25)
          }
          .width(45)
          .height(45)
          .backgroundColor(Color.Transparent)
          .onClick(() => {
            this.isPlaying = !this.isPlaying
            if (this.isPlaying === false) {
              if (this.fadeVolume === true) {
                this.fadeOut()
              } else {
                this.avPlayer!.pause()
              }
            } else {
              if (this.fadeVolume === true) {
                this.fadeIn()
              } else {
                this.avPlayer!.play()
              }
            }
          })
        }

        Button({ type: ButtonType.Circle }) {
          Image($r('app.media.next'))
            .width(30)
            .height(30)
        }
        .width(35)
        .height(35)
        .backgroundColor(Color.Transparent)
        .onClick(async () => {
          await this.avPlayer!.reset();
          if (this.nowPlayingIndex === this.nowPlayingList.length - 1) {
            this.nowPlayingIndex = 0
          } else {
            this.nowPlayingIndex += 1
          }
          this.nowPlayingSong = this.nowPlayingList[this.nowPlayingIndex]
          this.nowPlayingLyrics =
            await getLyrics(this.baseURL, this.username, this.password, this.nowPlayingSong.id)
          if (this.nowPlayingLyrics !== undefined) {
            this.groupLyrics()
          }
          this.avPlayer!.url =
            this.baseURL + '/rest/stream' + this.auth + `&id=${this.nowPlayingSong.id}&format=raw`;
        })
      }
      .width(125)
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .padding({ left: 15, right: 15, bottom: 5 })
    .onClick(() => {
      if (this.showPlay === true) {
        this.showPlay = false
      }
      this.getUIContext()?.animateTo({
        duration: 800,
        curve: Curve.Friction
      }, () => {
        this.showPlay = true
      })
    })
  } else {
    Column() {
      Play()
        .onKeyEvent((event?: KeyEvent) => {
          if (event && event.keyCode === KeyCode.KEYCODE_SPACE && event.type === KeyType.Up) {
            this.isPlaying = !this.isPlaying
            if (this.isPlaying === false) {
              if (this.fadeVolume === true) {
                this.fadeOut()
              } else {
                this.avPlayer!.pause()
              }
            } else {
              if (this.fadeVolume === true) {
                this.fadeIn()
              } else {
                this.avPlayer!.play()
              }
            }
          }
        })
    }
  }
}
.width('100%')
.height(this.showPlay ? '100%' : 85)
.justifyContent(FlexAlign.Center)
.backgroundBlurStyle(this.showPlay ? BlurStyle.NONE : BlurStyle.COMPONENT_THICK)
.position({
  bottom: 0
})
.visibility(this.startPlaying === true ? Visibility.Visible : Visibility.Hidden)
.transition(TransitionEffect.translate({ y: 100 }).animation({ curve: Curve.Ease }))
//Play.ets
Swiper(this.controller) {
  Column() {
    Image(this.baseURL + this.getCoverUrl + this.auth + `&id=${this.nowPlayingSong?.id}&size=${this.coverSize}`)
      .alt($rawfile('nocover.png'))
      .objectFit(ImageFit.Contain)
      .height('100%')
      .aspectRatio(1)
      .borderRadius(10)
      .geometryTransition("cover")
  }
  .width('100%')
  .height('100%')
  .margin({ bottom: 40 })
  .onTouch((event?: TouchEvent) => {
    if (event?.type === TouchType.Move) {
      this.lyricsScroller.scrollToIndex(this.lyricsIndex, false, ScrollAlign.CENTER)
    }
  })
  // ...其他代码
}

更多关于HarmonyOS鸿蒙Next中Swiper组件内的图片组件使用geometryTransition动效异常,该如何解决?的实战教程也可以访问 https://www.itying.com/category-93-b0.html

5 回复

【背景知识】

  • 一镜到底动效:通过共享元素建立视觉关联,使元素从起点到终点的位置、尺寸变化自然过渡。
  • geometryTransition:绑定相同ID实现元素映射,自动计算布局差异并生成补间动画。
  • bindContentCover:控制全屏模态显示,通过禁用默认动画实现自定义转场。

【解决方案】

  • 使用bindContentCover方法在视图层上方绑定一个模态内容覆盖层,当iDetailShow状态为true时显示由cardContentBuilder构建的卡片详情内容。

  • geometryTransition(index.toString())的作用是启用组件间的几何属性过渡动画,通过唯一标识符index关联不同状态下的组件,实现位置/尺寸变化的平滑衔接。

  • 卡片详情页的展开与收起:使用animateTo方法实现平滑过渡动画。

  • 完整示例代码如下:

import { ComponentContent, LengthMetrics } from '@kit.ArkUI';
@Builder
export function refreshingContent() {
  Stack() {
  } // 下拉区域不显示内容
  .width('100%')
}

class CardData {
  image: Resource = $r('app.media.background');
}

const allCardData: CardData[] = [
  new CardData(),
  new CardData()
];

@Entry
@Component
struct Market {
  @State isDetailShow: boolean = false; // 详情全屏页是否显示
  @State selectedIndex: number = 0; // 详情全屏页索引
  @State alphaValue: number = 1; // 透明度
  @StorageProp('topRectHeight') topRectHeight: number = 0 //获取状态栏顶部区域高度
  @StorageProp('bottomRectHeight') bottomRectHeight: number = 0 //获取状态栏底部区域高度
  private contentNode?: ComponentContent<Object> = undefined;

  aboutToAppear(): void {
    let uiContext = this.getUIContext();
    // 创建ComponentContent实例封装自定义弹窗内容
    this.contentNode = new ComponentContent(uiContext, wrapBuilder(refreshingContent));
  }

  /**
   * 全屏模态内容
   * @param index
   */
  @Builder
  cardContentBuilder(index: number) {
    Column({ space: 20 }) {
      Refresh({ refreshing: false, refreshingContent: this.contentNode, offset: 4 }) {
        Scroll() {
          Column() {
            Stack({ alignContent: Alignment.Start }) {
              Image(allCardData[index].image)
                .clip(true)
                .transition(TransitionEffect.opacity(1))
                .width('100%')
            }

            Row() {
              Text('guhjfgehijogfijdfhwdjehfjfghejoehjdqirhwe21whfuijeiqhiqoujw')
                .lineSpacing(LengthMetrics.px(30))
                .fontSize(18)
                .margin({ left: 14, right: 14, top: 20 })
                .fontColor('#e6000000')
            }
            .alignItems(VerticalAlign.Top)
            .height('57%')
          }
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .edgeEffect(EdgeEffect.None)
        .width('100%')
        .height('100%')
        .geometryTransition(index.toString()) //通过唯一标识符index关联不同状态下的组件,实现位置/尺寸变化的平滑衔接
      }
      .onRefreshing(() => {
        this.onDetailBack(index);
      })
    }
    .padding({
      bottom: px2vp(this.bottomRectHeight),
    })
    .width('100%')
    .height('100%')
    .backgroundColor(Color.White)
    .transition(TransitionEffect.asymmetric(
      TransitionEffect.opacity(1),
      TransitionEffect.OPACITY
    ))
  }

  build() {
    Swiper() {
      Column() {
        ForEach(allCardData, (cardData: CardData, index: number) => {
          Column() {
            Column() {
              Row() {
                Column() {
                  Stack({ alignContent: Alignment.TopStart }) {
                    Image(cardData.image)
                      .width('100%')
                      .height('100%')
                  }
                }
                .width('100%')
                .height('100%')
                .alignItems(HorizontalAlign.Start)
                .borderRadius(20)
                .clip(true)
                .onClick(() => {
                  this.onCardClicked(index);
                })
                .geometryTransition(index.toString(), { follow: true }) // 通过唯一标识符index关联不同状态下的组件,实现位置/尺寸变化的平滑衔接
                .transition(TransitionEffect.OPACITY.animation({
                  duration: 500,
                  curve: Curve.Friction
                })) // 设置组件出现/消失的透明度转场效果
              }
              .justifyContent(FlexAlign.Start)
            }
            .backgroundColor(Color.White)
            .size({ width: '92%', height: 446 })
            .alignItems(HorizontalAlign.Start)
            .borderRadius(20)
          }
          .margin({ top: 14 })
          .width('100%')
        }, (data: CardData, index: number) => index.toString())
      }
      .margin({ bottom: 20 })
    }
    .padding({
      top: px2vp(this.topRectHeight),
    })
    .size({ width: '100%', height: '100%' })
    .backgroundColor('#0d000000')
    // 绑定全屏模态页面实现卡片一镜到底转场
    .bindContentCover(this.isDetailShow,
      this.cardContentBuilder(this.selectedIndex), { modalTransition: ModalTransition.NONE })
    .opacity(this.alphaValue)
  }

  /*
   * 点击卡片全屏显示
   */
  private onCardClicked(index: number): void {
    this.selectedIndex = index;
    this.getUIContext()?.animateTo({
      duration: 500,
      curve: Curve.Friction
    }, () => {
      this.isDetailShow = !this.isDetailShow;
      this.alphaValue = 0;
    });
  }

  /*
   * 全屏返回卡片
   */
  private onDetailBack(index: number): void {
    this.getUIContext()?.animateTo({
      duration: 500,
      curve: Curve.Friction
    }, () => {
      this.isDetailShow = !this.isDetailShow;
      this.alphaValue = 1;
    });
  }
}

本地未复现您的问题,为了更快解决您的问题,麻烦请补充以下信息:

问题现象(如:异常动图);

复现代码(如最小可复现demo);

版本信息(如:开发工具、手机系统版本信息);

更多关于HarmonyOS鸿蒙Next中Swiper组件内的图片组件使用geometryTransition动效异常,该如何解决?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


Swiper 组件的存在会导致目标 Image 组件的布局计算延迟或异步完成,使得 geometryTransition 在动画启动时无法正确获取目标元素的初始位置信息

可以尝试如下方案

1.同步布局参数

//确保 Index 页面的播放条封面与 Play 页面的 Swiper 封面 布局属性完全一致:

// Index.ets

Image(...)

.width(60)

.aspectRatio(1)

.geometryTransition("cover")



// Play.ets

Image(...)

.width(60) // 强制指定与首页相同尺寸

.aspectRatio(1)

.geometryTransition("cover")

//通过固定尺寸避免 Swiper 自动布局导致的位置偏移1。
  1. 禁用干扰动画
//移除 Column 容器的 translate 动画,避免干扰 geometryTransition 计算:

// 原代码中的干扰项

.transition(TransitionEffect.translate({ y: 100 }).animation({ curve: Curve.Ease }))



// 修改后:移除 translate 动效

.transition(TransitionEffect.OPACITY)
  1. 使用节点迁移替代条件渲染
//通过 NodeContainer 实现封面元素的跨容器迁移,避免 Swiper 容器层级破坏转场上下文:

// Index.ets

NodeContainer(this.nodeController) {

Image(...)

.geometryTransition("cover")

}



// Play.ets

NodeContainer(this.nodeController) {

Image(...)

.geometryTransition("cover")

}

//通过 NodeController 控制节点迁移时机,确保转场动画连贯性。
  1. 优化动画触发逻辑
//在 animateTo 闭包中增加状态变更后的布局同步操作:

this.getUIContext()?.animateTo({

duration: 800,

curve: Curve.Friction,

onFinish: () => {

// 强制触发布局更新

this.nodeController.requestLayout()

}

}, () => {

this.showPlay = true

})

//通过 requestLayout 确保 Swiper 完成布局计算后再执行动画。

Swiper的页面切换会导致Image组件被动态创建/销毁,而geometryTransition要求两个关联组件必须同时存在于渲染树中才能正确计算过渡参数。当使用if (this.showPlay)条件渲染时,两个Image组件无法同时存在,导致系统无法正确获取初始位置信息。另外播放条中的封面(Index.ets)与播放页中的封面(Play.ets)处于不同组件层级,Swiper内部的测量机制可能导致geometryTransition的follow参数不能正确传递相对位置。Swiper自身的切换动画优先级可能与geometryTransition的显式动画时序冲突,导致动画阶段错乱。

改下面几处:

你将条件渲染改为通过visibility或opacity控制组件显隐:

// Index.ets

Image(this.baseURL + ...)

  .visibility(this.showPlay ? Visibility.None : Visibility.Visible)

  .geometryTransition("cover")

// Play.ets

Image(this.baseURL + ...)

  .visibility(this.showPlay ? Visibility.Visible : Visibility.None)

  .geometryTransition("cover")

添加geometryTransition的follow属性,强制关联组件跟随父容器布局变化:

.geometryTransition("cover", { follow: true })

通过SwiperController关闭Swiper的自动切换和手势滑动:

// Play.ets

private controller: SwiperController = new SwiperController();

build() {

  Swiper(this.controller) {

    //...

  }

  .autoPlay(false)  // 关闭自动播放

  .disableSwipe(true)  // 禁用手势滑动

}

将状态变更包裹在animateTo闭包中:

// 修改原动画逻辑

.onClick(() => {

  this.getUIContext().animateTo({

    duration: 800,

    curve: Curve.Friction

  }, () => {

    this.showPlay = !this.showPlay; // 直接切换状态

  })

})

如有帮助记得关注哦!

Swiper组件内图片使用geometryTransition动效异常,需检查以下配置:确保Swiper与目标组件在同一父节点下;确认transitionName属性设置一致;检查动效组件是否被正确声明为共享元素。可尝试将Swiper的cachedCount属性调整为1,避免页面缓存干扰。若仍异常,检查HarmonyOS SDK版本是否支持此动效组合。

问题可能源于Swiper组件内部布局与geometryTransition的交互冲突。在打开播放页时,Swiper的默认布局行为可能导致图片位置计算异常,产生向下移动的视觉效果。

建议检查Swiper的布局参数,确保图片组件在Swiper内的位置稳定。可以尝试显式设置Swiper的index属性或调整布局约束,避免动态布局干扰geometryTransition的坐标计算。同时确认动画过程中父容器尺寸变化是否影响过渡效果。

回到顶部