|
@@ -70,6 +70,11 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
Timer? _dealingPeriodicTimer;
|
|
Timer? _dealingPeriodicTimer;
|
|
|
int _dealingCount = 0; // 计数:记录执行次数
|
|
int _dealingCount = 0; // 计数:记录执行次数
|
|
|
|
|
|
|
|
|
|
+ // ✅ 优化:Picture 缓存静止背景,避免 flip 动画期间重绘 24 张静止碎片
|
|
|
|
|
+ ui.Picture? _staticBackgroundPicture;
|
|
|
|
|
+ // ✅ 优化:跟踪上次录制的关卡,实现增量更新(只更新新完成的碎片)
|
|
|
|
|
+ int? _lastRecordedLevel;
|
|
|
|
|
+
|
|
|
@override
|
|
@override
|
|
|
void initState() {
|
|
void initState() {
|
|
|
super.initState();
|
|
super.initState();
|
|
@@ -172,6 +177,14 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
setState(() {
|
|
setState(() {
|
|
|
board.status = HomeBoardStatus.playing;
|
|
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();
|
|
_dealingPeriodicTimer?.cancel();
|
|
|
_overlayEntry?.remove();
|
|
_overlayEntry?.remove();
|
|
|
_overlayEntry = null;
|
|
_overlayEntry = null;
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ 清理 Picture 缓存
|
|
|
|
|
+ _staticBackgroundPicture = null;
|
|
|
|
|
+ _lastRecordedLevel = null;
|
|
|
|
|
+
|
|
|
board.dispose(); // 调用优化后的 dispose
|
|
board.dispose(); // 调用优化后的 dispose
|
|
|
confettiLayer.dispose();
|
|
confettiLayer.dispose();
|
|
|
_flipController.dispose();
|
|
_flipController.dispose();
|
|
@@ -294,6 +312,12 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void startFlipAnimation() {
|
|
void startFlipAnimation() {
|
|
|
|
|
+ // ✅ 优化:每次 flip 前都重新录制背景(因为已完成关卡数在变化)
|
|
|
|
|
+ // 此时 TextPainter 缓存已预热,录制成本大幅降低
|
|
|
|
|
+ if (board.image != null && board.cardImage != null) {
|
|
|
|
|
+ _recordStaticBackgroundPicture();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
MemoryMonitor.logMemoryUsage('Collection flip animation');
|
|
MemoryMonitor.logMemoryUsage('Collection flip animation');
|
|
|
_flipController.forward(from: 0.0);
|
|
_flipController.forward(from: 0.0);
|
|
|
audio.playSfx(SfxType.flip);
|
|
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() {
|
|
void switchToNextCollection() {
|
|
|
if (currentCollectionItem == null) {
|
|
if (currentCollectionItem == null) {
|
|
|
Fluttertoast.showToast(msg: AppLocalizations.of(context)!.noMorePicture);
|
|
Fluttertoast.showToast(msg: AppLocalizations.of(context)!.noMorePicture);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ // ✅ 清理 Picture 缓存,因为集合改变了
|
|
|
|
|
+ _staticBackgroundPicture = null;
|
|
|
|
|
+ _lastRecordedLevel = null;
|
|
|
board.switchToNextCollection(currentCollectionItem!);
|
|
board.switchToNextCollection(currentCollectionItem!);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -330,6 +453,7 @@ class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMi
|
|
|
dealingAnimation: _dealingAnimation,
|
|
dealingAnimation: _dealingAnimation,
|
|
|
dealingPieceDuration: _dealingPieceDuration,
|
|
dealingPieceDuration: _dealingPieceDuration,
|
|
|
dealingPieceInterval: _dealingPieceInterval,
|
|
dealingPieceInterval: _dealingPieceInterval,
|
|
|
|
|
+ staticBackgroundPicture: _staticBackgroundPicture,
|
|
|
),
|
|
),
|
|
|
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)))
|
|
@@ -349,6 +473,19 @@ class CanvasPainter extends CustomPainter {
|
|
|
final int dealingPieceDuration;
|
|
final int dealingPieceDuration;
|
|
|
final int dealingPieceInterval;
|
|
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({
|
|
CanvasPainter({
|
|
|
required this.board,
|
|
required this.board,
|
|
|
required this.level,
|
|
required this.level,
|
|
@@ -359,8 +496,42 @@ class CanvasPainter extends CustomPainter {
|
|
|
required this.dealingAnimation,
|
|
required this.dealingAnimation,
|
|
|
required this.dealingPieceDuration,
|
|
required this.dealingPieceDuration,
|
|
|
required this.dealingPieceInterval,
|
|
required this.dealingPieceInterval,
|
|
|
|
|
+ this.staticBackgroundPicture,
|
|
|
}) : 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) {
|
|
|
|
|
+ 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
|
|
@override
|
|
|
void paint(Canvas canvas, Size size) {
|
|
void paint(Canvas canvas, Size size) {
|
|
|
final statusToPaint = forceStatus ?? board.status;
|
|
final statusToPaint = forceStatus ?? board.status;
|
|
@@ -441,19 +612,9 @@ class CanvasPainter extends CustomPainter {
|
|
|
final rect = Rect.fromLTWH(0, 0, w, h);
|
|
final rect = Rect.fromLTWH(0, 0, w, h);
|
|
|
canvas.drawImageRect(cardImg, cardSourceRect, rect, Paint()..isAntiAlias = true);
|
|
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();
|
|
canvas.restore();
|
|
|
|
|
|
|
|
_drawBorders(canvas, targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h);
|
|
_drawBorders(canvas, targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h);
|
|
@@ -500,11 +661,41 @@ class CanvasPainter extends CustomPainter {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _paintPlaying(Canvas canvas, Size size) {
|
|
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) {
|
|
void _drawPiece(Canvas canvas, Size size, int row, int col, bool flipped) {
|
|
|
final img = board.image;
|
|
final img = board.image;
|
|
|
final cardImg = board.cardImage;
|
|
final cardImg = board.cardImage;
|
|
|
- if (img == null || cardImg == null) return; // 双重检查
|
|
|
|
|
|
|
+ if (img == null || cardImg == null) return;
|
|
|
|
|
|
|
|
final w = size.width / board.cols;
|
|
final w = size.width / board.cols;
|
|
|
final h = size.height / board.rows;
|
|
final h = size.height / board.rows;
|
|
@@ -528,21 +719,83 @@ class CanvasPainter extends CustomPainter {
|
|
|
|
|
|
|
|
final curIndex = collectionIndex * board.count + row * board.rows + col;
|
|
final curIndex = collectionIndex * board.count + row * board.rows + col;
|
|
|
double flipProgress = (flipAnimation.isAnimating && curIndex == level - 1) ? flipAnimation.value : 0.0;
|
|
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();
|
|
canvas.save();
|
|
|
final centerX = left + w / 2;
|
|
final centerX = left + w / 2;
|
|
|
final centerY = top + h / 2;
|
|
final centerY = top + h / 2;
|
|
|
canvas.translate(centerX, centerY);
|
|
canvas.translate(centerX, centerY);
|
|
|
|
|
|
|
|
- if (flipProgress > 0.0) {
|
|
|
|
|
|
|
+ // ✅ 优化:缓存 Matrix4 变换矩阵,只在 flipProgress 变化时重新计算
|
|
|
|
|
+ Matrix4 transform;
|
|
|
|
|
+ if (_lastFlipProgress != flipProgress) {
|
|
|
double angle = flipProgress * pi;
|
|
double angle = flipProgress * pi;
|
|
|
- Matrix4 transform = Matrix4.identity()
|
|
|
|
|
|
|
+ transform = Matrix4.identity()
|
|
|
..setEntry(3, 2, 0.0015)
|
|
..setEntry(3, 2, 0.0015)
|
|
|
..rotateY(angle);
|
|
..rotateY(angle);
|
|
|
if (flipProgress > 0.5) transform.scale(-1.0, 1.0, 1.0);
|
|
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.translate(-centerX, -centerY);
|
|
|
canvas.clipRRect(rrect);
|
|
canvas.clipRRect(rrect);
|
|
@@ -555,23 +808,12 @@ class CanvasPainter extends CustomPainter {
|
|
|
canvas.drawImageRect(targetImg, sourceRect, rect, Paint()..isAntiAlias = true);
|
|
canvas.drawImageRect(targetImg, sourceRect, rect, Paint()..isAntiAlias = true);
|
|
|
|
|
|
|
|
if (flipProgress <= 0.5) {
|
|
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();
|
|
canvas.restore();
|
|
|
- _drawBorders(canvas, left, top, w, h);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void _drawBorders(Canvas canvas, double x, double y, double w, double h) {
|
|
void _drawBorders(Canvas canvas, double x, double y, double w, double h) {
|
|
@@ -595,5 +837,16 @@ class CanvasPainter extends CustomPainter {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
@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;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|