import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:puzzleweave/config/device.dart'; import 'package:puzzleweave/models/download.dart'; import 'package:puzzleweave/models/items.dart'; final Logger _log = Logger('home_board'); enum HomeBoardStatus { loading, dealing, playing, done, unlocking } class HomeBoard { final Device device; // 原合集图 ui.Image? image; // 纸牌背面图 ui.Image? cardImage; // board 的canvas绘制区域尺寸 final double canvasWidth; final double canvasHeight; /// 一张合集分为几宫格,固定5x5 final int rows = 5; final int cols = 5; int get count => rows * cols; // 每个piece的逻辑尺寸 double get pieceLogicalWidth => canvasWidth / cols; double get pieceLogicalHeight => canvasHeight / rows; // 用于触发重绘的通知器 final ValueNotifier boardNotifier = ValueNotifier(1); // 用于通知外部资源已准备就绪 final ValueNotifier isReadyNotifier = ValueNotifier(false); HomeBoardStatus status = HomeBoardStatus.loading; ListItem? _currentCollectionItem; // 2. 优化:记录正在加载的 ID,解决异步竞态问题 String? _loadingImageUrl; ListItem? get currentCollectionItem => _currentCollectionItem; set currentCollectionItem(ListItem? item) { if (item == null) return; if (_currentCollectionItem?.id != item.id) { _currentCollectionItem = item; _loadImage(); } } Offset _unlockTargetOffset = Offset.zero; double _unlockTargetScale = 1.0; Offset get unlockTargetOffset => _unlockTargetOffset; double get unlockTargetScale => _unlockTargetScale; void setUnlockAnimationTarget({required Offset targetOffset, required double targetScale}) { _unlockTargetOffset = targetOffset; _unlockTargetScale = targetScale; } HomeBoard({required this.canvasWidth, required this.canvasHeight, required this.device}) { _loadCardImage(); } void invalidate() { boardNotifier.value++; } // 3. 核心改进:带竞态检查和资源释放的图片加载 Future _loadImage() async { if (_currentCollectionItem == null) return; final String currentId = _currentCollectionItem!.id; _loadingImageUrl = currentId; // 记录当前请求的 ID // 如果需要切换时立即白屏,可以取消下面注释: // _clearImage(); try { double dpr = device.effectivePixelRatio; ItemLoader itemLoader = ItemLoader.load(_currentCollectionItem!, device.suggestedQuality); // 异步获取图片 ui.Image? loadedImage = await itemLoader.getImageBySize((canvasWidth * dpr).round(), (canvasHeight * dpr).round()); // --- 关键判断:竞态条件处理 --- // 如果图片回来时,用户已经切换到了下一个合集,则丢弃当前图片并释放内存 if (_loadingImageUrl != currentId) { _log.info('丢弃已过时的图片加载结果: $currentId'); loadedImage.dispose(); return; } // 4. 修改:在赋值新图前,先安全释放旧图内存 if (image != null) { image!.dispose(); } image = loadedImage; isReadyNotifier.value = true; invalidate(); _log.info('成功加载合集图片: $currentId'); } catch (e) { _log.severe('加载合集图片失败: $e'); isReadyNotifier.value = false; } } // 5. 修改:安全加载卡片背面图 Future _loadCardImage() async { try { double dpr = device.realPixelRatio; final Size bestCardSize = Size(pieceLogicalWidth * dpr, pieceLogicalHeight * dpr); final ByteData cardData = await rootBundle.load('assets/images/backcard_green.png'); final ui.Codec cardCodec = await ui.instantiateImageCodec( cardData.buffer.asUint8List(), targetWidth: bestCardSize.width.round(), targetHeight: bestCardSize.height.round(), ); final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame(); cardImage = cardFrameInfo.image; invalidate(); } catch (e) { _log.severe('加载卡片背面图失败: $e'); } } // 6. 新增:彻底释放所有图片内存,防止 OOM void dispose() { _clearImage(); if (cardImage != null) { cardImage!.dispose(); cardImage = null; } boardNotifier.dispose(); isReadyNotifier.dispose(); } void _clearImage() { if (image != null) { image!.dispose(); image = null; } isReadyNotifier.value = false; } void switchToNextCollection(ListItem newItem) { _log.info('切换到新的合集: ${newItem.id}'); // 切换状态,让 UI 进入 loading status = HomeBoardStatus.loading; currentCollectionItem = newItem; } }