|
@@ -64,6 +64,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
// 每个卡片移动时间(ms)
|
|
// 每个卡片移动时间(ms)
|
|
|
final int _dealingPieceDuration = 200;
|
|
final int _dealingPieceDuration = 200;
|
|
|
|
|
|
|
|
|
|
+
|
|
|
// 发牌动画总时长(需要考虑到最后一个卡片的动画)
|
|
// 发牌动画总时长(需要考虑到最后一个卡片的动画)
|
|
|
// 共有 board.rows * board.cols 个卡片
|
|
// 共有 board.rows * board.cols 个卡片
|
|
|
int get _totalDealingDuration => (board.rows * board.cols - 1) * _dealingPieceInterval + _dealingPieceDuration;
|
|
int get _totalDealingDuration => (board.rows * board.cols - 1) * _dealingPieceInterval + _dealingPieceDuration;
|
|
@@ -75,6 +76,9 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
// ✅ 优化:跟踪上次录制的关卡,实现增量更新(只更新新完成的碎片)
|
|
// ✅ 优化:跟踪上次录制的关卡,实现增量更新(只更新新完成的碎片)
|
|
|
int? _lastRecordedLevel;
|
|
int? _lastRecordedLevel;
|
|
|
|
|
|
|
|
|
|
+ // ✅ 优化:发牌动画每个卡片的预录制 Picture
|
|
|
|
|
+ List<ui.Picture>? _dealingPictures;
|
|
|
|
|
+
|
|
|
@override
|
|
@override
|
|
|
void initState() {
|
|
void initState() {
|
|
|
super.initState();
|
|
super.initState();
|
|
@@ -115,7 +119,19 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
_overlayEntry?.remove();
|
|
_overlayEntry?.remove();
|
|
|
_overlayEntry = null;
|
|
_overlayEntry = null;
|
|
|
widget.onCollectionDone?.call();
|
|
widget.onCollectionDone?.call();
|
|
|
- _startDealingAnimation();
|
|
|
|
|
|
|
+ // 如果是低端设备,直接跳过发牌动画
|
|
|
|
|
+ if (board.device.isLowEndDevice) {
|
|
|
|
|
+ _log.info('低端设备,跳过发牌动画');
|
|
|
|
|
+ switchToNextCollection();
|
|
|
|
|
+ if (mounted) {
|
|
|
|
|
+ setState(() {
|
|
|
|
|
+ board.status = HomeBoardStatus.playing;
|
|
|
|
|
+ board.invalidate();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _startDealingAnimation();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -191,6 +207,23 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
// !!! 改造点 2: 启动发牌动画
|
|
// !!! 改造点 2: 启动发牌动画
|
|
|
void _startDealingAnimation() {
|
|
void _startDealingAnimation() {
|
|
|
if (!mounted) return;
|
|
if (!mounted) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 低端设备无需发牌动画,直接进入下一合集状态
|
|
|
|
|
+ if (board.device.isLowEndDevice) {
|
|
|
|
|
+ _log.info('低端设备,跳过发牌动画 (_startDealingAnimation)');
|
|
|
|
|
+ switchToNextCollection();
|
|
|
|
|
+ if (mounted) {
|
|
|
|
|
+ setState(() {
|
|
|
|
|
+ board.status = HomeBoardStatus.playing;
|
|
|
|
|
+ board.invalidate();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 录制每张卡片图像(包括文字和边框)
|
|
|
|
|
+ _recordDealingPictures();
|
|
|
|
|
+
|
|
|
setState(() {
|
|
setState(() {
|
|
|
board.status = HomeBoardStatus.dealing;
|
|
board.status = HomeBoardStatus.dealing;
|
|
|
});
|
|
});
|
|
@@ -237,6 +270,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
_flipController.dispose();
|
|
_flipController.dispose();
|
|
|
_unlockController.dispose();
|
|
_unlockController.dispose();
|
|
|
_dealingController.dispose();
|
|
_dealingController.dispose();
|
|
|
|
|
+ _dealingPictures = null;
|
|
|
collectionSubscription?.cancel();
|
|
collectionSubscription?.cancel();
|
|
|
MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (after)');
|
|
MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (after)');
|
|
|
super.dispose();
|
|
super.dispose();
|
|
@@ -364,6 +398,54 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
_lastRecordedLevel = data.currentLevel;
|
|
_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
|
|
// ✅ 辅助方法:绘制单个碎片到 PictureRecorder
|
|
|
void _drawStaticPieceToRecorder(Canvas canvas, int row, int col, bool flipped, int curIndex) {
|
|
void _drawStaticPieceToRecorder(Canvas canvas, int row, int col, bool flipped, int curIndex) {
|
|
|
final img = board.image;
|
|
final img = board.image;
|
|
@@ -434,6 +516,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
// ✅ 清理 Picture 缓存,因为集合改变了
|
|
// ✅ 清理 Picture 缓存,因为集合改变了
|
|
|
_staticBackgroundPicture = null;
|
|
_staticBackgroundPicture = null;
|
|
|
_lastRecordedLevel = null;
|
|
_lastRecordedLevel = null;
|
|
|
|
|
+ _dealingPictures = null;
|
|
|
board.switchToNextCollection(currentCollectionItem!);
|
|
board.switchToNextCollection(currentCollectionItem!);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -454,6 +537,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
dealingPieceDuration: _dealingPieceDuration,
|
|
dealingPieceDuration: _dealingPieceDuration,
|
|
|
dealingPieceInterval: _dealingPieceInterval,
|
|
dealingPieceInterval: _dealingPieceInterval,
|
|
|
staticBackgroundPicture: _staticBackgroundPicture,
|
|
staticBackgroundPicture: _staticBackgroundPicture,
|
|
|
|
|
+ dealingPictures: _dealingPictures,
|
|
|
),
|
|
),
|
|
|
child: board.status == HomeBoardStatus.loading
|
|
child: board.status == HomeBoardStatus.loading
|
|
|
? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.slotBorderColor)))
|
|
? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.slotBorderColor)))
|
|
@@ -475,6 +559,8 @@ class CanvasPainter extends CustomPainter {
|
|
|
|
|
|
|
|
// ✅ 优化:Picture 缓存,flip 动画期间使用预录制的背景
|
|
// ✅ 优化:Picture 缓存,flip 动画期间使用预录制的背景
|
|
|
final ui.Picture? staticBackgroundPicture;
|
|
final ui.Picture? staticBackgroundPicture;
|
|
|
|
|
+ // ✅ 优化:缓存发牌碎片 Picture
|
|
|
|
|
+ final List<ui.Picture>? dealingPictures;
|
|
|
|
|
|
|
|
// ✅ 优化:缓存 Matrix4 变换矩阵,避免每帧重新计算
|
|
// ✅ 优化:缓存 Matrix4 变换矩阵,避免每帧重新计算
|
|
|
static double _lastFlipProgress = -1.0;
|
|
static double _lastFlipProgress = -1.0;
|
|
@@ -497,6 +583,7 @@ class CanvasPainter extends CustomPainter {
|
|
|
required this.dealingPieceDuration,
|
|
required this.dealingPieceDuration,
|
|
|
required this.dealingPieceInterval,
|
|
required this.dealingPieceInterval,
|
|
|
this.staticBackgroundPicture,
|
|
this.staticBackgroundPicture,
|
|
|
|
|
+ this.dealingPictures,
|
|
|
}) : super(repaint: Listenable.merge([board.boardNotifier, flipAnimation, unlockAnimation, dealingAnimation]));
|
|
}) : super(repaint: Listenable.merge([board.boardNotifier, flipAnimation, unlockAnimation, dealingAnimation]));
|
|
|
|
|
|
|
|
static TextPainter _getOrCreateTextPainter(String text, double fontSize) {
|
|
static TextPainter _getOrCreateTextPainter(String text, double fontSize) {
|
|
@@ -556,70 +643,56 @@ class CanvasPainter extends CustomPainter {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _paintDealing(Canvas canvas, Size size) {
|
|
void _paintDealing(Canvas canvas, Size size) {
|
|
|
|
|
+ final pictures = dealingPictures;
|
|
|
if (board.cardImage == null) return; // 防御性检查
|
|
if (board.cardImage == null) return; // 防御性检查
|
|
|
|
|
|
|
|
final totalPieces = board.rows * board.cols;
|
|
final totalPieces = board.rows * board.cols;
|
|
|
final totalAnimationDuration = dealingPieceInterval * (totalPieces - 1) + dealingPieceDuration;
|
|
final totalAnimationDuration = dealingPieceInterval * (totalPieces - 1) + dealingPieceDuration;
|
|
|
|
|
|
|
|
|
|
+ final double w = board.pieceLogicalWidth;
|
|
|
|
|
+ final double h = board.pieceLogicalHeight;
|
|
|
final startX = board.cols - 1;
|
|
final startX = board.cols - 1;
|
|
|
final startY = board.rows - 1;
|
|
final startY = board.rows - 1;
|
|
|
- final startPieceCenterX = startX * board.pieceLogicalWidth + board.pieceLogicalWidth / 2;
|
|
|
|
|
- final startPieceCenterY = startY * board.pieceLogicalHeight + board.pieceLogicalHeight / 2;
|
|
|
|
|
-
|
|
|
|
|
- for (var i = 0; i < board.rows; i++) {
|
|
|
|
|
- for (var j = 0; j < board.cols; j++) {
|
|
|
|
|
- final pieceIndex = i * board.cols + j;
|
|
|
|
|
- final animationStartTime = pieceIndex * dealingPieceInterval;
|
|
|
|
|
- final animationEndTime = animationStartTime + dealingPieceDuration;
|
|
|
|
|
- final currentGlobalTime = dealingAnimation.value * totalAnimationDuration;
|
|
|
|
|
-
|
|
|
|
|
- double progress;
|
|
|
|
|
- if (currentGlobalTime < animationStartTime) {
|
|
|
|
|
- progress = 0.0;
|
|
|
|
|
- } else if (currentGlobalTime >= animationEndTime) {
|
|
|
|
|
- progress = 1.0;
|
|
|
|
|
- } else {
|
|
|
|
|
- progress = (currentGlobalTime - animationStartTime) / dealingPieceDuration;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- double startOffX = startPieceCenterX - (j * board.pieceLogicalWidth + board.pieceLogicalWidth / 2);
|
|
|
|
|
- double startOffY = startPieceCenterY - (i * board.pieceLogicalHeight + board.pieceLogicalHeight / 2);
|
|
|
|
|
|
|
+ final startPieceCenterX = startX * w + w / 2;
|
|
|
|
|
+ final startPieceCenterY = startY * h + h / 2;
|
|
|
|
|
|
|
|
- _drawDealingPiece(canvas, size, i, j, startOffX, startOffY, progress);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ final currentGlobalTime = dealingAnimation.value * totalAnimationDuration;
|
|
|
|
|
|
|
|
- void _drawDealingPiece(Canvas canvas, Size size, int row, int col, double startOffsetX, double startOffsetY, double progress) {
|
|
|
|
|
- final cardImg = board.cardImage;
|
|
|
|
|
- if (cardImg == null) return;
|
|
|
|
|
|
|
+ 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 curIndex = collectionIndex * board.count + row * board.cols + col;
|
|
|
|
|
|
|
+ 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;
|
|
|
|
|
|
|
|
- final w = board.pieceLogicalWidth;
|
|
|
|
|
- final h = board.pieceLogicalHeight;
|
|
|
|
|
|
|
+ canvas.save();
|
|
|
|
|
+ canvas.translate(targetLeft + currentOffsetX, targetTop + currentOffsetY);
|
|
|
|
|
|
|
|
- final targetLeft = col * w;
|
|
|
|
|
- final targetTop = row * h;
|
|
|
|
|
-
|
|
|
|
|
- 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, cardImg.width.toDouble(), cardImg.height.toDouble());
|
|
|
|
|
- final rect = Rect.fromLTWH(0, 0, w, h);
|
|
|
|
|
- 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((w - textWidth) / 2, (h - textHeight) / 2));
|
|
|
|
|
- canvas.restore();
|
|
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- _drawBorders(canvas, targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h);
|
|
|
|
|
|
|
+ canvas.restore();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
void _paintUnlocking(Canvas canvas, Size size) {
|
|
void _paintUnlocking(Canvas canvas, Size size) {
|
|
|
final img = board.image;
|
|
final img = board.image;
|
|
|
if (img == null) return;
|
|
if (img == null) return;
|