Explorar o código

修复firebase null check crash; 洗牌动画调快

guoziyun hai 6 meses
pai
achega
886e6918c7
Modificáronse 4 ficheiros con 292 adicións e 479 borrados
  1. 88 38
      lib/homepage/home_board.dart
  2. 183 429
      lib/homepage/home_board_play.dart
  3. 11 2
      lib/homepage/home_screen.dart
  4. 10 10
      lib/play/board_play.dart

+ 88 - 38
lib/homepage/home_board.dart

@@ -18,7 +18,7 @@ class HomeBoard {
   ui.Image? image;
 
   // 纸牌背面图
-  late ui.Image cardImage;
+  ui.Image? cardImage;
 
   // board 的canvas绘制区域尺寸
   final double canvasWidth;
@@ -34,23 +34,28 @@ class HomeBoard {
   double get pieceLogicalWidth => canvasWidth / cols;
   double get pieceLogicalHeight => canvasHeight / rows;
 
-  ValueNotifier boardNotifier = ValueNotifier(1);
+  // 用于触发重绘的通知器
+  final ValueNotifier<int> boardNotifier = ValueNotifier<int>(1);
+  // 用于通知外部资源已准备就绪
+  final ValueNotifier<bool> isReadyNotifier = ValueNotifier<bool>(false);
 
   HomeBoardStatus status = HomeBoardStatus.loading;
 
   ListItem? _currentCollectionItem;
+
+  // 2. 优化:记录正在加载的 ID,解决异步竞态问题
+  String? _loadingImageUrl;
+
   ListItem? get currentCollectionItem => _currentCollectionItem;
+
   set currentCollectionItem(ListItem? item) {
     if (item == null) return;
-    if (_currentCollectionItem == null || _currentCollectionItem!.id != item.id) {
+    if (_currentCollectionItem?.id != item.id) {
       _currentCollectionItem = item;
       _loadImage();
     }
   }
 
-  ValueNotifier isReadyNotifier = ValueNotifier(false);
-
-  // 用于存储合集解锁动画相关信息
   Offset _unlockTargetOffset = Offset.zero;
   double _unlockTargetScale = 1.0;
 
@@ -70,45 +75,90 @@ class HomeBoard {
     boardNotifier.value++;
   }
 
-  // 加载合集图
-  void _loadImage() async {
-    double dpr = device.devicePixelRatio;
+  // 3. 核心改进:带竞态检查和资源释放的图片加载
+  Future<void> _loadImage() async {
+    if (_currentCollectionItem == null) return;
+
+    final String currentId = _currentCollectionItem!.id;
+    _loadingImageUrl = currentId; // 记录当前请求的 ID
+
+    // 如果需要切换时立即白屏,可以取消下面注释:
+    // _clearImage();
+
+    try {
+      double dpr = device.devicePixelRatio;
+      ItemLoader itemLoader = ItemLoader.load(_currentCollectionItem!);
+
+      // 异步获取图片
+      ui.Image? loadedImage = await itemLoader.getImageBySize((canvasWidth * dpr).round(), (canvasHeight * dpr).round());
+
+      // --- 关键判断:竞态条件处理 ---
+      // 如果图片回来时,用户已经切换到了下一个合集,则丢弃当前图片并释放内存
+      if (_loadingImageUrl != currentId) {
+        _log.info('丢弃已过时的图片加载结果: $currentId');
+        loadedImage.dispose();
+        return;
+      }
+
+      // 4. 修改:在赋值新图前,先安全释放旧图内存
+      if (image != null) {
+        image!.dispose();
+      }
+
+      image = loadedImage;
+      isReadyNotifier.value = true;
+      invalidate();
+      _log.info('成功加载合集图片: $currentId');
+    } catch (e) {
+      _log.severe('加载合集图片失败: $e');
+      isReadyNotifier.value = false;
+    }
+  }
 
-    ItemLoader itemLoader = ItemLoader.load(currentCollectionItem!);
-    image = await itemLoader.getImageBySize((canvasWidth * dpr).round(), (canvasHeight * dpr).round());
+  // 5. 修改:安全加载卡片背面图
+  Future<void> _loadCardImage() async {
+    try {
+      double dpr = device.devicePixelRatio;
+      final Size bestCardSize = Size(pieceLogicalWidth * dpr, pieceLogicalHeight * dpr);
+
+      final ByteData cardData = await rootBundle.load('assets/images/backcard_green.png');
+      final ui.Codec cardCodec = await ui.instantiateImageCodec(
+        cardData.buffer.asUint8List(),
+        targetWidth: bestCardSize.width.round(),
+        targetHeight: bestCardSize.height.round(),
+      );
+      final ui.FrameInfo cardFrameInfo = await cardCodec.getNextFrame();
+
+      cardImage = cardFrameInfo.image;
+      invalidate();
+    } catch (e) {
+      _log.severe('加载卡片背面图失败: $e');
+    }
+  }
 
-    isReadyNotifier.value = true;
-    invalidate();
-    _log.info('加载collection图片: ${currentCollectionItem.toString()}');
+  // 6. 新增:彻底释放所有图片内存,防止 OOM
+  void dispose() {
+    _clearImage();
+    if (cardImage != null) {
+      cardImage!.dispose();
+      cardImage = null;
+    }
+    boardNotifier.dispose();
+    isReadyNotifier.dispose();
   }
 
-  // 加载卡片图
-  void _loadCardImage() async {
-    double dpr = device.devicePixelRatio;
-    // 加载扑克背面图片,用于制作发牌动画
-    final Size bestCardImageSize = Size(pieceLogicalWidth * dpr, pieceLogicalHeight * dpr);
-    final ByteData cardData = await rootBundle.load('assets/images/backcard_green.png');
-    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;
-
-    invalidate();
+  void _clearImage() {
+    if (image != null) {
+      image!.dispose();
+      image = null;
+    }
+    isReadyNotifier.value = false;
   }
 
-  // 老的合集完成,切换到新的合集
   void switchToNextCollection(ListItem newItem) {
-    _log.info('合集完全解锁,切换到新的合集');
-
-    // // 先释放原来的资源
-    // image?.dispose();
-    // image = null;
-
-    // 重新加载新合集图
-    isReadyNotifier.value = false;
+    _log.info('切换到新的合集: ${newItem.id}');
+    // 切换状态,让 UI 进入 loading
+    status = HomeBoardStatus.loading;
     currentCollectionItem = newItem;
   }
 }

+ 183 - 429
lib/homepage/home_board_play.dart

@@ -13,7 +13,6 @@ import 'package:puzzleweave/models/data.dart';
 import 'package:puzzleweave/models/items.dart';
 import 'package:puzzleweave/play/confetti_layer.dart';
 import 'package:puzzleweave/skin/skin.dart';
-import 'package:puzzleweave/utils/star.dart';
 import 'package:logging/logging.dart';
 import 'package:provider/provider.dart';
 
@@ -62,7 +61,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
   // 发牌间隔(ms)
   final int _dealingPieceInterval = 70;
   // 每个卡片移动时间(ms)
-  final int _dealingPieceDuration = 300;
+  final int _dealingPieceDuration = 200;
 
   // 发牌动画总时长(需要考虑到最后一个卡片的动画)
   // 共有 board.rows * board.cols 个卡片
@@ -81,9 +80,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
     confettiLayer = ConfettiLayer(this);
 
     Future.delayed(Duration.zero, () {
-      if (mounted) {
-        confettiLayer.setup(context);
-      }
+      if (mounted) confettiLayer.setup(context);
     });
 
     audio = context.read<JcAudioController>();
@@ -97,57 +94,41 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
     }
     collectionSubscription = collectionCachedRequest.stream.listen(_onCollectionDataUpdate, onError: _onCollectionDataError);
 
-    // 初始化翻牌动画控制器
-    _flipController = AnimationController(
-      duration: const Duration(milliseconds: 1000), // 动画时长
-      vsync: this, // HomeBoardState 必须实现 TickerProviderStateMixin
-    );
-    // 创建一个 0.0 到 1.0 的动画,用于控制翻转角度
+    _flipController = AnimationController(duration: const Duration(milliseconds: 1000), vsync: this);
     _flipAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: _flipController, curve: Curves.easeOut))
       ..addStatusListener((status) {
         if (status == AnimationStatus.completed) {
-          // 检查整个合集是否全部完成
           _checkCollectionDone();
         }
       });
 
-    // 初始化解锁动画控制器
-    _unlockController = AnimationController(
-      duration: const Duration(milliseconds: 800), // 动画时长
-      vsync: this,
-    );
+    _unlockController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this);
     _unlockAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: _unlockController, curve: Curves.easeInBack))
       ..addStatusListener((status) {
         if (status == AnimationStatus.completed) {
-          _log.info('合集解锁动画结束');
-          _overlayEntry?.remove(); // 动画结束时移除浮层
+          _overlayEntry?.remove();
           _overlayEntry = null;
-
-          // 动画结束后,通知外部(HomeScreen)
           widget.onCollectionDone?.call();
-
-          // 启动发牌动画
           _startDealingAnimation();
         }
       });
 
