board_play.dart 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'dart:ui' as ui;
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:fluttertoast/fluttertoast.dart';
  8. import 'package:logging/logging.dart';
  9. import 'package:provider/provider.dart';
  10. import 'package:puzzleweave/audio/jc_audio_controller.dart';
  11. import 'package:puzzleweave/config/device.dart';
  12. import 'package:puzzleweave/firebase/adjust_helper.dart';
  13. import 'package:puzzleweave/firebase/firebase_helper.dart';
  14. import 'package:puzzleweave/l10n/app_localizations.dart';
  15. import 'package:puzzleweave/models/download.dart';
  16. import 'package:puzzleweave/models/items.dart';
  17. import 'package:puzzleweave/persistence/persistence.dart';
  18. import 'package:puzzleweave/play/board.dart';
  19. import 'package:puzzleweave/play/board_painter.dart';
  20. import 'package:puzzleweave/play/confetti_layer.dart';
  21. import 'package:puzzleweave/play/overlayer.dart';
  22. import 'package:puzzleweave/play/piece.dart';
  23. import 'package:puzzleweave/rating/rating_helper.dart';
  24. import 'package:puzzleweave/rating/rating_utils.dart';
  25. import 'package:puzzleweave/settings/settings_controller.dart';
  26. import 'package:puzzleweave/settings/settings_dialog.dart';
  27. import 'package:puzzleweave/skin/skin.dart';
  28. import 'package:puzzleweave/statistics/statistics.dart';
  29. import 'package:puzzleweave/utils/memory_monitor.dart';
  30. import 'package:puzzleweave/utils/mybutton.dart';
  31. import 'package:puzzleweave/utils/utils.dart';
  32. import 'package:vector_math/vector_math.dart' as vmath;
  33. import 'package:vibration/vibration.dart';
  34. import '../ads/ads_state.dart';
  35. import '../config/config.dart';
  36. final Logger _log = Logger('board_play.dart');
  37. enum MoveType { group, single }
  38. enum Action { revert, swap }
  39. class BoardPlay extends StatefulWidget {
  40. final ListItem item;
  41. final bool firstRun;
  42. final bool reset;
  43. final String tag;
  44. const BoardPlay({super.key, required this.item, this.firstRun = false, this.reset = false, this.tag = 'home'});
  45. @override
  46. State<StatefulWidget> createState() => _BoardPlayState();
  47. static PageRouteBuilder buildRoute(ListItem item, {bool firstRun = false, bool reset = false}) {
  48. return PageRouteBuilder(
  49. pageBuilder: (context, animation, secondaryAnimation) => BoardPlay(item: item, firstRun: firstRun, reset: reset),
  50. transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: child),
  51. );
  52. }
  53. }
  54. class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin {
  55. final GlobalKey boardKey = GlobalKey();
  56. Board? board;
  57. bool _isLoading = true;
  58. int progress = 0;
  59. bool isDownloadSlow = false;
  60. late Timer timer;
  61. // ⚠️ 新增:banner 延迟加载标记
  62. bool _bannerDelayedLoad = false;
  63. late ItemLoader itemLoader;
  64. late JcAudioController audio;
  65. late SettingsController settings;
  66. late ConfettiLayer confettiLayer;
  67. ui.Image? _fingerImage;
  68. OverLayer? _overLayer;
  69. Piece? _draggingPiece;
  70. List<MoveItem>? moveItems;
  71. // ✅ 优化:资源清理定时器
  72. Timer? _resourceCleanupTimer;
  73. // 动画控制器
  74. late AnimationController _moveAnimationController;
  75. late AnimationController _mergeAnimationController;
  76. late Animation<double> _mergeScaleAnimation;
  77. List<PieceGroup>? _mergeGroups;
  78. bool showDealing = true;
  79. late AnimationController _prepareAnimationController;
  80. late AnimationController dealingAnimationController;
  81. late AnimationController flipAnimationController;
  82. late Animation<double> _dealingAnimation;
  83. Timer? _dealingPeriodicTimer;
  84. int _dealingCount = 0;
  85. late AnimationController _successAnimationController;
  86. late Animation<double> _offsetAnimation;
  87. late Animation<double> _bottomSlideAnimation;
  88. late Animation<double> _topSlideAnimation;
  89. late AnimationController _hardModeBannerController;
  90. late Animation<double> _bannerScaleAnimation;
  91. late Animation<double> _bannerFadeAnimation;
  92. bool _showHardModeBanner = false;
  93. // ✅ 保留:重试标记,防止无限循环
  94. bool _hasRetried = false;
  95. @override
  96. initState() {
  97. super.initState();
  98. MemoryMonitor.logMemoryUsage('BoardPlay initState');
  99. final Device device = context.read<Device>();
  100. itemLoader = ItemLoader.load(widget.item, device.suggestedQuality);
  101. _onProgressUpdate();
  102. itemLoader.progress.addListener(_onProgressUpdate);
  103. timer = Timer(const Duration(seconds: 5), () {
  104. if (mounted && progress < 50) {
  105. if (progress <= 1) {
  106. Fluttertoast.showToast(
  107. msg: AppLocalizations.of(context)!.networkNotGood,
  108. toastLength: Toast.LENGTH_SHORT,
  109. gravity: ToastGravity.CENTER,
  110. timeInSecForIosWeb: 1,
  111. backgroundColor: SkinHelper.slotBorderColor,
  112. textColor: Colors.white,
  113. fontSize: 16.0,
  114. );
  115. Navigator.pop(context);
  116. } else {
  117. setState(() => isDownloadSlow = true);
  118. }
  119. }
  120. });
  121. audio = context.read<JcAudioController>();
  122. settings = context.read<SettingsController>();
  123. confettiLayer = ConfettiLayer(this);
  124. Future.delayed(Duration.zero, () {
  125. if (mounted) confettiLayer.setup(context);
  126. });
  127. _initAnimations();
  128. try {
  129. _init();
  130. } catch (error) {
  131. _log.info('board init error: $error');
  132. Fluttertoast.showToast(
  133. msg: AppLocalizations.of(context)!.networkNotGood,
  134. toastLength: Toast.LENGTH_SHORT,
  135. gravity: ToastGravity.CENTER,
  136. timeInSecForIosWeb: 1,
  137. backgroundColor: SkinHelper.slotBorderColor,
  138. textColor: Colors.white,
  139. fontSize: 16.0,
  140. );
  141. Navigator.pop(context);
  142. }
  143. }
  144. void _initAnimations() {
  145. final Device device = context.read<Device>();
  146. // ⚠️ 优化:只初始化必要的动画控制器,其他延迟创建
  147. _moveAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
  148. _moveAnimationController.addListener(_moveAnimationListener);
  149. _moveAnimationController.addStatusListener(_moveAnimationStatusListener);
  150. _mergeAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
  151. _mergeAnimationController.addListener(_mergeAnimationListener);
  152. _mergeAnimationController.addStatusListener(_mergeAnimationStatusListener);
  153. _mergeScaleAnimation = TweenSequence<double>([
  154. TweenSequenceItem(tween: Tween<double>(begin: 1.0, end: 1.06), weight: 50),
  155. TweenSequenceItem(tween: Tween<double>(begin: 1.06, end: 1.0), weight: 50),
  156. ]).animate(CurvedAnimation(parent: _mergeAnimationController, curve: Curves.easeInOut));
  157. _prepareAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 800));
  158. _prepareAnimationController.addListener(_prepareAnimationListener);
  159. _prepareAnimationController.addStatusListener(_prepareAnimationStatusListener);
  160. dealingAnimationController = AnimationController(vsync: this);
  161. _dealingAnimation = CurvedAnimation(parent: dealingAnimationController, curve: Curves.linear);
  162. dealingAnimationController.addListener(_dealingAnimationListener);
  163. dealingAnimationController.addStatusListener(_dealingAnimationStatusListener);
  164. flipAnimationController = AnimationController(vsync: this, duration: Duration(milliseconds: 500));
  165. flipAnimationController.addListener(_flipAnimationListener);
  166. flipAnimationController.addStatusListener(_flipAnimationStatusListener);
  167. // ⚠️ success 和 hardModeBanner 动画延迟创建(在需要时创建)
  168. _initSuccessAnimation(device);
  169. _initHardModeBannerAnimation();
  170. }
  171. void _initSuccessAnimation(Device device) {
  172. _successAnimationController = AnimationController(vsync: this, duration: Duration(milliseconds: 500));
  173. final deltaY = (device.targetRect.top - device.appBarHeight) / 3;
  174. _offsetAnimation = Tween<double>(begin: 0.0, end: -deltaY).animate(_successAnimationController);
  175. _bottomSlideAnimation = Tween<double>(
  176. begin: -500,
  177. end: device.screenSize.height - device.targetRect.bottom + deltaY - 60,
  178. ).animate(CurvedAnimation(parent: _successAnimationController, curve: Curves.easeOut));
  179. _topSlideAnimation = Tween<double>(
  180. begin: -200,
  181. end: device.targetRect.top - deltaY - 70,
  182. ).animate(CurvedAnimation(parent: _successAnimationController, curve: Curves.easeOut));
  183. _successAnimationController.addListener(_successAnimationListener);
  184. _successAnimationController.addStatusListener(_successAnimationStatusListener);
  185. }
  186. void _initHardModeBannerAnimation() {
  187. _hardModeBannerController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1500));
  188. _bannerScaleAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
  189. CurvedAnimation(
  190. parent: _hardModeBannerController,
  191. curve: const Interval(0.0, 0.4, curve: Curves.easeOut),
  192. ),
  193. );
  194. _bannerFadeAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
  195. CurvedAnimation(
  196. parent: _hardModeBannerController,
  197. curve: const Interval(0.3, 1.0, curve: Curves.easeIn),
  198. ),
  199. );
  200. _hardModeBannerController.addStatusListener((status) {
  201. if (status == AnimationStatus.completed) {
  202. if (mounted) setState(() => _showHardModeBanner = false);
  203. }
  204. });
  205. }
  206. _onProgressUpdate() {
  207. progress = (itemLoader.progress.value * 100).ceil();
  208. _log.info('onProgressUpdate: progress=$progress');
  209. setState(() {});
  210. }
  211. void saveProgress() async {
  212. _log.info('saveProgress');
  213. if (board != null && board!.isAllDone == false) {
  214. await saveJson(widget.item.jsonPath, board!.toJson());
  215. }
  216. }
  217. // ✅ 优化后的 _init() 方法 - 保留图片损坏检测和重试逻辑
  218. _init() async {
  219. Device device = context.read<Device>();
  220. setState(() => _isLoading = true);
  221. try {
  222. final dpr = device.effectivePixelRatio;
  223. final targetRect = device.targetRect;
  224. final bestImageSize = device.bestImageSize;
  225. // ✅ 1. 尝试获取并解码图片(保留原有的错误处理逻辑)
  226. ui.Image image;
  227. try {
  228. image = await itemLoader.getImageBySize(bestImageSize.width.round(), bestImageSize.height.round());
  229. } catch (e) {
  230. _log.severe('Image decode failed: $e. Possible corrupted file.');
  231. // ✅ 保留:如果解码失败且还没重试过,尝试删除缓存重来
  232. if (!_hasRetried) {
  233. _hasRetried = true;
  234. final file = await localFile(widget.item.cachePath);
  235. if (await file.exists()) {
  236. await file.delete();
  237. _log.warning('Deleted corrupted cache file: ${widget.item.cachePath}');
  238. }
  239. // 重新初始化下载逻辑
  240. itemLoader = ItemLoader.load(widget.item, device.suggestedQuality);
  241. // 递归调用一次
  242. return _init();
  243. } else {
  244. // 已经重试过还是失败,抛出异常进入 catch 块
  245. throw Exception("Image data invalid after retry");
  246. }
  247. }
  248. _log.info('imageSize: (${image.width},${image.height}), bestImageSize: ($bestImageSize)');
  249. // ✅ 2. 加载扑克背面图片
  250. final Size bestCardImageSize = Size(targetRect.width * dpr / widget.item.rows, targetRect.height * dpr / widget.item.cols);
  251. final ByteData cardData = await rootBundle.load(widget.item.hard ? 'assets/images/backcard_red.png' : 'assets/images/backcard_blue.png');
  252. ui.Image cardImage;
  253. final ui.Codec cardCodec = await ui.instantiateImageCodec(
  254. cardData.buffer.asUint8List(),
  255. targetWidth: bestCardImageSize.width.round(),
  256. targetHeight: bestCardImageSize.height.round(),
  257. );
  258. final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame();
  259. cardImage = cardFrameInfo.image;
  260. // ✅ 3. 构建或恢复 Board 实例
  261. if (widget.reset) {
  262. board = await Board.create(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device);
  263. } else {
  264. final jsonFile = await localFile(widget.item.jsonPath);
  265. if (await jsonFile.exists()) {
  266. showDealing = false;
  267. board = await Board.restore(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device, widget.item.jsonPath);
  268. } else {
  269. board = await Board.create(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device);
  270. _reportLevelStart();
  271. }
  272. }
  273. // 4. 初始化准备
  274. board!.prepare();
  275. _loadFingerImageAndSetupHint();
  276. if (mounted) {
  277. setState(() => _isLoading = false);
  278. }
  279. // 5. 启动入场动画
  280. if (showDealing) {
  281. _prepareAnimationController.forward(from: 0.0);
  282. } else {
  283. board!.start();
  284. }
  285. } catch (error) {
  286. _log.severe('Board _init critical error: $error');
  287. if (mounted) {
  288. Fluttertoast.showToast(
  289. msg: AppLocalizations.of(context)!.networkNotGood,
  290. toastLength: Toast.LENGTH_LONG,
  291. gravity: ToastGravity.CENTER,
  292. backgroundColor: SkinHelper.slotBorderColor,
  293. textColor: Colors.white,
  294. );
  295. Navigator.pop(context);
  296. }
  297. }
  298. }
  299. _reportLevelStart() {
  300. FirebaseHelper.logEvent("level_start", {'id': widget.item.id, 'level': data.currentLevel + 1});
  301. AdjustHelper.trackEvent(AdjustHelper.levelStartToken);
  302. Statistics.postEvent({
  303. "project_id": Persistence().projectId,
  304. "user_id": Persistence().uuid,
  305. "library_name": Persistence().libraryName,
  306. "library_version": Persistence().packageVersion,
  307. "name": 'level_start',
  308. "tab_source": widget.tag,
  309. "sku_id": widget.item.id,
  310. });
  311. }
  312. Future<void> _loadFingerImageAndSetupHint() async {
  313. if (!widget.firstRun) return;
  314. try {
  315. _fingerImage = await loadUiImageFromAsset('assets/images/finger.png');
  316. } catch (e) {
  317. _log.severe('Failed to load assets/images/finger.png: $e');
  318. return;
  319. }
  320. if (!mounted || board == null) return;
  321. _overLayer = OverLayer(board!, this);
  322. _overLayer!.setup(context);
  323. if (widget.firstRun) {
  324. Future.delayed(const Duration(seconds: 1), () => hint());
  325. Future.delayed(const Duration(seconds: 3), () {
  326. if (!mounted) return;
  327. if (_overLayer != null && _overLayer!.isHinting) {
  328. Fluttertoast.showToast(
  329. msg: AppLocalizations.of(context)!.moveToComplete,
  330. toastLength: Toast.LENGTH_SHORT,
  331. gravity: ToastGravity.BOTTOM,
  332. timeInSecForIosWeb: 1,
  333. backgroundColor: SkinHelper.slotBorderColor,
  334. textColor: Colors.white,
  335. fontSize: 16.0,
  336. );
  337. }
  338. });
  339. }
  340. }
  341. hint() async {
  342. if (_fingerImage == null || _overLayer == null || board == null || board!.status != BoardStatus.playing) return;
  343. double fingerSize = board!.pieceLogicalWidth / 2.5;
  344. final Offset centerStart = Offset(
  345. board!.targetRect.topLeft.dx + board!.pieceLogicalWidth,
  346. board!.targetRect.topLeft.dy + board!.pieceLogicalHeight * 5 / 2,
  347. );
  348. final Offset centerEnd = Offset(board!.targetRect.topLeft.dx + board!.pieceLogicalWidth, board!.targetRect.topLeft.dy + board!.pieceLogicalHeight * 3 / 2);
  349. final rectStart = Rect.fromCenter(center: centerStart, width: fingerSize, height: fingerSize);
  350. final rectEnd = Rect.fromCenter(center: centerEnd, width: fingerSize, height: fingerSize);
  351. final hintItem = HintItem(_fingerImage!, rectStart, rectEnd);
  352. _overLayer?.doHint(hintItem);
  353. }
  354. // 🔥 关键修复:成功回调,延迟资源清理到页面退出时
  355. _onSuccess() {
  356. _log.info('success! 游戏完成!');
  357. MemoryMonitor.logMemoryUsage('Level completed');
  358. data.workDone(widget.item);
  359. board!.success();
  360. audio.playSfx(SfxType.success);
  361. confettiLayer.play();
  362. _successAnimationController.forward(from: 0.0);
  363. setState(() {});
  364. // 数据上报
  365. FirebaseHelper.logEvent("level_done", {'id': widget.item.id, 'level': data.currentLevel});
  366. AdjustHelper.trackEvent(AdjustHelper.levelDoneToken);
  367. Statistics.postEvent({
  368. "project_id": Persistence().projectId,
  369. "user_id": Persistence().uuid,
  370. "library_name": Persistence().libraryName,
  371. "library_version": Persistence().packageVersion,
  372. "name": 'level_done',
  373. "tab_source": widget.tag,
  374. "sku_id": widget.item.id,
  375. });
  376. // 里程碑上报
  377. if (data.currentLevel == 3) {
  378. FirebaseHelper.logEvent("level_done_3", {});
  379. AdjustHelper.trackEvent(AdjustHelper.levelDone3Token);
  380. } else if (data.currentLevel == 10) {
  381. FirebaseHelper.logEvent("level_done_10", {});
  382. AdjustHelper.trackEvent(AdjustHelper.levelDone10Token);
  383. } else if (data.currentLevel == 20) {
  384. FirebaseHelper.logEvent("level_done_20", {});
  385. AdjustHelper.trackEvent(AdjustHelper.levelDone20Token);
  386. } else if (data.currentLevel == 30) {
  387. FirebaseHelper.logEvent("level_done_30", {});
  388. AdjustHelper.trackEvent(AdjustHelper.levelDone30Token);
  389. }
  390. }
  391. // 动画监听器方法(保持原有逻辑)
  392. void _successAnimationListener() {
  393. final delta = _offsetAnimation.value;
  394. board!.finalRect = board!.targetRect.translate(0, delta);
  395. board!.invalidate();
  396. }
  397. void _successAnimationStatusListener(AnimationStatus status) async {
  398. if (status == AnimationStatus.completed) {
  399. Future.delayed(Duration(seconds: 1), () async {
  400. if (!mounted) return;
  401. final bool shouldShowRateDialog = await RatingHelper.shouldShowRateDialog(data.currentLevel);
  402. if (shouldShowRateDialog && mounted) {
  403. showRateDialog(context);
  404. }
  405. });
  406. }
  407. }
  408. // 其他动画监听器方法保持原有逻辑...
  409. void _dealingAnimationListener() {
  410. if (board == null) return;
  411. final currentTime = _dealingAnimation.value * _totalDealingDuration;
  412. for (int i = 0; i < board!.pieces.length - 1; i++) {
  413. final piece = board!.pieces[i];
  414. final startTime = i * _dealingPieceInterval;
  415. final duration = _dealingPieceDuration;
  416. if (currentTime < startTime) continue;
  417. double progress = (currentTime - startTime) / duration;
  418. progress = progress.clamp(0.0, 1.0);
  419. final startTransform = board!.getBottomRightTransform();
  420. final endTransform = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
  421. final tween = Matrix4Tween(begin: startTransform, end: endTransform);
  422. piece.transform = tween.lerp(progress);
  423. }
  424. board!.invalidate();
  425. }
  426. void _dealingAnimationStatusListener(AnimationStatus status) {
  427. if (status == AnimationStatus.completed) {
  428. board!.resetAllPieces();
  429. board!.shuffle(ShuffleStep.flipping);
  430. flipAnimationController.forward(from: 0.0);
  431. audio.playSfx(SfxType.flip);
  432. }
  433. }
  434. void _flipAnimationListener() {
  435. if (board == null) return;
  436. final flipValue = flipAnimationController.value;
  437. for (final piece in board!.pieces) {
  438. final angle = flipValue * pi;
  439. final targetTranslate = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
  440. piece.updateFlipTransform(angle, targetTranslate);
  441. }
  442. board!.invalidate();
  443. }
  444. void _flipAnimationStatusListener(AnimationStatus status) {
  445. if (status == AnimationStatus.completed) {
  446. board!.resetAllPieces();
  447. board!.rebuildAllGroups();
  448. final mergeGroups = board!.compareAllGroups();
  449. if (mergeGroups.isNotEmpty) {
  450. _log.info('Merge animation start for ${mergeGroups.length} groups.');
  451. _mergeGroups = mergeGroups;
  452. _mergeAnimationController.forward(from: 0.0);
  453. audio.playSfx(SfxType.pop);
  454. }
  455. board!.start();
  456. }
  457. }
  458. void _moveAnimationListener() {
  459. if (moveItems == null || moveItems!.isEmpty) return;
  460. for (var item in moveItems!) {
  461. item.move();
  462. }
  463. board!.invalidate();
  464. }
  465. void _moveAnimationStatusListener(AnimationStatus status) {
  466. if (status == AnimationStatus.completed) {
  467. if (moveItems != null) {
  468. bool needRebuildGroup = moveItems!.any((item) => item.action == Action.swap);
  469. for (var item in moveItems!) {
  470. item.stop();
  471. }
  472. if (needRebuildGroup) {
  473. board!.backupAllGroups();
  474. board!.rebuildAllGroups();
  475. final mergeGroups = board!.compareAllGroups();
  476. if (mergeGroups.isNotEmpty) {
  477. _log.info('Merge animation start for ${mergeGroups.length} groups.');
  478. _mergeGroups = mergeGroups;
  479. _mergeAnimationController.forward(from: 0.0);
  480. audio.playSfx(SfxType.pop);
  481. moveItems = null;
  482. return;
  483. }
  484. }
  485. moveItems = null;
  486. }
  487. }
  488. }
  489. void _mergeAnimationListener() {
  490. if (_mergeGroups == null || _mergeGroups!.isEmpty || board == null) return;
  491. final double scale = _mergeScaleAnimation.value;
  492. for (var group in _mergeGroups!) {
  493. final groupCenter = group.center;
  494. for (var piece in group.pieces) {
  495. final baseTransform = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
  496. final pieceTopLeft = Offset(baseTransform.storage[12], baseTransform.storage[13]);
  497. final offsetToCenter = groupCenter - pieceTopLeft;
  498. final scaleMatrix = vmath.Matrix4.identity()
  499. ..translate(offsetToCenter.dx, offsetToCenter.dy)
  500. ..scale(scale, scale, 1.0)
  501. ..translate(-offsetToCenter.dx, -offsetToCenter.dy);
  502. piece.transform = baseTransform * scaleMatrix;
  503. }
  504. }
  505. board!.invalidate();
  506. }
  507. void _mergeAnimationStatusListener(AnimationStatus status) {
  508. if (status == AnimationStatus.completed) {
  509. if (_mergeGroups != null && _mergeGroups!.isNotEmpty) {
  510. for (var group in _mergeGroups!) {
  511. for (var piece in group.pieces) {
  512. piece.transform = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
  513. }
  514. }
  515. }
  516. _mergeGroups = null;
  517. if (board!.checkWinCondition()) {
  518. _onSuccess();
  519. }
  520. board!.invalidate();
  521. }
  522. }
  523. void _prepareAnimationListener() {
  524. board!.invalidate();
  525. }
  526. void _prepareAnimationStatusListener(AnimationStatus status) {
  527. if (status == AnimationStatus.completed) {
  528. if (board != null && board!.hard == true) {
  529. setState(() => _showHardModeBanner = true);
  530. _hardModeBannerController.forward(from: 0.0);
  531. }
  532. board!.setAllPieceToBottomRight();
  533. board!.shuffle(ShuffleStep.dealing);
  534. dealingAnimationController.duration = Duration(milliseconds: _totalDealingDuration);
  535. dealingAnimationController.forward(from: 0.0);
  536. audio.playSfx(SfxType.card);
  537. _dealingPeriodicTimer = Timer.periodic(Duration(milliseconds: 130), (timer) {
  538. if (mounted) {
  539. _dealingCount++;
  540. if (_dealingCount >= (_totalDealingDuration / 130) - 2) {
  541. timer.cancel();
  542. } else {
  543. audio.playSfx(SfxType.card);
  544. }
  545. }
  546. });
  547. // ⚠️ 关键优化:发牌动画开始后 2 秒再加载 Banner
  548. Future.delayed(const Duration(seconds: 2), () {
  549. if (mounted) {
  550. setState(() => _bannerDelayedLoad = true);
  551. }
  552. });
  553. }
  554. }
  555. // 发牌动画参数
  556. int get _dealingPieceInterval {
  557. if (board!.rows <= 3) return 90;
  558. if (board!.rows == 4) return 80;
  559. if (board!.rows == 5) return 70;
  560. if (board!.rows == 6) return 50;
  561. return 50;
  562. }
  563. int get _dealingPieceDuration {
  564. if (board!.rows <= 3) return 400;
  565. if (board!.rows == 4) return 300;
  566. if (board!.rows == 5) return 200;
  567. if (board!.rows == 6) return 150;
  568. return 100;
  569. }
  570. int get _totalDealingDuration => (board!.pieces.length - 1) * _dealingPieceInterval + _dealingPieceDuration;
  571. @override
  572. void didChangeDependencies() async {
  573. super.didChangeDependencies();
  574. _log.info("didChangeDependencies");
  575. }
  576. // ✅ 优化:增强资源释放顺序和完整性
  577. @override
  578. dispose() {
  579. MemoryMonitor.logMemoryUsage('BoardPlay dispose (before)');
  580. _log.info('dispose - starting cleanup');
  581. // ✅ 1. 先停止所有动画(防止回调访问已销毁的资源)
  582. _moveAnimationController.stop();
  583. _mergeAnimationController.stop();
  584. _prepareAnimationController.stop();
  585. dealingAnimationController.stop();
  586. flipAnimationController.stop();
  587. _successAnimationController.stop();
  588. _hardModeBannerController.stop();
  589. // ✅ 2. 取消所有定时器
  590. timer.cancel();
  591. _dealingPeriodicTimer?.cancel();
  592. _resourceCleanupTimer?.cancel();
  593. // ✅ 3. 清理原生 Banner
  594. cleanBanner();
  595. // ✅ 4. 移除监听器
  596. itemLoader.progress.removeListener(_onProgressUpdate);
  597. _moveAnimationController.removeListener(_moveAnimationListener);
  598. _moveAnimationController.removeStatusListener(_moveAnimationStatusListener);
  599. _mergeAnimationController.removeListener(_mergeAnimationListener);
  600. _mergeAnimationController.removeStatusListener(_mergeAnimationStatusListener);
  601. _prepareAnimationController.removeListener(_prepareAnimationListener);
  602. _prepareAnimationController.removeStatusListener(_prepareAnimationStatusListener);
  603. dealingAnimationController.removeListener(_dealingAnimationListener);
  604. dealingAnimationController.removeStatusListener(_dealingAnimationStatusListener);
  605. flipAnimationController.removeListener(_flipAnimationListener);
  606. flipAnimationController.removeStatusListener(_flipAnimationStatusListener);
  607. _successAnimationController.removeListener(_successAnimationListener);
  608. _successAnimationController.removeStatusListener(_successAnimationStatusListener);
  609. // ✅ 5. 释放动画控制器
  610. _moveAnimationController.dispose();
  611. _mergeAnimationController.dispose();
  612. _prepareAnimationController.dispose();
  613. dealingAnimationController.dispose();
  614. flipAnimationController.dispose();
  615. _successAnimationController.dispose();
  616. _hardModeBannerController.dispose();
  617. // ✅ 6. 清空动画数据
  618. moveItems = null;
  619. _mergeGroups = null;
  620. // ✅ 7. 释放其他资源
  621. confettiLayer.dispose();
  622. _overLayer?.destroy();
  623. _overLayer = null;
  624. // ✅ 8. 最后释放 Board(最大的内存占用)
  625. board?.dispose();
  626. board = null;
  627. MemoryMonitor.logMemoryUsage('BoardPlay dispose (after)');
  628. _log.info('dispose - cleanup complete');
  629. super.dispose();
  630. }
  631. @override
  632. onInactive() {
  633. super.onInactive();
  634. saveProgress();
  635. }
  636. // 🔥 优化:页面退出时先保存再清理
  637. void _onWillPop(bool didPop, dynamic result) async {
  638. _log.info('board play will pop, didPop=$didPop, result=$result');
  639. if (didPop) return;
  640. // 立即停止所有动画,防止在销毁过程中动画还在执行 setState
  641. _moveAnimationController.stop();
  642. _mergeAnimationController.stop();
  643. _prepareAnimationController.stop();
  644. dealingAnimationController.stop();
  645. // 🔥 先保存进度,再清理资源
  646. saveProgress();
  647. // 延迟清理,确保保存完成
  648. Future.delayed(const Duration(milliseconds: 100)).then((_) {
  649. if (board != null) {
  650. _log.info('Cleaning up board resources on page exit');
  651. board!.dispose();
  652. board = null;
  653. }
  654. });
  655. if (!mounted) return;
  656. Navigator.of(context).pop(result);
  657. }
  658. @override
  659. Widget build(BuildContext context) {
  660. Device device = context.read<Device>();
  661. return PopScope(
  662. canPop: false,
  663. onPopInvokedWithResult: _onWillPop,
  664. child: Scaffold(
  665. body: Stack(
  666. children: <Widget>[
  667. if (!_isLoading) Positioned.fill(child: _buildPuzzleCanvas(device.screenSize.width, device.screenSize.height)),
  668. if (board == null || board!.status != BoardStatus.success) Positioned(top: 0, left: 0, right: 0, child: appBar),
  669. Positioned(
  670. bottom: 0,
  671. left: 0,
  672. right: 0,
  673. child: SafeArea(
  674. child: SizedBox(
  675. height: context.read<Device>().bannerHeight,
  676. width: double.infinity,
  677. // ⚠️ 关键优化:只有在延迟加载标记为 true 时才渲染 Banner
  678. child: (isBannerVisible && _bannerDelayedLoad) ? getBanner('playBottom') : const SizedBox.shrink(),
  679. ),
  680. ),
  681. ),
  682. successBanner,
  683. nextButton,
  684. // Release模式下显示内存信息
  685. if (Config.isDebug) Positioned(top: 100, right: 10, child: MemoryMonitor.getMemoryWidget()),
  686. if (_isLoading)
  687. Positioned.fill(
  688. child: Container(
  689. color: SkinHelper.wholeBgColor,
  690. child: const Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Colors.white))),
  691. ),
  692. ),
  693. ],
  694. ),
  695. ),
  696. );
  697. }
  698. Widget get appBar => SafeArea(
  699. child: Padding(
  700. padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 10.0),
  701. child: Row(
  702. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  703. crossAxisAlignment: CrossAxisAlignment.center,
  704. children: [
  705. const SizedBox(width: 30),
  706. Text(
  707. board != null && board!.status == BoardStatus.success
  708. ? AppLocalizations.of(context)!.levelPass
  709. : '${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}',
  710. style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600),
  711. ),
  712. SizedBox(
  713. width: 30,
  714. height: 30,
  715. child: IconButton(
  716. icon: const Icon(Icons.settings, color: Colors.white, size: 22),
  717. iconSize: 22,
  718. padding: EdgeInsets.zero,
  719. onPressed: () {
  720. audio.playSfx(SfxType.click);
  721. saveProgress();
  722. Navigator.push(context, SettingsDialog.buildRoute(showReturn: true, showRestart: true, item: widget.item));
  723. },
  724. style: ButtonStyle(
  725. backgroundColor: WidgetStateProperty.all(SkinHelper.slotBorderColor),
  726. shape: WidgetStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))),
  727. minimumSize: WidgetStateProperty.all(const Size(30, 30)),
  728. maximumSize: WidgetStateProperty.all(const Size(30, 30)),
  729. ),
  730. ),
  731. ),
  732. ],
  733. ),
  734. ),
  735. );
  736. Widget _buildPuzzleCanvas(double width, double height) {
  737. // ✅ 此时如果为了播广告已经把 board 释放了,直接返回空,避免任何 GPU 绘制指令
  738. if (board == null) {
  739. return Container(color: SkinHelper.wholeBgColor);
  740. }
  741. return RepaintBoundary(
  742. child: CustomPaint(
  743. painter: BoardPainter(board: board!, prepareAnimation: _prepareAnimationController),
  744. size: Size(width, height),
  745. child: GestureDetector(
  746. key: boardKey,
  747. onPanStart: _onPanStart,
  748. onPanUpdate: _onPanUpdate,
  749. onPanEnd: _onPanEnd,
  750. child: board != null && board!.hard && _showHardModeBanner ? _hardModeBanner : Container(color: Colors.transparent),
  751. ),
  752. ),
  753. );
  754. }
  755. Widget get _hardModeBanner => Center(
  756. child: AnimatedBuilder(
  757. animation: _hardModeBannerController,
  758. builder: (context, child) {
  759. return FadeTransition(
  760. opacity: _bannerFadeAnimation,
  761. child: ScaleTransition(scale: _bannerScaleAnimation, child: child),
  762. );
  763. },
  764. child: Container(
  765. width: double.infinity,
  766. height: 60,
  767. margin: const EdgeInsets.symmetric(horizontal: 10),
  768. decoration: BoxDecoration(
  769. color: Colors.red,
  770. borderRadius: BorderRadius.circular(10),
  771. border: Border.all(color: const Color.fromARGB(255, 247, 143, 135), width: 2),
  772. boxShadow: [BoxShadow(color: Color.fromRGBO(0, 0, 0, 0.3), blurRadius: 5, offset: const Offset(0, 3))],
  773. ),
  774. child: Center(
  775. child: Text(
  776. AppLocalizations.of(context)!.hardMode,
  777. style: TextStyle(color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold),
  778. ),
  779. ),
  780. ),
  781. ),
  782. );
  783. bool _isExiting = false;
  784. Widget get nextButton {
  785. Device device = context.read<Device>();
  786. return AnimatedBuilder(
  787. animation: _bottomSlideAnimation,
  788. builder: (context, child) {
  789. return AnimatedPositioned(
  790. duration: _successAnimationController.duration!,
  791. bottom: _bottomSlideAnimation.value,
  792. left: (device.screenSize.width - 200) / 2,
  793. child: child!,
  794. );
  795. },
  796. child: MyElevatedButton(
  797. width: 200,
  798. borderRadius: BorderRadius.circular(20),
  799. gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]),
  800. onPressed: () async {
  801. if (_isExiting) return;
  802. _isExiting = true;
  803. audio.playSfx(SfxType.click);
  804. // 🔥 策略:在广告弹出前,先主动销毁最占资源的 Board 和 UI Image
  805. // 此时用户已经看到成功界面,底层画布即使变白也没关系(被动画层挡住了)
  806. // MemoryMonitor().manualCleanup(); // 广告前清理
  807. if (board != null) {
  808. _log.info('Pre-Ad Cleanup: Disposing board to free GPU memory');
  809. board!.dispose();
  810. board = null;
  811. }
  812. // 尝试直接pop up, 不等待返回结果,避免用户等待广告结束后才看到界面响应
  813. if (data.currentLevel % 25 != 0) {
  814. // 完成一个合集的最后一张图, 这个时候不展示插屏广告, 因为返回首页需要展示一系列的动画
  815. showInterstitialAd('level_done', widget.item.id, data.currentLevel - 1);
  816. }
  817. _safePop(result: true); // 返回true表示关卡完成
  818. },
  819. child: Text(AppLocalizations.of(context)!.next, style: TextStyle(color: Colors.white, fontSize: 20)),
  820. ),
  821. );
  822. }
  823. void _safePop({bool result = true}) {
  824. if (!mounted) return;
  825. final navigator = Navigator.of(context);
  826. if (navigator.canPop()) {
  827. navigator.pop(result);
  828. }
  829. }
  830. Widget get successBanner {
  831. Device device = context.read<Device>();
  832. final bannerWidth = device.screenSize.width - 40 * 2;
  833. final bannerHeight = 60.0;
  834. return AnimatedBuilder(
  835. animation: _bottomSlideAnimation,
  836. builder: (context, child) {
  837. return AnimatedPositioned(duration: _successAnimationController.duration!, top: _topSlideAnimation.value, left: 40, child: child!);
  838. },
  839. child: SizedBox(
  840. width: bannerWidth,
  841. height: bannerHeight,
  842. child: Stack(
  843. children: [
  844. Image.asset(
  845. 'assets/images/banner.png',
  846. width: double.infinity,
  847. height: double.infinity,
  848. fit: BoxFit.cover,
  849. cacheWidth: (context.watch<Device>().realPixelRatio * bannerWidth).toInt(),
  850. cacheHeight: (context.watch<Device>().realPixelRatio * bannerHeight).toInt(),
  851. ),
  852. Center(
  853. child: Padding(
  854. padding: EdgeInsets.only(top: 16.0),
  855. child: Text(
  856. AppLocalizations.of(context)!.levelPass,
  857. style: TextStyle(
  858. color: Colors.white,
  859. fontSize: 22,
  860. fontWeight: FontWeight.bold,
  861. shadows: [Shadow(color: Colors.black54, offset: Offset(1, 1), blurRadius: 2)],
  862. ),
  863. ),
  864. ),
  865. ),
  866. ],
  867. ),
  868. ),
  869. );
  870. }
  871. // 手势处理方法保持原有逻辑...
  872. Offset _globalToLocal(Offset globalPosition) {
  873. final RenderBox renderBox = boardKey.currentContext!.findRenderObject() as RenderBox;
  874. return renderBox.globalToLocal(globalPosition);
  875. }
  876. void _onPanStart(DragStartDetails details) {
  877. _log.info('_onPanStart');
  878. _overLayer?.stopHint();
  879. if (board!.status != BoardStatus.playing) {
  880. _log.info('不是playing状态,不响应onPanStart');
  881. return;
  882. }
  883. if (board!.checkWinCondition()) {
  884. _log.info('游戏已经完成,不再响应onPanStart');
  885. return;
  886. }
  887. // 动画中断逻辑
  888. if (_moveAnimationController.isAnimating && moveItems != null) {
  889. _log.info('移动动画中断,强制归位/交换');
  890. for (var item in moveItems!) {
  891. item.stop();
  892. }
  893. bool needRebuildGroup = moveItems!.any((item) => item.action == Action.swap);
  894. if (needRebuildGroup) {
  895. board!.backupAllGroups();
  896. board!.rebuildAllGroups();
  897. }
  898. moveItems = null;
  899. _moveAnimationController.stop();
  900. board!.invalidate();
  901. }
  902. if (_mergeAnimationController.isAnimating && _mergeGroups != null) {
  903. _log.info('合并动画中断,强制归位');
  904. for (var group in _mergeGroups!) {
  905. for (var piece in group.pieces) {
  906. piece.transform = board!.getTransformByCoordinate(piece.curRow, piece.curCol);
  907. }
  908. }
  909. _mergeGroups = null;
  910. _mergeAnimationController.stop();
  911. board!.invalidate();
  912. }
  913. _moveAnimationController.stop();
  914. _mergeAnimationController.stop();
  915. moveItems = null;
  916. final localPosition = _globalToLocal(details.globalPosition);
  917. final touchedPiece = board?.findPieceAt(localPosition);
  918. if (touchedPiece != null) {
  919. audio.playSfx(SfxType.panstart);
  920. if (settings.vibrate.value) {
  921. if (Platform.isAndroid) {
  922. Vibration.vibrate(duration: 60, amplitude: 50);
  923. } else {
  924. HapticFeedback.mediumImpact();
  925. }
  926. }
  927. _draggingPiece = touchedPiece;
  928. final draggingGroup = _draggingPiece!.group;
  929. if (draggingGroup != null) {
  930. board!.pieces.removeWhere((p) => draggingGroup.contains(p));
  931. board!.pieces.addAll(draggingGroup.pieces);
  932. } else {
  933. board!.pieces.remove(_draggingPiece);
  934. board!.pieces.add(_draggingPiece!);
  935. }
  936. board!.invalidate();
  937. }
  938. }
  939. void _onPanUpdate(DragUpdateDetails details) {
  940. _overLayer?.stopHint();
  941. if (_draggingPiece == null) return;
  942. final Offset delta = details.delta;
  943. final draggingGroup = _draggingPiece!.group;
  944. if (draggingGroup != null) {
  945. for (var piece in draggingGroup.pieces) {
  946. piece.applyDelta(delta);
  947. }
  948. } else {
  949. _draggingPiece!.applyDelta(delta);
  950. }
  951. board!.invalidate();
  952. }
  953. void _onPanEnd(DragEndDetails details) {
  954. _log.info('_onPanEnd');
  955. _overLayer?.stopHint();
  956. if (_draggingPiece == null) return;
  957. audio.playSfx(SfxType.tap);
  958. Piece leaderPiece = _draggingPiece!;
  959. _draggingPiece = null;
  960. board!.invalidate();
  961. Piece? targetPiece = board!.findPieceAtExclude(leaderPiece.currentCenter, leaderPiece);
  962. if (targetPiece == null && leaderPiece.group != null) {
  963. for (var p in leaderPiece.group!.pieces) {
  964. targetPiece = board!.findPieceAtExclude(p.currentCenter, p);
  965. if (targetPiece != null) {
  966. _log.info('推举 ${p.toString()} 为新leader');
  967. leaderPiece = p;
  968. break;
  969. }
  970. }
  971. }
  972. if (targetPiece != null && targetPiece != leaderPiece && leaderPiece.canPlaceTo(targetPiece)) {
  973. _log.info("swap animation start");
  974. _animateSwap(leaderPiece, targetPiece);
  975. } else {
  976. _log.info("revert animation start");
  977. _animateRevert(leaderPiece);
  978. }
  979. }
  980. void _animateSwap(Piece leaderPiece, Piece targetPiece) {
  981. List<MoveItem> items = [];
  982. final List<Piece> draggingPieces = leaderPiece.group != null ? leaderPiece.group!.pieces : [leaderPiece];
  983. final int dr = targetPiece.curRow - leaderPiece.curRow;
  984. final int dc = targetPiece.curCol - leaderPiece.curCol;
  985. List<Piece> displacedPieces = [];
  986. if (leaderPiece.group != null) {
  987. for (var p in draggingPieces) {
  988. final int targetRow = p.curRow + dr;
  989. final int targetCol = p.curCol + dc;
  990. final Piece? other = board!.getPieceByCoordinate(targetRow, targetCol);
  991. if (other != null && !p.isSameGroup(other)) {
  992. displacedPieces.add(other);
  993. }
  994. }
  995. } else {
  996. displacedPieces.add(targetPiece);
  997. }
  998. for (var p in draggingPieces) {
  999. p.curRow += dr;
  1000. p.curCol += dc;
  1001. }
  1002. if (leaderPiece.group == null) {
  1003. targetPiece.curRow -= dr;
  1004. targetPiece.curCol -= dc;
  1005. } else {
  1006. for (var p in displacedPieces) {
  1007. int newRow = p.curRow - dr;
  1008. int newCol = p.curCol - dc;
  1009. do {
  1010. final Piece? pieceInSlot = board!.getPieceByCoordinate(newRow, newCol);
  1011. if (pieceInSlot == null) {
  1012. p.curRow = newRow;
  1013. p.curCol = newCol;
  1014. break;
  1015. } else {
  1016. newRow -= dr;
  1017. newCol -= dc;
  1018. }
  1019. } while (newRow >= 0 && newRow < p.rows && newCol >= 0 && newCol < p.cols);
  1020. }
  1021. }
  1022. for (var p in displacedPieces) {
  1023. final startTransform = p.transform;
  1024. final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol);
  1025. final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController);
  1026. items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.swap));
  1027. board!.pieces.remove(p);
  1028. board!.pieces.add(p);
  1029. }
  1030. for (var p in draggingPieces) {
  1031. final startTransform = p.transform;
  1032. final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol);
  1033. final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController);
  1034. items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.swap));
  1035. board!.pieces.remove(p);
  1036. board!.pieces.add(p);
  1037. }
  1038. moveItems = items;
  1039. _moveAnimationController.forward(from: 0.0);
  1040. }
  1041. void _animateRevert(Piece piece) {
  1042. List<MoveItem> items = [];
  1043. final List<Piece> groupPieces = piece.group != null ? piece.group!.pieces : [piece];
  1044. for (var p in groupPieces) {
  1045. final startTransform = p.transform;
  1046. final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol);
  1047. final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController);
  1048. items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.revert));
  1049. }
  1050. moveItems = items;
  1051. _moveAnimationController.forward(from: 0.0);
  1052. }
  1053. }
  1054. class Matrix4Tween extends Tween<vmath.Matrix4> {
  1055. Matrix4Tween({required vmath.Matrix4 begin, required vmath.Matrix4 end}) : super(begin: begin, end: end);
  1056. @override
  1057. vmath.Matrix4 lerp(double t) {
  1058. if (begin == null || end == null) return begin ?? end ?? vmath.Matrix4.identity();
  1059. final List<double> lerpedStorage = List.generate(16, (i) {
  1060. return ui.lerpDouble(begin!.storage[i], end!.storage[i], t)!;
  1061. });
  1062. return vmath.Matrix4.fromList(lerpedStorage.cast<double>());
  1063. }
  1064. }
  1065. class MoveItem {
  1066. final Piece piece;
  1067. final Animation<vmath.Matrix4> animation;
  1068. final vmath.Matrix4 startTransform;
  1069. final vmath.Matrix4 endTransform;
  1070. final Action action;
  1071. MoveItem({required this.piece, required this.animation, required this.startTransform, required this.endTransform, required this.action});
  1072. void move() {
  1073. piece.transform = animation.value;
  1074. }
  1075. void stop() {
  1076. piece.transform = endTransform;
  1077. }
  1078. }