board_play.dart 45 KB

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