import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:puzzleweave/audio/jc_audio_controller.dart'; import 'package:puzzleweave/config/device.dart'; import 'package:puzzleweave/homepage/home_board.dart'; import 'package:puzzleweave/l10n/app_localizations.dart'; import 'package:puzzleweave/models/cached_request.dart'; 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:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:puzzleweave/utils/memory_monitor.dart'; final Logger _log = Logger('home_board_play'); // ignore: must_be_immutable class HomeBoardPlay extends StatefulWidget { final double canvasWidth; final double canvasHeight; final GlobalKey collectionKey; // 接收 Collection Button Key, 方便定位 VoidCallback? onCollectionDone; // 新增一个合集完成的回调,外部home_screen可能会关心,当合集解锁动画完成,home_screen的左上角合集icon需要放大再复原 HomeBoardPlay({super.key, required this.canvasWidth, required this.canvasHeight, required this.collectionKey, this.onCollectionDone}); @override State createState() => HomeBoardPlayState(); } class HomeBoardPlayState extends State with TickerProviderStateMixin { late HomeBoard board; late JcAudioController audio; late Data data; List? collection; late CachedRequest collectionCachedRequest; late StreamSubscription? collectionSubscription; late ConfettiLayer confettiLayer; // 翻牌动画,完成一个关卡后翻开一个卡片 late AnimationController _flipController; late Animation _flipAnimation; // todo... 合集解锁动画,完成整个合集之后执行, 效果是整个图片缩小并位移到左上角的colleciton iconbutton, 形成合集“收纳”的效果 late AnimationController _unlockController; // 解锁动画控制器 late Animation _unlockAnimation; // 0.0 -> 1.0 // 当前合集是否已经完成(一个合集需要完成5x5即25个关卡才能解锁)(在切换到下一个合集之前调用判断) bool get _isCurCollectionDone => data.currentLevel != 0 && (data.currentCollectionIndex) * 25 == data.currentLevel; // bool get _isCurCollectionDone => true; // for test OverlayEntry? _overlayEntry; // 新增:用于管理全屏动画层 // 发牌动画 late AnimationController _dealingController; // 发牌动画控制器 late Animation _dealingAnimation; // 发牌间隔(ms) final int _dealingPieceInterval = 70; // 每个卡片移动时间(ms) final int _dealingPieceDuration = 200; // 发牌动画总时长(需要考虑到最后一个卡片的动画) // 共有 board.rows * board.cols 个卡片 int get _totalDealingDuration => (board.rows * board.cols - 1) * _dealingPieceInterval + _dealingPieceDuration; Timer? _dealingPeriodicTimer; int _dealingCount = 0; // 计数:记录执行次数 // ✅ 优化:Picture 缓存静止背景,避免 flip 动画期间重绘 24 张静止碎片 ui.Picture? _staticBackgroundPicture; // ✅ 优化:跟踪上次录制的关卡,实现增量更新(只更新新完成的碎片) int? _lastRecordedLevel; // ✅ 优化:发牌动画每个卡片的预录制 Picture List? _dealingPictures; @override void initState() { super.initState(); Device device = context.read(); board = HomeBoard(canvasWidth: widget.canvasWidth, canvasHeight: widget.canvasHeight, device: device); board.isReadyNotifier.addListener(_onBoardReady); confettiLayer = ConfettiLayer(this); Future.delayed(Duration.zero, () { if (mounted) confettiLayer.setup(context); }); audio = context.read(); data = context.read(); collectionCachedRequest = data.collection; // 主动获取缓存数据(关键) final collectionCachedData = collectionCachedRequest.cachedData; if (collectionCachedData != null) { _onCollectionDataUpdate(collectionCachedData); } collectionSubscription = collectionCachedRequest.stream.listen(_onCollectionDataUpdate, onError: _onCollectionDataError); _flipController = AnimationController(duration: const Duration(milliseconds: 1000), vsync: this); _flipAnimation = Tween(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); _unlockAnimation = Tween(begin: 0, end: 1).animate(CurvedAnimation(parent: _unlockController, curve: Curves.easeInBack)) ..addStatusListener((status) { if (status == AnimationStatus.completed) { _overlayEntry?.remove(); _overlayEntry = null; widget.onCollectionDone?.call(); // 如果是低端设备,直接跳过发牌动画 if (board.device.isLowEndDevice) { _log.info('低端设备,跳过发牌动画'); switchToNextCollection(); if (mounted) { setState(() { board.status = HomeBoardStatus.playing; board.invalidate(); }); } } else { _startDealingAnimation(); } } }); _dealingController = AnimationController( duration: Duration(milliseconds: _totalDealingDuration), vsync: this, ); _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 { _log.info('_onCollectionDataUpdate.... '); if (colldata != null) { collection = colldata as List; if (collection != null && collection!.isNotEmpty) { if ((data.completedWorks.value.length / 25).floor() > data.currentCollectionIndex) { if (currentCollectionItem != null) { _log.info('合集落后于关卡,没有正常切换,矫正!'); data.collectionDone(currentCollectionItem!); } } board.currentCollectionItem = currentCollectionItem; } if (mounted) setState(() {}); if (colldata.length < 5) { Future.delayed(const Duration(seconds: 3), () => refresh()); } } } _onCollectionDataError(error) { _log.info('_onCollectionDataError.... $error'); if (collection == null || collection!.isEmpty || collection!.length <= 2) { // 列表数据如果少于20,说明只是内置图,仍然刷新远程请求 _log.warning("_onCollectionDataError, retry again"); Future.delayed(Duration(seconds: 3), () => refresh()); } } Future refresh() async { _log.info('refresh...'); await collectionCachedRequest.refresh(); } // board 图片等资源加载完成的回调 _onBoardReady() { if (board.status == HomeBoardStatus.loading && mounted) { 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(); } }); } } // !!! 改造点 2: 启动发牌动画 void _startDealingAnimation() { if (!mounted) return; // 低端设备无需发牌动画,直接进入下一合集状态 if (board.device.isLowEndDevice) { _log.info('低端设备,跳过发牌动画 (_startDealingAnimation)'); switchToNextCollection(); if (mounted) { setState(() { board.status = HomeBoardStatus.playing; board.invalidate(); }); } return; } // 录制每张卡片图像(包括文字和边框) _recordDealingPictures(); setState(() { board.status = HomeBoardStatus.dealing; }); _dealingController.duration = Duration(milliseconds: _totalDealingDuration); _dealingController.forward(from: 0.0); _dealingCount = 0; audio.playSfx(SfxType.card); _dealingPeriodicTimer?.cancel(); _dealingPeriodicTimer = Timer.periodic(const Duration(milliseconds: 130), (timer) { if (mounted) { _dealingCount++; if (_dealingCount >= (_totalDealingDuration / 130) - 2) { timer.cancel(); } else { audio.playSfx(SfxType.card); } } else { timer.cancel(); } }); } ListItem? get currentCollectionItem { if (collection != null && collection!.isNotEmpty && data.currentCollectionIndex < collection!.length) { return collection![data.currentCollectionIndex]; } return null; } @override void dispose() { MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (before)'); board.isReadyNotifier.removeListener(_onBoardReady); _dealingPeriodicTimer?.cancel(); _overlayEntry?.remove(); _overlayEntry = null; // ✅ 清理 Picture 缓存 _staticBackgroundPicture = null; _lastRecordedLevel = null; board.dispose(); // 调用优化后的 dispose confettiLayer.dispose(); _flipController.dispose(); _unlockController.dispose(); _dealingController.dispose(); _dealingPictures = null; collectionSubscription?.cancel(); MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (after)'); super.dispose(); } void _checkCollectionDone() async { if (_isCurCollectionDone) { if (mounted) { setState(() { board.status = HomeBoardStatus.done; board.invalidate(); }); } _startUnlockAnimation(); } } void _startUnlockAnimation() { // 防御检查 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 (targetRenderBox == null || canvasRenderBox == null || !mounted) return; 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 * 0.2 / widget.canvasWidth); _overlayEntry = OverlayEntry( builder: (context) { return Positioned( left: canvasGlobalTL.dx, top: canvasGlobalTL.dy, child: SizedBox( width: widget.canvasWidth, height: widget.canvasHeight, child: CustomPaint( painter: CanvasPainter( board: board, level: data.currentLevel, collectionIndex: data.currentCollectionIndex, flipAnimation: _flipAnimation, unlockAnimation: _unlockAnimation, forceStatus: HomeBoardStatus.unlocking, dealingAnimation: _dealingAnimation, dealingPieceInterval: _dealingPieceInterval, dealingPieceDuration: _dealingPieceDuration, ), ), ), ); }, ); Overlay.of(context).insert(_overlayEntry!); if (mounted) { setState(() { board.status = HomeBoardStatus.unlocking; }); } _unlockController.forward(from: 0.0); } 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); if (data.currentLevel != 0 && (data.currentCollectionIndex + 1) * 25 == data.currentLevel && currentCollectionItem != null) { data.collectionDone(currentCollectionItem!); audio.playSfx(SfxType.star); confettiLayer.play(); // 合集完成时清理内存 // MemoryMonitor().manualCleanup(); } } // ✅ 新增:录制所有静止碎片到 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; } // ✅ 新增:录制发牌动画所需的每张卡片 Picture void _recordDealingPictures() { if (board.cardImage == null) return; final int collectionIndex = data.currentCollectionIndex; final img = board.cardImage!; final w = widget.canvasWidth / board.cols; final h = widget.canvasHeight / board.rows; _dealingPictures = []; for (var i = 0; i < board.rows; i++) { for (var j = 0; j < board.cols; j++) { final int curIndex = i * board.cols + j; final int displayIndex = collectionIndex * board.count + curIndex; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder, Rect.fromLTWH(0, 0, w, h)); // draw card image final cardSourceRect = Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble()); final rect = Rect.fromLTWH(0, 0, w, h); canvas.drawImageRect(img, cardSourceRect, rect, Paint()..isAntiAlias = true); // number text (include collection offset) final (textPainter, textWidth, textHeight) = CanvasPainter._getTextPainterWithLayout((displayIndex + 1).toString(), h * 0.25, w); textPainter.paint(canvas, Offset((w - textWidth) / 2, (h - textHeight) / 2)); // borders 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, ); _dealingPictures!.add(recorder.endRecording()); } } } // ✅ 辅助方法:绘制单个碎片到 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; _dealingPictures = null; board.switchToNextCollection(currentCollectionItem!); } @override Widget build(BuildContext context) { final isUnlocking = _unlockController.isAnimating || board.status == HomeBoardStatus.unlocking; return CustomPaint( size: Size(widget.canvasWidth, widget.canvasHeight), painter: isUnlocking && _overlayEntry != null ? null : CanvasPainter( board: board, level: data.currentLevel, collectionIndex: data.currentCollectionIndex, flipAnimation: _flipAnimation, unlockAnimation: _unlockAnimation, dealingAnimation: _dealingAnimation, dealingPieceDuration: _dealingPieceDuration, dealingPieceInterval: _dealingPieceInterval, staticBackgroundPicture: _staticBackgroundPicture, dealingPictures: _dealingPictures, ), child: board.status == HomeBoardStatus.loading ? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(SkinHelper.slotBorderColor))) : Container(), ); } } class CanvasPainter extends CustomPainter { final HomeBoard board; final int level; final int collectionIndex; final Animation flipAnimation; final Animation unlockAnimation; final HomeBoardStatus? forceStatus; final Animation dealingAnimation; final int dealingPieceDuration; final int dealingPieceInterval; // ✅ 优化:Picture 缓存,flip 动画期间使用预录制的背景 final ui.Picture? staticBackgroundPicture; // ✅ 优化:缓存发牌碎片 Picture final List? dealingPictures; // ✅ 优化:缓存 Matrix4 变换矩阵,避免每帧重新计算 static double _lastFlipProgress = -1.0; static Matrix4? _cachedFlipTransform; // ✅ 优化:缓存 TextPainter 的布局结果(宽高),避免重复 layout() 调用 static final Map _textPainterLayoutCache = {}; // ⚠️ 优化:缓存 TextPainter 避免每帧重建 static final Map _textPainterCache = {}; CanvasPainter({ required this.board, required this.level, required this.collectionIndex, required this.flipAnimation, required this.unlockAnimation, this.forceStatus, required this.dealingAnimation, required this.dealingPieceDuration, required this.dealingPieceInterval, this.staticBackgroundPicture, this.dealingPictures, }) : 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; // 根据不同状态执行绘制,每个方法内部现在都有 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; } } void _paintDealing(Canvas canvas, Size size) { final pictures = dealingPictures; if (board.cardImage == null) return; // 防御性检查 final totalPieces = board.rows * board.cols; final totalAnimationDuration = dealingPieceInterval * (totalPieces - 1) + dealingPieceDuration; final double w = board.pieceLogicalWidth; final double h = board.pieceLogicalHeight; final startX = board.cols - 1; final startY = board.rows - 1; final startPieceCenterX = startX * w + w / 2; final startPieceCenterY = startY * h + h / 2; final currentGlobalTime = dealingAnimation.value * totalAnimationDuration; for (int idx = 0; idx < totalPieces; idx++) { final int i = idx ~/ board.cols; final int j = idx % board.cols; final double startTime = idx * dealingPieceInterval.toDouble(); if (currentGlobalTime < startTime) continue; double progress = (currentGlobalTime - startTime) / dealingPieceDuration; progress = progress.clamp(0.0, 1.0); final double targetLeft = j * w; final double targetTop = i * h; final double currentOffsetX = ui.lerpDouble(startPieceCenterX - (j * w + w / 2), 0.0, progress) ?? 0.0; final double currentOffsetY = ui.lerpDouble(startPieceCenterY - (i * h + h / 2), 0.0, progress) ?? 0.0; canvas.save(); canvas.translate(targetLeft + currentOffsetX, targetTop + currentOffsetY); if (pictures != null && idx < pictures.length) { canvas.drawPicture(pictures[idx]); } else { // fallback to old drawing path final cardImg = board.cardImage!; 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); final int displayIdx = collectionIndex * board.count + idx; final (textPainter, textWidth, textHeight) = CanvasPainter._getTextPainterWithLayout((displayIdx + 1).toString(), h * 0.25, w); textPainter.paint(canvas, Offset((w - textWidth) / 2, (h - textHeight) / 2)); _drawBorders(canvas, 0, 0, w, h); } canvas.restore(); } } void _paintUnlocking(Canvas canvas, Size size) { final img = board.image; if (img == null) return; 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, ); canvas.save(); final centerX = size.width / 2; final centerY = size.height / 2; canvas.translate(centerX + currentOffset.dx, centerY + currentOffset.dy); canvas.scale(currentScale); canvas.translate(-centerX, -centerY); final rect = Rect.fromLTWH(0, 0, size.width, size.height); final sourceRect = Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble()); canvas.drawImageRect(img, sourceRect, rect, Paint()..isAntiAlias = true); canvas.restore(); } void _paintSuccess(Canvas canvas, Size size) { final img = board.image; if (img == null) return; 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(img, sourceRect, rect, Paint()..isAntiAlias = true); canvas.restore(); _drawBorders(canvas, 0, 0, size.width, size.height); } void _paintPlaying(Canvas canvas, Size size) { // ✅ 优化:如果 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); } } } } 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; final w = size.width / board.cols; final h = size.height / 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()); final curIndex = collectionIndex * board.count + row * board.rows + col; double flipProgress = (flipAnimation.isAnimating && curIndex == level - 1) ? flipAnimation.value : 0.0; // ⚠️ 优化:只有当前翻转的碎片才计算 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); // ✅ 优化:缓存 Matrix4 变换矩阵,只在 flipProgress 变化时重新计算 Matrix4 transform; if (_lastFlipProgress != flipProgress) { double angle = flipProgress * pi; transform = Matrix4.identity() ..setEntry(3, 2, 0.0015) ..rotateY(angle); if (flipProgress > 0.5) transform.scale(-1.0, 1.0, 1.0); _cachedFlipTransform = transform; _lastFlipProgress = flipProgress; } else { transform = _cachedFlipTransform ?? Matrix4.identity(); } canvas.transform(transform.storage); canvas.translate(-centerX, -centerY); canvas.clipRRect(rrect); if (flipped) { canvas.drawImageRect(img, imageSourceRect, rect, Paint()..isAntiAlias = true); } else { 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) { // ✅ 优化:使用缓存的 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 _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) { // ✅ 优化: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; } }