Эх сурвалжийг харах

实现播放条变成圆形的播放球 动画效果

onecold 4 сар өмнө
parent
commit
0d4fa8543d

+ 133 - 0
entry/src/main/ets/common/util/PlayerDismissHelper.ets

@@ -0,0 +1,133 @@
+export interface PlayerDismissTarget {
+  translateX: number
+  translateY: number
+  scale: number
+  targetCenterX: number
+  targetCenterY: number
+}
+
+export interface PlayerDismissMorphTarget {
+  translateX: number
+  translateY: number
+  targetCenterX: number
+  targetCenterY: number
+  circleDiameter: number
+  dotDiameter: number
+  circleScaleX: number
+  circleScaleY: number
+  dotScaleX: number
+  dotScaleY: number
+}
+
+export interface MiniPlayerMorphTarget {
+  barWidth: number
+  barHeight: number
+  circleDiameter: number
+  dotDiameter: number
+  circleScaleX: number
+  circleScaleY: number
+  dotScaleX: number
+  dotScaleY: number
+}
+
+export interface MiniPlayerRevealAnimationPlan {
+  startScaleX: number
+  startScaleY: number
+  startOpacity: number
+  startBorderRadius: number
+  startProxySize: number
+  startProxyOpacity: number
+  shakeDelayMs: number
+}
+
+export function resolveMiniPlayerCoverCenterOffset(barWidthVp: number, coverSizeVp: number = 48,
+  horizontalPaddingVp: number = 16, coverLeftMarginVp: number = 5): number {
+  const safeBarWidth = barWidthVp > 0 ? barWidthVp : 324
+  const safeCoverSize = coverSizeVp > 0 ? coverSizeVp : 48
+  const safePadding = horizontalPaddingVp >= 0 ? horizontalPaddingVp : 16
+  const safeCoverLeftMargin = coverLeftMarginVp >= 0 ? coverLeftMarginVp : 5
+  const coverCenterX = safePadding + safeCoverLeftMargin + safeCoverSize / 2
+  return Math.max(0, safeBarWidth / 2 - coverCenterX)
+}
+
+export function resolvePlayerDismissTarget(viewportWidthVp: number, viewportHeightVp: number, bottomSafeHeightVp: number,
+  useFixedBottomMargin: boolean, miniPlayerHeightVp: number = 70, fixedBottomMarginVp: number = 30): PlayerDismissTarget {
+  const safeWidth = viewportWidthVp > 0 ? viewportWidthVp : 360
+  const safeHeight = viewportHeightVp > 0 ? viewportHeightVp : 800
+  const safeMiniPlayerHeight = miniPlayerHeightVp > 0 ? miniPlayerHeightVp : 70
+  const bottomMargin = useFixedBottomMargin ? fixedBottomMarginVp : Math.max(0, bottomSafeHeightVp)
+  const viewportCenterX = safeWidth / 2
+  const viewportCenterY = safeHeight / 2
+  const targetCenterY = Math.max(safeMiniPlayerHeight / 2, safeHeight - bottomMargin - safeMiniPlayerHeight / 2)
+  const scale = Math.max(0.08, Math.min(0.14, safeMiniPlayerHeight / safeHeight))
+  return {
+    translateX: 0,
+    translateY: targetCenterY - viewportCenterY,
+    scale,
+    targetCenterX: viewportCenterX,
+    targetCenterY
+  }
+}
+
+export function resolvePlayerDismissMorphTarget(viewportWidthVp: number, viewportHeightVp: number,
+  bottomSafeHeightVp: number, useFixedBottomMargin: boolean, miniPlayerHeightVp: number = 70,
+  fixedBottomMarginVp: number = 30, circleDiameterVp: number = 42, dotDiameterVp: number = 10): PlayerDismissMorphTarget {
+  const baseTarget = resolvePlayerDismissTarget(viewportWidthVp, viewportHeightVp, bottomSafeHeightVp, useFixedBottomMargin,
+    miniPlayerHeightVp, fixedBottomMarginVp)
+  const safeWidth = viewportWidthVp > 0 ? viewportWidthVp : 360
+  const safeHeight = viewportHeightVp > 0 ? viewportHeightVp : 800
+  const safeCircleDiameter = Math.max(dotDiameterVp + 8, circleDiameterVp)
+  const safeDotDiameter = Math.max(2, Math.min(dotDiameterVp, safeCircleDiameter))
+
+  return {
+    translateX: baseTarget.translateX,
+    translateY: baseTarget.translateY,
+    targetCenterX: baseTarget.targetCenterX,
+    targetCenterY: baseTarget.targetCenterY,
+    circleDiameter: safeCircleDiameter,
+    dotDiameter: safeDotDiameter,
+    circleScaleX: safeCircleDiameter / safeWidth,
+    circleScaleY: safeCircleDiameter / safeHeight,
+    dotScaleX: safeDotDiameter / safeWidth,
+    dotScaleY: safeDotDiameter / safeHeight,
+  }
+}
+
+export function resolveMiniPlayerMorphTarget(viewportWidthVp: number, miniPlayerWidthPercent: number = 0.9,
+  miniPlayerHeightVp: number = 70, circleDiameterVp: number = 48, dotDiameterVp: number = 10): MiniPlayerMorphTarget {
+  const safeWidth = viewportWidthVp > 0 ? viewportWidthVp : 360
+  const safeHeight = miniPlayerHeightVp > 0 ? miniPlayerHeightVp : 70
+  const safeWidthPercent = miniPlayerWidthPercent > 0 && miniPlayerWidthPercent <= 1 ? miniPlayerWidthPercent : 0.9
+  const safeBarWidth = safeWidth * safeWidthPercent
+  const safeCircleDiameter = Math.max(dotDiameterVp + 8, Math.min(circleDiameterVp, safeHeight))
+  const safeDotDiameter = Math.max(2, Math.min(dotDiameterVp, safeCircleDiameter))
+
+  return {
+    barWidth: safeBarWidth,
+    barHeight: safeHeight,
+    circleDiameter: safeCircleDiameter,
+    dotDiameter: safeDotDiameter,
+    circleScaleX: safeCircleDiameter / safeBarWidth,
+    circleScaleY: safeCircleDiameter / safeHeight,
+    dotScaleX: safeDotDiameter / safeBarWidth,
+    dotScaleY: safeDotDiameter / safeHeight,
+  }
+}
+
+export function resolveMiniPlayerRevealAnimationPlan(target: MiniPlayerMorphTarget, morphDurationMs: number,
+  frameDelayMs: number = 16): MiniPlayerRevealAnimationPlan {
+  const safeMorphDuration = morphDurationMs >= 0 ? morphDurationMs : 0
+  const safeFrameDelay = frameDelayMs >= 0 ? frameDelayMs : 16
+  const widthRatio = target.circleDiameter / Math.max(1, target.barWidth)
+  const heightRatio = target.circleDiameter / Math.max(1, target.barHeight)
+
+  return {
+    startScaleX: Math.max(0.9, 1 - widthRatio * 0.7),
+    startScaleY: Math.max(0.76, 1 - heightRatio * 0.35),
+    startOpacity: 0,
+    startBorderRadius: 200,
+    startProxySize: 0,
+    startProxyOpacity: 0,
+    shakeDelayMs: safeMorphDuration + safeFrameDelay,
+  }
+}

+ 680 - 142
entry/src/main/ets/pages/NewIndex.ets

@@ -59,9 +59,12 @@ import { ServerLogUtil } from '../common/util/ServerLogUtil';
 import { PlayStatus } from '../common/PlayStatus';
 import { PointLightDefaultButton } from '../view/PointLight/PointLightDeFaultButton';
 import { PointLightContentButton } from '../view/PointLight/PointLightContentButton';
+import { PlayingIndicator } from '../view/PlayingIndicator';
 import { FindView } from '../view/FindView';
 import { hdsEffect } from '@kit.UIDesignKit';
-import { resolveMiniPlayerMorphTarget } from '../common/util/PlayerDismissHelper';
+import {
+  resolveMiniPlayerMorphTarget,
+} from '../common/util/PlayerDismissHelper';
 
 // import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
 
