|
|
@@ -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;
|
|
|
}
|