import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:puzzleweave/audio/jc_audio_controller.dart'; import 'package:puzzleweave/config/device.dart'; import 'package:puzzleweave/firebase/adjust_helper.dart'; import 'package:puzzleweave/firebase/firebase_helper.dart'; import 'package:puzzleweave/l10n/app_localizations.dart'; import 'package:puzzleweave/models/download.dart'; import 'package:puzzleweave/models/items.dart'; import 'package:puzzleweave/persistence/persistence.dart'; import 'package:puzzleweave/play/board.dart'; import 'package:puzzleweave/play/board_painter.dart'; import 'package:puzzleweave/play/confetti_layer.dart'; import 'package:puzzleweave/play/overlayer.dart'; import 'package:puzzleweave/play/piece.dart'; import 'package:puzzleweave/rating/rating_helper.dart'; import 'package:puzzleweave/rating/rating_utils.dart'; import 'package:puzzleweave/settings/settings_controller.dart'; import 'package:puzzleweave/settings/settings_dialog.dart'; import 'package:puzzleweave/skin/skin.dart'; import 'package:puzzleweave/statistics/statistics.dart'; import 'package:puzzleweave/utils/memory_monitor.dart'; import 'package:puzzleweave/utils/mybutton.dart'; import 'package:puzzleweave/utils/utils.dart'; import 'package:vector_math/vector_math.dart' as vmath; import 'package:vibration/vibration.dart'; import '../ads/ads_state.dart'; import '../config/config.dart'; final Logger _log = Logger('board_play.dart'); enum MoveType { group, single } enum Action { revert, swap } class BoardPlay extends StatefulWidget { final ListItem item; final bool firstRun; final bool reset; final String tag; const BoardPlay({super.key, required this.item, this.firstRun = false, this.reset = false, this.tag = 'home'}); @override State createState() => _BoardPlayState(); static PageRouteBuilder buildRoute(ListItem item, {bool firstRun = false, bool reset = false}) { return PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => BoardPlay(item: item, firstRun: firstRun, reset: reset), transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition(opacity: animation, child: child), ); } } class _BoardPlayState extends AdsState with TickerProviderStateMixin { final GlobalKey boardKey = GlobalKey(); Board? board; bool _isLoading = true; int progress = 0; bool isDownloadSlow = false; late Timer timer; // ⚠️ 新增:banner 延迟加载标记 bool _bannerDelayedLoad = false; late ItemLoader itemLoader; late JcAudioController audio; late SettingsController settings; late ConfettiLayer confettiLayer; ui.Image? _fingerImage; OverLayer? _overLayer; Piece? _draggingPiece; List? moveItems; // ✅ 优化:资源清理定时器 Timer? _resourceCleanupTimer; // 动画控制器 late AnimationController _moveAnimationController; late AnimationController _mergeAnimationController; late Animation _mergeScaleAnimation; List? _mergeGroups; bool showDealing = true; late AnimationController _prepareAnimationController; late AnimationController dealingAnimationController; late AnimationController flipAnimationController; late Animation _dealingAnimation; Timer? _dealingPeriodicTimer; int _dealingCount = 0; late AnimationController _successAnimationController; late Animation _offsetAnimation; late Animation _bottomSlideAnimation; late Animation _topSlideAnimation; late AnimationController _hardModeBannerController; late Animation _bannerScaleAnimation; late Animation _bannerFadeAnimation; bool _showHardModeBanner = false; // ✅ 保留:重试标记,防止无限循环 bool _hasRetried = false; @override initState() { super.initState(); MemoryMonitor.logMemoryUsage('BoardPlay initState'); final Device device = context.read(); itemLoader = ItemLoader.load(widget.item, device.suggestedQuality); _onProgressUpdate(); itemLoader.progress.addListener(_onProgressUpdate); timer = Timer(const Duration(seconds: 5), () { if (mounted && progress < 50) { if (progress <= 1) { Fluttertoast.showToast( msg: AppLocalizations.of(context)!.networkNotGood, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIosWeb: 1, backgroundColor: SkinHelper.slotBorderColor, textColor: Colors.white, fontSize: 16.0, ); Navigator.pop(context); } else { setState(() => isDownloadSlow = true); } } }); audio = context.read(); settings = context.read(); confettiLayer = ConfettiLayer(this); Future.delayed(Duration.zero, () { if (mounted) confettiLayer.setup(context); }); _initAnimations(); try { _init(); } catch (error) { _log.info('board init error: $error'); Fluttertoast.showToast( msg: AppLocalizations.of(context)!.networkNotGood, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIosWeb: 1, backgroundColor: SkinHelper.slotBorderColor, textColor: Colors.white, fontSize: 16.0, ); Navigator.pop(context); } } void _initAnimations() { final Device device = context.read(); // ⚠️ 优化:只初始化必要的动画控制器,其他延迟创建 _moveAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200)); _moveAnimationController.addListener(_moveAnimationListener); _moveAnimationController.addStatusListener(_moveAnimationStatusListener); _mergeAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); _mergeAnimationController.addListener(_mergeAnimationListener); _mergeAnimationController.addStatusListener(_mergeAnimationStatusListener); _mergeScaleAnimation = TweenSequence([ TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.06), weight: 50), TweenSequenceItem(tween: Tween(begin: 1.06, end: 1.0), weight: 50), ]).animate(CurvedAnimation(parent: _mergeAnimationController, curve: Curves.easeInOut)); _prepareAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 800)); _prepareAnimationController.addListener(_prepareAnimationListener); _prepareAnimationController.addStatusListener(_prepareAnimationStatusListener); dealingAnimationController = AnimationController(vsync: this); _dealingAnimation = CurvedAnimation(parent: dealingAnimationController, curve: Curves.linear); dealingAnimationController.addListener(_dealingAnimationListener); dealingAnimationController.addStatusListener(_dealingAnimationStatusListener); flipAnimationController = AnimationController(vsync: this, duration: Duration(milliseconds: 500)); flipAnimationController.addListener(_flipAnimationListener); flipAnimationController.addStatusListener(_flipAnimationStatusListener); // ⚠️ success 和 hardModeBanner 动画延迟创建(在需要时创建) _initSuccessAnimation(device); _initHardModeBannerAnimation(); } void _initSuccessAnimation(Device device) { _successAnimationController = AnimationController(vsync: this, duration: Duration(milliseconds: 500)); final deltaY = (device.targetRect.top - device.appBarHeight) / 3; _offsetAnimation = Tween(begin: 0.0, end: -deltaY).animate(_successAnimationController); _bottomSlideAnimation = Tween( begin: -500, end: device.screenSize.height - device.targetRect.bottom + deltaY - 60, ).animate(CurvedAnimation(parent: _successAnimationController, curve: Curves.easeOut)); _topSlideAnimation = Tween( begin: -200, end: device.targetRect.top - deltaY - 70, ).animate(CurvedAnimation(parent: _successAnimationController, curve: Curves.easeOut)); _successAnimationController.addListener(_successAnimationListener); _successAnimationController.addStatusListener(_successAnimationStatusListener); } void _initHardModeBannerAnimation() { _hardModeBannerController = AnimationController(vsync: this, duration: const Duration(milliseconds: 1500)); _bannerScaleAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _hardModeBannerController, curve: const Interval(0.0, 0.4, curve: Curves.easeOut), ), ); _bannerFadeAnimation = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation( parent: _hardModeBannerController, curve: const Interval(0.3, 1.0, curve: Curves.easeIn), ), ); _hardModeBannerController.addStatusListener((status) { if (status == AnimationStatus.completed) { if (mounted) setState(() => _showHardModeBanner = false); } }); } _onProgressUpdate() { progress = (itemLoader.progress.value * 100).ceil(); _log.info('onProgressUpdate: progress=$progress'); setState(() {}); } void saveProgress() async { _log.info('saveProgress'); if (board != null && board!.isAllDone == false) { await saveJson(widget.item.jsonPath, board!.toJson()); } } // ✅ 优化后的 _init() 方法 - 保留图片损坏检测和重试逻辑 _init() async { Device device = context.read(); setState(() => _isLoading = true); try { final dpr = device.effectivePixelRatio; final targetRect = device.targetRect; final bestImageSize = device.bestImageSize; // ✅ 1. 尝试获取并解码图片(保留原有的错误处理逻辑) ui.Image image; try { image = await itemLoader.getImageBySize(bestImageSize.width.round(), bestImageSize.height.round()); } catch (e) { _log.severe('Image decode failed: $e. Possible corrupted file.'); // ✅ 保留:如果解码失败且还没重试过,尝试删除缓存重来 if (!_hasRetried) { _hasRetried = true; final file = await localFile(widget.item.cachePath); if (await file.exists()) { await file.delete(); _log.warning('Deleted corrupted cache file: ${widget.item.cachePath}'); } // 重新初始化下载逻辑 itemLoader = ItemLoader.load(widget.item, device.suggestedQuality); // 递归调用一次 return _init(); } else { // 已经重试过还是失败,抛出异常进入 catch 块 throw Exception("Image data invalid after retry"); } } _log.info('imageSize: (${image.width},${image.height}), bestImageSize: ($bestImageSize)'); // ✅ 2. 加载扑克背面图片 final Size bestCardImageSize = Size(targetRect.width * dpr / widget.item.rows, targetRect.height * dpr / widget.item.cols); final ByteData cardData = await rootBundle.load(widget.item.hard ? 'assets/images/backcard_red.png' : 'assets/images/backcard_blue.png'); ui.Image cardImage; final ui.Codec cardCodec = await ui.instantiateImageCodec( cardData.buffer.asUint8List(), targetWidth: bestCardImageSize.width.round(), targetHeight: bestCardImageSize.height.round(), ); final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame(); cardImage = cardFrameInfo.image; // ✅ 3. 构建或恢复 Board 实例 if (widget.reset) { board = await Board.create(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device); } else { final jsonFile = await localFile(widget.item.jsonPath); if (await jsonFile.exists()) { showDealing = false; board = await Board.restore(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device, widget.item.jsonPath); } else { board = await Board.create(this, image, cardImage, widget.item.rows, widget.item.cols, widget.item.hard, targetRect, device); _reportLevelStart(); } } // 4. 初始化准备 board!.prepare(); _loadFingerImageAndSetupHint(); if (mounted) { setState(() => _isLoading = false); } // 5. 启动入场动画 if (showDealing) { _prepareAnimationController.forward(from: 0.0); } else { board!.start(); } } catch (error) { _log.severe('Board _init critical error: $error'); if (mounted) { Fluttertoast.showToast( msg: AppLocalizations.of(context)!.networkNotGood, toastLength: Toast.LENGTH_LONG, gravity: ToastGravity.CENTER, backgroundColor: SkinHelper.slotBorderColor, textColor: Colors.white, ); Navigator.pop(context); } } } _reportLevelStart() { FirebaseHelper.logEvent("level_start", {'id': widget.item.id, 'level': data.currentLevel + 1}); AdjustHelper.trackEvent(AdjustHelper.levelStartToken); Statistics.postEvent({ "project_id": Persistence().projectId, "user_id": Persistence().uuid, "library_name": Persistence().libraryName, "library_version": Persistence().packageVersion, "name": 'level_start', "tab_source": widget.tag, "sku_id": widget.item.id, }); } Future _loadFingerImageAndSetupHint() async { if (!widget.firstRun) return; try { _fingerImage = await loadUiImageFromAsset('assets/images/finger.png'); } catch (e) { _log.severe('Failed to load assets/images/finger.png: $e'); return; } if (!mounted || board == null) return; _overLayer = OverLayer(board!, this); _overLayer!.setup(context); if (widget.firstRun) { Future.delayed(const Duration(seconds: 1), () => hint()); Future.delayed(const Duration(seconds: 3), () { if (!mounted) return; if (_overLayer != null && _overLayer!.isHinting) { Fluttertoast.showToast( msg: AppLocalizations.of(context)!.moveToComplete, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, timeInSecForIosWeb: 1, backgroundColor: SkinHelper.slotBorderColor, textColor: Colors.white, fontSize: 16.0, ); } }); } } hint() async { if (_fingerImage == null || _overLayer == null || board == null || board!.status != BoardStatus.playing) return; double fingerSize = board!.pieceLogicalWidth / 2.5; final Offset centerStart = Offset( board!.targetRect.topLeft.dx + board!.pieceLogicalWidth, board!.targetRect.topLeft.dy + board!.pieceLogicalHeight * 5 / 2, ); final Offset centerEnd = Offset(board!.targetRect.topLeft.dx + board!.pieceLogicalWidth, board!.targetRect.topLeft.dy + board!.pieceLogicalHeight * 3 / 2); final rectStart = Rect.fromCenter(center: centerStart, width: fingerSize, height: fingerSize); final rectEnd = Rect.fromCenter(center: centerEnd, width: fingerSize, height: fingerSize); final hintItem = HintItem(_fingerImage!, rectStart, rectEnd); _overLayer?.doHint(hintItem); } // 🔥 关键修复:成功回调,延迟资源清理到页面退出时 _onSuccess() { _log.info('success! 游戏完成!'); MemoryMonitor.logMemoryUsage('Level completed'); data.workDone(widget.item); board!.success(); audio.playSfx(SfxType.success); confettiLayer.play(); _successAnimationController.forward(from: 0.0); setState(() {}); // 数据上报 FirebaseHelper.logEvent("level_done", {'id': widget.item.id, 'level': data.currentLevel}); AdjustHelper.trackEvent(AdjustHelper.levelDoneToken); Statistics.postEvent({ "project_id": Persistence().projectId, "user_id": Persistence().uuid, "library_name": Persistence().libraryName, "library_version": Persistence().packageVersion, "name": 'level_done', "tab_source": widget.tag, "sku_id": widget.item.id, }); // 里程碑上报 if (data.currentLevel == 3) { FirebaseHelper.logEvent("level_done_3", {}); AdjustHelper.trackEvent(AdjustHelper.levelDone3Token); } else if (data.currentLevel == 10) { FirebaseHelper.logEvent("level_done_10", {}); AdjustHelper.trackEvent(AdjustHelper.levelDone10Token); } else if (data.currentLevel == 20) { FirebaseHelper.logEvent("level_done_20", {}); AdjustHelper.trackEvent(AdjustHelper.levelDone20Token); } else if (data.currentLevel == 30) { FirebaseHelper.logEvent("level_done_30", {}); AdjustHelper.trackEvent(AdjustHelper.levelDone30Token); } } // 动画监听器方法(保持原有逻辑) void _successAnimationListener() { final delta = _offsetAnimation.value; board!.finalRect = board!.targetRect.translate(0, delta); board!.invalidate(); } void _successAnimationStatusListener(AnimationStatus status) async { if (status == AnimationStatus.completed) { Future.delayed(Duration(seconds: 1), () async { if (!mounted) return; final bool shouldShowRateDialog = await RatingHelper.shouldShowRateDialog(data.currentLevel); if (shouldShowRateDialog && mounted) { showRateDialog(context); } }); } } // 其他动画监听器方法保持原有逻辑... void _dealingAnimationListener() { if (board == null) return; final currentTime = _dealingAnimation.value * _totalDealingDuration; for (int i = 0; i < board!.pieces.length - 1; i++) { final piece = board!.pieces[i]; final startTime = i * _dealingPieceInterval; final duration = _dealingPieceDuration; if (currentTime < startTime) continue; double progress = (currentTime - startTime) / duration; progress = progress.clamp(0.0, 1.0); final startTransform = board!.getBottomRightTransform(); final endTransform = board!.getTransformByCoordinate(piece.curRow, piece.curCol); final tween = Matrix4Tween(begin: startTransform, end: endTransform); piece.transform = tween.lerp(progress); } board!.invalidate(); } void _dealingAnimationStatusListener(AnimationStatus status) { if (status == AnimationStatus.completed) { board!.resetAllPieces(); board!.shuffle(ShuffleStep.flipping); flipAnimationController.forward(from: 0.0); audio.playSfx(SfxType.flip); } } void _flipAnimationListener() { if (board == null) return; final flipValue = flipAnimationController.value; for (final piece in board!.pieces) { final angle = flipValue * pi; final targetTranslate = board!.getTransformByCoordinate(piece.curRow, piece.curCol); piece.updateFlipTransform(angle, targetTranslate); } board!.invalidate(); } void _flipAnimationStatusListener(AnimationStatus status) { if (status == AnimationStatus.completed) { board!.resetAllPieces(); board!.rebuildAllGroups(); final mergeGroups = board!.compareAllGroups(); if (mergeGroups.isNotEmpty) { _log.info('Merge animation start for ${mergeGroups.length} groups.'); _mergeGroups = mergeGroups; _mergeAnimationController.forward(from: 0.0); audio.playSfx(SfxType.pop); } board!.start(); } } void _moveAnimationListener() { if (moveItems == null || moveItems!.isEmpty) return; for (var item in moveItems!) { item.move(); } board!.invalidate(); } void _moveAnimationStatusListener(AnimationStatus status) { if (status == AnimationStatus.completed) { if (moveItems != null) { bool needRebuildGroup = moveItems!.any((item) => item.action == Action.swap); for (var item in moveItems!) { item.stop(); } if (needRebuildGroup) { board!.backupAllGroups(); board!.rebuildAllGroups(); final mergeGroups = board!.compareAllGroups(); if (mergeGroups.isNotEmpty) { _log.info('Merge animation start for ${mergeGroups.length} groups.'); _mergeGroups = mergeGroups; _mergeAnimationController.forward(from: 0.0); audio.playSfx(SfxType.pop); moveItems = null; return; } } moveItems = null; } } } void _mergeAnimationListener() { if (_mergeGroups == null || _mergeGroups!.isEmpty || board == null) return; final double scale = _mergeScaleAnimation.value; for (var group in _mergeGroups!) { final groupCenter = group.center; for (var piece in group.pieces) { final baseTransform = board!.getTransformByCoordinate(piece.curRow, piece.curCol); final pieceTopLeft = Offset(baseTransform.storage[12], baseTransform.storage[13]); final offsetToCenter = groupCenter - pieceTopLeft; final scaleMatrix = vmath.Matrix4.identity() ..translate(offsetToCenter.dx, offsetToCenter.dy) ..scale(scale, scale, 1.0) ..translate(-offsetToCenter.dx, -offsetToCenter.dy); piece.transform = baseTransform * scaleMatrix; } } board!.invalidate(); } void _mergeAnimationStatusListener(AnimationStatus status) { if (status == AnimationStatus.completed) { if (_mergeGroups != null && _mergeGroups!.isNotEmpty) { for (var group in _mergeGroups!) { for (var piece in group.pieces) { piece.transform = board!.getTransformByCoordinate(piece.curRow, piece.curCol); } } } _mergeGroups = null; if (board!.checkWinCondition()) { _onSuccess(); } board!.invalidate(); } } void _prepareAnimationListener() { board!.invalidate(); } void _prepareAnimationStatusListener(AnimationStatus status) { if (status == AnimationStatus.completed) { if (board != null && board!.hard == true) { setState(() => _showHardModeBanner = true); _hardModeBannerController.forward(from: 0.0); } board!.setAllPieceToBottomRight(); board!.shuffle(ShuffleStep.dealing); dealingAnimationController.duration = Duration(milliseconds: _totalDealingDuration); dealingAnimationController.forward(from: 0.0); audio.playSfx(SfxType.card); _dealingPeriodicTimer = Timer.periodic(Duration(milliseconds: 130), (timer) { if (mounted) { _dealingCount++; if (_dealingCount >= (_totalDealingDuration / 130) - 2) { timer.cancel(); } else { audio.playSfx(SfxType.card); } } }); // ⚠️ 关键优化:发牌动画开始后 2 秒再加载 Banner Future.delayed(const Duration(seconds: 2), () { if (mounted) { setState(() => _bannerDelayedLoad = true); } }); } } // 发牌动画参数 int get _dealingPieceInterval { if (board!.rows <= 3) return 90; if (board!.rows == 4) return 80; if (board!.rows == 5) return 70; if (board!.rows == 6) return 50; return 50; } int get _dealingPieceDuration { if (board!.rows <= 3) return 400; if (board!.rows == 4) return 300; if (board!.rows == 5) return 200; if (board!.rows == 6) return 150; return 100; } int get _totalDealingDuration => (board!.pieces.length - 1) * _dealingPieceInterval + _dealingPieceDuration; @override void didChangeDependencies() async { super.didChangeDependencies(); _log.info("didChangeDependencies"); } // ✅ 优化:增强资源释放顺序和完整性 @override dispose() { MemoryMonitor.logMemoryUsage('BoardPlay dispose (before)'); _log.info('dispose - starting cleanup'); // ✅ 1. 先停止所有动画(防止回调访问已销毁的资源) _moveAnimationController.stop(); _mergeAnimationController.stop(); _prepareAnimationController.stop(); dealingAnimationController.stop(); flipAnimationController.stop(); _successAnimationController.stop(); _hardModeBannerController.stop(); // ✅ 2. 取消所有定时器 timer.cancel(); _dealingPeriodicTimer?.cancel(); _resourceCleanupTimer?.cancel(); // ✅ 3. 清理原生 Banner cleanBanner(); // ✅ 4. 移除监听器 itemLoader.progress.removeListener(_onProgressUpdate); _moveAnimationController.removeListener(_moveAnimationListener); _moveAnimationController.removeStatusListener(_moveAnimationStatusListener); _mergeAnimationController.removeListener(_mergeAnimationListener); _mergeAnimationController.removeStatusListener(_mergeAnimationStatusListener); _prepareAnimationController.removeListener(_prepareAnimationListener); _prepareAnimationController.removeStatusListener(_prepareAnimationStatusListener); dealingAnimationController.removeListener(_dealingAnimationListener); dealingAnimationController.removeStatusListener(_dealingAnimationStatusListener); flipAnimationController.removeListener(_flipAnimationListener); flipAnimationController.removeStatusListener(_flipAnimationStatusListener); _successAnimationController.removeListener(_successAnimationListener); _successAnimationController.removeStatusListener(_successAnimationStatusListener); // ✅ 5. 释放动画控制器 _moveAnimationController.dispose(); _mergeAnimationController.dispose(); _prepareAnimationController.dispose(); dealingAnimationController.dispose(); flipAnimationController.dispose(); _successAnimationController.dispose(); _hardModeBannerController.dispose(); // ✅ 6. 清空动画数据 moveItems = null; _mergeGroups = null; // ✅ 7. 释放其他资源 confettiLayer.dispose(); _overLayer?.destroy(); _overLayer = null; // ✅ 8. 最后释放 Board(最大的内存占用) board?.dispose(); board = null; MemoryMonitor.logMemoryUsage('BoardPlay dispose (after)'); _log.info('dispose - cleanup complete'); super.dispose(); } @override onInactive() { super.onInactive(); saveProgress(); } // 🔥 优化:页面退出时先保存再清理 void _onWillPop(bool didPop, dynamic result) async { _log.info('board play will pop, didPop=$didPop, result=$result'); if (didPop) return; // 立即停止所有动画,防止在销毁过程中动画还在执行 setState _moveAnimationController.stop(); _mergeAnimationController.stop(); _prepareAnimationController.stop(); dealingAnimationController.stop(); // 🔥 先保存进度,再清理资源 saveProgress(); // 延迟清理,确保保存完成 Future.delayed(const Duration(milliseconds: 100)).then((_) { if (board != null) { _log.info('Cleaning up board resources on page exit'); board!.dispose(); board = null; } }); if (!mounted) return; Navigator.of(context).pop(result); } @override Widget build(BuildContext context) { Device device = context.read(); return PopScope( canPop: false, onPopInvokedWithResult: _onWillPop, child: Scaffold( body: Stack( children: [ if (!_isLoading) Positioned.fill(child: _buildPuzzleCanvas(device.screenSize.width, device.screenSize.height)), if (board == null || board!.status != BoardStatus.success) Positioned(top: 0, left: 0, right: 0, child: appBar), Positioned( bottom: 0, left: 0, right: 0, child: SafeArea( child: SizedBox( height: context.read().bannerHeight, width: double.infinity, // ⚠️ 关键优化:只有在延迟加载标记为 true 时才渲染 Banner child: (isBannerVisible && _bannerDelayedLoad) ? getBanner('playBottom') : const SizedBox.shrink(), ), ), ), successBanner, nextButton, // Release模式下显示内存信息 if (Config.isDebug) Positioned(top: 100, right: 10, child: MemoryMonitor.getMemoryWidget()), if (_isLoading) Positioned.fill( child: Container( color: SkinHelper.wholeBgColor, child: const Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Colors.white))), ), ), ], ), ), ); } Widget get appBar => SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(width: 30), Text( board != null && board!.status == BoardStatus.success ? AppLocalizations.of(context)!.levelPass : '${AppLocalizations.of(context)!.level} ${data.currentLevel + 1}', style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600), ), SizedBox( width: 30, height: 30, child: IconButton( icon: const Icon(Icons.settings, color: Colors.white, size: 22), iconSize: 22, padding: EdgeInsets.zero, onPressed: () { audio.playSfx(SfxType.click); saveProgress(); Navigator.push(context, SettingsDialog.buildRoute(showReturn: true, showRestart: true, item: widget.item)); }, style: ButtonStyle( backgroundColor: WidgetStateProperty.all(SkinHelper.slotBorderColor), shape: WidgetStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), minimumSize: WidgetStateProperty.all(const Size(30, 30)), maximumSize: WidgetStateProperty.all(const Size(30, 30)), ), ), ), ], ), ), ); Widget _buildPuzzleCanvas(double width, double height) { // ✅ 此时如果为了播广告已经把 board 释放了,直接返回空,避免任何 GPU 绘制指令 if (board == null) { return Container(color: SkinHelper.wholeBgColor); } return RepaintBoundary( child: CustomPaint( painter: BoardPainter(board: board!, prepareAnimation: _prepareAnimationController), size: Size(width, height), child: GestureDetector( key: boardKey, onPanStart: _onPanStart, onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, child: board != null && board!.hard && _showHardModeBanner ? _hardModeBanner : Container(color: Colors.transparent), ), ), ); } Widget get _hardModeBanner => Center( child: AnimatedBuilder( animation: _hardModeBannerController, builder: (context, child) { return FadeTransition( opacity: _bannerFadeAnimation, child: ScaleTransition(scale: _bannerScaleAnimation, child: child), ); }, child: Container( width: double.infinity, height: 60, margin: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color.fromARGB(255, 247, 143, 135), width: 2), boxShadow: [BoxShadow(color: Color.fromRGBO(0, 0, 0, 0.3), blurRadius: 5, offset: const Offset(0, 3))], ), child: Center( child: Text( AppLocalizations.of(context)!.hardMode, style: TextStyle(color: Colors.white, fontSize: 30, fontWeight: FontWeight.bold), ), ), ), ), ); bool _isExiting = false; Widget get nextButton { Device device = context.read(); return AnimatedBuilder( animation: _bottomSlideAnimation, builder: (context, child) { return AnimatedPositioned( duration: _successAnimationController.duration!, bottom: _bottomSlideAnimation.value, left: (device.screenSize.width - 200) / 2, child: child!, ); }, child: MyElevatedButton( width: 200, borderRadius: BorderRadius.circular(20), gradient: LinearGradient(colors: [SkinHelper.coreBgColor, SkinHelper.slotBorderColor]), onPressed: () async { if (_isExiting) return; _isExiting = true; audio.playSfx(SfxType.click); // 🔥 策略:在广告弹出前,先主动销毁最占资源的 Board 和 UI Image // 此时用户已经看到成功界面,底层画布即使变白也没关系(被动画层挡住了) // MemoryMonitor().manualCleanup(); // 广告前清理 if (board != null) { _log.info('Pre-Ad Cleanup: Disposing board to free GPU memory'); board!.dispose(); board = null; } // 尝试直接pop up, 不等待返回结果,避免用户等待广告结束后才看到界面响应 if (data.currentLevel % 25 != 0) { // 完成一个合集的最后一张图, 这个时候不展示插屏广告, 因为返回首页需要展示一系列的动画 showInterstitialAd('level_done', widget.item.id, data.currentLevel - 1); } _safePop(result: true); // 返回true表示关卡完成 }, child: Text(AppLocalizations.of(context)!.next, style: TextStyle(color: Colors.white, fontSize: 20)), ), ); } void _safePop({bool result = true}) { if (!mounted) return; final navigator = Navigator.of(context); if (navigator.canPop()) { navigator.pop(result); } } Widget get successBanner { Device device = context.read(); final bannerWidth = device.screenSize.width - 40 * 2; final bannerHeight = 60.0; return AnimatedBuilder( animation: _bottomSlideAnimation, builder: (context, child) { return AnimatedPositioned(duration: _successAnimationController.duration!, top: _topSlideAnimation.value, left: 40, child: child!); }, child: SizedBox( width: bannerWidth, height: bannerHeight, child: Stack( children: [ Image.asset( 'assets/images/banner.png', width: double.infinity, height: double.infinity, fit: BoxFit.cover, cacheWidth: (context.watch().realPixelRatio * bannerWidth).toInt(), cacheHeight: (context.watch().realPixelRatio * bannerHeight).toInt(), ), Center( child: Padding( padding: EdgeInsets.only(top: 16.0), child: Text( AppLocalizations.of(context)!.levelPass, style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, shadows: [Shadow(color: Colors.black54, offset: Offset(1, 1), blurRadius: 2)], ), ), ), ), ], ), ), ); } // 手势处理方法保持原有逻辑... Offset _globalToLocal(Offset globalPosition) { final RenderBox renderBox = boardKey.currentContext!.findRenderObject() as RenderBox; return renderBox.globalToLocal(globalPosition); } void _onPanStart(DragStartDetails details) { _log.info('_onPanStart'); _overLayer?.stopHint(); if (board!.status != BoardStatus.playing) { _log.info('不是playing状态,不响应onPanStart'); return; } if (board!.checkWinCondition()) { _log.info('游戏已经完成,不再响应onPanStart'); return; } // 动画中断逻辑 if (_moveAnimationController.isAnimating && moveItems != null) { _log.info('移动动画中断,强制归位/交换'); for (var item in moveItems!) { item.stop(); } bool needRebuildGroup = moveItems!.any((item) => item.action == Action.swap); if (needRebuildGroup) { board!.backupAllGroups(); board!.rebuildAllGroups(); } moveItems = null; _moveAnimationController.stop(); board!.invalidate(); } if (_mergeAnimationController.isAnimating && _mergeGroups != null) { _log.info('合并动画中断,强制归位'); for (var group in _mergeGroups!) { for (var piece in group.pieces) { piece.transform = board!.getTransformByCoordinate(piece.curRow, piece.curCol); } } _mergeGroups = null; _mergeAnimationController.stop(); board!.invalidate(); } _moveAnimationController.stop(); _mergeAnimationController.stop(); moveItems = null; final localPosition = _globalToLocal(details.globalPosition); final touchedPiece = board?.findPieceAt(localPosition); if (touchedPiece != null) { audio.playSfx(SfxType.panstart); if (settings.vibrate.value) { if (Platform.isAndroid) { Vibration.vibrate(duration: 60, amplitude: 50); } else { HapticFeedback.mediumImpact(); } } _draggingPiece = touchedPiece; final draggingGroup = _draggingPiece!.group; if (draggingGroup != null) { board!.pieces.removeWhere((p) => draggingGroup.contains(p)); board!.pieces.addAll(draggingGroup.pieces); } else { board!.pieces.remove(_draggingPiece); board!.pieces.add(_draggingPiece!); } board!.invalidate(); } } void _onPanUpdate(DragUpdateDetails details) { _overLayer?.stopHint(); if (_draggingPiece == null) return; final Offset delta = details.delta; final draggingGroup = _draggingPiece!.group; if (draggingGroup != null) { for (var piece in draggingGroup.pieces) { piece.applyDelta(delta); } } else { _draggingPiece!.applyDelta(delta); } board!.invalidate(); } void _onPanEnd(DragEndDetails details) { _log.info('_onPanEnd'); _overLayer?.stopHint(); if (_draggingPiece == null) return; audio.playSfx(SfxType.tap); Piece leaderPiece = _draggingPiece!; _draggingPiece = null; board!.invalidate(); Piece? targetPiece = board!.findPieceAtExclude(leaderPiece.currentCenter, leaderPiece); if (targetPiece == null && leaderPiece.group != null) { for (var p in leaderPiece.group!.pieces) { targetPiece = board!.findPieceAtExclude(p.currentCenter, p); if (targetPiece != null) { _log.info('推举 ${p.toString()} 为新leader'); leaderPiece = p; break; } } } if (targetPiece != null && targetPiece != leaderPiece && leaderPiece.canPlaceTo(targetPiece)) { _log.info("swap animation start"); _animateSwap(leaderPiece, targetPiece); } else { _log.info("revert animation start"); _animateRevert(leaderPiece); } } void _animateSwap(Piece leaderPiece, Piece targetPiece) { List items = []; final List draggingPieces = leaderPiece.group != null ? leaderPiece.group!.pieces : [leaderPiece]; final int dr = targetPiece.curRow - leaderPiece.curRow; final int dc = targetPiece.curCol - leaderPiece.curCol; List displacedPieces = []; if (leaderPiece.group != null) { for (var p in draggingPieces) { final int targetRow = p.curRow + dr; final int targetCol = p.curCol + dc; final Piece? other = board!.getPieceByCoordinate(targetRow, targetCol); if (other != null && !p.isSameGroup(other)) { displacedPieces.add(other); } } } else { displacedPieces.add(targetPiece); } for (var p in draggingPieces) { p.curRow += dr; p.curCol += dc; } if (leaderPiece.group == null) { targetPiece.curRow -= dr; targetPiece.curCol -= dc; } else { for (var p in displacedPieces) { int newRow = p.curRow - dr; int newCol = p.curCol - dc; do { final Piece? pieceInSlot = board!.getPieceByCoordinate(newRow, newCol); if (pieceInSlot == null) { p.curRow = newRow; p.curCol = newCol; break; } else { newRow -= dr; newCol -= dc; } } while (newRow >= 0 && newRow < p.rows && newCol >= 0 && newCol < p.cols); } } for (var p in displacedPieces) { final startTransform = p.transform; final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol); final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController); items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.swap)); board!.pieces.remove(p); board!.pieces.add(p); } for (var p in draggingPieces) { final startTransform = p.transform; final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol); final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController); items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.swap)); board!.pieces.remove(p); board!.pieces.add(p); } moveItems = items; _moveAnimationController.forward(from: 0.0); } void _animateRevert(Piece piece) { List items = []; final List groupPieces = piece.group != null ? piece.group!.pieces : [piece]; for (var p in groupPieces) { final startTransform = p.transform; final endTransform = board!.getTransformByCoordinate(p.curRow, p.curCol); final animation = Matrix4Tween(begin: startTransform, end: endTransform).animate(_moveAnimationController); items.add(MoveItem(piece: p, animation: animation, startTransform: startTransform, endTransform: endTransform, action: Action.revert)); } moveItems = items; _moveAnimationController.forward(from: 0.0); } } class Matrix4Tween extends Tween { Matrix4Tween({required vmath.Matrix4 begin, required vmath.Matrix4 end}) : super(begin: begin, end: end); @override vmath.Matrix4 lerp(double t) { if (begin == null || end == null) return begin ?? end ?? vmath.Matrix4.identity(); final List lerpedStorage = List.generate(16, (i) { return ui.lerpDouble(begin!.storage[i], end!.storage[i], t)!; }); return vmath.Matrix4.fromList(lerpedStorage.cast()); } } class MoveItem { final Piece piece; final Animation animation; final vmath.Matrix4 startTransform; final vmath.Matrix4 endTransform; final Action action; MoveItem({required this.piece, required this.animation, required this.startTransform, required this.endTransform, required this.action}); void move() { piece.transform = animation.value; } void stop() { piece.transform = endTransform; } }