Selaa lähdekoodia

home_board_play 优化, 启用recorder预录制大量减少ui jank

guoziyun 4 kuukautta sitten
vanhempi
sitoutus
09dae14be1

+ 5 - 1
CHANGELOG.md

@@ -25,6 +25,10 @@
 - "下一关"按钮防抖和安全pop up优化(根据firebase报错的修改)
 - 内存分级, 低内存设备不展示banner广告
 - 启动优化, 启动main中所有堵塞性的await, 改为立即启动ui,后台并行初始化firebase,本地存储等await动作; 广告延迟加载,分散启动压力
-- 资源竞争优化: 关卡结束点击next, 立即pop up返回主页, 不再等待插屏广告结果再次回到play页面, 这样的可以立即释放play界面资源,为插屏广告腾出内存空间。 用户体验也会更好,广告结束直接就在首页,不会闪; 页面切换或退出, 手动销毁banner广告
+- home page 页面优化, 采用recorder预录制方案优化绘制性能, 大幅减少 ui jank
+- 资源竞争优化:
+  1. 关卡结束点击next, 立即pop up返回主页, 不再等待插屏广告结果再次回到play页面, 这样的可以立即释放play界面资源,为插屏广告腾出内存空间。 用户体验也会更好,广告结束直接就在首页,不会闪;
+  2. 页面切换或退出, 手动销毁banner广告
+  3. 游戏界面延迟2秒加载banner,刚进入play时各种初始化发牌翻牌动画, 资源比较紧张, 如果叠加banner渲染,容易造成竞争
 - 黑名单机制: hisense, itel, huawei y9 等高频ANR设备列入黑名单, 不展示广告。 (配合firebase remote config 使用支持以远程配置, 增加配置参数 ad_crash_prone_devices, 格式为逗号分隔的设备名称(如 "hisense,itel,huawei y9") )
 - 低端机的不启用自适应 Banner,因其渲染开销较大

+ 2 - 2
android/app/src/main/AndroidManifest.xml

@@ -11,12 +11,12 @@
         android:hardwareAccelerated="true"
         android:icon="@mipmap/launcher_icon">
         <!-- 还是使用skia引擎,稳定性好点-->
-        <meta-data
+        <!-- <meta-data
             android:name="io.flutter.embedding.android.EnableImpeller"
             android:value="false" />
         <meta-data
             android:name="io.flutter.embedding.android.Renderer"
-            android:value="skia" />
+            android:value="skia" /> -->
         <activity
             android:name=".MainActivity"
             android:exported="true"

+ 291 - 38
lib/homepage/home_board_play.dart

@@ -70,6 +70,11 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
   Timer? _dealingPeriodicTimer;
   int _dealingCount = 0; // 计数:记录执行次数
 
+  // ✅ 优化:Picture 缓存静止背景,避免 flip 动画期间重绘 24 张静止碎片
+  ui.Picture? _staticBackgroundPicture;
+  // ✅ 优化:跟踪上次录制的关卡,实现增量更新(只更新新完成的碎片)
+  int? _lastRecordedLevel;
+
   @override
   void initState() {
     super.initState();
@@ -172,6 +177,14 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
       setState(() {
         board.status = HomeBoardStatus.playing;
       });
+
+      // ✅ 优化:图片加载完成后立即预热 Picture 缓存
+      // 这样 flip 时就不需要等待 PictureRecorder,避免首次 flip 的卡顿
+      Future.delayed(const Duration(milliseconds: 100), () {
+        if (mounted && board.image != null && board.cardImage != null && _staticBackgroundPicture == null) {
+          _recordStaticBackgroundPicture();
+        }
+      });
     }
   }
 
@@ -214,6 +227,11 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
     _dealingPeriodicTimer?.cancel();
     _overlayEntry?.remove();
     _overlayEntry = null;
+
+    // ✅ 清理 Picture 缓存
+    _staticBackgroundPicture = null;
+    _lastRecordedLevel = null;
+
     board.dispose(); // 调用优化后的 dispose
     confettiLayer.dispose();
     _flipController.dispose();
@@ -294,6 +312,12 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
   }
 
   void startFlipAnimation() {
+    // ✅ 优化:每次 flip 前都重新录制背景(因为已完成关卡数在变化)
+    // 此时 TextPainter 缓存已预热,录制成本大幅降低
+    if (board.image != null && board.cardImage != null) {
+      _recordStaticBackgroundPicture();
+    }
+
     MemoryMonitor.logMemoryUsage('Collection flip animation');
     _flipController.forward(from: 0.0);
     audio.playSfx(SfxType.flip);
@@ -306,11 +330,110 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
     }
   }
 