-    // 初始化发牌动画
     _dealingController = AnimationController(
-      duration: Duration(milliseconds: _totalDealingDuration), // 动态设置总时长
+      duration: Duration(milliseconds: _totalDealingDuration),
       vsync: this,
     );
-    _dealingAnimation =
-        CurvedAnimation(parent: _dealingController, curve: Curves.easeOut) // 使用缓动曲线
-          ..addStatusListener((status) {
-            if (status == AnimationStatus.completed) {
-              // 发牌结束,这个时候再来切换到下一个合集
-              switchToNextCollection();
-              setState(() {
-                board.status = HomeBoardStatus.playing; // 发牌结束进入正常的绘制状态
-                board.invalidate(); // 确保最终状态绘制正确
-              });
-            }
-          });
+    _dealingAnimation = CurvedAnimation(parent: _dealingController, curve: Curves.easeOut)
+      ..addStatusListener((status) {
+        if (status == AnimationStatus.completed) {
+          switchToNextCollection();
+          if (mounted) {
+            setState(() {
+              board.status = HomeBoardStatus.playing;
+              board.invalidate();
+            });
+          }
+        }
+      });
   }
 
   _onCollectionDataUpdate(colldata) async {
@@ -155,7 +136,6 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
     if (colldata != null) {
       collection = colldata as List<ListItem>;
       if (collection != null && collection!.isNotEmpty) {
-        // 做一个矫正,避免没有正常退出,合集没有切换的情况
         if ((data.completedWorks.value.length / 25).floor() > data.currentCollectionIndex) {
           if (currentCollectionItem != null) {
             _log.info('合集落后于关卡,没有正常切换,矫正!');
@@ -164,11 +144,9 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
         }
         board.currentCollectionItem = currentCollectionItem;
       }
-      setState(() {});
-
-      // 远程数据没有加载到,3秒后重试
+      if (mounted) setState(() {});
       if (colldata.length < 5) {
-        Future.delayed(Duration(seconds: 3), () => refresh());
+        Future.delayed(const Duration(seconds: 3), () => refresh());
       }
     }
   }
@@ -189,7 +167,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
 
   // board 图片等资源加载完成的回调
   _onBoardReady() {
-    if (board.status == HomeBoardStatus.loading) {
+    if (board.status == HomeBoardStatus.loading && mounted) {
       setState(() {
         board.status = HomeBoardStatus.playing;
       });
@@ -198,21 +176,25 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
 
   // !!! 改造点 2: 启动发牌动画
   void _startDealingAnimation() {
+    if (!mounted) return;
     setState(() {
       board.status = HomeBoardStatus.dealing;
     });
-    _dealingController.duration = Duration(milliseconds: _totalDealingDuration); // 确保duration是正确的
+    _dealingController.duration = Duration(milliseconds: _totalDealingDuration);
     _dealingController.forward(from: 0.0);
     _dealingCount = 0;
     audio.playSfx(SfxType.card);
-    _dealingPeriodicTimer = Timer.periodic(Duration(milliseconds: 150), (timer) {
+    _dealingPeriodicTimer?.cancel();
+    _dealingPeriodicTimer = Timer.periodic(const Duration(milliseconds: 130), (timer) {
       if (mounted) {
         _dealingCount++;
-        if (_dealingCount >= (_totalDealingDuration / 150) - 2) {
+        if (_dealingCount >= (_totalDealingDuration / 130) - 2) {
           timer.cancel();
         } else {
           audio.playSfx(SfxType.card);
         }
+      } else {
+        timer.cancel();
       }
     });
   }
@@ -227,6 +209,10 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
   @override
   void dispose() {
     board.isReadyNotifier.removeListener(_onBoardReady);
+    _dealingPeriodicTimer?.cancel();
+    _overlayEntry?.remove();
+    _overlayEntry = null;
+    board.dispose(); // 调用优化后的 dispose
     confettiLayer.dispose();
     _flipController.dispose();
     _unlockController.dispose();
@@ -235,103 +221,55 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
     super.dispose();
   }
 
-  // for test
-  void testAnimation() async {
-    // setState(() {
-    //   board.status = HomeBoardStatus.done;
-    //   board.invalidate();
-    // });
-
-    // audio.playSfx(SfxType.star);
-    // confettiLayer.play();
-
-    // // 等待confetti动画结束, 然后启动解锁动画
-    // await Future.delayed(Duration(milliseconds: 500));
-    // _startUnlockAnimation();
-    setState(() {
-      board.status = HomeBoardStatus.dealing;
-      board.invalidate();
-    });
-    _startDealingAnimation();
-    _dealingCount = 0;
-    audio.playSfx(SfxType.card);
-    _dealingPeriodicTimer = Timer.periodic(Duration(milliseconds: 150), (timer) {
-      if (mounted) {
-        _dealingCount++;
-        if (_dealingCount >= (_totalDealingDuration / 150) - 2) {
-          timer.cancel();
-        } else {
-          audio.playSfx(SfxType.card);
-        }
-      }
-    });
-  }
-
-  // 翻牌动画结束后,检查整个合集是否全部完成,如果全部完成,需要展示一系列的动画
   void _checkCollectionDone() async {
     if (_isCurCollectionDone) {
-      // 将状态置为done,canvas绘制一整张图,不再是单个卡片
-      setState(() {
-        board.status = HomeBoardStatus.done;
-        board.invalidate();
-      });
-
+      if (mounted) {
+        setState(() {
+          board.status = HomeBoardStatus.done;
+          board.invalidate();
+        });
+      }
       _startUnlockAnimation();
     }
   }
 
-  // 实现解锁动画启动方法
   void _startUnlockAnimation() {
-    // 1. 获取动画参数 (与您之前的逻辑保持一致,用于计算位移和缩放目标)
-    final RenderBox? targetRenderBox = widget.collectionKey.currentContext?.findRenderObject() as RenderBox?;
-    if (targetRenderBox == null || !mounted) return;
-
-    final targetPosition = targetRenderBox.localToGlobal(targetRenderBox.size.center(Offset.zero));
+    // 防御检查 1: 确保图片已加载,否则跳过动画直接发牌
+    if (board.image == null) {
+      _log.warning('Unlock image not ready, skipping animation.');
+      widget.onCollectionDone?.call();
+      _startDealingAnimation();
+      return;
+    }
 
+    final RenderBox? targetRenderBox = widget.collectionKey.currentContext?.findRenderObject() as RenderBox?;
     final RenderBox? canvasRenderBox = context.findRenderObject() as RenderBox?;
-    if (canvasRenderBox == null) return;
 
-    // 获取 CustomPaint 顶层左上角的全局坐标
-    final canvasGlobalTL = canvasRenderBox.localToGlobal(Offset.zero);
+    if (targetRenderBox == null || canvasRenderBox == null || !mounted) return;
 
-    // 计算 Canvas 中心到目标图标中心的全局位移量
+    final targetPosition = targetRenderBox.localToGlobal(targetRenderBox.size.center(Offset.zero));
+    final canvasGlobalTL = canvasRenderBox.localToGlobal(Offset.zero);
     final canvasCenter = canvasRenderBox.localToGlobal(canvasRenderBox.size.center(Offset.zero));
     final Offset delta = targetPosition - canvasCenter;
 
-    // 存储计算出的动画目标数据
-    board.setUnlockAnimationTarget(targetOffset: delta, targetScale: targetRenderBox.size.width / widget.canvasWidth);
-
-    // 存储计算出的动画目标数据,供 CustomPainter 使用
-    board.setUnlockAnimationTarget(
-      targetOffset: delta,
-      // 目标缩放比例
-      targetScale: targetRenderBox.size.width * 0.2 / widget.canvasWidth,
-      // targetScale: 0,
-    );
+    board.setUnlockAnimationTarget(targetOffset: delta, targetScale: targetRenderBox.size.width * 0.2 / widget.canvasWidth);
 
-    // 2. 创建并插入全屏 Overlay Entry
     _overlayEntry = OverlayEntry(
       builder: (context) {
-        // OverlayEntry 的 (0,0) 是屏幕的左上角。
-
-        // 我们将 CustomPaint 放置在它原来的全局位置
         return Positioned(
-          left: canvasGlobalTL.dx, // 初始 X 坐标
-          top: canvasGlobalTL.dy, // 初始 Y 坐标
+          left: canvasGlobalTL.dx,
+          top: canvasGlobalTL.dy,
           child: SizedBox(
-            // 尺寸和 HomeBoard 一致
             width: widget.canvasWidth,
             height: widget.canvasHeight,
             child: CustomPaint(
-              size: Size(widget.canvasWidth, widget.canvasHeight),
-              // 使用 CanvasPainter 绘制,并强制设置为 unlocking 状态
               painter: CanvasPainter(
                 board: board,
                 level: data.currentLevel,
                 collectionIndex: data.currentCollectionIndex,
                 flipAnimation: _flipAnimation,
                 unlockAnimation: _unlockAnimation,
-                forceStatus: HomeBoardStatus.unlocking, // 强制状态,用于 Overlay 绘制
+                forceStatus: HomeBoardStatus.unlocking,
                 dealingAnimation: _dealingAnimation,
                 dealingPieceInterval: _dealingPieceInterval,
                 dealingPieceDuration: _dealingPieceDuration,
@@ -344,20 +282,19 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
 
     Overlay.of(context).insert(_overlayEntry!);
 
-    // 启动动画
-    setState(() {
-      board.status = HomeBoardStatus.unlocking; // 切换到 unlocking 状态
-    });
+    if (mounted) {
+      setState(() {
+        board.status = HomeBoardStatus.unlocking;
+      });
+    }
     _unlockController.forward(from: 0.0);
   }
 
-  // 对外暴露的触发动画方法 (供 HomeScreen 调用)
   void startFlipAnimation() {
     _flipController.forward(from: 0.0);
     audio.playSfx(SfxType.flip);
     if (data.currentLevel != 0 && (data.currentCollectionIndex + 1) * 25 == data.currentLevel && currentCollectionItem != null) {
       data.collectionDone(currentCollectionItem!);
-      // 展示撒花动画
       audio.playSfx(SfxType.star);
       confettiLayer.play();
     }
@@ -365,16 +302,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
 
   void switchToNextCollection() {
     if (currentCollectionItem == null) {
-      _log.info('没有更多的合集了');
-      Fluttertoast.showToast(
-        msg: AppLocalizations.of(context)!.noMorePicture,
-        toastLength: Toast.LENGTH_SHORT,
-        gravity: ToastGravity.CENTER,
-        timeInSecForIosWeb: 1,
-        backgroundColor: SkinHelper.slotBorderColor,
-        textColor: Colors.white,
-        fontSize: 16.0,
-      );
+      Fluttertoast.showToast(msg: AppLocalizations.of(context)!.noMorePicture);
       return;
     }
     board.switchToNextCollection(currentCollectionItem!);
@@ -382,13 +310,11 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
 
   @override
   Widget build(BuildContext context) {
-    // 检查是否正在执行全屏动画
     final isUnlocking = _unlockController.isAnimating || board.status == HomeBoardStatus.unlocking;
     return CustomPaint(
-      size: Size(widget.canvasWidth, widget.canvasHeight), // 固定画布尺寸
-      // 正在解锁动画时,原 CustomPaint 不绘制内容 (或者只绘制一个透明的占位图)
+      size: Size(widget.canvasWidth, widget.canvasHeight),
       painter: isUnlocking && _overlayEntry != null
-          ? null // 动画运行时,原 CustomPaint 不绘制,避免冲突
+          ? null
           : CanvasPainter(
               board: board,
               level: data.currentLevel,
@@ -402,34 +328,20 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
       child: board.status == HomeBoardStatus.loading
           ? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.slotBorderColor)))
           : Container(),
-      // child: childWidget,
     );
   }
-
-  Widget get childWidget {
-    if (board.status == HomeBoardStatus.loading) {
-      return Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.slotBorderColor)));
-    } else if (board.status == HomeBoardStatus.done) {
-      return const ShiningStars(size: 80);
-    } else {
-      return Container();
-    }
-  }
 }
 
-// 自定义画笔实现(实际绘制逻辑在这里)
 class CanvasPainter extends CustomPainter {
   final HomeBoard board;
-  final int level; //当前关卡序号
-  final int collectionIndex; // 当前合集序号
-
-  final Animation<double> flipAnimation; // 0.0 -> 1.0
+  final int level;
+  final int collectionIndex;
+  final Animation<double> flipAnimation;
   final Animation<double> unlockAnimation;
-  final HomeBoardStatus? forceStatus; // !!! 新增字段
-
-  final Animation<double> dealingAnimation; // !!! 改造点 6: 发牌动画
-  final int dealingPieceDuration; // !!! 改造点 7: 单个卡片动画时长
-  final int dealingPieceInterval; // !!! 改造点 8: 单个卡片动画间隔
+  final HomeBoardStatus? forceStatus;
+  final Animation<double> dealingAnimation;
+  final int dealingPieceDuration;
+  final int dealingPieceInterval;
 
   CanvasPainter({
     required this.board,
@@ -441,33 +353,37 @@ class CanvasPainter extends CustomPainter {
     required this.dealingAnimation,
     required this.dealingPieceDuration,
     required this.dealingPieceInterval,
-  }) : super(repaint: Listenable.merge([board.boardNotifier, flipAnimation, unlockAnimation, dealingAnimation])); // 触发重绘;
+  }) : super(repaint: Listenable.merge([board.boardNotifier, flipAnimation, unlockAnimation, dealingAnimation]));
 
   @override
   void paint(Canvas canvas, Size size) {
-    // !!! 改造点 1: 优先使用强制状态,否则使用 Board 状态
     final statusToPaint = forceStatus ?? board.status;
 
-    if (statusToPaint == HomeBoardStatus.playing) {
-      _paintPlaying(canvas, size);
-    } else if (statusToPaint == HomeBoardStatus.done) {
-      // 动画结束前,原 CustomPaint 处于 done 状态
-      _paintSuccess(canvas, size);
-    } else if (statusToPaint == HomeBoardStatus.unlocking) {
-      // 仅在 Overlay 中调用,执行动画
-      _paintUnlocking(canvas, size);
-    } else if (statusToPaint == HomeBoardStatus.dealing) {
-      _paintDealing(canvas, size);
+    // 根据不同状态执行绘制,每个方法内部现在都有 null 检查
+    switch (statusToPaint) {
+      case HomeBoardStatus.playing:
+        _paintPlaying(canvas, size);
+        break;
+      case HomeBoardStatus.done:
+        _paintSuccess(canvas, size);
+        break;
+      case HomeBoardStatus.unlocking:
+        _paintUnlocking(canvas, size);
+        break;
+      case HomeBoardStatus.dealing:
+        _paintDealing(canvas, size);
+        break;
+      case HomeBoardStatus.loading:
+        break;
     }
   }
 
-  // !!! 改造点 11: 实现 _paintDealing 方法
   void _paintDealing(Canvas canvas, Size size) {
-    _log.info('_paintDealing');
+    if (board.cardImage == null) return; // 防御性检查
+
     final totalPieces = board.rows * board.cols;
     final totalAnimationDuration = dealingPieceInterval * (totalPieces - 1) + dealingPieceDuration;
 
-    // 起始位置 (右下角宫格的中心)
     final startX = board.cols - 1;
     final startY = board.rows - 1;
     final startPieceCenterX = startX * board.pieceLogicalWidth + board.pieceLogicalWidth / 2;
@@ -475,65 +391,32 @@ class CanvasPainter extends CustomPainter {
 
     for (var i = 0; i < board.rows; i++) {
       for (var j = 0; j < board.cols; j++) {
-        final pieceIndex = i * board.cols + j; // 卡片索引 (0 到 24)
-
-        // 计算该卡片的动画起始时间和结束时间 (相对于 _dealingController 的总 duration)
-        final animationStartTime = pieceIndex * dealingPieceInterval; // 当前卡片开始动画的毫秒数
-        final animationEndTime = animationStartTime + dealingPieceDuration; // 当前卡片结束动画的毫秒数
-
-        // 计算当前全局动画进度 (0.0 - 1.0) 对应的毫秒数
+        final pieceIndex = i * board.cols + j;
+        final animationStartTime = pieceIndex * dealingPieceInterval;
+        final animationEndTime = animationStartTime + dealingPieceDuration;
         final currentGlobalTime = dealingAnimation.value * totalAnimationDuration;
 
-        // 判断当前卡片是否应该开始动画
+        double progress;
         if (currentGlobalTime < animationStartTime) {
-          // 还没轮到这个卡片,保持在起始位置
-          _drawDealingPiece(
-            canvas,
-            size,
-            i,
-            j,
-            startPieceCenterX - (j * board.pieceLogicalWidth + board.pieceLogicalWidth / 2), // 补偿目标位置的x
-            startPieceCenterY - (i * board.pieceLogicalHeight + board.pieceLogicalHeight / 2), // 补偿目标位置的y
-            0.0, // 进度为0,即起始位置
-          );
+          progress = 0.0;
         } else if (currentGlobalTime >= animationEndTime) {
-          // 动画已结束,停留在目标位置
-          _drawDealingPiece(
-            canvas,
-            size,
-            i,
-            j,
-            0.0, // 目标位置的相对偏移量为0
-            0.0,
-            1.0, // 进度为1,即目标位置
-          );
+          progress = 1.0;
         } else {
-          // 正在动画中,计算该卡片的局部动画进度
-          final localProgress = (currentGlobalTime - animationStartTime) / dealingPieceDuration;
-          _drawDealingPiece(
-            canvas,
-            size,
-            i,
-            j,
-            startPieceCenterX - (j * board.pieceLogicalWidth + board.pieceLogicalWidth / 2), // 补偿目标位置的x
-            startPieceCenterY - (i * board.pieceLogicalHeight + board.pieceLogicalHeight / 2), // 补偿目标位置的y
-            localProgress,
-          );
+          progress = (currentGlobalTime - animationStartTime) / dealingPieceDuration;
         }
+
+        double startOffX = startPieceCenterX - (j * board.pieceLogicalWidth + board.pieceLogicalWidth / 2);
+        double startOffY = startPieceCenterY - (i * board.pieceLogicalHeight + board.pieceLogicalHeight / 2);
+
+        _drawDealingPiece(canvas, size, i, j, startOffX, startOffY, progress);
       }
     }
   }
 
-  // 辅助方法:绘制发牌动画中的单个卡片
-  void _drawDealingPiece(
-    Canvas canvas,
-    Size size,
-    int row,
-    int col,
-    double startOffsetX,
-    double startOffsetY, // 起始位置相对于目标位置的偏移量
-    double progress, // 0.0 -> 1.0
-  ) {
+  void _drawDealingPiece(Canvas canvas, Size size, int row, int col, double startOffsetX, double startOffsetY, double progress) {
+    final cardImg = board.cardImage;
+    if (cardImg == null) return;
+
     final curIndex = collectionIndex * board.count + row * board.cols + col;
 
     final w = board.pieceLogicalWidth;
@@ -542,298 +425,169 @@ class CanvasPainter extends CustomPainter {
     final targetLeft = col * w;
     final targetTop = row * h;
 
-    // 计算当前动画帧的偏移量
-    final currentOffsetX = ui.lerpDouble(startOffsetX, 0.0, progress)!;
-    final currentOffsetY = ui.lerpDouble(startOffsetY, 0.0, progress)!;
+    final currentOffsetX = ui.lerpDouble(startOffsetX, 0.0, progress) ?? 0.0;
+    final currentOffsetY = ui.lerpDouble(startOffsetY, 0.0, progress) ?? 0.0;
 
     canvas.save();
-    // 先平移到卡片最终位置的左上角,再应用动画偏移
     canvas.translate(targetLeft + currentOffsetX, targetTop + currentOffsetY);
 
-    // 绘制卡片背面
-    final cardSourceRect = Rect.fromLTWH(0, 0, board.cardImage.width.toDouble(), board.cardImage.height.toDouble());
-    final rect = Rect.fromLTWH(0, 0, w, h); // 绘制在当前变换后的 (0,0,w,h)
+    final cardSourceRect = Rect.fromLTWH(0, 0, cardImg.width.toDouble(), cardImg.height.toDouble());
+    final rect = Rect.fromLTWH(0, 0, w, h);
+    canvas.drawImageRect(cardImg, cardSourceRect, rect, Paint()..isAntiAlias = true);
 
-    canvas.drawImageRect(board.cardImage, cardSourceRect, rect, Paint()..isAntiAlias = true);
-
-    // 绘制关卡数字 (与 _drawPiece 中的逻辑类似)
     final textStyle = TextStyle(
       color: Colors.white,
       fontSize: h * 0.25,
       fontWeight: FontWeight.bold,
-      shadows: [Shadow(offset: const Offset(1, 1), blurRadius: 2, color: Colors.black38)],
+      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,
     );
-    final textSpan = TextSpan(text: (curIndex + 1).toString(), style: textStyle);
-    final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, textAlign: TextAlign.center);
     textPainter.layout(minWidth: 0, maxWidth: w);
-    final textOffset = Offset((w - textPainter.width) / 2, (h - textPainter.height) / 2);
-    textPainter.paint(canvas, textOffset);
-
+    textPainter.paint(canvas, Offset((w - textPainter.width) / 2, (h - textPainter.height) / 2));
     canvas.restore();
 
-    // 绘制边框(为了避免裁剪,在 restore 后绘制)
-    final cornerRadius = 4.0;
-    final outerRRect = RRect.fromRectAndRadius(
-      Rect.fromLTWH(targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h).deflate(0.5),
-      Radius.circular(cornerRadius),
-    );
-    final innerRRect = RRect.fromRectAndRadius(
-      Rect.fromLTWH(targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h).deflate(1.5),
-      Radius.circular(cornerRadius),
-    );
-    canvas.drawRRect(outerRRect, outerBorderPaint);
-    canvas.drawRRect(innerRRect, innerBorderPaint);
+    _drawBorders(canvas, targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h);
   }
 
   void _paintUnlocking(Canvas canvas, Size size) {
-    _log.info('_paintUnlocking');
-    // 1. 获取动画进度 (0.0 -> 1.0)
-    final progress = unlockAnimation.value;
+    final img = board.image;
+    if (img == null) return;
 
-    // 3. 计算当前的位移和缩放
-    // 缩放:从 1.0 缩小到 targetScale
-    final startScale = 1.0;
-    final endScale = board.unlockTargetScale;
-    final currentScale = ui.lerpDouble(startScale, endScale, progress)!;
-
-    // 位移:从 (0, 0) 平移到 targetOffset
-    final startOffset = Offset.zero;
-    final endOffset = board.unlockTargetOffset;
-    final currentOffset = Offset(ui.lerpDouble(startOffset.dx, endOffset.dx, progress)!, ui.lerpDouble(startOffset.dy, endOffset.dy, progress)!);
+    final progress = unlockAnimation.value;
+    final currentScale = ui.lerpDouble(1.0, board.unlockTargetScale, progress) ?? 1.0;
+    final currentOffset = Offset(
+      ui.lerpDouble(0.0, board.unlockTargetOffset.dx, progress) ?? 0.0,
+      ui.lerpDouble(0.0, board.unlockTargetOffset.dy, progress) ?? 0.0,
+    );
 
-    // 4. 应用 Canvas 变换
     canvas.save();
-
-    // 缩放:以 Canvas 中心为缩放原点进行缩放
     final centerX = size.width / 2;
     final centerY = size.height / 2;
 
-    // 4.1. 移动到 Canvas 中心点 (将原点移到 CustomPaint 的中心)
-    canvas.translate(centerX, centerY);
-
-    // 4.2. 应用位移 (这是中心点相对 CustomPaint 中心点的移动)
-    canvas.translate(currentOffset.dx, currentOffset.dy);
-
-    // 4.3. 应用缩放 (以当前中心点为原点)
+    canvas.translate(centerX + currentOffset.dx, centerY + currentOffset.dy);
     canvas.scale(currentScale);
-
-    // 4.4. 移回 Canvas 原点 (回到 CustomPaint 的左上角,已应用了 位移 + 缩放)
     canvas.translate(-centerX, -centerY);
 
-    // 5. 绘制完整的合集图片 (与 _paintSuccess 逻辑相同)
-    // 简单点, 动画就不画边框圆角这些了
-    // final cornerRadius = 4.0;
     final rect = Rect.fromLTWH(0, 0, size.width, size.height);
-    // final rrect = RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius));
-    // final outerRRect = RRect.fromRectAndRadius(rect.deflate(0.5), Radius.circular(cornerRadius));
-    // final innerRRect = RRect.fromRectAndRadius(rect.deflate(1.5), Radius.circular(cornerRadius));
-
-    final sourceRect = Rect.fromLTWH(0, 0, board.image!.width.toDouble(), board.image!.height.toDouble());
-
-    // 裁剪并绘制图片
-    // canvas.clipRRect(rrect);  // 动画就不用clipRRect了
-    canvas.drawImageRect(board.image!, sourceRect, rect, Paint()..isAntiAlias = true);
-
-    // 绘制边框
-    // canvas.drawRRect(outerRRect, outerBorderPaint);
-    // canvas.drawRRect(innerRRect, innerBorderPaint);
-
+    final sourceRect = Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble());
+    canvas.drawImageRect(img, sourceRect, rect, Paint()..isAntiAlias = true);
     canvas.restore();
   }
 
-  _paintSuccess(Canvas canvas, Size size) {
-    _log.info('_paintSuccess');
-    final cornerRadius = 4.0;
-    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
-    final rrect = RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius));
-    final outerRRect = RRect.fromRectAndRadius(rect.deflate(0.5), Radius.circular(cornerRadius));
-    final innerRRect = RRect.fromRectAndRadius(rect.deflate(1.5), Radius.circular(cornerRadius));
+  void _paintSuccess(Canvas canvas, Size size) {
+    final img = board.image;
+    if (img == null) return;
 
-    final sourceRect = Rect.fromLTWH(0, 0, board.image!.width.toDouble(), board.image!.height.toDouble());
+    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
+    final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(4.0));
+    final sourceRect = Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble());
 
     canvas.save();
-
     canvas.clipRRect(rrect);
-
-    canvas.drawImageRect(board.image!, sourceRect, rect, Paint()..isAntiAlias = true);
-
+    canvas.drawImageRect(img, sourceRect, rect, Paint()..isAntiAlias = true);
     canvas.restore();
-
-    // 绘制边框
-    canvas.drawRRect(outerRRect, outerBorderPaint);
-    canvas.drawRRect(innerRRect, innerBorderPaint);
+    _drawBorders(canvas, 0, 0, size.width, size.height);
   }
 
-  _paintPlaying(Canvas canvas, Size size) {
-    _log.info('_paintPlaying');
-
+  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);
       }
     }
   }
 
-  final Paint outerBorderPaint = Paint()
-    ..color = SkinHelper.outLineBorderColor
-    ..style = PaintingStyle.stroke
-    ..strokeWidth = 1.0
-    ..isAntiAlias = true;
-
-  // 边框画笔
-  final Paint innerBorderPaint = Paint()
-    ..color = SkinHelper.innerLineBorderColor
-    ..style = PaintingStyle.stroke
-    ..strokeWidth = 1.0
-    ..isAntiAlias = true;
-
   void _drawPiece(Canvas canvas, Size size, int row, int col, bool flipped) {
-    final cornerRadius = 4.0;
+    final img = board.image;
+    final cardImg = board.cardImage;
+    if (img == null || cardImg == null) return; // 双重检查
 
     final w = size.width / board.cols;
     final h = size.height / board.rows;
-
-    final pieceWidth = board.image!.width / board.cols;
-    final pieceHeight = board.image!.height / board.rows;
-
     final left = col * w;
     final top = row * h;
-    final right = left + w;
-    final bottom = top + h;
+    final rect = Rect.fromLTWH(left, top, w, h);
+    final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(4.0));
 
-    // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke
-    final rect = Rect.fromLTRB(left, top, right, bottom);
-    final rrect = RRect.fromRectAndRadius(rect, Radius.circular(cornerRadius));
-    final outerRRect = RRect.fromRectAndRadius(rect.deflate(0.5), Radius.circular(cornerRadius));
-    final innerRRect = RRect.fromRectAndRadius(rect.deflate(1.5), Radius.circular(cornerRadius));
-
-    final cardSourceRect = Rect.fromLTWH(0, 0, board.cardImage.width.toDouble(), board.cardImage.height.toDouble());
+    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());
 
-    // 0-based index
     final curIndex = collectionIndex * board.count + row * board.rows + col;
-
-    // 1. 计算当前的翻转状态
-    double flipProgress = 0.0;
-
-    if (flipAnimation.isAnimating && curIndex == level - 1) {
-      // for test,只为方便查看动画效果,真正的代码是上面注释掉的
-      // if (flipAnimation.isAnimating && curIndex == 24) {
-      flipProgress = flipAnimation.value; // 0.0 -> 1.0
-      flipped = (flipProgress > 0.5);
-    }
-    // _log.info('level=$level, row=$row, col=$col, flippingIndex=$flippingIndex, flipProgress=$flipProgress, currentPieceFlipped=$currentPieceFlipped');
+    double flipProgress = (flipAnimation.isAnimating && curIndex == level - 1) ? flipAnimation.value : 0.0;
+    if (flipProgress > 0) flipped = flipProgress > 0.5;
 
     canvas.save();
-
-    // 2. 居中变换原点到拼图块中心
     final centerX = left + w / 2;
     final centerY = top + h / 2;
-
     canvas.translate(centerX, centerY);
-    // 3. 应用 3D 旋转 (围绕 Y 轴)
+
     if (flipProgress > 0.0) {
-      // 旋转角度从 0 到 pi (180度)
       double angle = flipProgress * pi;
-      // 引入透视投影(z轴缩放),让翻转效果更立体
-      const double perspective = 0.0015;
-
-      // 3D 变换矩阵
-      Matrix4 transform;
-      if (flipProgress <= 0.5) {
-        transform = Matrix4.identity()
-          ..setEntry(3, 2, perspective) // 3D 效果
-          ..rotateY(angle);
-      } else {
-        transform = Matrix4.identity()
-          ..setEntry(3, 2, perspective) // 3D 效果
-          ..rotateY(angle)
-          ..scale(-1.0, 1.0, 1.0); // 3. X轴缩放-1:抵消旋转带来的左右镜像
-      }
-
+      Matrix4 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);
     }
 
-    // 4. 移回原点
     canvas.translate(-centerX, -centerY);
-
-    // ... 现有裁剪逻辑 ...
     canvas.clipRRect(rrect);
 
     if (flipped) {
-      // 绘制正面
-      canvas.drawImageRect(board.image!, imageSourceRect, rect, Paint()..isAntiAlias = true);
+      canvas.drawImageRect(img, imageSourceRect, rect, Paint()..isAntiAlias = true);
     } else {
-      // 绘制背面
-      // 必须反转图片源矩形,以修正翻转180度后图像的镜像问题
-      final sourceRect = flipProgress > 0.5
-          ? imageSourceRect // 翻转后使用正面图像
-          : cardSourceRect; // 翻转前使用背面卡片
-
-      final targetImage = flipProgress > 0.5 ? board.image! : board.cardImage;
-
-      canvas.drawImageRect(targetImage, sourceRect, rect, Paint()..isAntiAlias = true);
+      final targetImg = flipProgress > 0.5 ? img : cardImg;
+      final sourceRect = flipProgress > 0.5 ? imageSourceRect : cardSourceRect;
+      canvas.drawImageRect(targetImg, sourceRect, rect, Paint()..isAntiAlias = true);
 
       if (flipProgress <= 0.5) {
-        // todo... 绘制关卡数字, 在卡片中间位置把curIndex绘制上去, 颜色白色
-        // 1. 配置文字样式:白色、加粗、动态字体大小(适配卡片尺寸)
         final textStyle = TextStyle(
           color: Colors.white,
-          fontSize: h * 0.25, // 字体大小为卡片高度的40%,适配不同尺寸
+          fontSize: h * 0.25,
           fontWeight: FontWeight.bold,
-          shadows: [
-            // 增加黑色阴影,让白色文字在卡片背景上更清晰(可选但推荐)
-            Shadow(offset: const Offset(1, 1), blurRadius: 2, color: Colors.black38),
-          ],
+          shadows: const [Shadow(offset: Offset(1, 1), blurRadius: 2, color: Colors.black38)],
         );
-
-        // 2. 初始化文字绘制器
-        final textSpan = TextSpan(text: (curIndex + 1).toString(), style: textStyle);
         final textPainter = TextPainter(
-          text: textSpan,
+          text: TextSpan(text: (curIndex + 1).toString(), style: textStyle),
           textDirection: TextDirection.ltr,
-          textAlign: TextAlign.center, // 文字水平居中
+          textAlign: TextAlign.center,
         );
-
-        // 3. 计算文字尺寸(必须调用layout())
-        textPainter.layout(
-          minWidth: 0,
-          maxWidth: w, // 文字最大宽度不超过卡片宽度
-        );
-
-        // 4. 计算文字居中偏移量
-        final textOffset = Offset(
-          left + (w - textPainter.width) / 2, // 水平居中
-          top + (h - textPainter.height) / 2, // 垂直居中
-        );
-
-        // 5. 绘制文字
-        textPainter.paint(canvas, textOffset);
+        textPainter.layout(minWidth: 0, maxWidth: w);
+        textPainter.paint(canvas, Offset(left + (w - textPainter.width) / 2, top + (h - textPainter.height) / 2));
       }
     }
-
-    canvas.restore(); // 恢复 Canvas 状态 (移除裁剪和 transform)
-
-    // --- 绘制边框 ---
-    // 为了确保边框线宽向外延伸的部分不会被裁剪,我们需要在裁剪区域被移除后重新绘制。
-    canvas.save();
-
-    canvas.drawRRect(outerRRect, outerBorderPaint);
-    canvas.drawRRect(innerRRect, innerBorderPaint);
-
     canvas.restore();
+    _drawBorders(canvas, left, top, w, h);
   }
 
-  @override
-  bool shouldRepaint(covariant CanvasPainter oldDelegate) {
-    return oldDelegate.level != level ||
-        oldDelegate.flipAnimation != flipAnimation ||
-        oldDelegate.unlockAnimation != unlockAnimation ||
-        oldDelegate.dealingAnimation != dealingAnimation ||
-        oldDelegate.board != board;
+  void _drawBorders(Canvas canvas, double x, double y, double w, double h) {
+    final rect = Rect.fromLTWH(x, y, w, h);
+    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,
+    );
   }
+
+  @override
+  bool shouldRepaint(covariant CanvasPainter oldDelegate) => true;
 }

+ 11 - 2
lib/homepage/home_screen.dart

@@ -135,8 +135,12 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
   void checkGoPlay() async {
     if (currentItem != null) {
       final jsonFile = await localFile(currentItem!.jsonPath);
-      if (await jsonFile.exists()) {
-        // 之前玩过有缓存, 直接进入play界面
+      final exists = await jsonFile.exists();
+
+      // !!! 关键修复:检查当前组件是否还在组件树中
+      if (!mounted) return;
+
+      if (exists) {
         gotoPlay(currentItem!);
       }
     }
@@ -444,9 +448,14 @@ class _HomeScreen extends AdsState<HomeScreen> with TickerProviderStateMixin {
   void gotoPlay(ListItem item, {bool firstRun = false}) async {
     _log.info('goto play, firstRun = $firstRun');
 
+    // !!! 增加保护
+    if (!mounted) return;
+
     PageRouteBuilder? pageRouteBuilder = BoardPlay.buildRoute(item, firstRun: firstRun);
     final result = await Navigator.push(context, pageRouteBuilder);
+
     if (!mounted) return;
+
     if (result != null && result == true) {
       // 通关返回, 展示翻牌
       _canvasKey.currentState?.startFlipAnimation();

+ 10 - 10
lib/play/board_play.dart

@@ -122,19 +122,19 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
   // 发牌动画参数
   // 发牌间隔(ms)
   int get _dealingPieceInterval {
-    if (board!.rows <= 3) return 120;
-    if (board!.rows == 4) return 100;
-    if (board!.rows == 5) return 80;
-    if (board!.rows == 6) return 60;
+    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;
   }
 
   // 每个卡片移动时间(ms)
   int get _dealingPieceDuration {
-    if (board!.rows <= 3) return 500;
-    if (board!.rows == 4) return 400;
-    if (board!.rows == 5) return 300;
-    if (board!.rows == 6) return 200;
+    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;
   }
 
@@ -556,10 +556,10 @@ class _BoardPlayState extends AdsState<BoardPlay> with TickerProviderStateMixin
       dealingAnimationController.duration = Duration(milliseconds: _totalDealingDuration);
       dealingAnimationController.forward(from: 0.0);
       audio.playSfx(SfxType.card);
-      _dealingPeriodicTimer = Timer.periodic(Duration(milliseconds: 150), (timer) {
+      _dealingPeriodicTimer = Timer.periodic(Duration(milliseconds: 130), (timer) {
         if (mounted) {
           _dealingCount++;
-          if (_dealingCount >= (_totalDealingDuration / 150) - 2) {
+          if (_dealingCount >= (_totalDealingDuration / 130) - 2) {
             timer.cancel();
           } else {
             audio.playSfx(SfxType.card);