@@ -88,13 +91,26 @@ struct NewIndex {
   @Provide CONTROL_PlayStatus: number = PlayStatus.INIT;
   @Provide progressValue: number = 0;
   @State isShowPrecious: boolean = false //播控条显示上一首按钮
-  @State bottomBarHeight: number = 70;
+  @State bottomBarHeight: number = 64;
+  // 迷你播放条的整体形变状态,既用于出现/消失,也用于 full bar <-> orb 切换。
   @State miniPlayerScaleX: number = 1;
   @State miniPlayerScaleY: number = 1;
   @State miniPlayerOpacity: number = 1;
   @State miniPlayerBorderRadius: number = 200;
   @State miniPlayerProxySize: number = 0;
   @State miniPlayerProxyOpacity: number = 0;
+  // 内容层和圆球层分开控制,保证收拢到右侧圆球时有独立位移/透明度/缩放。
+  @State miniPlayerContentScaleY: number = 1;
+  @State miniPlayerContentTranslateY: number = 0;
+  @State miniPlayerFullContentTranslateX: number = 0;
+  @State miniPlayerTranslateY: number = 0;
+  @State miniPlayerSurfaceWidth: number = 0;
+  @State miniPlayerSurfaceHeight: number = 0;
+  @State miniPlayerFullContentOpacity: number = 1;
+  @State miniPlayerOrbOpacity: number = 0;
+  @State miniPlayerOrbScale: number = 1;
+  @State miniPlayerOrbTranslateX: number = 0;
+  @State isMiniPlayerOrbMode: boolean = false;
   @State isMiniPlayerMounted: boolean = true;
   @StorageProp('bottomSafeHeight') bottomSafeHeight: number = 0;
   @Provide cover: string | undefined = '';
@@ -134,10 +150,10 @@ struct NewIndex {
   /** 本地音乐列表,持久化存储 */
   @StorageLink('musicLocalList') musicLocalList: Array<VideoItem> = []
   @Provide isFavMusic: boolean = false
-  @StorageProp('windowWidth') windowWidth: number = 0;
-  @StorageProp('windowHeight') windowHeight: number = 0;
+  @StorageProp('windowWidth') @Watch('onMiniPlayerLayoutContextChange') windowWidth: number = 0;
+  @StorageProp('windowHeight') @Watch('onMiniPlayerLayoutContextChange') windowHeight: number = 0;
   @StorageProp('isHiCarStatus') isHiCarStatus: boolean = false;
-  @StorageProp('curDisplayIsHiCar') curDisplayIsHiCar: boolean = false;
+  @StorageProp('curDisplayIsHiCar') @Watch('onMiniPlayerLayoutContextChange') curDisplayIsHiCar: boolean = false;
   /**
    * 是否显示更新日志开关
    */
@@ -161,7 +177,8 @@ struct NewIndex {
    */
   private breakpointSystem: BreakpointSystem = new BreakpointSystem();
   /** 当前断点类型(如大屏/小屏) */
-  @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
+  @StorageProp('currentBreakpoint') @Watch('onMiniPlayerLayoutContextChange') currentBreakpoint: string =
+    BreakpointTypeEnum.MD;
   /** 记录上一次点击返回键的时间戳,用于双击退出 */
   private backTime: number = 0;
   private isFindDefaultPromptShowing: boolean = false;
@@ -178,10 +195,22 @@ struct NewIndex {
     }
   })
   @State @Watch('tabSelectedIndexesChanged') tabSelectedIndexes: number[] = [0]
+  // 播放条动画拆成多个阶段,用定时器串起抖动、回弹和最终落位。
   private miniPlayerAnimationTimer: number = -1;
   private miniPlayerCleanupTimer: number = -1;
-  private readonly miniPlayerMorphDuration: number = 220;
-  private readonly miniPlayerDotDuration: number = 140;
+  private miniPlayerShakeTimer: number = -1;
+  private miniPlayerShakeSettleTimer: number = -1;
+  private miniPlayerModeAnimationTimer: number = -1;
+  private miniPlayerModeSettleTimer: number = -1;
+  private miniPlayerModeCompleteTimer: number = -1;
+  private readonly miniPlayerMorphDuration: number = 240;
+  private readonly miniPlayerDotDuration: number = 190;
+  private readonly miniPlayerShakeFirstDuration: number = 110;
+  private readonly miniPlayerShakeSecondDuration: number = 140;
+  private readonly miniPlayerShakeSettleDuration: number = 170;
+  private readonly miniPlayerModeDuration: number = 360;
+  private readonly miniPlayerModeBounceDuration: number = 220;
+  private isMiniPlayerModeTransitioning: boolean = false;
   //当胶囊按钮的选择发生变化时调用此函数
   tabSelectedIndexesChanged() {
     if(this.tabSelectedIndexes[0]==1){
@@ -593,38 +622,154 @@ struct NewIndex {
       clearTimeout(this.miniPlayerCleanupTimer)
       this.miniPlayerCleanupTimer = -1
     }
+    if (this.miniPlayerShakeTimer >= 0) {
+      clearTimeout(this.miniPlayerShakeTimer)
+      this.miniPlayerShakeTimer = -1
+    }
+    if (this.miniPlayerShakeSettleTimer >= 0) {
+      clearTimeout(this.miniPlayerShakeSettleTimer)
+      this.miniPlayerShakeSettleTimer = -1
+    }
+    if (this.miniPlayerModeAnimationTimer >= 0) {
+      clearTimeout(this.miniPlayerModeAnimationTimer)
+      this.miniPlayerModeAnimationTimer = -1
+    }
+    if (this.miniPlayerModeSettleTimer >= 0) {
+      clearTimeout(this.miniPlayerModeSettleTimer)
+      this.miniPlayerModeSettleTimer = -1
+    }
+    if (this.miniPlayerModeCompleteTimer >= 0) {
+      clearTimeout(this.miniPlayerModeCompleteTimer)
+      this.miniPlayerModeCompleteTimer = -1
+    }
+    this.isMiniPlayerModeTransitioning = false
+  }
+
+  // 窗口尺寸、断点或显示设备切换时,重新同步播放条表面尺寸。
+  // 这样从手机态切到 HiCar 横屏后,surface 会立即按当前显示环境重新拉伸。
+  private onMiniPlayerLayoutContextChange(): void {
+    if (this.isMiniPlayerModeTransitioning) {
+      return
+    }
+    this.syncMiniPlayerVisibility(false)
   }
 
+  // 解析当前屏宽下播放条完整宽度,为 full/orb 切换提供统一目标值。
   private resolveMiniPlayerMorphMetrics() {
     const viewportWidthVp = this.windowWidth > 0 ? px2vp(this.windowWidth) : 360
     const miniPlayerHeightVp = this.bottomBarHeight > 0 ? this.bottomBarHeight : 70
     return resolveMiniPlayerMorphTarget(viewportWidthVp, 0.9, miniPlayerHeightVp)
   }
 
+  // orb 直径跟随播放条高度,但限制在较稳定的范围内,避免不同设备上忽大忽小。
+  private resolveMiniPlayerOrbSize(): number {
+    const barHeightVp = this.bottomBarHeight > 0 ? this.bottomBarHeight : 70
+    return Math.max(54, Math.min(60, barHeightVp - 8))
+  }
+
+  private resolveMiniPlayerSurfaceTargetWidth(): number {
+    return this.isMiniPlayerOrbMode ? this.resolveMiniPlayerOrbSize() : this.resolveMiniPlayerMorphMetrics().barWidth
+  }
+
+  private resolveMiniPlayerSurfaceTargetHeight(): number {
+    return this.isMiniPlayerOrbMode ? this.resolveMiniPlayerOrbSize() : this.bottomBarHeight
+  }
+
+  private resolveMiniPlayerSurfaceTargetRadius(): number {
+    return this.isMiniPlayerOrbMode ? this.resolveMiniPlayerOrbSize() / 2 : 200
+  }
+
+  private applyMiniPlayerPresentationState(): void {
+    this.miniPlayerSurfaceWidth = this.resolveMiniPlayerSurfaceTargetWidth()
+    this.miniPlayerSurfaceHeight = this.resolveMiniPlayerSurfaceTargetHeight()
+    this.miniPlayerBorderRadius = this.resolveMiniPlayerSurfaceTargetRadius()
+    this.miniPlayerFullContentOpacity = this.isMiniPlayerOrbMode ? 0 : 1
+    this.miniPlayerOrbOpacity = this.isMiniPlayerOrbMode ? 1 : 0
+  }
+
+  // 重置到稳定停留态,避免中途中断动画后状态残留。
   private resetMiniPlayerAnimationState(): void {
     this.miniPlayerScaleX = 1
     this.miniPlayerScaleY = 1
     this.miniPlayerOpacity = 1
-    this.miniPlayerBorderRadius = 200
     this.miniPlayerProxySize = 0
     this.miniPlayerProxyOpacity = 0
+    this.miniPlayerContentScaleY = 1
+    this.miniPlayerContentTranslateY = 0
+    this.miniPlayerFullContentTranslateX = 0
+    this.miniPlayerTranslateY = 0
+    this.miniPlayerOrbScale = 1
+    this.miniPlayerOrbTranslateX = 0
+    this.applyMiniPlayerPresentationState()
+  }
+
+  // 播放条重新出现时做一次上下抖动,带动内部封面/文字/按钮一起轻微回弹。
+  private startMiniPlayerRevealShake(): void {
+    this.miniPlayerContentScaleY = 1
+    this.miniPlayerContentTranslateY = 0
+    this.getUIContext()?.animateTo({
+      duration: this.miniPlayerShakeFirstDuration,
+      curve: Curve.Sharp
+    }, () => {
+      this.miniPlayerScaleX = 1.012
+      this.miniPlayerScaleY = 0.9
+      this.miniPlayerContentScaleY = 0.965
+      this.miniPlayerContentTranslateY = 1.2
+    })
+    this.miniPlayerShakeTimer = setTimeout(() => {
+      this.miniPlayerShakeTimer = -1
+      this.getUIContext()?.animateTo({
+        duration: this.miniPlayerShakeSecondDuration,
+        curve: Curve.Friction
+      }, () => {
+        this.miniPlayerScaleX = 0.996
+        this.miniPlayerScaleY = 1.036
+        this.miniPlayerContentScaleY = 1.018
+        this.miniPlayerContentTranslateY = -0.8
+      })
+    }, this.miniPlayerShakeFirstDuration + 16)
+    this.miniPlayerShakeSettleTimer = setTimeout(() => {
+      this.miniPlayerShakeSettleTimer = -1
+      this.getUIContext()?.animateTo({
+        duration: this.miniPlayerShakeSettleDuration,
+        curve: Curve.Friction
+      }, () => {
+        this.miniPlayerScaleX = 1
+        this.miniPlayerScaleY = 1
+        this.miniPlayerContentScaleY = 1
+        this.miniPlayerContentTranslateY = 0
+      })
+    }, this.miniPlayerShakeFirstDuration + this.miniPlayerShakeSecondDuration + 32)
   }
 
   private isMiniPlayerFullyVisible(): boolean {
+    const targetWidth = this.resolveMiniPlayerSurfaceTargetWidth()
+    const targetRadius = this.resolveMiniPlayerSurfaceTargetRadius()
+    const targetFullContentOpacity = this.isMiniPlayerOrbMode ? 0 : 1
+    const targetOrbOpacity = this.isMiniPlayerOrbMode ? 1 : 0
     return this.isMiniPlayerMounted
       && this.miniPlayerOpacity >= 0.99
       && this.miniPlayerScaleX >= 0.999
       && this.miniPlayerScaleY >= 0.999
       && this.miniPlayerProxySize <= 0
+      && Math.abs(this.miniPlayerSurfaceWidth - targetWidth) < 1
+      && Math.abs(this.miniPlayerSurfaceHeight - this.resolveMiniPlayerSurfaceTargetHeight()) < 1
+      && Math.abs(this.miniPlayerBorderRadius - targetRadius) < 1
+      && Math.abs(this.miniPlayerFullContentOpacity - targetFullContentOpacity) < 0.05
+      && Math.abs(this.miniPlayerOrbOpacity - targetOrbOpacity) < 0.05
+      && Math.abs(this.miniPlayerContentScaleY - 1) < 0.01
+      && Math.abs(this.miniPlayerContentTranslateY) < 0.2
+      && Math.abs(this.miniPlayerTranslateY) < 1
   }
 
   private shouldShowMiniPlayer(): boolean {
     return !(this.isMultiSelect || (this.mType > 0 && this.mType < 6) || this.isShowPlay)
   }
 
+  // 同步播放条挂载与显示状态。
+  // show: 直接挂载后做抖动;hide: 向下轻压并淡出。
   private syncMiniPlayerVisibility(animated: boolean = true): void {
     const shouldShow = this.shouldShowMiniPlayer()
-    const target = this.resolveMiniPlayerMorphMetrics()
     this.clearMiniPlayerAnimationTimer()
     if (!animated) {
       this.isMiniPlayerMounted = shouldShow
@@ -635,76 +780,177 @@ struct NewIndex {
       if (this.isMiniPlayerFullyVisible()) {
         return
       }
-      if (!this.isMiniPlayerMounted) {
-        this.isMiniPlayerMounted = true
-        this.miniPlayerScaleX = target.circleScaleX
-        this.miniPlayerScaleY = target.circleScaleY
-        this.miniPlayerOpacity = 0
-        this.miniPlayerBorderRadius = target.circleDiameter / 2
-        this.miniPlayerProxySize = target.dotDiameter
-        this.miniPlayerProxyOpacity = 1
-      } else if (this.miniPlayerProxySize <= 0) {
-        this.miniPlayerProxySize = target.circleDiameter
-        this.miniPlayerProxyOpacity = 1
+      this.isMiniPlayerMounted = true
+      this.resetMiniPlayerAnimationState()
+      this.miniPlayerAnimationTimer = setTimeout(() => {
+        if (!this.shouldShowMiniPlayer() || !this.isMiniPlayerMounted) {
+          this.miniPlayerAnimationTimer = -1
+          return
+        }
+        this.miniPlayerAnimationTimer = -1
+        this.startMiniPlayerRevealShake()
+      }, 16)
+      return
+    }
+    if (!this.isMiniPlayerMounted) {
+      this.resetMiniPlayerAnimationState()
+      return
+    }
+    this.getUIContext()?.animateTo({
+      duration: this.miniPlayerDotDuration,
+      curve: Curve.Ease
+    }, () => {
+      this.miniPlayerScaleX = 0.96
+      this.miniPlayerScaleY = 0.88
+      this.miniPlayerOpacity = 0
+      this.miniPlayerBorderRadius = 200
+      this.miniPlayerProxySize = 0
+      this.miniPlayerProxyOpacity = 0
+      this.miniPlayerContentScaleY = 0.96
+      this.miniPlayerContentTranslateY = 0.8
+      this.miniPlayerTranslateY = 18
+    })
+    this.miniPlayerCleanupTimer = setTimeout(() => {
+      this.miniPlayerCleanupTimer = -1
+      if (!this.shouldShowMiniPlayer()) {
+        this.isMiniPlayerMounted = false
+      }
+      this.resetMiniPlayerAnimationState()
+    }, this.miniPlayerDotDuration + 16)
+  }
+
+  // 点击左侧封面后,把完整播放条收拢成右侧圆球,并在右侧做一次左右回弹。
+  private collapseMiniPlayerToOrb(): void {
+    if (this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted || this.isMiniPlayerModeTransitioning) {
+      return
+    }
+    const fullWidth = this.resolveMiniPlayerMorphMetrics().barWidth
+    const orbSize = this.resolveMiniPlayerOrbSize()
+    const collapseOvershootWidth = Math.min(fullWidth, orbSize + 10)
+    this.clearMiniPlayerAnimationTimer()
+    this.isMiniPlayerModeTransitioning = true
+    this.miniPlayerScaleX = 1
+    this.miniPlayerScaleY = 1
+    this.miniPlayerOpacity = 1
+    this.miniPlayerContentScaleY = 1
+    this.miniPlayerContentTranslateY = 0
+    this.miniPlayerFullContentTranslateX = 0
+    this.miniPlayerTranslateY = 0
+    this.miniPlayerOrbScale = 0.82
+    this.miniPlayerOrbTranslateX = 10
+    this.isMiniPlayerOrbMode = true
+    this.miniPlayerFullContentOpacity = 1
+    this.miniPlayerOrbOpacity = 0
+    this.getUIContext()?.animateTo({
+      duration: this.miniPlayerModeDuration,
+      curve: Curve.Sharp
+    }, () => {
+      this.miniPlayerSurfaceWidth = orbSize
+      this.miniPlayerSurfaceHeight = orbSize
+      this.miniPlayerBorderRadius = orbSize / 2
+      this.miniPlayerFullContentOpacity = 0
+      this.miniPlayerFullContentTranslateX = 24
+      this.miniPlayerOrbOpacity = 1
+      this.miniPlayerOrbScale = 1.06
+      this.miniPlayerOrbTranslateX = 0
+    })
+    this.miniPlayerModeAnimationTimer = setTimeout(() => {
+      this.miniPlayerModeAnimationTimer = -1
+      if (!this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted) {
+        return
       }
       this.getUIContext()?.animateTo({
-        duration: this.miniPlayerDotDuration,
-        curve: Curve.Sharp
+        duration: this.miniPlayerModeBounceDuration,
+        curve: Curve.Friction
       }, () => {
-        this.miniPlayerProxySize = target.circleDiameter
-        this.miniPlayerProxyOpacity = 1
+        this.miniPlayerSurfaceWidth = collapseOvershootWidth
+        this.miniPlayerBorderRadius = collapseOvershootWidth / 2
+        this.miniPlayerOrbScale = 0.97
       })
-      this.miniPlayerAnimationTimer = setTimeout(() => {
-        this.miniPlayerAnimationTimer = -1
+      this.miniPlayerModeSettleTimer = setTimeout(() => {
+        this.miniPlayerModeSettleTimer = -1
+        if (!this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted) {
+          return
+        }
         this.getUIContext()?.animateTo({
-          duration: this.miniPlayerMorphDuration,
+          duration: this.miniPlayerModeBounceDuration,
           curve: Curve.Friction
         }, () => {
-          this.miniPlayerScaleX = 1
-          this.miniPlayerScaleY = 1
-          this.miniPlayerOpacity = 1
-          this.miniPlayerBorderRadius = 200
-          this.miniPlayerProxySize = target.circleDiameter
-          this.miniPlayerProxyOpacity = 0
+          this.miniPlayerSurfaceWidth = orbSize
+          this.miniPlayerBorderRadius = orbSize / 2
+          this.miniPlayerOrbScale = 1
+          this.miniPlayerFullContentTranslateX = 0
         })
-      }, this.miniPlayerDotDuration + 16)
-      return
-    }
-    if (!this.isMiniPlayerMounted) {
-      this.resetMiniPlayerAnimationState()
+      }, this.miniPlayerModeBounceDuration + 16)
+      this.miniPlayerModeCompleteTimer = setTimeout(() => {
+        this.miniPlayerModeCompleteTimer = -1
+        this.isMiniPlayerModeTransitioning = false
+        this.miniPlayerOrbScale = 1
+        this.miniPlayerOrbTranslateX = 0
+        this.miniPlayerFullContentTranslateX = 0
+      }, this.miniPlayerModeBounceDuration * 2 + 32)
+    }, this.miniPlayerModeDuration + 16)
+  }
+
+  // 再次点击右侧圆球时,从右向左展开回完整播放条,并保留回弹的收尾。
+  private expandMiniPlayerFromOrb(): void {
+    if (!this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted || this.isMiniPlayerModeTransitioning) {
       return
     }
+    const fullWidth = this.resolveMiniPlayerMorphMetrics().barWidth
+    const orbSize = this.resolveMiniPlayerOrbSize()
+    const expandOvershootWidth = fullWidth + 16
+    this.clearMiniPlayerAnimationTimer()
+    this.isMiniPlayerModeTransitioning = true
+    this.miniPlayerScaleX = 1
+    this.miniPlayerScaleY = 1
+    this.miniPlayerOpacity = 1
+    this.miniPlayerContentScaleY = 1
+    this.miniPlayerContentTranslateY = 0
+    this.miniPlayerFullContentTranslateX = 24
+    this.miniPlayerTranslateY = 0
+    this.miniPlayerOrbScale = 1
+    this.miniPlayerOrbTranslateX = 0
+    this.isMiniPlayerOrbMode = false
+    this.miniPlayerSurfaceWidth = orbSize
+    this.miniPlayerSurfaceHeight = orbSize
+    this.miniPlayerBorderRadius = orbSize / 2
+    this.miniPlayerFullContentOpacity = 0
+    this.miniPlayerOrbOpacity = 1
     this.getUIContext()?.animateTo({
-      duration: this.miniPlayerMorphDuration,
+      duration: this.miniPlayerModeDuration,
       curve: Curve.Friction
     }, () => {
-      this.miniPlayerScaleX = target.circleScaleX
-      this.miniPlayerScaleY = target.circleScaleY
-      this.miniPlayerOpacity = 0
-      this.miniPlayerBorderRadius = target.circleDiameter / 2
-      this.miniPlayerProxySize = target.circleDiameter
-      this.miniPlayerProxyOpacity = 1
+      this.miniPlayerSurfaceWidth = expandOvershootWidth
+      this.miniPlayerSurfaceHeight = this.bottomBarHeight
+      this.miniPlayerBorderRadius = 200
+      this.miniPlayerFullContentOpacity = 1
+      this.miniPlayerFullContentTranslateX = -10
+      this.miniPlayerOrbOpacity = 0
+      this.miniPlayerOrbScale = 0.82
+      this.miniPlayerOrbTranslateX = 12
     })
-    this.miniPlayerAnimationTimer = setTimeout(() => {
-      this.miniPlayerAnimationTimer = -1
-      this.miniPlayerOpacity = 0
-      this.miniPlayerProxySize = target.circleDiameter
-      this.miniPlayerProxyOpacity = 1
+    this.miniPlayerModeAnimationTimer = setTimeout(() => {
+      this.miniPlayerModeAnimationTimer = -1
+      if (this.isMiniPlayerOrbMode || !this.isMiniPlayerMounted) {
+        return
+      }
       this.getUIContext()?.animateTo({
-        duration: this.miniPlayerDotDuration,
-        curve: Curve.EaseIn
+        duration: this.miniPlayerModeBounceDuration,
+        curve: Curve.Friction
       }, () => {
-        this.miniPlayerProxySize = target.dotDiameter
-        this.miniPlayerProxyOpacity = 0
+        this.miniPlayerSurfaceWidth = fullWidth
+        this.miniPlayerBorderRadius = 200
+        this.miniPlayerFullContentTranslateX = 0
       })
-      this.miniPlayerCleanupTimer = setTimeout(() => {
-        this.miniPlayerCleanupTimer = -1
-        if (!this.shouldShowMiniPlayer()) {
-          this.isMiniPlayerMounted = false
-        }
-        this.resetMiniPlayerAnimationState()
-      }, this.miniPlayerDotDuration + 16)
-    }, this.miniPlayerMorphDuration + 16)
+      this.miniPlayerModeCompleteTimer = setTimeout(() => {
+        this.miniPlayerModeCompleteTimer = -1
+        this.isMiniPlayerModeTransitioning = false
+        this.miniPlayerOrbScale = 1
+        this.miniPlayerOrbTranslateX = 0
+        this.miniPlayerFullContentTranslateX = 0
+      }, this.miniPlayerModeBounceDuration + 16)
+    }, this.miniPlayerModeDuration + 16)
   }
 
   @Builder
@@ -806,6 +1052,61 @@ struct NewIndex {
 
   @State private pointLightOptions: hdsEffect.PointLightOptions | undefined = undefined
 
+  // 迷你播放条挂载后统一走这个 Builder,避免 build() 里堆叠过多手机/HiCar/orb 分支。
+  @Builder
+  private MiniPlayerBarBuilder() {
+    Row() {
+      Stack() {
+        // 完整播放条层在收拢时向右退出,展开时再从右侧拉回。
+        if (!this.isMiniPlayerOrbMode || this.miniPlayerFullContentOpacity > 0.02) {
+          Stack() {
+            if (this.curDisplayIsHiCar) {
+              this.HiCarPlayController()
+            } else {
+              this.PlayController()
+            }
+          }
+          .width('100%')
+          .height('100%')
+          .opacity(this.miniPlayerFullContentOpacity)
+          .translate({ x: this.miniPlayerFullContentTranslateX })
+        }
+
+        // orb 层固定贴右侧,负责接住收拢后的封面并作为展开入口。
+        if (this.isMiniPlayerOrbMode || this.miniPlayerOrbOpacity > 0.02) {
+          Row() {
+            this.MiniPlayerOrbControl()
+          }
+          .width('100%')
+          .height('100%')
+          .justifyContent(FlexAlign.End)
+          .alignItems(VerticalAlign.Center)
+        }
+
+        if (this.miniPlayerProxySize > 0) {
+          this.MiniPlayerMorphProxyDot()
+        }
+      }
+      .width(this.miniPlayerSurfaceWidth)
+      .height(this.miniPlayerSurfaceHeight)
+      .borderRadius(this.miniPlayerBorderRadius)
+      .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
+      .backgroundImage(StrUtil.isEmpty(this.cover) ? $r('app.media.alt') : this.cover)
+      .backgroundImageSize({ width: '100%' })
+      .scale({ x: this.miniPlayerScaleX, y: this.miniPlayerScaleY, centerX: '50%', centerY: '50%' })
+      .opacity(Math.min(0.99, this.miniPlayerOpacity))
+      .clip(true)
+      .clickEffect({ level: ClickEffectLevel.HEAVY })
+    }
+    .width('90%')
+    .height(this.bottomBarHeight)
+    .justifyContent(FlexAlign.End)
+    .alignItems(VerticalAlign.Center)
+    .translate({ y: this.miniPlayerTranslateY })
+    .margin({ bottom: DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_2IN1 || this.curDisplayIsHiCar
+      ? 30 : this.bottomSafeHeight })
+  }
+
   build() {
     SideBarContainer(SideBarContainerType.AUTO) {
       Column() {
@@ -815,30 +1116,7 @@ struct NewIndex {
       Stack() {
         this.ContentBuild()
         if (this.isMiniPlayerMounted) {
-          Stack({ alignContent: Alignment.Center }) {
-            Stack() {
-              this.PlayController()
-            }
-            .width('100%')
-            .height('100%')
-            .borderRadius(this.miniPlayerBorderRadius)
-            .backgroundBlurStyle(BlurStyle.BACKGROUND_ULTRA_THICK)
-            .backgroundImage(StrUtil.isEmpty(this.cover) ?$r('app.media.alt'):this.cover)
-            .backgroundImageSize( {  width: '100%' })
-            .scale({ x: this.miniPlayerScaleX, y: this.miniPlayerScaleY, centerX: '50%', centerY: '50%' })
-            .opacity(Math.min(0.99, this.miniPlayerOpacity))
-            .clip(true)
-            .clickEffect({ level: ClickEffectLevel.HEAVY })
-
-            if (this.miniPlayerProxySize > 0) {
-              this.MiniPlayerMorphProxyDot()
-            }
-          }
-          .width('90%')
-          .height(this.bottomBarHeight)
-          .borderRadius(200)
-          .margin({ bottom:DeviceUtil.getDeviceType() === resourceManager.DeviceType.DEVICE_TYPE_2IN1||this.curDisplayIsHiCar
-            ? 30 :this.bottomSafeHeight })
+          this.MiniPlayerBarBuilder()
         }
       }
       .alignContent(Alignment.Bottom)
@@ -891,34 +1169,88 @@ struct NewIndex {
   }
 
   setShowPlayTrue(){
-    if (this.isShowPlay) {
+    if (this.isShowPlay || this.isMiniPlayerModeTransitioning) {
       return
     }
     this.getUIContext().getHostContext()!.eventHub.emit('openPlayerViewFromMiniBar');
   }
 
+  // 右侧圆球内部内容:封面、暗罩、白色环形进度和中心播放状态指示。
   @Builder
-  PlayController() {
-    Row() {
-      this.playConLeft(!this.curDisplayIsHiCar)
-      if(!this.curDisplayIsHiCar){
-        this.playConRigth()
-      }
-      if(this.curDisplayIsHiCar){
-        Column(){
-          this.centerName()
-        }
-        .layoutWeight(1)
-        .onClick(()=>{
-          this.setShowPlayTrue()
+  private buildMiniPlayerOrbButtonContent(orbSize: number) {
+    Stack({ alignContent: Alignment.Center }) {
+      Image(StrUtil.isNotEmpty(this.cover) ? this.cover as string : $r('app.media.alt'))
+        .width('100%')
+        .height('100%')
+        .objectFit(ImageFit.Cover)
+        .alt($r('app.media.alt'))
+        .borderRadius(100)
+      Column()
+        .width('100%')
+        .height('100%')
+        .backgroundColor('#66000000')
+
+      Progress({
+        value: Math.floor(this.progressValue),
+        total: 100,
+        type: ProgressType.Ring,
+      })
+        .color(Color.White)
+        .width('100%')
+        .height('100%')
+        .style({ strokeWidth: 3 })
+
+      if (this.CONTROL_PlayStatus === PlayStatus.PLAY) {
+        PlayingIndicator({
+          isActive: true,
+          indicatorSize: Math.max(16, Math.floor(orbSize * 0.34)),
+          indicatorColor: '#FFFFFF'
         })
       }
     }
     .width('100%')
+    .height('100%')
+    .clip(true)
+    .borderRadius(100)
+  }
+
+  // 右侧圆球外层按钮,负责点光、缩放、位移和点击展开。
+  @Builder
+  private MiniPlayerOrbControl() {
+    PointLightContentButton({
+      pointColor: this.themeColor,
+      buttonRadius: this.resolveMiniPlayerOrbSize() / 2,
+      pointLightHeight: 96,
+      pressScale: 0.94,
+      useShadow: true,
+      builder: (): void => {
+        this.buildMiniPlayerOrbButtonContent(this.resolveMiniPlayerOrbSize())
+      }
+    })
+      .width(this.resolveMiniPlayerOrbSize())
+      .height(this.resolveMiniPlayerOrbSize())
+      .opacity(this.miniPlayerOrbOpacity)
+      .borderRadius(this.resolveMiniPlayerOrbSize() / 2)
+      .scale({ x: this.miniPlayerOrbScale, y: this.miniPlayerOrbScale, centerX: '50%', centerY: '50%' })
+      .translate({ x: this.miniPlayerOrbTranslateX })
+      .onClick((): void => {
+        this.expandMiniPlayerFromOrb()
+      })
+  }
+
+  @Builder
+  PlayController() {
+    Row() {
+      this.playConLeft()
+      this.playConRigth()
+    }
+    .width('100%')
     .height(this.bottomBarHeight)
+    .scale({ x: 1, y: this.miniPlayerContentScaleY, centerX: '50%', centerY: '50%' })
+    .translate({ y: this.miniPlayerContentTranslateY })
     .hitTestBehavior(HitTestMode.Transparent)
     .zIndex(2)
-    .onTouch((event: TouchEvent) => {
+    .onTouch((event: TouchEvent): void => {
       if (event.type === TouchType.Down) {
         this.pointLightOptions = {
           color: this.themeColor,
@@ -945,59 +1277,169 @@ struct NewIndex {
 
   }
   @Builder
-  playConLeft(isLeft: boolean = true) {
+  private HiCarPlayController() {
+    Stack({ alignContent: Alignment.Center }) {
+      this.hiCarLeadingCluster()
+      this.hiCarCenterInfo()
+    }
+    .width('100%')
+    .height(this.bottomBarHeight)
+    .hitTestBehavior(HitTestMode.Transparent)
+    .zIndex(2)
+    .onTouch((event: TouchEvent): void => {
+      if (event.type === TouchType.Down) {
+        this.pointLightOptions = {
+          color: this.themeColor,
+          intensity: 1,
+          height: 60
+        }
+      } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
+        this.pointLightOptions = undefined
+      }
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.pointLightOptions,
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
+    .padding({
+      left: 16,
+      right: 16
+    })
+  }
+
+  @Builder
+  private hiCarLeadingCluster() {
+    Row() {
+      this.hiCarCoverControl()
+      this.hiCarPlayConRigth()
+      Row()
+        .layoutWeight(1)
+    }
+    .width('100%')
+    .height('100%')
+    .justifyContent(FlexAlign.Start)
+    .alignItems(VerticalAlign.Center)
+  }
+
+  @Builder
+  playConLeft() {
     Row() {
-      Image( StrUtil.isNotEmpty(this.currentSong?.pixelMapPath)?
+      Stack({ alignContent: Alignment.Center }) {
+        Image( StrUtil.isNotEmpty(this.currentSong?.pixelMapPath)?
+          this.currentSong?.pixelMapPath:$r('app.media.alt'))
+          .width(48)
+          .height(48)
+          .objectFit(ImageFit.Contain)
+          .alt( $r('app.media.alt'))
+          .fillColor(this.themeColor)
+          .borderRadius(8)
+          .shadow({
+            radius: 15,
+            type: ShadowType.BLUR,
+            color: 'on_primary'
+          })
+      }
+      .width(48)
+      .height(48)
+      .margin({ left: 5 })
+      .zIndex(3)
+      .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.9 })
+      .onClick((): void => {
+        this.collapseMiniPlayerToOrb()
+      })
+
+      Column() {
+        Text(this.currentSong?.name)
+          .fontSize(16)
+          .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
+          .maxLines(1)
+          .fontColor(Color.White)
+          .fontWeight(FontWeight.Bolder);
+        Row() {
+          Text(this.currentSong?.artist)
+            .margin({ top: 2 })
+            .fontSize(13)
+            .textAlign(TextAlign.Start)
+            .maxLines(1)
+            .fontWeight(FontWeight.Bold)
+            .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
+            .fontColor(Color.White)
+        }
+        .visibility(this.currentSong?.artist? Visibility.Visible:Visibility.None)
+      }
+      .padding({left:8})
+      .alignItems(HorizontalAlign.Start)
+      .margin({ right: 22 })
+      .onClick((): void => {
+        this.setShowPlayTrue()
+      })
+
+    }
+    .padding({ right: 18 })
+    .layoutWeight(1)
+    .alignItems(VerticalAlign.Center)
+    .justifyContent(FlexAlign.Start)
+
+  }
+
+  @Builder
+  private hiCarCoverControl() {
+    Stack({ alignContent: Alignment.Center }) {
+      Image(StrUtil.isNotEmpty(this.currentSong?.pixelMapPath)?
         this.currentSong?.pixelMapPath:$r('app.media.alt'))
+        .width(48)
         .height(48)
         .objectFit(ImageFit.Contain)
-        .alt( $r('app.media.alt'))
+        .alt($r('app.media.alt'))
         .fillColor(this.themeColor)
-        .margin({left:5})
         .borderRadius(8)
         .shadow({
           radius: 15,
           type: ShadowType.BLUR,
           color: 'on_primary'
         })
-        // .geometryTransition('cover', { follow: true }) // 绑定标识符
-      if(!this.curDisplayIsHiCar){
-        Column() {
-          Text(this.currentSong?.name)
-            .fontSize(16)
-            .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
-            .maxLines(1)
-            .fontColor(Color.White)
-            .fontWeight(FontWeight.Bolder);
-          Row() {
-            Text(this.currentSong?.artist)
-              .margin({ top: 2 })
-              .fontSize(13)
-              .textAlign(TextAlign.Start)
-              .maxLines(1)
-              .fontWeight(FontWeight.Bold)
-              .textOverflow({ overflow: TextOverflow.MARQUEE }) //超长滚动
-              .fontColor(Color.White)
-          }
-          .visibility(this.currentSong?.artist? Visibility.Visible:Visibility.None)
-        }
-        .padding({left:8})
-        .alignItems(isLeft?HorizontalAlign.Start:HorizontalAlign.End)
-        .margin({ right: isLeft?22:10 })
-      }else{
-        this.playConRigth()
-      }
+    }
+    .width(48)
+    .height(48)
+    .margin({ left: 5 })
+    .zIndex(3)
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.9 })
+    .onClick((): void => {
+      this.collapseMiniPlayerToOrb()
+    })
+  }
 
+  @Builder
+  private hiCarCenterInfo() {
+    Column() {
+      Text(this.currentSong?.name)
+        .fontSize(16)
+        .textOverflow({ overflow: TextOverflow.MARQUEE })
+        .maxLines(1)
+        .textAlign(TextAlign.Center)
+        .fontColor(Color.White)
+        .fontWeight(FontWeight.Bolder)
+      Row() {
+        Text(this.currentSong?.artist)
+          .margin({ top: 2 })
+          .fontSize(13)
+          .textAlign(TextAlign.Center)
+          .maxLines(1)
+          .fontWeight(FontWeight.Bold)
+          .textOverflow({ overflow: TextOverflow.MARQUEE })
+          .fontColor(Color.White)
+      }
+      .visibility(this.currentSong?.artist ? Visibility.Visible : Visibility.None)
     }
-    .padding({ right: 18 })
-    .layoutWeight(1)
-    .alignItems(VerticalAlign.Center)
-    .justifyContent(FlexAlign.Start)
-    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.8 })
-    .onClick(() => {
+    .width('42%')
+    .alignItems(HorizontalAlign.Center)
+    .onClick((): void => {
       this.setShowPlayTrue()
     })
-
   }
 
 
@@ -1080,6 +1522,9 @@ struct NewIndex {
         .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
         .displayPriority(2)
         .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
           this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
         })
       // 播放进度条与播放键共用一个点光点击区域
@@ -1096,6 +1541,93 @@ struct NewIndex {
         .height(46)
         .displayPriority(3)
         .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
+          this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
+        })
+
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 22,
+        pointLightHeight: 56,
+        pressScale: 0.88,
+        builder: () => {
+          this.buildPlayNextControlContent()
+        }
+      })
+        .width(44)
+        .height(44)
+        .displayPriority(2)
+        .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
+          this.getUIContext().getHostContext()!.eventHub.emit('playNext');
+        })
+
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 21,
+        pointLightHeight: 52,
+        pressScale: 0.88,
+        builder: () => {
+          this.buildPlayListControlContent()
+        }
+      })
+        .width(42)
+        .height(42)
+        .displayPriority(1)
+        .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
+          this.getUIContext().getHostContext()!.eventHub.emit('openPlayList');
+        })
+    }
+    .margin({left:5})
+    .justifyContent(FlexAlign.End)
+    .alignItems(VerticalAlign.Center)
+  }
+
+  @Builder
+  private hiCarPlayConRigth() {
+    Row() {
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 22,
+        pointLightHeight: 56,
+        pressScale: 0.88,
+        builder: () => {
+          this.buildPlayPreviousControlContent()
+        }
+      })
+        .width(44)
+        .height(44)
+        .visibility(this.isShowPrecious ? Visibility.Visible : Visibility.None)
+        .displayPriority(2)
+        .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
+          this.getUIContext().getHostContext()!.eventHub.emit('playPrevious');
+        })
+      PointLightContentButton({
+        pointColor: this.themeColor,
+        buttonRadius: 23,
+        pointLightHeight: 62,
+        pressScale: 0.9,
+        builder: () => {
+          this.buildPlayToggleControlContent()
+        }
+      })
+        .width(46)
+        .height(46)
+        .displayPriority(3)
+        .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
           this.getUIContext().getHostContext()!.eventHub.emit('playOrPause');
         })
 
@@ -1112,6 +1644,9 @@ struct NewIndex {
         .height(44)
         .displayPriority(2)
         .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
           this.getUIContext().getHostContext()!.eventHub.emit('playNext');
         })
 
@@ -1128,11 +1663,14 @@ struct NewIndex {
         .height(42)
         .displayPriority(1)
         .onClick(() => {
+          if (this.isMiniPlayerModeTransitioning) {
+            return
+          }
           this.getUIContext().getHostContext()!.eventHub.emit('openPlayList');
         })
     }
-    .margin({left:this.curDisplayIsHiCar?20:5})
-    .justifyContent(this.curDisplayIsHiCar?FlexAlign.Start:FlexAlign.End)
+    .margin({ left: 16 })
+    .justifyContent(FlexAlign.Start)
     .alignItems(VerticalAlign.Center)
   }
 

+ 38 - 71
entry/src/main/ets/view/LocalMusic.ets

@@ -793,7 +793,7 @@ export struct LocalMusic {
   private playOpenFromMiniBarPending: boolean = false;
   private playOpenTimer: number = -1;
   private readonly playOpenDotDuration: number = 120;
-  private readonly playOpenExpandDuration: number = 300;
+  private readonly playOpenExpandDuration: number = 320;
   @StorageLink('currIndex') curIndex: number = 0;
   @Consume isShowDrawer: boolean;
   @Consume offsetX: number;
@@ -9047,51 +9047,39 @@ export struct LocalMusic {
     const target = this.resolveMiniBarDismissTarget()
     this.playOpenFromMiniBarPending = true
     this.translateY = target.translateY
-    this.playDismissTranslateX = target.translateX
-    this.playDismissScale = target.circleScaleX
-    this.playDismissScaleY = target.circleScaleY
+    this.playDismissTranslateX = 0
+    this.playDismissScale = 0.9
+    this.playDismissScaleY = 0.8
     this.playDismissOpacity = 0
-    this.playDismissBorderRadius = target.circleDiameter / 2
-    this.playDismissProxySize = target.dotDiameter
-    this.playDismissProxyOpacity = 1
+    this.playDismissBorderRadius = 48
+    this.playDismissProxySize = 0
+    this.playDismissProxyOpacity = 0
     this.playDragUiOpacity = 0
-    this.scaleValueImage = Math.max(0.72, target.circleScaleX)
-    this.scaleValueText = Math.max(0.88, target.circleScaleY)
+    this.scaleValueImage = 0.82
+    this.scaleValueText = 0.88
   }
 
   private startPlayerViewOpenAnimationIfNeeded(): void {
     if (!this.playOpenFromMiniBarPending) {
       return
     }
-    const target = this.resolveMiniBarDismissTarget()
     this.playOpenFromMiniBarPending = false
     this.getUIContext()?.animateTo({
-      duration: this.playOpenDotDuration,
-      curve: Curve.Sharp
+      duration: this.playOpenExpandDuration,
+      curve: Curve.Friction
     }, () => {
-      this.playDismissProxySize = target.circleDiameter
-      this.playDismissProxyOpacity = 1
-      this.playDismissOpacity = 0
+      this.playDismissProxySize = 0
+      this.playDismissProxyOpacity = 0
+      this.translateY = 0
+      this.playDismissTranslateX = 0
+      this.playDismissScale = 1
+      this.playDismissScaleY = 1
+      this.playDismissOpacity = 1
+      this.playDismissBorderRadius = 0
+      this.playDragUiOpacity = 1
+      this.scaleValueImage = 1
+      this.scaleValueText = 1
     })
-    this.playOpenTimer = setTimeout(() => {
-      this.playOpenTimer = -1
-      this.getUIContext()?.animateTo({
-        duration: this.playOpenExpandDuration,
-        curve: Curve.Friction
-      }, () => {
-        this.playDismissProxySize = target.circleDiameter
-        this.playDismissProxyOpacity = 0
-        this.translateY = 0
-        this.playDismissTranslateX = 0
-        this.playDismissScale = 1
-        this.playDismissScaleY = 1
-        this.playDismissOpacity = 1
-        this.playDismissBorderRadius = 0
-        this.playDragUiOpacity = 1
-        this.scaleValueImage = 1
-        this.scaleValueText = 1
-      })
-    }, this.playOpenDotDuration + 16)
   }
 
   setShowPlayTrue(fromMiniBar: boolean = false){
@@ -9107,12 +9095,6 @@ export struct LocalMusic {
     }
     this.isShowPlay = true;
     this.showCoverScaleGuideIfNeeded()
-    if (fromMiniBar) {
-      this.playOpenTimer = setTimeout(() => {
-        this.playOpenTimer = -1
-        this.startPlayerViewOpenAnimationIfNeeded()
-      }, 16)
-    }
   }
 
   private closePlayerViewWithMiniBarAnimation(): void {
@@ -9124,38 +9106,26 @@ export struct LocalMusic {
     this.clearPlayDismissTimer()
     this.clearPlayOpenTimer()
     this.getUIContext()?.animateTo({
-      duration: this.playDismissContentDuration,
+      duration: this.playDismissContentDuration + 40,
       curve: Curve.Sharp
     }, () => {
       this.translateY = target.translateY
-      this.playDismissTranslateX = target.translateX
-      this.playDismissScale = target.circleScaleX
-      this.playDismissScaleY = target.circleScaleY
-      this.playDismissOpacity = 0.12
-      this.playDismissBorderRadius = target.circleDiameter / 2
-      this.playDismissProxySize = target.circleDiameter
-      this.playDismissProxyOpacity = 1
+      this.playDismissTranslateX = 0
+      this.playDismissScale = 0.9
+      this.playDismissScaleY = 0.8
+      this.playDismissOpacity = 0.04
+      this.playDismissBorderRadius = 48
+      this.playDismissProxySize = 0
+      this.playDismissProxyOpacity = 0
       this.playDragUiOpacity = 0
-      this.scaleValueImage = Math.max(0.72, target.circleScaleX)
-      this.scaleValueText = Math.max(0.88, target.circleScaleY)
+      this.scaleValueImage = 0.8
+      this.scaleValueText = 0.86
     })
     this.playDismissTimer = setTimeout(() => {
-      this.playDismissOpacity = 0
-      this.playDismissProxySize = target.circleDiameter
-      this.playDismissProxyOpacity = 1
-      this.getUIContext()?.animateTo({
-        duration: this.playDismissDotDuration,
-        curve: Curve.EaseIn
-      }, () => {
-        this.playDismissProxySize = target.dotDiameter
-        this.playDismissProxyOpacity = 0
-      })
-      this.playDismissTimer = setTimeout(() => {
-        this.playDismissTimer = -1
-        this.isShowPlay = false
-        this.isShowCoverScaleGuide = false
-      }, this.playDismissDotDuration + 16)
-    }, this.playDismissContentDuration + 16)
+      this.playDismissTimer = -1
+      this.isShowPlay = false
+      this.isShowCoverScaleGuide = false
+    }, this.playDismissContentDuration + 56)
   }
 
   setShowPlayFalse(){
@@ -12413,7 +12383,7 @@ export struct LocalMusic {
                 duration: 500,
                 curve: Curve.Sharp
               }, () => {
-                this.scaleValueImage = Math.min(1, Math.max(0.4, 1 - this.translateY / 400));
+                this.scaleValueImage = Math.min(1, Math.max(0.46, 1 - this.translateY / 900));
                 console.info('onecold scaleValueImage:', this.scaleValueImage)
                 this.scaleValueText = Math.min(1, Math.max(0.88, 1 - this.translateY / 2000));
                 this.playDragUiOpacity = Math.min(1, Math.max(0, 1 - this.translateY / 300));
@@ -14434,10 +14404,7 @@ export struct LocalMusic {
             PreferencesUtil.put(SettingPage.RECTANGLE_COVER_SCALE, this.rectangleCoverScale)
           }))
     }
-    // .scale({ x: this.scaleValueImage, y: this.scaleValueImage,
-    //   // 设置左下角为缩放中心点
-    //   centerX: '0%',
-    //   centerY: '50%' })
+    .scale({ x: this.scaleValueImage, y: this.scaleValueImage})
   }
 
   private getRectangleCoverWidth(): string | number {

+ 3 - 1
entry/src/main/ets/view/PlayingIndicator.ets

@@ -5,6 +5,7 @@ import { normalizeHexColor, rgbaFromHex } from './spectrum/SpectrumRenderer'
 export struct PlayingIndicator {
   @Prop @Watch('onActiveChange') isActive: boolean = false
   @Prop indicatorSize: number = 18
+  @Prop indicatorColor: string = ''
   @Prop marginRight: number = 0
   @Prop marginTop: number = 0
   @Prop marginBottom: number = 0
@@ -73,7 +74,8 @@ export struct PlayingIndicator {
       return
     }
 
-    const brandColor: string = normalizeHexColor(this.themeColor, CommonConstants.DEFAULT_THEME_COLOR)
+    const colorSource: string = this.indicatorColor.length > 0 ? this.indicatorColor : this.themeColor
+    const brandColor: string = normalizeHexColor(colorSource, CommonConstants.DEFAULT_THEME_COLOR)
     const barCount: number = 5
     const barWidth: number = Math.max(1.0, Math.min(1.8, this.canvasWidth * 0.075))
     const barGap: number = Math.max(1.0, Math.min(1.9, this.canvasWidth * 0.092))

+ 144 - 140
entry/src/main/ets/view/RemoteMusicPage.ets

@@ -3123,6 +3123,17 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.playSong(song, index);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_list_song', song.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_song', song.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   @Builder
@@ -3171,6 +3182,17 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.onArtistSelected(artist);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_list_artist', artist.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_artist', artist.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   @Builder
@@ -3227,6 +3249,17 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.onAlbumSelected(album);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_list_album', album.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_album', album.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   @Builder
@@ -3280,6 +3313,17 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.onPlaylistSelected(playlist);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_list_playlist', playlist.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_playlist', playlist.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   @Builder
@@ -3787,68 +3831,24 @@ export struct RemoteMusicPage {
           GridItem() {
             this.buildSongItemGrid(item, index)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_song', item.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_song', item.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (item: VideoItem) => item.id)
       } else if (this.selectedTab === 1) {
         LazyForEach(this.artistDataSource, (artist: NavidromeRestArtist) => {
           GridItem() {
             this.buildArtistItemGrid(artist)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_artist', artist.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_artist', artist.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (artist: NavidromeRestArtist) => artist.id)
       } else if (this.selectedTab === 2) {
         LazyForEach(this.albumDataSource, (album: NavidromeRestAlbum) => {
           GridItem() {
             this.buildAlbumItemGrid(album)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_album', album.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_album', album.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (album: NavidromeRestAlbum) => album.id)
       } else if (this.selectedTab === 3) {
         LazyForEach(this.playlistDataSource, (playlist: NavidromeRestPlaylist) => {
           GridItem() {
             this.buildPlaylistItemGrid(playlist)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_playlist', playlist.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_playlist', playlist.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (playlist: NavidromeRestPlaylist) => playlist.id)
       }
 
@@ -3938,7 +3938,7 @@ export struct RemoteMusicPage {
 
   @Builder
   buildSongItemGrid(item: VideoItem, index: number) {
-    Button({ type: ButtonType.Normal, stateEffect: false }) {
+    Button({ type: ButtonType.Normal, stateEffect: true }) {
       Stack() {
         Column() {
           Image(item.pixelMapPath ? item.pixelMapPath : $r('app.media.nocover'))
@@ -4035,6 +4035,7 @@ export struct RemoteMusicPage {
 
       }
     }
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.9 })
     .backgroundColor(Color.Transparent)
     .width('100%')
     .padding({ top: 15 })
@@ -4043,6 +4044,17 @@ export struct RemoteMusicPage {
       this.playSong(item, index);
 
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_song', item.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_song', item.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
 
   }
 
@@ -4096,7 +4108,7 @@ export struct RemoteMusicPage {
 
   @Builder
   buildArtistItemGrid(artist: NavidromeRestArtist) {
-    Button({ type: ButtonType.Normal, stateEffect: false }) {
+    Button({ type: ButtonType.Normal, stateEffect: true }) {
       Stack() {
         Column() {
           Image(artist.coverUrl ?? $r('app.media.nocover'))
@@ -4170,6 +4182,7 @@ export struct RemoteMusicPage {
 
       }
     }
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.9 })
     .backgroundColor(Color.Transparent)
     .width('100%')
     .padding({ top: 15 })
@@ -4177,11 +4190,22 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.onArtistSelected(artist);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_artist', artist.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_artist', artist.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   @Builder
   buildAlbumItemGrid(album: NavidromeRestAlbum) {
-    Button({ type: ButtonType.Normal, stateEffect: false }) {
+    Button({ type: ButtonType.Normal, stateEffect: true }) {
       Stack() {
         Column() {
           Image(album.coverUrl ?? $r('app.media.nocover'))
@@ -4254,6 +4278,7 @@ export struct RemoteMusicPage {
 
       }
     }
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.9 })
     .backgroundColor(Color.Transparent)
     .width('100%')
     .padding({ top: 15 })
@@ -4261,11 +4286,22 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.onAlbumSelected(album);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_album', album.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_album', album.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   @Builder
   buildPlaylistItemGrid(playlist: NavidromeRestPlaylist) {
-    Button({ type: ButtonType.Normal, stateEffect: false }) {
+    Button({ type: ButtonType.Normal, stateEffect: true }) {
       Stack() {
         Column() {
           Image(playlist.coverUrl ?? $r('app.media.nocover'))
@@ -4338,6 +4374,7 @@ export struct RemoteMusicPage {
 
       }
     }
+    .clickEffect({ level: ClickEffectLevel.MIDDLE, scale: 0.9 })
     .backgroundColor(Color.Transparent)
     .width('100%')
     .padding({ top: 15 })
@@ -4345,6 +4382,17 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.onPlaylistSelected(playlist);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_grid_playlist', playlist.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_grid_playlist', playlist.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   @Builder
@@ -4356,68 +4404,24 @@ export struct RemoteMusicPage {
           ListItem() {
             this.buildSongItem(item, index)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_list_song', item.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_song', item.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (item: VideoItem) => item.id)
       } else if (this.selectedTab === 1) {
         LazyForEach(this.artistDataSource, (artist: NavidromeRestArtist) => {
           ListItem() {
             this.buildArtistItem(artist)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_list_artist', artist.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_artist', artist.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (artist: NavidromeRestArtist) => artist.id)
       } else if (this.selectedTab === 2) {
         LazyForEach(this.albumDataSource, (album: NavidromeRestAlbum) => {
           ListItem() {
             this.buildAlbumItem(album)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_list_album', album.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_album', album.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (album: NavidromeRestAlbum) => album.id)
       } else if (this.selectedTab === 3) {
         LazyForEach(this.playlistDataSource, (playlist: NavidromeRestPlaylist) => {
           ListItem() {
             this.buildPlaylistItem(playlist)
           }
-          .onTouch((event: TouchEvent) => {
-            this.handlePointLightTouch(this.getPointLightItemKey('remote_list_playlist', playlist.id), event)
-          })
-          .visualEffect(deviceInfo.sdkApiVersion >= 20
-            ? new hdsEffect.HdsEffectBuilder()
-              .pointLight({
-                options: this.getPointLightOptions(this.getPointLightItemKey('remote_list_playlist', playlist.id)),
-                illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-              })
-              .buildEffect()
-            : undefined)
         }, (playlist: NavidromeRestPlaylist) => playlist.id)
       }
     }
@@ -4559,18 +4563,6 @@ export struct RemoteMusicPage {
                 this.buildSongWaterCardItem(item, index)
               }
               .width('100%')
-              .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
-              .onTouch((event: TouchEvent) => {
-                this.handlePointLightTouch(this.getPointLightItemKey('remote_water_song', item.id), event)
-              })
-              .visualEffect(deviceInfo.sdkApiVersion >= 20
-                ? new hdsEffect.HdsEffectBuilder()
-                  .pointLight({
-                    options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_song', item.id)),
-                    illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-                  })
-                  .buildEffect()
-                : undefined)
             }, (item: VideoItem) => item.id)
           } else if (this.selectedTab === 1) {
             // 艺术家瀑布流
@@ -4579,18 +4571,6 @@ export struct RemoteMusicPage {
                 this.buildArtistWaterCardItem(artist)
               }
               .width('100%')
-              .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
-              .onTouch((event: TouchEvent) => {
-                this.handlePointLightTouch(this.getPointLightItemKey('remote_water_artist', artist.id), event)
-              })
-              .visualEffect(deviceInfo.sdkApiVersion >= 20
-                ? new hdsEffect.HdsEffectBuilder()
-                  .pointLight({
-                    options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_artist', artist.id)),
-                    illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-                  })
-                  .buildEffect()
-                : undefined)
             }, (artist: NavidromeRestArtist) => artist.id)
           } else if (this.selectedTab === 2) {
             // 专辑瀑布流
@@ -4599,18 +4579,6 @@ export struct RemoteMusicPage {
                 this.buildAlbumWaterCardItem(album)
               }
               .width('100%')
-              .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
-              .onTouch((event: TouchEvent) => {
-                this.handlePointLightTouch(this.getPointLightItemKey('remote_water_album', album.id), event)
-              })
-              .visualEffect(deviceInfo.sdkApiVersion >= 20
-                ? new hdsEffect.HdsEffectBuilder()
-                  .pointLight({
-                    options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_album', album.id)),
-                    illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-                  })
-                  .buildEffect()
-                : undefined)
             }, (album: NavidromeRestAlbum) => album.id)
           } else if (this.selectedTab === 3) {
             // 歌单瀑布流
@@ -4619,18 +4587,6 @@ export struct RemoteMusicPage {
                 this.buildPlaylistWaterCardItem(playlist)
               }
               .width('100%')
-              .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
-              .onTouch((event: TouchEvent) => {
-                this.handlePointLightTouch(this.getPointLightItemKey('remote_water_playlist', playlist.id), event)
-              })
-              .visualEffect(deviceInfo.sdkApiVersion >= 20
-                ? new hdsEffect.HdsEffectBuilder()
-                  .pointLight({
-                    options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_playlist', playlist.id)),
-                    illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
-                  })
-                  .buildEffect()
-                : undefined)
             }, (playlist: NavidromeRestPlaylist) => playlist.id)
           }
         }
@@ -4858,11 +4814,23 @@ export struct RemoteMusicPage {
       color: 'on_primary'
     })
     .height('auto')
+    .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
     .onClick(() => {
       this.playSong(item, index);
 
     })
     .backgroundColor(Color.Transparent)
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_water_song', item.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_song', item.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   // 艺术家瀑布流卡片项
@@ -4919,6 +4887,7 @@ export struct RemoteMusicPage {
       }
     }
     .width('100%')
+    .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
     .shadow({
       radius: 10,
       type: ShadowType.BLUR,
@@ -4928,6 +4897,17 @@ export struct RemoteMusicPage {
       this.onArtistSelected(artist);
     })
     .backgroundColor(Color.Transparent)
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_water_artist', artist.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_artist', artist.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   // 专辑瀑布流卡片项
@@ -4986,6 +4966,7 @@ export struct RemoteMusicPage {
       }
     }
     .width('100%')
+    .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
     .shadow({
       radius: 10,
       type: ShadowType.BLUR,
@@ -4995,6 +4976,17 @@ export struct RemoteMusicPage {
     .onClick(() => {
       this.onAlbumSelected(album);
     })
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_water_album', album.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_album', album.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 
   // 歌单瀑布流卡片项
@@ -5051,6 +5043,7 @@ export struct RemoteMusicPage {
       }
     }
     .width('100%')
+    .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
     .shadow({
       radius: 10,
       type: ShadowType.BLUR,
@@ -5060,6 +5053,17 @@ export struct RemoteMusicPage {
       this.onPlaylistSelected(playlist);
     })
     .backgroundColor(Color.Transparent)
+    .onTouch((event: TouchEvent) => {
+      this.handlePointLightTouch(this.getPointLightItemKey('remote_water_playlist', playlist.id), event)
+    })
+    .visualEffect(deviceInfo.sdkApiVersion >= 20
+      ? new hdsEffect.HdsEffectBuilder()
+        .pointLight({
+          options: this.getPointLightOptions(this.getPointLightItemKey('remote_water_playlist', playlist.id)),
+          illuminatedType: hdsEffect.PointLightIlluminatedType.BORDER_CONTENT
+        })
+        .buildEffect()
+      : undefined)
   }
 }
 

+ 92 - 0
entry/src/ohosTest/ets/test/PlayerDismissHelper.test.ets

@@ -0,0 +1,92 @@
+import { describe, it, expect } from '@ohos/hypium'
+import {
+  resolveMiniPlayerCoverCenterOffset,
+  resolveMiniPlayerRevealAnimationPlan,
+  resolveMiniPlayerMorphTarget,
+  resolvePlayerDismissMorphTarget,
+  resolvePlayerDismissTarget
+} from '../../../main/ets/common/util/PlayerDismissHelper'
+
+export default function playerDismissHelperTest() {
+  describe('PlayerDismissHelperTest', () => {
+    it('resolvePhoneMiniPlayerCenterTarget', 0, () => {
+      const target = resolvePlayerDismissTarget(360, 800, 16, false)
+
+      expect(target.translateX).assertEqual(0)
+      expect(target.targetCenterX).assertEqual(180)
+      expect(target.targetCenterY).assertEqual(749)
+      expect(target.translateY).assertEqual(349)
+      expect(target.scale).assertEqual(0.0875)
+    })
+
+    it('resolveFixedBottomMarginTargetForHiCarLikeLayout', 0, () => {
+      const target = resolvePlayerDismissTarget(1280, 720, 0, true)
+
+      expect(target.targetCenterX).assertEqual(640)
+      expect(target.targetCenterY).assertEqual(655)
+      expect(target.translateY).assertEqual(295)
+      expect(target.scale).assertEqual(0.09722222222222222)
+    })
+
+    it('resolvePhoneMorphTargetForCircleAndDot', 0, () => {
+      const target = resolvePlayerDismissMorphTarget(360, 800, 16, false)
+
+      expect(target.targetCenterX).assertEqual(180)
+      expect(target.targetCenterY).assertEqual(749)
+      expect(target.translateX).assertEqual(0)
+      expect(target.translateY).assertEqual(349)
+      expect(target.circleDiameter).assertEqual(42)
+      expect(target.dotDiameter).assertEqual(10)
+      expect(target.circleScaleX).assertEqual(0.11666666666666667)
+      expect(target.circleScaleY).assertEqual(0.0525)
+      expect(target.dotScaleX).assertEqual(0.027777777777777776)
+      expect(target.dotScaleY).assertEqual(0.0125)
+    })
+
+    it('resolveHiCarMorphTargetForCircleAndDot', 0, () => {
+      const target = resolvePlayerDismissMorphTarget(1280, 720, 0, true)
+
+      expect(target.targetCenterX).assertEqual(640)
+      expect(target.targetCenterY).assertEqual(655)
+      expect(target.translateY).assertEqual(295)
+      expect(target.circleDiameter).assertEqual(42)
+      expect(target.dotDiameter).assertEqual(10)
+      expect(target.circleScaleX).assertEqual(0.0328125)
+      expect(target.circleScaleY).assertEqual(0.058333333333333334)
+      expect(target.dotScaleX).assertEqual(0.0078125)
+      expect(target.dotScaleY).assertEqual(0.013888888888888888)
+    })
+
+    it('resolvePhoneMiniPlayerMorphTarget', 0, () => {
+      const target = resolveMiniPlayerMorphTarget(360)
+
+      expect(target.barWidth).assertEqual(324)
+      expect(target.barHeight).assertEqual(70)
+      expect(target.circleDiameter).assertEqual(48)
+      expect(target.dotDiameter).assertEqual(10)
+      expect(target.circleScaleX).assertEqual(0.14814814814814814)
+      expect(target.circleScaleY).assertEqual(0.6857142857142857)
+      expect(target.dotScaleX).assertEqual(0.030864197530864196)
+      expect(target.dotScaleY).assertEqual(0.14285714285714285)
+    })
+
+    it('resolveMiniPlayerCoverCenterOffset', 0, () => {
+      const offset = resolveMiniPlayerCoverCenterOffset(324)
+
+      expect(offset).assertEqual(117)
+    })
+
+    it('resolveMiniPlayerRevealAnimationPlanUsesDirectDotReveal', 0, () => {
+      const target = resolveMiniPlayerMorphTarget(360)
+      const plan = resolveMiniPlayerRevealAnimationPlan(target, 220)
+
+      expect(plan.startScaleX).assertEqual(0.030864197530864196)
+      expect(plan.startScaleY).assertEqual(0.14285714285714285)
+      expect(plan.startOpacity).assertEqual(1)
+      expect(plan.startBorderRadius).assertEqual(200)
+      expect(plan.startProxySize).assertEqual(0)
+      expect(plan.startProxyOpacity).assertEqual(0)
+      expect(plan.shakeDelayMs).assertEqual(236)
+    })
+  })
+}