home_board.dart 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import 'dart:ui' as ui;
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:puzzleweave/config/device.dart';
  6. import 'package:puzzleweave/models/download.dart';
  7. import 'package:puzzleweave/models/items.dart';
  8. final Logger _log = Logger('home_board');
  9. enum HomeBoardStatus { loading, dealing, playing, done, unlocking }
  10. class HomeBoard {
  11. final Device device;
  12. // 原合集图
  13. ui.Image? image;
  14. // 纸牌背面图
  15. ui.Image? cardImage;
  16. // board 的canvas绘制区域尺寸
  17. final double canvasWidth;
  18. final double canvasHeight;
  19. /// 一张合集分为几宫格,固定5x5
  20. final int rows = 5;
  21. final int cols = 5;
  22. int get count => rows * cols;
  23. // 每个piece的逻辑尺寸
  24. double get pieceLogicalWidth => canvasWidth / cols;
  25. double get pieceLogicalHeight => canvasHeight / rows;
  26. // 用于触发重绘的通知器
  27. final ValueNotifier<int> boardNotifier = ValueNotifier<int>(1);
  28. // 用于通知外部资源已准备就绪
  29. final ValueNotifier<bool> isReadyNotifier = ValueNotifier<bool>(false);
  30. HomeBoardStatus status = HomeBoardStatus.loading;
  31. ListItem? _currentCollectionItem;
  32. // 2. 优化:记录正在加载的 ID,解决异步竞态问题
  33. String? _loadingImageUrl;
  34. ListItem? get currentCollectionItem => _currentCollectionItem;
  35. set currentCollectionItem(ListItem? item) {
  36. if (item == null) return;
  37. if (_currentCollectionItem?.id != item.id) {
  38. _currentCollectionItem = item;
  39. _loadImage();
  40. }
  41. }
  42. Offset _unlockTargetOffset = Offset.zero;
  43. double _unlockTargetScale = 1.0;
  44. Offset get unlockTargetOffset => _unlockTargetOffset;
  45. double get unlockTargetScale => _unlockTargetScale;
  46. void setUnlockAnimationTarget({required Offset targetOffset, required double targetScale}) {
  47. _unlockTargetOffset = targetOffset;
  48. _unlockTargetScale = targetScale;
  49. }
  50. HomeBoard({required this.canvasWidth, required this.canvasHeight, required this.device}) {
  51. _loadCardImage();
  52. }
  53. void invalidate() {
  54. boardNotifier.value++;
  55. }
  56. // 3. 核心改进:带竞态检查和资源释放的图片加载
  57. Future<void> _loadImage() async {
  58. if (_currentCollectionItem == null) return;
  59. final String currentId = _currentCollectionItem!.id;
  60. _loadingImageUrl = currentId; // 记录当前请求的 ID
  61. // 如果需要切换时立即白屏,可以取消下面注释:
  62. // _clearImage();
  63. try {
  64. double dpr = device.effectivePixelRatio;
  65. ItemLoader itemLoader = ItemLoader.load(_currentCollectionItem!, device.suggestedQuality);
  66. // 异步获取图片
  67. ui.Image? loadedImage = await itemLoader.getImageBySize((canvasWidth * dpr).round(), (canvasHeight * dpr).round());
  68. // --- 关键判断:竞态条件处理 ---
  69. // 如果图片回来时,用户已经切换到了下一个合集,则丢弃当前图片并释放内存
  70. if (_loadingImageUrl != currentId) {
  71. _log.info('丢弃已过时的图片加载结果: $currentId');
  72. loadedImage.dispose();
  73. return;
  74. }
  75. // 4. 修改:在赋值新图前,先安全释放旧图内存
  76. if (image != null) {
  77. image!.dispose();
  78. }
  79. image = loadedImage;
  80. isReadyNotifier.value = true;
  81. invalidate();
  82. _log.info('成功加载合集图片: $currentId');
  83. } catch (e) {
  84. _log.severe('加载合集图片失败: $e');
  85. isReadyNotifier.value = false;
  86. }
  87. }
  88. // 5. 修改:安全加载卡片背面图
  89. Future<void> _loadCardImage() async {
  90. try {
  91. double dpr = device.realPixelRatio;
  92. final Size bestCardSize = Size(pieceLogicalWidth * dpr, pieceLogicalHeight * dpr);
  93. final ByteData cardData = await rootBundle.load('assets/images/backcard_green.png');
  94. final ui.Codec cardCodec = await ui.instantiateImageCodec(
  95. cardData.buffer.asUint8List(),
  96. targetWidth: bestCardSize.width.round(),
  97. targetHeight: bestCardSize.height.round(),
  98. );
  99. final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame();
  100. cardImage = cardFrameInfo.image;
  101. invalidate();
  102. } catch (e) {
  103. _log.severe('加载卡片背面图失败: $e');
  104. }
  105. }
  106. // 6. 新增:彻底释放所有图片内存,防止 OOM
  107. void dispose() {
  108. _clearImage();
  109. if (cardImage != null) {
  110. cardImage!.dispose();
  111. cardImage = null;
  112. }
  113. boardNotifier.dispose();
  114. isReadyNotifier.dispose();
  115. }
  116. void _clearImage() {
  117. if (image != null) {
  118. image!.dispose();
  119. image = null;
  120. }
  121. isReadyNotifier.value = false;
  122. }
  123. void switchToNextCollection(ListItem newItem) {
  124. _log.info('切换到新的合集: ${newItem.id}');
  125. // 切换状态,让 UI 进入 loading
  126. status = HomeBoardStatus.loading;
  127. currentCollectionItem = newItem;
  128. }
  129. }