Преглед изворни кода

WebDavMainPage加载大量歌曲优化

onecold пре 4 месеци
родитељ
комит
c33973173f
2 измењених фајлова са 116 додато и 58 уклоњено
  1. 108 41
      entry/src/main/ets/pages/WebDavMainPage.ets
  2. 8 17
      entry/src/main/ets/view/LocalMusic.ets

+ 108 - 41
entry/src/main/ets/pages/WebDavMainPage.ets

@@ -37,6 +37,8 @@ import { WebDavUrlUtil } from '../common/util/WebDavUrlUtil';
 import { SearchHistoryUtil } from '../common/util/SearchHistoryUtil';
 import { PlayingIndicator } from '../view/PlayingIndicator';
 import { PlaylistPlayRequest, savePendingPlaylistPlay } from '../common/util/PlaylistPlayRequestStore';
+import { buildWebDavVisibleThumbPrefetchWindow, WebDavThumbPrefetchRange } from '../common/util/WebDavThumbPrefetchHelper';
+import { buildWebDavSongItemRenderKey } from '../common/util/WebDavListRenderHelper';
 
 interface WebDavMetadataUpdatePayload {
   filePath: string;
@@ -54,15 +56,12 @@ interface RemoteThumbSource {
 
 const TAG = 'heanup WebDavMainPage';
 const REMOTE_THUMB_CACHE_DIR: string = 'remote_thumbs';
-const REMOTE_THUMB_MAX_TASK_COUNT: number = 12;
-const REMOTE_THUMB_LARGE_LIST_THRESHOLD: number = 120;
-const REMOTE_THUMB_LARGE_LIST_PREFETCH_COUNT: number = 6;
-const REMOTE_THUMB_LARGE_LIST_DELAY_MS: number = 900;
 const REMOTE_THUMB_CAPTURE_SECONDS: string = '1.2';
 const WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD: number = 180;
 const WEBDAV_PROGRESSIVE_RELOAD_INITIAL_COUNT: number = 20;
 const WEBDAV_PROGRESSIVE_RELOAD_BATCH_SIZE: number = 20;
 const WEBDAV_PROGRESSIVE_RELOAD_DELAY_MS: number = 32;
+const WEBDAV_VISIBLE_THUMB_PREFETCH_DELAY_MS: number = 120;
 
 // WebDAV歌曲数据全局内存存储
 let globalWebdavVideoItems: VideoItem[] = [];
@@ -144,7 +143,6 @@ export struct WebDavMainPage {
   @State isShowFileName: boolean = false//是否显示文件名
   @State isLongNameRoLL: boolean = true//长歌名滚动
   @State sortType: number = 0 //默认排序方式
-  @State listRefreshKey: number = 0 // 列表刷新标识
   @Consume isMultiSelect: boolean
   @State selectedSongs: VideoItem[] = []
   @State selectedFolders: FileInfo[] = []
@@ -164,6 +162,11 @@ export struct WebDavMainPage {
   private readonly metadataUpdateEvent: emitter.InnerEvent = { eventId: EventConstants.EVENT_WEBDAV_METADATA_UPDATED };
   private thumbnailTaskToken: number = 0;
   private thumbnailRunningKeys: Set<string> = new Set<string>();
+  private visibleListStartIndex: number = -1;
+  private visibleListEndIndex: number = -1;
+  private scheduledThumbPrefetchTimer: number = -1;
+  private lastVisibleThumbStart: number = -1;
+  private lastVisibleThumbEndExclusive: number = -1;
   private readonly downloadCenterManager: DownloadCenterManager = DownloadCenterManager.getInstance();
   private readonly downloadCenterListener: () => void = (): void => {
     this.scheduleDownloadCenterRefresh();
@@ -186,8 +189,7 @@ export struct WebDavMainPage {
   async onSwitchAccount(){
     console.log('heanup 切换账户:', this.selectedAccount.name);
     this.cancelPendingDirectoryRefresh();
-    this.thumbnailTaskToken += 1;
-    this.thumbnailRunningKeys.clear();
+    this.resetRemoteThumbPrefetchState();
     this.searchTicket++;
     this.searchText = '';
     this.filteredList = [];
@@ -220,6 +222,7 @@ export struct WebDavMainPage {
 
   private refreshDisplaySongs(mList: Array<VideoItem>, shouldScrollToTop: boolean = true): void {
     const reloadToken: number = ++this.displayReloadToken;
+    this.invalidateRemoteThumbPrefetchRange()
     const displayItems: Array<VideoItem> = mList ? [...mList] : [];
     if (displayItems.length <= WEBDAV_PROGRESSIVE_RELOAD_THRESHOLD) {
       this.dataSource.pushArrayData(displayItems)
@@ -550,8 +553,39 @@ export struct WebDavMainPage {
     if (dataIndex >= 0) {
       this.dataSource.notifyDataChange(dataIndex);
     }
-    // 旧版本这里会额外刷新列表 key,ArkUI 长列表在异步封面回填时更稳定。
-    this.listRefreshKey += 1;
+  }
+
+  private getDisplayedFolderCount(): number {
+    return this.isSearchMode ? this.filteredFolderList.length : this.visibleFoldersState.length
+  }
+
+  private stopScheduledRemoteThumbPrefetch(): void {
+    if (this.scheduledThumbPrefetchTimer >= 0) {
+      clearTimeout(this.scheduledThumbPrefetchTimer)
+      this.scheduledThumbPrefetchTimer = -1
+    }
+  }
+
+  // 保留当前视口索引,但让新的列表内容可以重新触发同一可见区间的封面补全。
+  private invalidateRemoteThumbPrefetchRange(): void {
+    this.stopScheduledRemoteThumbPrefetch()
+    this.thumbnailTaskToken += 1
+    this.thumbnailRunningKeys.clear()
+    this.lastVisibleThumbStart = -1
+    this.lastVisibleThumbEndExclusive = -1
+  }
+
+  // 列表数据源切换时需要显式取消旧任务,避免后台继续为已经离开的可见区生成封面。
+  private resetRemoteThumbPrefetchState(): void {
+    this.invalidateRemoteThumbPrefetchRange()
+    this.visibleListStartIndex = -1
+    this.visibleListEndIndex = -1
+  }
+
+  private updateVisibleRemoteThumbRange(start: number, end: number): void {
+    this.visibleListStartIndex = start
+    this.visibleListEndIndex = end
+    this.scheduleRemoteThumbPrefetch()
   }
 
   private async prepareRemoteThumbSource(item: VideoItem): Promise<RemoteThumbSource> {
@@ -671,22 +705,44 @@ export struct WebDavMainPage {
   }
 
   private scheduleRemoteThumbPrefetch(): void {
-    const token: number = this.thumbnailTaskToken + 1;
-    this.thumbnailTaskToken = token;
-    this.thumbnailRunningKeys.clear();
-    const mediaItems: VideoItem[] = this.songs.slice();
-    if (mediaItems.length <= 0) {
-      return;
+    if (this.isLoading || this.isRefreshingCache) {
+      this.stopScheduledRemoteThumbPrefetch()
+      this.thumbnailTaskToken += 1
+      this.thumbnailRunningKeys.clear()
+      return
+    }
+    const totalSongs: number = this.dataSource.totalCount()
+    const visibleWindow = buildWebDavVisibleThumbPrefetchWindow(totalSongs, this.getDisplayedFolderCount(),
+      this.visibleListStartIndex, this.visibleListEndIndex)
+    if (!visibleWindow.hasVisibleSongs) {
+      this.stopScheduledRemoteThumbPrefetch()
+      this.thumbnailTaskToken += 1
+      this.thumbnailRunningKeys.clear()
+      return
     }
-    const isLargeList: boolean = mediaItems.length >= REMOTE_THUMB_LARGE_LIST_THRESHOLD;
-    const limitCount: number = isLargeList ?
-      Math.min(mediaItems.length, REMOTE_THUMB_LARGE_LIST_PREFETCH_COUNT) :
-      Math.min(mediaItems.length, REMOTE_THUMB_MAX_TASK_COUNT);
-    const targetItems: VideoItem[] = mediaItems.slice(0, limitCount);
-    const delayMs: number = isLargeList ? REMOTE_THUMB_LARGE_LIST_DELAY_MS : 120;
-    setTimeout((): void => {
-      void this.runRemoteThumbPrefetchQueue(targetItems, token);
-    }, delayMs);
+    const range: WebDavThumbPrefetchRange = visibleWindow.range
+    const isSameVisibleRange: boolean = range.start === this.lastVisibleThumbStart &&
+      range.endExclusive === this.lastVisibleThumbEndExclusive
+    if (isSameVisibleRange) {
+      return
+    }
+    this.lastVisibleThumbStart = range.start
+    this.lastVisibleThumbEndExclusive = range.endExclusive
+    this.stopScheduledRemoteThumbPrefetch()
+    const token: number = this.thumbnailTaskToken + 1
+    this.thumbnailTaskToken = token
+    this.thumbnailRunningKeys.clear()
+    this.scheduledThumbPrefetchTimer = setTimeout((): void => {
+      this.scheduledThumbPrefetchTimer = -1
+      if (token !== this.thumbnailTaskToken) {
+        return
+      }
+      const mediaItems: VideoItem[] = this.dataSource.dataArray.slice(range.start, range.endExclusive)
+      if (mediaItems.length <= 0) {
+        return
+      }
+      void this.runRemoteThumbPrefetchQueue(mediaItems, token)
+    }, WEBDAV_VISIBLE_THUMB_PREFETCH_DELAY_MS)
   }
 
   private async runRemoteThumbPrefetchQueue(items: VideoItem[], token: number): Promise<void> {
@@ -1117,9 +1173,9 @@ export struct WebDavMainPage {
     if (this.isSearchMode && this.searchText.length > 0) {
       this.syncSelectionAfterRefresh()
     } else {
-      this.listRefreshKey += 1
       this.syncSelectionAfterRefresh()
     }
+    this.invalidateRemoteThumbPrefetchRange()
     this.scheduleRemoteThumbPrefetch()
   }
 
@@ -2161,8 +2217,7 @@ export struct WebDavMainPage {
   aboutToDisappear(): void {
     // 取消订阅
     this.cancelPendingDirectoryRefresh();
-    this.thumbnailTaskToken += 1;
-    this.thumbnailRunningKeys.clear();
+    this.resetRemoteThumbPrefetchState();
     this.webdavManager.unsubscribe(this.eventHandler);
     this.downloadCenterManager.unsubscribe(this.downloadCenterListener);
     this.stopDownloadCenterRefresh();
@@ -2173,6 +2228,7 @@ export struct WebDavMainPage {
   private handleWebdavEvent(event: string): void {
     switch (event) {
       case RemoteDriveManagerStates.LoadFilesInfoStart:
+        this.resetRemoteThumbPrefetchState();
         if (this.dataSource.totalCount() > 0 || this.visibleFoldersState.length > 0) {
           this.isLoading = false;
           this.isRefreshingCache = true;
@@ -2183,6 +2239,7 @@ export struct WebDavMainPage {
         break;
       case RemoteDriveManagerStates.LoadFilesInfoSucceed:
         const shouldScrollToTop: boolean = !this.isRefreshingCache;
+        this.resetRemoteThumbPrefetchState();
         this.songs = this.webdavManager.webDavSongs;
         // 直接引用webdavManager的数组,避免@Observed序列化问题
         this.webDavFiles = this.webdavManager.webDavFiles;
@@ -2194,7 +2251,6 @@ export struct WebDavMainPage {
         this.updateVisibleFolders();
         this.breadcrumbs = this.webdavManager.getBreadcrumbs();
         this.syncSelectionAfterRefresh();
-        this.scheduleRemoteThumbPrefetch();
         if (this.isSearchMode && this.searchText.length > 0) {
           void this.applyGlobalSearch(this.searchText, ++this.searchTicket);
         } else {
@@ -2245,36 +2301,42 @@ export struct WebDavMainPage {
       return;
     }
     Logger.info(TAG, 'handleWebDavMetadataUpdates start');
-    let hasChanges = false;
+    const changedIndices: number[] = [];
     for (let i = 0; i < payloads.length; i++) {
       const payload = payloads[i];
       if (!payload || !payload.filePath) {
         continue;
       }
       Logger.info(TAG, `WebDav metadata detail path=${payload.pixelMapPath ?? 'null'}`);
-      const songUpdated = this.updateSongMetadata(payload);
-      if (songUpdated) {
-        hasChanges = true;
+      const changedIndex = this.updateSongMetadata(payload);
+      if (changedIndex >= 0) {
+        if (changedIndices.indexOf(changedIndex) < 0) {
+          changedIndices.push(changedIndex);
+        }
         Logger.info(TAG, `metadata updated for ${payload.filePath}`);
       }
     }
-    if (!hasChanges) {
+    if (changedIndices.length === 0) {
       Logger.info(TAG, 'handleWebDavMetadataUpdates no changes detected');
       return;
     }
-    // 更新dataSource的数据,不滚动,只触发刷新
-    this.dataSource.pushArrayData(this.songs);
-    this.listRefreshKey++;
+    // 元数据回填只刷新变更行,避免后台补封面时整列表反复重建导致卡顿。
+    for (let index = 0; index < changedIndices.length; index += 1) {
+      const changedIndex = changedIndices[index];
+      if (changedIndex >= 0 && changedIndex < this.dataSource.totalCount()) {
+        this.dataSource.notifyDataChange(changedIndex);
+      }
+    }
     Logger.info(TAG, 'handleWebDavMetadataUpdates trigger refresh');
   }
 
-  private updateSongMetadata(payload: WebDavMetadataUpdatePayload): boolean {
+  private updateSongMetadata(payload: WebDavMetadataUpdatePayload): number {
     if (!payload.filePath) {
-      return false;
+      return -1;
     }
     const targetIndex = this.findSongIndex(payload.filePath);
     if (targetIndex < 0) {
-      return false;
+      return -1;
     }
     const targetSong = this.songs[targetIndex];
     let mutated = false;
@@ -2293,7 +2355,7 @@ export struct WebDavMainPage {
       targetSong.artist = payload.artist;
       mutated = true;
     }
-    return mutated;
+    return mutated ? targetIndex : -1;
   }
 
   private findSongIndex(filePath: string): number {
@@ -3638,9 +3700,14 @@ export struct WebDavMainPage {
                 edgeEffect: SwipeEdgeEffect.None
               })
             }
-          }, (item: VideoItem, index: number) =>  item.filePath + '_' + index+this.listRefreshKey)
+          }, (item: VideoItem, index: number) => {
+            return buildWebDavSongItemRenderKey(item.filePath, index, item.pixelMapPath)
+          })
         }
         .scrollBar(BarState.Off)
+        .onScrollIndex((start: number, end: number) => {
+          this.updateVisibleRemoteThumbRange(start, end)
+        })
         .onScrollFrameBegin((offset: number) => {
           // 获取当前滚动偏移量
           if(this.autoHideTitle){

+ 8 - 17
entry/src/main/ets/view/LocalMusic.ets

@@ -5904,22 +5904,13 @@ export struct LocalMusic {
   private DirItem(item: VideoItem, index?: number) {
     Button({ type: ButtonType.Capsule, stateEffect: true }) {
       Row() {
-        Image(this.modeType !== 0 && StrUtil.isNotEmpty(item.pixelMapPath) ?
-          this.getListImageSource(item, index ?? -1, this.coverThumbVersion) :
-          $r('app.media.music_group'))
-          .height(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 50 : 40)
-          .width(this.twoFingerType == 3 ? 55 : this.twoFingerType == 2 ? 50 : 40)
-          .fillColor(this.themeColor)// .fontSize(33)
-          .alt($r('app.media.music_group'))
-          .borderRadius(this.isCoverRectangle?9:'100%')
-          .clip(true)
-          .shadow({
-            radius: 2,
-            type: ShadowType.BLUR,
-            color: 'on_primary'
-          })
-          .sourceSize({width:38, height:38})
-          .margin({ left: 20, top: 8, bottom: 8 })
+        SymbolGlyph($r('sys.symbol.folder'))
+          .fontSize(this.twoFingerType==3?48:this.twoFingerType==2?42:38)
+          .fontColor([this.themeColor])
+          .alignSelf(ItemAlign.Center)
+          // .height(this.twoFingerType==3?46:this.twoFingerType==2?40:35)
+          // .width(this.twoFingerType==3?46:this.twoFingerType==2?40:35)
+          .margin({ left: 28 ,top:8,bottom:8})
         Column() {
           Text(item.name.startsWith('.') ? item.name.replace(/\./g, '') : item.name)
             .fontSize(this.twoFingerType == 1 ? 12 : this.twoFingerType == 2 ? 15 : 18)
@@ -14605,7 +14596,7 @@ export struct LocalMusic {
     .width(this.isLandscape?'83%':'90%')
     .scale({ x: this.scaleValueText, y: this.scaleValueText }) // 添加缩放效果
     .opacity(this.playDragUiOpacity)
-    .margin({ top:this.isCoverRectangle?10: 0 })
+    .margin({ top:this.isCoverRectangle?15: 0 })
     .visibility(this.isCoverOpacity() || isHidden ? Visibility.None : Visibility.Visible)
   }