Pārlūkot izejas kodu

针对List和Grid和WaterFlow滚动的掉帧卡顿问题优化

onecold 5 mēneši atpakaļ
vecāks
revīzija
02eddfe4a0

+ 205 - 0
entry/src/main/ets/common/util/CoverThumbCache.ets

@@ -0,0 +1,205 @@
+import { common } from '@kit.AbilityKit';
+import { fileIo, fileUri } from '@kit.CoreFileKit';
+import { image } from '@kit.ImageKit';
+import { FileUtil, MD5, StrUtil } from '@pura/harmony-utils';
+import Logger from './Logger';
+
+interface CoverThumbJob {
+  cacheKey: string;
+  sourcePath: string;
+  targetSize: number;
+}
+
+interface FileHandleLike {
+  fd: number;
+}
+
+const TAG = 'CoverThumbCache';
+
+export class CoverThumbCache {
+  private context?: common.UIAbilityContext;
+  private thumbMap: Map<string, string> = new Map();
+  private pending: Set<string> = new Set();
+  private queue: Array<CoverThumbJob> = [];
+  private workerCount: number = 0;
+  private refreshTimer: number = 0;
+  private notifyThumbReady?: () => void;
+  private readonly maxWorkers: number;
+  private readonly maxEntries: number;
+  private dirPath: string = '';
+  private dirReady: boolean = false;
+
+  constructor(maxWorkers: number = 1, maxEntries: number = 420) {
+    this.maxWorkers = Math.max(1, maxWorkers);
+    this.maxEntries = Math.max(64, maxEntries);
+  }
+
+  setContext(context: common.UIAbilityContext): void {
+    this.context = context;
+  }
+
+  setOnThumbReady(notify: () => void): void {
+    this.notifyThumbReady = notify;
+  }
+
+  dispose(): void {
+    if (this.refreshTimer) {
+      clearTimeout(this.refreshTimer);
+      this.refreshTimer = 0;
+    }
+    this.queue = [];
+    this.pending.clear();
+    this.workerCount = 0;
+  }
+
+  clearPending(): void {
+    this.queue = [];
+    this.pending.clear();
+  }
+
+  isRemoteImageSource(sourceUri: string): boolean {
+    const lower = sourceUri.toLowerCase();
+    return lower.startsWith('http://') || lower.startsWith('https://');
+  }
+
+  getThumbUri(sourceUri: string, targetSize: number): string | undefined {
+    return this.thumbMap.get(this.buildCacheKey(sourceUri, targetSize));
+  }
+
+  ensureThumbRequested(sourceUri: string, targetSize: number): void {
+    if (!this.context || StrUtil.isEmpty(sourceUri) || this.isRemoteImageSource(sourceUri)) {
+      return;
+    }
+    const cacheKey = this.buildCacheKey(sourceUri, targetSize);
+    if (this.thumbMap.has(cacheKey) || this.pending.has(cacheKey)) {
+      return;
+    }
+    const sourcePath = this.normalizeLocalImagePath(sourceUri);
+    if (StrUtil.isEmpty(sourcePath) || !FileUtil.accessSync(sourcePath)) {
+      return;
+    }
+    this.pending.add(cacheKey);
+    if (this.queue.length > 220) {
+      this.queue.shift();
+    }
+    this.queue.push({
+      cacheKey,
+      sourcePath,
+      targetSize
+    });
+    this.processQueue();
+  }
+
+  private buildCacheKey(sourceUri: string, targetSize: number): string {
+    return `${sourceUri}|${targetSize}`;
+  }
+
+  private normalizeLocalImagePath(sourceUri: string): string {
+    if (StrUtil.isEmpty(sourceUri)) {
+      return '';
+    }
+    if (sourceUri.startsWith('file://')) {
+      let normalized = sourceUri.substring('file://'.length);
+      try {
+        normalized = decodeURIComponent(normalized);
+      } catch (_e) {
+      }
+      return normalized;
+    }
+    return sourceUri;
+  }
+
+  private ensureThumbDir(): string {
+    if (this.dirReady && StrUtil.isNotEmpty(this.dirPath)) {
+      return this.dirPath;
+    }
+    const nextDir = this.context!.filesDir + FileUtil.separator + 'cover_thumb_cache';
+    if (!FileUtil.accessSync(nextDir)) {
+      fileIo.mkdirSync(nextDir);
+    }
+    this.dirPath = nextDir;
+    this.dirReady = true;
+    return nextDir;
+  }
+
+  private processQueue(): void {
+    if (this.workerCount >= this.maxWorkers) {
+      return;
+    }
+    const job = this.queue.shift();
+    if (!job) {
+      return;
+    }
+    this.workerCount++;
+    void this.buildThumb(job).then((thumbUri?: string) => {
+      if (StrUtil.isNotEmpty(thumbUri)) {
+        this.thumbMap.set(job.cacheKey, thumbUri!);
+        while (this.thumbMap.size > this.maxEntries) {
+          const oldest = this.thumbMap.keys().next().value as string | undefined;
+          if (!oldest) {
+            break;
+          }
+          this.thumbMap.delete(oldest);
+        }
+        this.scheduleNotify();
+      }
+    }).catch((err: Error) => {
+      Logger.warn(TAG, `buildThumb failed: ${err.message}`);
+    }).finally(() => {
+      this.pending.delete(job.cacheKey);
+      this.workerCount = Math.max(this.workerCount - 1, 0);
+      this.processQueue();
+    });
+  }
+
+  private scheduleNotify(): void {
+    if (this.refreshTimer) {
+      return;
+    }
+    this.refreshTimer = setTimeout(() => {
+      this.refreshTimer = 0;
+      this.notifyThumbReady?.();
+    }, 150);
+  }
+
+  private async buildThumb(job: CoverThumbJob): Promise<string | undefined> {
+    let srcFile: object | undefined;
+    let outFile: object | undefined;
+    try {
+      const thumbDir = this.ensureThumbDir();
+      const hashName = await MD5.digestSync(`${job.cacheKey}|thumb`);
+      const thumbPath = `${thumbDir}${FileUtil.separator}${hashName}.jpg`;
+      if (FileUtil.accessSync(thumbPath)) {
+        return fileUri.getUriFromPath(thumbPath);
+      }
+
+      srcFile = fileIo.openSync(job.sourcePath, fileIo.OpenMode.READ_ONLY);
+      const imageSource = image.createImageSource((srcFile as FileHandleLike).fd);
+      const decodeOpts: image.DecodingOptions = { editable: false };
+      const pixelMap = await imageSource.createPixelMap(decodeOpts);
+      const ratio = job.targetSize <= 160 ? 0.26 : (job.targetSize <= 220 ? 0.38 : 0.50);
+      try {
+        await pixelMap.scale(ratio, ratio);
+      } catch (_scaleErr) {
+      }
+
+      const imagePacker = image.createImagePacker();
+      const packOpt: image.PackingOption = { format: 'image/jpeg', quality: 68 };
+      const imageData = await imagePacker.packing(pixelMap, packOpt);
+      outFile = fileIo.openSync(thumbPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.TRUNC);
+      await fileIo.write((outFile as FileHandleLike).fd, imageData);
+      return fileUri.getUriFromPath(thumbPath);
+    } catch (err) {
+      const error = err as Error;
+      Logger.warn(TAG, `buildThumb error: ${error.message}`);
+      return undefined;
+    } finally {
+      if (srcFile) {
+        fileIo.closeSync((srcFile as FileHandleLike).fd);
+      }
+      if (outFile) {
+        fileIo.closeSync((outFile as FileHandleLike).fd);
+      }
+    }
+  }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 306 - 261
entry/src/main/ets/view/LocalMusic.ets


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels