浏览代码

refactor(playlist): 重构播放列表拖拽功能并优化本地主机地址获取

chendeben 7 月之前
父节点
当前提交
4ddc529c08
共有 2 个文件被更改,包括 467 次插入271 次删除
  1. 18 18
      entry/src/main/ets/common/network/SmbStreamingHttpServer.ets
  2. 449 253
      entry/src/main/ets/pages/PlaylistDetailPage.ets

+ 18 - 18
entry/src/main/ets/common/network/SmbStreamingHttpServer.ets

@@ -107,27 +107,27 @@ export default class SmbStreamingHttpServer {
   private async resolveLocalHost(): Promise<string> {
     try {
       const netHandle = await connection.getDefaultNet();
-      const addressGetter = (connection as unknown as { getAddressesByNetwork?: (handle: unknown) => Promise<unknown[]> })
-        .getAddressesByNetwork;
-      if (!addressGetter) {
+      if (netHandle.netId === 0) {
         return DEFAULT_HOST;
       }
-      const addresses = await addressGetter(netHandle);
-      if (addresses && addresses.length > 0) {
-        for (let i = 0; i < addresses.length; i++) {
-          const raw = addresses[i] as Record<string, unknown>;
-          const address = (raw.address ?? raw.addr ?? raw.ip) as string | undefined;
-          if (!address) {
-            continue;
-          }
-          if (address.startsWith('127.') || address === '0.0.0.0') {
-            continue;
-          }
-          if (address.includes(':')) {
-            continue;
-          }
-          return address;
+      const properties = await connection.getConnectionProperties(netHandle);
+      const linkAddresses = properties.linkAddresses;
+      if (!linkAddresses || linkAddresses.length === 0) {
+        return DEFAULT_HOST;
+      }
+      for (let i = 0; i < linkAddresses.length; i++) {
+        const addressInfo = linkAddresses[i].address;
+        const address = addressInfo.address;
+        if (!address) {
+          continue;
+        }
+        if (address.startsWith('127.') || address === '0.0.0.0') {
+          continue;
+        }
+        if (address.includes(':')) {
+          continue;
         }
+        return address;
       }
     } catch (error) {
       Logger.warn(TAG, `解析本机IP失败: ${(error as Error).message}`);

+ 449 - 253
entry/src/main/ets/pages/PlaylistDetailPage.ets

@@ -73,12 +73,22 @@ export struct PlaylistDetailPage {
 
   //长按拖动
   @State dragItem: number = -1
+  @State dragStartIndex: number = -1
+  @State dragTargetIndex: number = -1
   @State scaleItem: number = -1
   @State neighborItem: number = -1
-  @State neighborScale: number = -1
+  @State neighborScale: number = 1
   @State offsetY: number = 0
-  private dragRefOffset: number = 0
   private ITEM_INTV: number = 70
+  private AUTO_SCROLL_EDGE: number = 60
+  private AUTO_SCROLL_STEP: number = 20
+  private listScroller: ListScroller = new ListScroller()
+  private listArea?: Area
+  private dragStartScrollOffset: number = 0
+  private dragStartTop: number = 0
+  @State dragOverlayItem: VideoItem | null = null
+  @State dragOverlayTop: number = 0
+  private lastDragLogTime: number = 0
   //播放列表的拖动List 重新排序
   @State sonDataSource: LazyDataSource<VideoItem> = new LazyDataSource(this.songList)
   scaleSelect(item: number): number {
@@ -91,15 +101,81 @@ export struct PlaylistDetailPage {
     }
   }
 
-  async itemMove(index: number, newIndex: number): Promise<void> {
-    if (newIndex < 0 || newIndex >= this.songList.length) {
-      return;
+  private getDragTranslateY(index: number): number {
+    if (this.dragItem < 0 || this.dragStartIndex < 0) {
+      return 0
     }
-    let tmp = this.songList.splice(index, 1);
-    this.songList.splice(newIndex, 0, tmp[0]);
-    this.sonDataSource.pushArrayData(this.songList)
-    // 3. 更新数据库中的sortOrder
-    await this.updateAllSongsSortOrder()
+    if (index === this.dragStartIndex) {
+      return 0
+    }
+    if (this.dragTargetIndex < 0 || this.dragTargetIndex === this.dragStartIndex) {
+      return 0
+    }
+    if (this.dragTargetIndex > this.dragStartIndex) {
+      return index > this.dragStartIndex && index <= this.dragTargetIndex ? -this.ITEM_INTV : 0
+    }
+    return index >= this.dragTargetIndex && index < this.dragStartIndex ? this.ITEM_INTV : 0
+  }
+
+  private updateDragTarget(offsetY: number): void {
+    if (this.dragStartIndex < 0) {
+      return
+    }
+    const rawIndex = this.dragStartIndex + Math.round(offsetY / this.ITEM_INTV)
+    const clampedIndex = Math.max(0, Math.min(this.songList.length - 1, rawIndex))
+    this.dragTargetIndex = clampedIndex
+    if (this.dragTargetIndex !== this.dragStartIndex) {
+      this.neighborItem = this.dragTargetIndex
+      this.neighborScale = 0.98
+    } else {
+      this.neighborItem = -1
+      this.neighborScale = 1
+    }
+  }
+
+  private autoScrollWhileDragging(offsetY: number): void {
+    if (!this.listArea) {
+      return
+    }
+    const areaHeight = typeof this.listArea.height === 'number' ? this.listArea.height : Number(this.listArea.height)
+    if (!Number.isFinite(areaHeight) || areaHeight <= 0) {
+      return
+    }
+    const currentOffsetVp = this.listScroller.currentOffset().yOffset
+    const maxOffsetVp = Math.max(0, this.songList.length * this.ITEM_INTV - areaHeight)
+    if (maxOffsetVp <= 0) {
+      return
+    }
+    const topIndex = Math.floor(currentOffsetVp / this.ITEM_INTV)
+    const visibleCount = Math.max(1, Math.floor(areaHeight / this.ITEM_INTV))
+    const bottomIndex = topIndex + visibleCount - 1
+    let nextOffsetVp = currentOffsetVp
+
+    const pointerY = this.dragOverlayTop + this.ITEM_INTV / 2
+    if (pointerY > areaHeight - this.AUTO_SCROLL_EDGE) {
+      nextOffsetVp = Math.min(maxOffsetVp, currentOffsetVp + this.AUTO_SCROLL_STEP)
+    } else if (pointerY < this.AUTO_SCROLL_EDGE) {
+      nextOffsetVp = Math.max(0, currentOffsetVp - this.AUTO_SCROLL_STEP)
+    }
+
+    if (nextOffsetVp !== currentOffsetVp) {
+      this.listScroller.scrollTo({ xOffset: 0, yOffset: nextOffsetVp })
+      LogUtil.info('drag-debug',
+        `autoScroll offsetY=${offsetY} pointerY=${pointerY} currentOffsetVp=${currentOffsetVp} nextOffsetVp=${nextOffsetVp} topIndex=${topIndex} bottomIndex=${bottomIndex}`)
+    }
+  }
+
+  private resetDragState(): void {
+    this.dragItem = -1
+    this.dragStartIndex = -1
+    this.dragTargetIndex = -1
+    this.neighborItem = -1
+    this.neighborScale = 1
+    this.offsetY = 0
+    this.dragStartScrollOffset = 0
+    this.dragStartTop = 0
+    this.dragOverlayItem = null
+    this.dragOverlayTop = 0
   }
 
   aboutToAppear() {
@@ -570,6 +646,272 @@ export struct PlaylistDetailPage {
     .width('100%')
   }
 
+  @Builder
+  private buildSongContent(song: VideoItem, index: number, isOverlay: boolean = false) {
+    if (isOverlay) {
+      Button({ type: ButtonType.Normal, stateEffect: true }) {
+        Row() {
+          // 歌曲封面 - 改为圆形
+          Stack({ alignContent: Alignment.TopStart }) {
+            Image(song.pixelMapPath || $r('app.media.alt'))
+              .width(48)
+              .height(48)
+              .borderRadius(24) // 改为圆形
+              .objectFit(ImageFit.Cover)
+              .interpolation(ImageInterpolation.High)
+              .autoResize(true)
+              .margin({ left: 20 })
+              .fillColor(song.pixelMapPath ? undefined : (this.isDarkMode ? Color.White : this.themeColor))
+          }
+          .width(64)
+
+          // 歌曲信息 - 参考LocalMusic的布局
+          Column() {
+            Row() {
+              // 歌曲名称
+              Text(song.name || song.fileName || '')
+                .fontSize(16)
+                .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
+                .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Bold : FontWeight.Normal)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .layoutWeight(1)
+                .margin({ top: 8, left: 12 })
+            }
+            .width('100%')
+
+            // 歌手和专辑信息 - 参考LocalMusic的第二行布局
+            Row() {
+              // 音质标签
+              if (song.md5Str) {
+                Text(song.md5Str?.includes('Lossless') ? '无损' : song.md5Str)
+                  .fontSize(10)
+                  .fontColor($r('app.color.text_color'))
+                  .fontWeight(500)
+                  .padding({ top: 2, right: 6, left: 6, bottom: 2 })
+                  .borderRadius(4)
+                  .backgroundColor('#FFC107')
+                  .visibility(song.md5Str ? Visibility.Visible : Visibility.None)
+              }
+              Text(song.artist)
+                .fontSize(13)
+                .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
+                .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
+                .maxLines(1)
+                .margin({ left: 8 })
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
+              Text(song.duration)
+                .fontSize(13)
+                .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
+                .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
+                .maxLines(1)
+                .margin({ left: 8 })
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
+            }
+            .width('100%')
+            .margin({ left: 12, top: 4 })
+            .alignItems(VerticalAlign.Center)
+          }
+          .layoutWeight(1)
+          .alignItems(HorizontalAlign.Start)
+          .justifyContent(FlexAlign.Center)
+
+          // 排序模式下显示上下移动按钮,否则显示更多按钮
+          if (this.isSortMode) {
+            Row({ space: 8 }) {
+              // 上移按钮
+              Text('▲')
+                .fontSize(16)
+                .fontColor(index === 0 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                .enabled(index > 0)
+
+              // 下移按钮
+              Text('▼')
+                .fontSize(16)
+                .fontColor(index === this.songList.length - 1 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                .enabled(index < this.songList.length - 1)
+            }
+            .margin({ right: 15 })
+          } else {
+            Text('⋯')
+              .fontSize(20)
+              .fontColor($r('app.color.text_color'))
+              .opacity(0.6)
+              .margin({ right: 15 })
+          }
+        }
+        .width('100%')
+        .height(70)
+        .justifyContent(FlexAlign.Start)
+        .alignItems(VerticalAlign.Center)
+      }
+      .backgroundColor(Color.Transparent)
+      .height(70)
+      .width('100%')
+      // 当前播放歌曲的背景高亮
+      .backgroundColor(this.isPlaying && this.curIndex === index ? themeColorWithAlpha(this.themeColor, 0.08, this.isDarkMode) : Color.Transparent)
+      .border({
+        width: { left: this.isPlaying && this.curIndex === index ? 3 : 0 },
+        color: this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode) : this.themeColor
+      })
+    } else {
+      Button({ type: ButtonType.Normal, stateEffect: true }) {
+        Row() {
+          // 歌曲封面 - 改为圆形
+          Stack() {
+            Image(song.pixelMapPath || $r('app.media.alt'))
+              .width(48)
+              .height(48)
+              .borderRadius(24) // 改为圆形
+              .objectFit(ImageFit.Cover)
+              .interpolation(ImageInterpolation.High)
+              .autoResize(true)
+              .margin({ left: 20 })
+              .fillColor(song.pixelMapPath ? undefined : (this.isDarkMode ? Color.White : this.themeColor))
+              .onClick(() => {
+                this.playSong(song, index)
+              })
+          }
+          .width(64)
+
+          // 歌曲信息 - 参考LocalMusic的布局
+          Column() {
+            Row() {
+              // 歌曲名称
+              Text(song.name || song.fileName || '')
+                .fontSize(16)
+                .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
+                .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Bold : FontWeight.Normal)
+                .maxLines(1)
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .layoutWeight(1)
+                .margin({ top: 8, left: 12 })
+            }
+            .width('100%')
+
+            // 歌手和专辑信息 - 参考LocalMusic的第二行布局
+            Row() {
+              // 音质标签
+              if (song.md5Str) {
+                Text(song.md5Str?.includes('Lossless') ? '无损' : song.md5Str)
+                  .fontSize(10)
+                  .fontColor($r('app.color.text_color'))
+                  .fontWeight(500)
+                  .padding({ top: 2, right: 6, left: 6, bottom: 2 })
+                  .borderRadius(4)
+                  .backgroundColor('#FFC107')
+                  .visibility(song.md5Str ? Visibility.Visible : Visibility.None)
+              }
+              Text(song.artist)
+                .fontSize(13)
+                .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
+                .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
+                .maxLines(1)
+                .margin({ left: 8 })
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
+              Text(song.duration)
+                .fontSize(13)
+                .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
+                .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
+                .maxLines(1)
+                .margin({ left: 8 })
+                .textOverflow({ overflow: TextOverflow.Ellipsis })
+                .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
+            }
+            .width('100%')
+            .margin({ left: 12, top: 4 })
+            .alignItems(VerticalAlign.Center)
+          }
+          .layoutWeight(1)
+          .alignItems(HorizontalAlign.Start)
+          .justifyContent(FlexAlign.Center)
+
+          // 排序模式下显示上下移动按钮,否则显示更多按钮
+          if (this.isSortMode) {
+            Row({ space: 8 }) {
+              // 上移按钮
+              Text('▲')
+                .fontSize(16)
+                .fontColor(index === 0 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                .onClick(() => {
+                  if (index > 0) {
+                    this.moveSong(index, index - 1)
+                  }
+                })
+                .enabled(index > 0)
+
+              // 下移按钮
+              Text('▼')
+                .fontSize(16)
+                .fontColor(index === this.songList.length - 1 ? (this.isDarkMode ? '#666666' : '#cccccc') :
+                  (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
+                .onClick(() => {
+                  if (index < this.songList.length - 1) {
+                    this.moveSong(index, index + 1)
+                  }
+                })
+                .enabled(index < this.songList.length - 1)
+            }
+            .margin({ right: 15 })
+          } else {
+            Text('⋯')
+              .fontSize(20)
+              .fontColor($r('app.color.text_color'))
+              .opacity(0.6)
+              .margin({ right: 15 })
+              .bindMenu(this.MoreMenuBuilder(song))
+          }
+        }
+        .width('100%')
+        .height(70)
+        .justifyContent(FlexAlign.Start)
+        .alignItems(VerticalAlign.Center)
+      }
+      .backgroundColor(Color.Transparent)
+      .height(70)
+      .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
+      .width('100%')
+      .onClick(() => {
+        // 排序模式下禁用点击播放
+        if (!this.isSortMode) {
+          this.playSong(song, index)
+        }
+      })
+      .stateStyles({
+        normal: {
+          .backgroundColor(Color.Transparent)
+        },
+        pressed: {
+          .backgroundColor(this.isSortMode ? Color.Transparent : themeColorWithAlpha(this.themeColor, 0.05, this.isDarkMode))
+        }
+      })
+      // 当前播放歌曲的背景高亮
+      .backgroundColor(this.isPlaying && this.curIndex === index ? themeColorWithAlpha(this.themeColor, 0.08, this.isDarkMode) : Color.Transparent)
+      .border({
+        width: { left: this.isPlaying && this.curIndex === index ? 3 : 0 },
+        color: this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode) : this.themeColor
+      })
+      .opacity(this.dragItem === index ? 0.5 : this.pageOpacity)
+      .translate({ x: 0, y: this.showContent ? 0 : 20 })
+      .transition(TransitionEffect.OPACITY.animation({
+        duration: 600,
+        curve: Curve.EaseOut,
+        delay: 300 + index * 30
+      }))
+      .transition(TransitionEffect.translate({ y: 20 }).animation({
+        duration: 600,
+        curve: Curve.EaseOut,
+        delay: 300 + index * 30
+      }))
+    }
+  }
+
   build() {
     Column() {
       // 顶部安全区和标题栏
@@ -870,272 +1212,126 @@ export struct PlaylistDetailPage {
 
         // 歌曲列表 - 参考LocalMusic的MusicItem样式
         if (this.songList.length > 0) {
-          List({ space: 0 }) {
-            LazyForEach(this.sonDataSource, (song: VideoItem, index: number) => {
-              ListItem() {
-                Button({ type: ButtonType.Normal, stateEffect: true }) {
-                  Row() {
-                    // 歌曲封面 - 改为圆形
-                    Stack() {
-                      Image(song.pixelMapPath || $r('app.media.alt'))
-                        .width(48)
-                        .height(48)
-                        .borderRadius(24) // 改为圆形
-                        .objectFit(ImageFit.Cover)
-                        .interpolation(ImageInterpolation.High)
-                        .autoResize(true)
-                        .margin({ left: 20 })
-                        .fillColor(song.pixelMapPath ? undefined : (this.isDarkMode ? Color.White : this.themeColor))
-                        .onClick(() => {
-                          this.playSong(song, index)
+          Stack({ alignContent: Alignment.TopStart }) {
+            List({ space: 0, scroller: this.listScroller }) {
+              LazyForEach(this.sonDataSource, (song: VideoItem, index: number) => {
+                ListItem() {
+                  this.buildSongContent(song, index)
+                }
+                //拖动List关键代码开始
+                .scale({ x: this.scaleSelect(index), y: this.scaleSelect(index) })
+                .zIndex(this.dragItem === index ? 1 : 0)
+                .translate({ y: this.getDragTranslateY(index) })
+                .gesture(
+                  GestureGroup(GestureMode.Sequence,
+                    LongPressGesture({ repeat: true })
+                      .onAction((event?: GestureEvent) => {
+                        this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
+                          this.scaleItem = index
                         })
-                    }
-                    .width(64)
-
-                    // 歌曲信息 - 参考LocalMusic的布局
-                    Column() {
-                      Row() {
-                        // 歌曲名称
-                        Text(song.name || song.fileName || '')
-                          .fontSize(16)
-                          .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor : $r('app.color.text_color'))
-                          .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Bold : FontWeight.Normal)
-                          .maxLines(1)
-                          .textOverflow({ overflow: TextOverflow.Ellipsis })
-                          .layoutWeight(1)
-                          .margin({ top: 8, left: 12 })
-
-
-                      }
-                      .width('100%')
-
-                      // 歌手和专辑信息 - 参考LocalMusic的第二行布局
-                      Row() {
-                        // 音质标签
-                        if (song.md5Str) {
-                          Text(song.md5Str?.includes('Lossless') ? '无损' : song.md5Str)
-                            .fontSize(10)
-                            .fontColor($r('app.color.text_color'))
-                            .fontWeight(500)
-                            .padding({ top: 2, right: 6, left: 6, bottom: 2 })
-                            .borderRadius(4)
-                            .backgroundColor( '#FFC107')
-                            .visibility(song.md5Str ? Visibility.Visible : Visibility.None)
-                        }
-                          Text(song.artist)
-                            .fontSize(13)
-                            .fontColor(this.isPlaying && this.curIndex === index ?this.themeColor : $r('app.color.text_color'))
-                            .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
-                            .maxLines(1)
-                            .margin({ left: 8 })
-                            .textOverflow({ overflow: TextOverflow.Ellipsis })
-                            .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
-                          Text(song.duration)
-                            .fontSize(13)
-                            .fontColor(this.isPlaying && this.curIndex === index ? this.themeColor  : $r('app.color.text_color'))
-                            .fontWeight(this.isPlaying && this.curIndex === index ? FontWeight.Medium : FontWeight.Normal)
-                            .maxLines(1)
-                            .margin({ left: 8 })
-                            .textOverflow({ overflow: TextOverflow.Ellipsis })
-                            .opacity(this.isPlaying && this.curIndex === index ? 1.0 : 0.8)
-                      }
-                      .width('100%')
-                      .margin({ left: 12, top: 4 })
-                      .alignItems(VerticalAlign.Center)
-                    }
-                    .layoutWeight(1)
-                    .alignItems(HorizontalAlign.Start)
-                    .justifyContent(FlexAlign.Center)
-
-
-                    // 排序模式下显示上下移动按钮,否则显示更多按钮
-                    if (this.isSortMode) {
-                      Row({ space: 8 }) {
-                        // 上移按钮
-                        Text('▲')
-                          .fontSize(16)
-                          .fontColor(index === 0 ? (this.isDarkMode ? '#666666' : '#cccccc') :
-                            (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
-                          .onClick(() => {
-                            if (index > 0) {
-                              this.moveSong(index, index - 1)
-                            }
-                          })
-                          .enabled(index > 0)
-
-                        // 下移按钮
-                        Text('▼')
-                          .fontSize(16)
-                          .fontColor(index === this.songList.length - 1 ? (this.isDarkMode ? '#666666' : '#cccccc') :
-                            (this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.9, false) : this.themeColor))
-                          .onClick(() => {
-                            if (index < this.songList.length - 1) {
-                              this.moveSong(index, index + 1)
-                            }
-                          })
-                          .enabled(index < this.songList.length - 1)
-                      }
-                      .margin({ right: 15 })
-                    } else {
-                      Text('⋯')
-                        .fontSize(20)
-                        .fontColor($r('app.color.text_color'))
-                        .opacity(0.6)
-                        .margin({ right: 15 })
-                        .bindMenu(this.MoreMenuBuilder(song))
+                      })
+                      .onActionEnd(() => {
+                        this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
+                          this.scaleItem = -1
 
-                    }
-                  }
-                  .width('100%')
-                  .height(70)
-                  .justifyContent(FlexAlign.Start)
-                  .alignItems(VerticalAlign.Center)
-                }
-                .backgroundColor(Color.Transparent)
-                .height(70)
-                .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.9 })
-                .width('100%')
-                .onClick(() => {
-                  // 排序模式下禁用点击播放
-                  if (!this.isSortMode) {
-                    this.playSong(song, index)
-                  }
-                })
+                        })
 
-                .stateStyles({
-                  normal: {
-                    .backgroundColor(Color.Transparent)
-                  },
-                  pressed: {
-                    .backgroundColor(this.isSortMode ? Color.Transparent : themeColorWithAlpha(this.themeColor, 0.05, this.isDarkMode))
-                  }
-                })
-                // 当前播放歌曲的背景高亮
-                .backgroundColor(this.isPlaying && this.curIndex === index ? themeColorWithAlpha(this.themeColor, 0.08, this.isDarkMode) : Color.Transparent)
-                .border({
-                  width: { left: this.isPlaying && this.curIndex === index ? 3 : 0 },
-                  color: this.isDarkMode ? themeColorWithAlpha(this.themeColor, 0.8, this.isDarkMode) : this.themeColor
-                })
-                .opacity(this.pageOpacity)
-                .translate({ x: 0, y: this.showContent ? 0 : 20 })
-                .transition(TransitionEffect.OPACITY.animation({
-                  duration: 600,
-                  curve: Curve.EaseOut,
-                  delay: 300 + index * 30
-                }))
-                .transition(TransitionEffect.translate({ y: 20 }).animation({
-                  duration: 600,
-                  curve: Curve.EaseOut,
-                  delay: 300 + index * 30
-                }))
-              }
-              //拖动List关键代码开始
-              .scale({ x: this.scaleSelect(index), y: this.scaleSelect(index) })
-              .zIndex(this.dragItem === index ? 1 : 0)
-              .translate(this.dragItem === index ? { y: this.offsetY } : { y: 0 })
-              .gesture(
-                GestureGroup(GestureMode.Sequence,
-                  LongPressGesture({ repeat: true })
-                    .onAction((event?: GestureEvent) => {
-                      this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
-                        this.scaleItem = index
+                      }),
+                    PanGesture({ fingers: 1, direction: null, distance: 0 })
+                      .onActionStart(() => {
+                        const currentOffsetVp = this.listScroller.currentOffset().yOffset
+                        this.dragItem = index
+                        this.dragStartIndex = index
+                        this.dragTargetIndex = index
+                        this.offsetY = 0
+                        this.dragStartScrollOffset = currentOffsetVp
+                        this.dragStartTop = index * this.ITEM_INTV - currentOffsetVp
+                        this.dragOverlayTop = this.dragStartTop
+                        this.dragOverlayItem = song
+                        LogUtil.info('drag-debug',
+                          `start index=${index} currentOffsetVp=${currentOffsetVp} dragStartTop=${this.dragStartTop} listHeight=${this.listArea?.height ?? 'n/a'} listTop=${this.listArea?.position?.y ?? 'n/a'}`)
                       })
-                    })
-                    .onActionEnd(() => {
-                      this.getUIContext().animateTo({ curve: Curve.Friction, duration: 300 }, () => {
-                        this.scaleItem = -1
-
+                      .onActionUpdate((event: GestureEvent) => {
+                        const currentOffsetVp = this.listScroller.currentOffset().yOffset
+                        const scrollDeltaVp = currentOffsetVp - this.dragStartScrollOffset
+                        const gestureOffset = event.offsetY
+                        const targetOffset = gestureOffset + scrollDeltaVp
+                        this.offsetY = targetOffset
+                        this.dragOverlayTop = this.dragStartTop + gestureOffset
+                        this.updateDragTarget(targetOffset)
+                        this.autoScrollWhileDragging(targetOffset)
+                        if (event.timestamp - this.lastDragLogTime > 100000000) {
+                          this.lastDragLogTime = event.timestamp
+                          LogUtil.info('drag-debug',
+                            `update offsetY=${event.offsetY} gestureOffset=${gestureOffset} currentOffsetVp=${currentOffsetVp} scrollDeltaVp=${scrollDeltaVp} targetOffset=${targetOffset} dragOverlayTop=${this.dragOverlayTop} dragStartIndex=${this.dragStartIndex} dragTargetIndex=${this.dragTargetIndex} listHeight=${this.listArea?.height ?? 'n/a'} listTop=${this.listArea?.position?.y ?? 'n/a'}`)
+                        }
                       })
-
-                    }),
-                  PanGesture({ fingers: 1, direction: null, distance: 0 })
-                    .onActionStart(() => {
-                      this.dragItem = index // 记录当前拖动项索引
-                      this.dragRefOffset = 0 // 重置偏移基准
-                    })
-                    .onActionUpdate((event: GestureEvent) => {
-                      this.offsetY = event.offsetY - this.dragRefOffset;
-                      this.neighborItem = -1;
-                      let indexA = this.songList.indexOf(song);
-                      // LogUtil.info('onecold indexA = '+indexA +'  index=='+index)
-                      let curveValue: curves.ICurve = curves.initCurve(Curve.Sharp);
-                      let value: number = 0;
-
-                      if (this.offsetY < 0) {
-                        value = curveValue.interpolate(-this.offsetY / this.ITEM_INTV);
-                        this.neighborItem = indexA - 1;
-                        this.neighborScale = 1 - value / 20;
-                      } else if (this.offsetY > 0) {
-                        value = curveValue.interpolate(this.offsetY / this.ITEM_INTV);
-                        this.neighborItem = indexA + 1;
-                        this.neighborScale = 1 - value / 20;
-                      }
-
-
-                      if (this.offsetY > this.ITEM_INTV / 2 && indexA + 1 < this.songList.length) {
-                        this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
-                          this.offsetY -= this.ITEM_INTV;
-                          this.dragRefOffset += this.ITEM_INTV;
-                          this.itemMove(indexA, indexA + 1);
-                        });
-                      } else if (this.offsetY < -this.ITEM_INTV / 2 && indexA - 1 >= 0) {
+                      .onActionEnd((event: GestureEvent) => {
+                        const fromIndex = this.dragStartIndex
+                        const toIndex = this.dragTargetIndex
                         this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
-                          this.offsetY += this.ITEM_INTV;
-                          this.dragRefOffset -= this.ITEM_INTV;
-                          this.itemMove(indexA, indexA - 1);
-                        });
-                      }
-
+                          this.resetDragState()
+                        })
+                        this.getUIContext().animateTo({
+                          curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
+                        }, () => {
+                          this.scaleItem = -1
+                        })
+                        if (fromIndex >= 0 && toIndex >= 0 && fromIndex !== toIndex) {
+                          void this.moveSong(fromIndex, toIndex)
+                        }
+                        this.isSortMode = true
 
-                    })
-                    .onActionEnd((event: GestureEvent) => {
+                      })
 
+                  )
+                    .onCancel(() => {
                       this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
-                        this.dragItem = -1
-                        this.neighborItem = -1
+                        this.resetDragState()
                       })
                       this.getUIContext().animateTo({
                         curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
                       }, () => {
                         this.scaleItem = -1
                       })
-                      this.isSortMode = true
 
                     })
+                ) //拖动List关键代码结束
 
-                )
-                  .onCancel(() => {
-                    this.getUIContext().animateTo({ curve: curves.interpolatingSpring(0, 1, 400, 38) }, () => {
-                      this.dragItem = -1
-                      this.neighborItem = -1
-                    })
-                    this.getUIContext().animateTo({
-                      curve: curves.interpolatingSpring(14, 1, 170, 17), delay: 150
-                    }, () => {
-                      this.scaleItem = -1
-                    })
-
-                  })
-              ) //拖动List关键代码结束
+              }, (item: VideoItem) => item.filePath)
+            }
+            .onAreaChange((oldValue: Area, newValue: Area) => {
+              this.listArea = newValue
+            })
+            .width('100%')
+            .reuseId('list_item')
+            .backgroundColor($r('app.color.bg_card'))
+            .borderRadius(12)
+            .divider({
+              strokeWidth: 0.5,
+              color: '#0000001a',
+              startMargin: 80,
+              endMargin: 20
+            })
+            .opacity(this.pageOpacity)
+            .zIndex(0)
+            .scale({ x: this.contentScale, y: this.contentScale })
+            .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
+            .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
 
-            }, (item: VideoItem) => item.filePath)
+            if (this.dragOverlayItem) {
+              Column() {
+                this.buildSongContent(this.dragOverlayItem, this.dragStartIndex, true)
+              }
+              .width('100%')
+              .position({ x: 0, y: this.dragOverlayTop })
+              .zIndex(100)
+            }
           }
+          .clip(false)
           .width('100%')
           .layoutWeight(1)
-          .reuseId('list_item')
-          .backgroundColor($r('app.color.bg_card'))
           .margin({ left: 16, right: 16 })
-          .borderRadius(12)
-          .divider({
-            strokeWidth: 0.5,
-            color: '#0000001a',
-            startMargin: 80,
-            endMargin: 20
-          })
-          .opacity(this.pageOpacity)
-          .scale({ x: this.contentScale, y: this.contentScale })
-          .transition(TransitionEffect.OPACITY.animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
-          .transition(TransitionEffect.scale({ x: 0.95, y: 0.95 }).animation({ duration: 800, curve: Curve.EaseOut, delay: 200 }))
         } else {
           // 空状态
           Column() {