+  // ✅ 新增:录制所有静止碎片到 Picture,供 flip 动画复用
+  void _recordStaticBackgroundPicture() {
+    if (board.image == null || board.cardImage == null) return;
+
+    final recorder = ui.PictureRecorder();
+    final canvas = Canvas(recorder, Rect.fromLTWH(0, 0, widget.canvasWidth, widget.canvasHeight));
+
+    // ✅ 优化:增量更新(只更新新完成的碎片)
+    if (_staticBackgroundPicture != null && _lastRecordedLevel != null && data.currentLevel == _lastRecordedLevel! + 1) {
+      // 增量模式:复用上次的 Picture,只绘制新完成的碎片
+      canvas.drawPicture(_staticBackgroundPicture!);
+
+      // 只更新新完成的碎片(新翻转的碎片在上一层绘制,覆盖背景中的对应位置)
+      final int newFlippedIndex = data.currentLevel - 1 - (data.currentCollectionIndex * board.count);
+      if (newFlippedIndex >= 0 && newFlippedIndex < board.count) {
+        final int row = newFlippedIndex ~/ board.cols;
+        final int col = newFlippedIndex % board.cols;
+        _drawStaticPieceToRecorder(canvas, row, col, true, newFlippedIndex);
+      }
+    } else {
+      // 首次录制或状态不连续(集合切换等),全量重新录制
+      for (var i = 0; i < board.rows; i++) {
+        for (var j = 0; j < board.cols; j++) {
+          final int curIndex = i * board.rows + j;
+          final bool flipped = data.currentLevel > data.currentCollectionIndex * board.count + curIndex;
+          _drawStaticPieceToRecorder(canvas, i, j, flipped, curIndex);
+        }
+      }
+    }
+
+    _staticBackgroundPicture = recorder.endRecording();
+    _lastRecordedLevel = data.currentLevel;
+  }
+
+  // ✅ 辅助方法:绘制单个碎片到 PictureRecorder
+  void _drawStaticPieceToRecorder(Canvas canvas, int row, int col, bool flipped, int curIndex) {
+    final img = board.image;
+    final cardImg = board.cardImage;
+    if (img == null || cardImg == null) return;
+
+    final w = widget.canvasWidth / board.cols;
+    final h = widget.canvasHeight / board.rows;
+    final left = col * w;
+    final top = row * h;
+    final rect = Rect.fromLTWH(left, top, w, h);
+    final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(4.0));
+
+    final pieceWidth = img.width / board.cols;
+    final pieceHeight = img.height / board.rows;
+    final imageSourceRect = Rect.fromLTWH(pieceWidth * col, pieceHeight * row, pieceWidth, pieceHeight);
+    final cardSourceRect = Rect.fromLTWH(0, 0, cardImg.width.toDouble(), cardImg.height.toDouble());
+
+    canvas.save();
+    canvas.clipRRect(rrect);
+
+    if (flipped) {
+      canvas.drawImageRect(img, imageSourceRect, rect, Paint()..isAntiAlias = true);
+    } else {
+      canvas.drawImageRect(cardImg, cardSourceRect, rect, Paint()..isAntiAlias = true);
+
+      // ✅ 修复:绘制背卡上的数字(直接创建临时 TextPainter)
+      final textStyle = TextStyle(
+        color: Colors.white,
+        fontSize: h * 0.25,
+        fontWeight: FontWeight.bold,
+        shadows: const [Shadow(offset: Offset(1, 1), blurRadius: 2, color: Colors.black38)],
+      );
+      final textPainter = TextPainter(
+        text: TextSpan(text: (curIndex + 1).toString(), style: textStyle),
+        textDirection: TextDirection.ltr,
+        textAlign: TextAlign.center,
+      );
+      textPainter.layout(minWidth: 0, maxWidth: w);
+      textPainter.paint(canvas, Offset(left + (w - textPainter.width) / 2, top + (h - textPainter.height) / 2));
+    }
+    canvas.restore();
+
+    // 绘制边框
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(rect.deflate(0.5), const Radius.circular(4.0)),
+      Paint()
+        ..color = SkinHelper.outLineBorderColor
+        ..style = PaintingStyle.stroke
+        ..strokeWidth = 1.0
+        ..isAntiAlias = true,
+    );
+    canvas.drawRRect(
+      RRect.fromRectAndRadius(rect.deflate(1.5), const Radius.circular(4.0)),
+      Paint()
+        ..color = SkinHelper.innerLineBorderColor
+        ..style = PaintingStyle.stroke
+        ..strokeWidth = 1.0
+        ..isAntiAlias = true,
+    );
+  }
+
   void switchToNextCollection() {
     if (currentCollectionItem == null) {
       Fluttertoast.showToast(msg: AppLocalizations.of(context)!.noMorePicture);
       return;
     }
+    // ✅ 清理 Picture 缓存,因为集合改变了
+    _staticBackgroundPicture = null;
+    _lastRecordedLevel = null;
     board.switchToNextCollection(currentCollectionItem!);
   }
 
