| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925 |
- 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<HomeBoardPlay> createState() => HomeBoardPlayState();
- }
- class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMixin {
- late HomeBoard board;
- late JcAudioController audio;
- late Data data;
- List<ListItem>? collection;
- late CachedRequest collectionCachedRequest;
- late StreamSubscription? collectionSubscription;
- late ConfettiLayer confettiLayer;
- // 翻牌动画,完成一个关卡后翻开一个卡片
- late AnimationController _flipController;
- late Animation<double> _flipAnimation;
- // todo... 合集解锁动画,完成整个合集之后执行, 效果是整个图片缩小并位移到左上角的colleciton iconbutton, 形成合集“收纳”的效果
- late AnimationController _unlockController; // 解锁动画控制器
- late Animation<double> _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<double> _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<ui.Picture>? _dealingPictures;
- @override
- void initState() {
- super.initState();
- Device device = context.read<Device>();
- 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<JcAudioController>();
- data = context.read<Data>();
- 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<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);
- _unlockAnimation = Tween<double>(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<ListItem>;
- 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<void> 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<Color>(SkinHelper.slotBorderColor)))
- : Container(),
- );
- }
- }
- class CanvasPainter extends CustomPainter {
- final HomeBoard board;
- final int level;
- final int collectionIndex;
- final Animation<double> flipAnimation;
- final Animation<double> unlockAnimation;
- final HomeBoardStatus? forceStatus;
- final Animation<double> dealingAnimation;
- final int dealingPieceDuration;
- final int dealingPieceInterval;
- // ✅ 优化:Picture 缓存,flip 动画期间使用预录制的背景
- final ui.Picture? staticBackgroundPicture;
- // ✅ 优化:缓存发牌碎片 Picture
- final List<ui.Picture>? dealingPictures;
- // ✅ 优化:缓存 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({
- 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;
- }
- }
|