@@ -330,6 +453,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
               dealingAnimation: _dealingAnimation,
               dealingPieceDuration: _dealingPieceDuration,
               dealingPieceInterval: _dealingPieceInterval,
+              staticBackgroundPicture: _staticBackgroundPicture,
             ),
       child: board.status == HomeBoardStatus.loading
           ? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.slotBorderColor)))
@@ -349,6 +473,19 @@ class CanvasPainter extends CustomPainter {
   final int dealingPieceDuration;
   final int dealingPieceInterval;
 
+  // ✅ 优化:Picture 缓存,flip 动画期间使用预录制的背景
+  final ui.Picture? staticBackgroundPicture;
+
+  // ✅ 优化:缓存 Matrix4 变换矩阵,避免每帧重新计算
+  static double _lastFlipProgress = -1.0;
+  static Matrix4? _cachedFlipTransform;
+
+  // ✅ 优化:缓存 TextPainter 的布局结果(宽高),避免重复 layout() 调用
+  static final Map<String, (double width, double height)> _textPainterLayoutCache = {};
+
+  // ⚠️ 优化:缓存 TextPainter 避免每帧重建
+  static final Map<String, TextPainter> _textPainterCache = {};
+
   CanvasPainter({
     required this.board,
     required this.level,
@@ -359,8 +496,42 @@ class CanvasPainter extends CustomPainter {
     required this.dealingAnimation,
     required this.dealingPieceDuration,
     required this.dealingPieceInterval,
+    this.staticBackgroundPicture,
   }) : super(repaint: Listenable.merge([board.boardNotifier, flipAnimation, unlockAnimation, dealingAnimation]));
 
+  static TextPainter _getOrCreateTextPainter(String text, double fontSize) {
+    final key = '$text-$fontSize';
+    if (!_textPainterCache.containsKey(key)) {
+      final textStyle = TextStyle(
+        color: Colors.white,
+        fontSize: fontSize,
+        fontWeight: FontWeight.bold,
+        shadows: const [Shadow(offset: Offset(1, 1), blurRadius: 2, color: Colors.black38)],
+      );
+      _textPainterCache[key] = TextPainter(
+        text: TextSpan(text: text, style: textStyle),
+        textDirection: TextDirection.ltr,
+        textAlign: TextAlign.center,
+      );
+    }
+    return _textPainterCache[key]!;
+  }
+
+  // ✅ 新增:获取 TextPainter 并返回其宽高(缓存布局结果,避免重复 layout)
+  static (TextPainter, double width, double height) _getTextPainterWithLayout(String text, double fontSize, double maxWidth) {
+    final key = '$text-$fontSize-$maxWidth';
+    final textPainter = _getOrCreateTextPainter(text, fontSize);
+
+    // 检查是否已缓存布局结果
+    if (!_textPainterLayoutCache.containsKey(key)) {
+      textPainter.layout(minWidth: 0, maxWidth: maxWidth);
+      _textPainterLayoutCache[key] = (textPainter.width, textPainter.height);
+    }
+
+    final (width, height) = _textPainterLayoutCache[key]!;
+    return (textPainter, width, height);
+  }
+
   @override
   void paint(Canvas canvas, Size size) {
     final statusToPaint = forceStatus ?? board.status;
@@ -441,19 +612,9 @@ class CanvasPainter extends CustomPainter {
     final rect = Rect.fromLTWH(0, 0, w, h);
     canvas.drawImageRect(cardImg, cardSourceRect, rect, Paint()..isAntiAlias = true);
 
-    final textStyle = TextStyle(
-      color: Colors.white,
-      fontSize: h * 0.25,
-      fontWeight: FontWeight.bold,
-      shadows: const [Shadow(offset: Offset(1, 1), blurRadius: 2, color: Colors.black38)],
-    );
-    final textPainter = TextPainter(
-      text: TextSpan(text: (curIndex + 1).toString(), style: textStyle),
-      textDirection: TextDirection.ltr,
-      textAlign: TextAlign.center,
-    );
-    textPainter.layout(minWidth: 0, maxWidth: w);
-    textPainter.paint(canvas, Offset((w - textPainter.width) / 2, (h - textPainter.height) / 2));
+    // ✅ 优化:使用缓存的 TextPainter 和布局结果
+    final (textPainter, textWidth, textHeight) = _getTextPainterWithLayout((curIndex + 1).toString(), h * 0.25, w);
+    textPainter.paint(canvas, Offset((w - textWidth) / 2, (h - textHeight) / 2));
     canvas.restore();
 
     _drawBorders(canvas, targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h);
@@ -500,11 +661,41 @@ class CanvasPainter extends CustomPainter {
   }
 
   void _paintPlaying(Canvas canvas, Size size) {
-    for (var i = 0; i < board.rows; i++) {
-      for (var j = 0; j < board.cols; j++) {
-        final int curIndex = i * board.rows + j;
-        bool flipped = level > collectionIndex * board.count + curIndex;
-        _drawPiece(canvas, size, i, j, flipped);
+    // ✅ 优化:如果 flip 动画正在进行且背景已缓存,直接绘制 Picture
+    final isFlipping = flipAnimation.isAnimating;
+    if (isFlipping && staticBackgroundPicture != null) {
+      // 绘制预录制的背景(所有非翻转和已翻转的碎片都在其中)
+      canvas.drawPicture(staticBackgroundPicture!);
+
+      // 只在翻转碎片上绘制 3D 变换版本(覆盖 Picture 中该位置的内容)
+      // 但首先需要清空预录制背景中该位置的内容,使翻转过程中显示为空白
+      final int curFlippingIndex = level - 1;
+      if (curFlippingIndex >= 0) {
+        final int row = curFlippingIndex ~/ board.cols;
+        final int col = curFlippingIndex % board.cols;
+
+        // 计算碎片在画布上的区域并清空该区域
+        final double w = size.width / board.cols;
+        final double h = size.height / board.rows;
+        final double left = col * w;
+        final double top = row * h;
+        final Rect clearRect = Rect.fromLTWH(left, top, w, h);
+
+        // 填充为白色背景,覆盖预录制的碎片,使翻转过程中该位置显示为白色
+        final rrect = RRect.fromRectAndRadius(clearRect, const Radius.circular(4.0));
+        canvas.drawRRect(rrect, Paint()..color = Colors.white);
+
+        bool flipped = level > collectionIndex * board.count + curFlippingIndex;
+        _drawPiece(canvas, size, row, col, flipped);
+      }
+    } else {
+      // 常规方式:绘制所有碎片(首次加载、非翻转状态)
+      for (var i = 0; i < board.rows; i++) {
+        for (var j = 0; j < board.cols; j++) {
+          final int curIndex = i * board.cols + j;
+          bool flipped = level > collectionIndex * board.count + curIndex;
+          _drawPiece(canvas, size, i, j, flipped);
+        }
       }
     }
   }
@@ -512,7 +703,7 @@ class CanvasPainter extends CustomPainter {
   void _drawPiece(Canvas canvas, Size size, int row, int col, bool flipped) {
     final img = board.image;
     final cardImg = board.cardImage;
-    if (img == null || cardImg == null) return; // 双重检查
+    if (img == null || cardImg == null) return;
 
     final w = size.width / board.cols;
     final h = size.height / board.rows;
@@ -528,21 +719,83 @@ class CanvasPainter extends CustomPainter {
 
     final curIndex = collectionIndex * board.count + row * board.rows + col;
     double flipProgress = (flipAnimation.isAnimating && curIndex == level - 1) ? flipAnimation.value : 0.0;
-    if (flipProgress > 0) flipped = flipProgress > 0.5;
 
+    // ⚠️ 优化:只有当前翻转的碎片才计算 3D 变换
+    if (flipProgress > 0) {
+      flipped = flipProgress > 0.5;
+      _drawFlippingPiece(canvas, rect, rrect, img, cardImg, imageSourceRect, cardSourceRect, flipProgress, flipped, curIndex, w, h, left, top);
+    } else {
+      // 静态碎片,无需 3D 变换
+      _drawStaticPiece(canvas, rect, rrect, img, cardImg, imageSourceRect, cardSourceRect, flipped, curIndex, w, h, left, top);
+    }
+
+    _drawBorders(canvas, left, top, w, h);
+  }
+
+  void _drawStaticPiece(
+    Canvas canvas,
+    Rect rect,
+    RRect rrect,
+    ui.Image img,
+    ui.Image cardImg,
+    Rect imageSourceRect,
+    Rect cardSourceRect,
+    bool flipped,
+    int curIndex,
+    double w,
+    double h,
+    double left,
+    double top,
+  ) {
+    canvas.save();
+    canvas.clipRRect(rrect);
+
+    if (flipped) {
+      canvas.drawImageRect(img, imageSourceRect, rect, Paint()..isAntiAlias = true);
+    } else {
+      canvas.drawImageRect(cardImg, cardSourceRect, rect, Paint()..isAntiAlias = true);
+      // ✅ 优化:使用缓存的 TextPainter 和布局结果
+      final (textPainter, textWidth, textHeight) = _getTextPainterWithLayout((curIndex + 1).toString(), h * 0.25, w);
+      textPainter.paint(canvas, Offset(left + (w - textWidth) / 2, top + (h - textHeight) / 2));
+    }
+    canvas.restore();
+  }
+
+  void _drawFlippingPiece(
+    Canvas canvas,
+    Rect rect,
+    RRect rrect,
+    ui.Image img,
+    ui.Image cardImg,
+    Rect imageSourceRect,
+    Rect cardSourceRect,
+    double flipProgress,
+    bool flipped,
+    int curIndex,
+    double w,
+    double h,
+    double left,
+    double top,
+  ) {
     canvas.save();
     final centerX = left + w / 2;
     final centerY = top + h / 2;
     canvas.translate(centerX, centerY);
 
-    if (flipProgress > 0.0) {
+    // ✅ 优化:缓存 Matrix4 变换矩阵,只在 flipProgress 变化时重新计算
+    Matrix4 transform;
+    if (_lastFlipProgress != flipProgress) {
       double angle = flipProgress * pi;
-      Matrix4 transform = Matrix4.identity()
+      transform = Matrix4.identity()
         ..setEntry(3, 2, 0.0015)
         ..rotateY(angle);
       if (flipProgress > 0.5) transform.scale(-1.0, 1.0, 1.0);
-      canvas.transform(transform.storage);
+      _cachedFlipTransform = transform;
+      _lastFlipProgress = flipProgress;
+    } else {
+      transform = _cachedFlipTransform ?? Matrix4.identity();
     }
+    canvas.transform(transform.storage);
 
     canvas.translate(-centerX, -centerY);
     canvas.clipRRect(rrect);
@@ -555,23 +808,12 @@ class CanvasPainter extends CustomPainter {
       canvas.drawImageRect(targetImg, sourceRect, rect, Paint()..isAntiAlias = true);
 
       if (flipProgress <= 0.5) {
-        final textStyle = TextStyle(
-          color: Colors.white,
-          fontSize: h * 0.25,
-          fontWeight: FontWeight.bold,
-          shadows: const [Shadow(offset: Offset(1, 1), blurRadius: 2, color: Colors.black38)],
-        );
-        final textPainter = TextPainter(
-          text: TextSpan(text: (curIndex + 1).toString(), style: textStyle),
-          textDirection: TextDirection.ltr,
-          textAlign: TextAlign.center,
-        );
-        textPainter.layout(minWidth: 0, maxWidth: w);
-        textPainter.paint(canvas, Offset(left + (w - textPainter.width) / 2, top + (h - textPainter.height) / 2));
+        // ✅ 优化:使用缓存的 TextPainter 和布局结果
+        final (textPainter, textWidth, textHeight) = _getTextPainterWithLayout((curIndex + 1).toString(), h * 0.25, w);
+        textPainter.paint(canvas, Offset(left + (w - textWidth) / 2, top + (h - textHeight) / 2));
       }
     }
     canvas.restore();
-    _drawBorders(canvas, left, top, w, h);
   }
 
   void _drawBorders(Canvas canvas, double x, double y, double w, double h) {
@@ -595,5 +837,16 @@ class CanvasPainter extends CustomPainter {
   }
 
   @override
-  bool shouldRepaint(covariant CanvasPainter oldDelegate) => true;
+  bool shouldRepaint(covariant CanvasPainter oldDelegate) {
+    // ✅ 优化:flip 动画期间,如果使用了 Picture 缓存,仍需重绘但不需要重新计算所有碎片
+    // - 状态变化 / level 变化 / collectionIndex 变化 → 总是需要重绘
+    // - flipAnimation.isAnimating → 需要重绘,但使用 Picture 缓存只绘制翻转碎片
+    // - unlockAnimation.isAnimating / dealingAnimation.isAnimating → 总是需要重绘
+    return oldDelegate.board.status != (forceStatus ?? board.status) ||
+        oldDelegate.level != level ||
+        oldDelegate.collectionIndex != collectionIndex ||
+        flipAnimation.isAnimating ||
+        unlockAnimation.isAnimating ||
+        dealingAnimation.isAnimating;
+  }
 }

+ 3 - 1
lib/homepage/home_screen.dart

@@ -452,6 +452,8 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
 
     _isNavigating = true;
 
+    interPending = false;
+
     try {
       cleanBanner();
 
@@ -467,7 +469,7 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
         // 打印下当下的插屏广告状态
         _log.info('==================>interState =  $intersState');
 
-        if (intersState == AdState.ready && shouldShowInterstitialAd("level_done", data.currentLevel - 1)) {
+        if (intersState == AdState.ready && data.currentLevel % 25 != 0 && shouldShowInterstitialAd("level_done", data.currentLevel - 1)) {
           // 这种情况表示有插屏广告在播放,需要等待广告关闭之后才能执行翻牌动画等逻辑
           interPending = true;
           _log.info('Interstitial ad is currently playing, will execute post-ad logic after dismissal.');

+ 24 - 3
lib/play/board_play.dart

@@ -3,7 +3,6 @@ import 'dart:io';
 import 'dart:math';
 import 'dart:ui' as ui;
 
-import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:fluttertoast/fluttertoast.dart';
@@ -28,11 +27,12 @@ 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:puzzleweave/utils/memory_monitor.dart';
 import 'package:vector_math/vector_math.dart' as vmath;
 import 'package:vibration/vibration.dart';
+
 import '../ads/ads_state.dart';
 import '../config/config.dart';
 
@@ -69,6 +69,9 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
   bool isDownloadSlow = false;
   late Timer timer;
 
+  // ⚠️ 新增:banner 延迟加载标记
+  bool _bannerDelayedLoad = false;
+
   late ItemLoader itemLoader;
   late JcAudioController audio;
   late SettingsController settings;
@@ -171,6 +174,7 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
   void _initAnimations() {
     final Device device = context.read<Device>();
 
+    // ⚠️ 优化:只初始化必要的动画控制器,其他延迟创建
     _moveAnimationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200));
     _moveAnimationController.addListener(_moveAnimationListener);
     _moveAnimationController.addStatusListener(_moveAnimationStatusListener);
@@ -196,6 +200,12 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
     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<double>(begin: 0.0, end: -deltaY).animate(_successAnimationController);
@@ -209,7 +219,9 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
     ).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<double>(begin: 0.0, end: 1.0).animate(
       CurvedAnimation(
@@ -615,6 +627,13 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
           }
         }
       });
+
+      // ⚠️ 关键优化:发牌动画开始后 2 秒再加载 Banner
+      Future.delayed(const Duration(seconds: 2), () {
+        if (mounted) {
+          setState(() => _bannerDelayedLoad = true);
+        }
+      });
     }
   }
 
@@ -763,7 +782,8 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
                 child: SizedBox(
                   height: context.read<Device>().bannerHeight,
                   width: double.infinity,
-                  child: isBannerVisible ? getBanner('playBottom') : const SizedBox.shrink(),
+                  // ⚠️ 关键优化:只有在延迟加载标记为 true 时才渲染 Banner
+                  child: (isBannerVisible && _bannerDelayedLoad) ? getBanner('playBottom') : const SizedBox.shrink(),
                 ),
               ),
             ),
@@ -907,6 +927,7 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
 
           // 尝试直接pop up, 不等待返回结果,避免用户等待广告结束后才看到界面响应
           if (data.currentLevel % 25 != 0) {
+            // 完成一个合集的最后一张图, 这个时候不展示插屏广告, 因为返回首页需要展示一系列的动画
             showInterstitialAd('level_done', widget.item.id, data.currentLevel - 1);
           }