home_board_play.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. import 'dart:async';
  2. import 'dart:math';
  3. import 'dart:ui' as ui;
  4. import 'package:flutter/material.dart';
  5. import 'package:fluttertoast/fluttertoast.dart';
  6. import 'package:puzzleweave/audio/jc_audio_controller.dart';
  7. import 'package:puzzleweave/config/device.dart';
  8. import 'package:puzzleweave/homepage/home_board.dart';
  9. import 'package:puzzleweave/l10n/app_localizations.dart';
  10. import 'package:puzzleweave/models/cached_request.dart';
  11. import 'package:puzzleweave/models/data.dart';
  12. import 'package:puzzleweave/models/items.dart';
  13. import 'package:puzzleweave/play/confetti_layer.dart';
  14. import 'package:puzzleweave/skin/skin.dart';
  15. import 'package:logging/logging.dart';
  16. import 'package:provider/provider.dart';
  17. import 'package:puzzleweave/utils/memory_monitor.dart';
  18. final Logger _log = Logger('home_board_play');
  19. // ignore: must_be_immutable
  20. class HomeBoardPlay extends StatefulWidget {
  21. final double canvasWidth;
  22. final double canvasHeight;
  23. final GlobalKey collectionKey; // 接收 Collection Button Key, 方便定位
  24. VoidCallback? onCollectionDone; // 新增一个合集完成的回调,外部home_screen可能会关心,当合集解锁动画完成,home_screen的左上角合集icon需要放大再复原
  25. HomeBoardPlay({super.key, required this.canvasWidth, required this.canvasHeight, required this.collectionKey, this.onCollectionDone});
  26. @override
  27. State<HomeBoardPlay> createState() => HomeBoardPlayState();
  28. }
  29. class HomeBoardPlayState extends State<HomeBoardPlay> with TickerProviderStateMixin {
  30. late HomeBoard board;
  31. late JcAudioController audio;
  32. late Data data;
  33. List<ListItem>? collection;
  34. late CachedRequest collectionCachedRequest;
  35. late StreamSubscription? collectionSubscription;
  36. late ConfettiLayer confettiLayer;
  37. // 翻牌动画,完成一个关卡后翻开一个卡片
  38. late AnimationController _flipController;
  39. late Animation<double> _flipAnimation;
  40. // todo... 合集解锁动画,完成整个合集之后执行, 效果是整个图片缩小并位移到左上角的colleciton iconbutton, 形成合集“收纳”的效果
  41. late AnimationController _unlockController; // 解锁动画控制器
  42. late Animation<double> _unlockAnimation; // 0.0 -> 1.0
  43. // 当前合集是否已经完成(一个合集需要完成5x5即25个关卡才能解锁)(在切换到下一个合集之前调用判断)
  44. bool get _isCurCollectionDone => data.currentLevel != 0 && (data.currentCollectionIndex) * 25 == data.currentLevel;
  45. // bool get _isCurCollectionDone => true; // for test
  46. OverlayEntry? _overlayEntry; // 新增:用于管理全屏动画层
  47. // 发牌动画
  48. late AnimationController _dealingController; // 发牌动画控制器
  49. late Animation<double> _dealingAnimation;
  50. // 发牌间隔(ms)
  51. final int _dealingPieceInterval = 70;
  52. // 每个卡片移动时间(ms)
  53. final int _dealingPieceDuration = 200;
  54. // 发牌动画总时长(需要考虑到最后一个卡片的动画)
  55. // 共有 board.rows * board.cols 个卡片
  56. int get _totalDealingDuration => (board.rows * board.cols - 1) * _dealingPieceInterval + _dealingPieceDuration;
  57. Timer? _dealingPeriodicTimer;
  58. int _dealingCount = 0; // 计数:记录执行次数
  59. @override
  60. void initState() {
  61. super.initState();
  62. Device device = context.read<Device>();
  63. board = HomeBoard(canvasWidth: widget.canvasWidth, canvasHeight: widget.canvasHeight, device: device);
  64. board.isReadyNotifier.addListener(_onBoardReady);
  65. confettiLayer = ConfettiLayer(this);
  66. Future.delayed(Duration.zero, () {
  67. if (mounted) confettiLayer.setup(context);
  68. });
  69. audio = context.read<JcAudioController>();
  70. data = context.read<Data>();
  71. collectionCachedRequest = data.collection;
  72. // 主动获取缓存数据(关键)
  73. final collectionCachedData = collectionCachedRequest.cachedData;
  74. if (collectionCachedData != null) {
  75. _onCollectionDataUpdate(collectionCachedData);
  76. }
  77. collectionSubscription = collectionCachedRequest.stream.listen(_onCollectionDataUpdate, onError: _onCollectionDataError);
  78. _flipController = AnimationController(duration: const Duration(milliseconds: 1000), vsync: this);
  79. _flipAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: _flipController, curve: Curves.easeOut))
  80. ..addStatusListener((status) {
  81. if (status == AnimationStatus.completed) {
  82. _checkCollectionDone();
  83. }
  84. });
  85. _unlockController = AnimationController(duration: const Duration(milliseconds: 800), vsync: this);
  86. _unlockAnimation = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: _unlockController, curve: Curves.easeInBack))
  87. ..addStatusListener((status) {
  88. if (status == AnimationStatus.completed) {
  89. _overlayEntry?.remove();
  90. _overlayEntry = null;
  91. widget.onCollectionDone?.call();
  92. _startDealingAnimation();
  93. }
  94. });
  95. _dealingController = AnimationController(
  96. duration: Duration(milliseconds: _totalDealingDuration),
  97. vsync: this,
  98. );
  99. _dealingAnimation = CurvedAnimation(parent: _dealingController, curve: Curves.easeOut)
  100. ..addStatusListener((status) {
  101. if (status == AnimationStatus.completed) {
  102. switchToNextCollection();
  103. if (mounted) {
  104. setState(() {
  105. board.status = HomeBoardStatus.playing;
  106. board.invalidate();
  107. });
  108. }
  109. }
  110. });
  111. }
  112. _onCollectionDataUpdate(colldata) async {
  113. _log.info('_onCollectionDataUpdate.... ');
  114. if (colldata != null) {
  115. collection = colldata as List<ListItem>;
  116. if (collection != null && collection!.isNotEmpty) {
  117. if ((data.completedWorks.value.length / 25).floor() > data.currentCollectionIndex) {
  118. if (currentCollectionItem != null) {
  119. _log.info('合集落后于关卡,没有正常切换,矫正!');
  120. data.collectionDone(currentCollectionItem!);
  121. }
  122. }
  123. board.currentCollectionItem = currentCollectionItem;
  124. }
  125. if (mounted) setState(() {});
  126. if (colldata.length < 5) {
  127. Future.delayed(const Duration(seconds: 3), () => refresh());
  128. }
  129. }
  130. }
  131. _onCollectionDataError(error) {
  132. _log.info('_onCollectionDataError.... $error');
  133. if (collection == null || collection!.isEmpty || collection!.length <= 2) {
  134. // 列表数据如果少于20,说明只是内置图,仍然刷新远程请求
  135. _log.warning("_onCollectionDataError, retry again");
  136. Future.delayed(Duration(seconds: 3), () => refresh());
  137. }
  138. }
  139. Future<void> refresh() async {
  140. _log.info('refresh...');
  141. await collectionCachedRequest.refresh();
  142. }
  143. // board 图片等资源加载完成的回调
  144. _onBoardReady() {
  145. if (board.status == HomeBoardStatus.loading && mounted) {
  146. setState(() {
  147. board.status = HomeBoardStatus.playing;
  148. });
  149. }
  150. }
  151. // !!! 改造点 2: 启动发牌动画
  152. void _startDealingAnimation() {
  153. if (!mounted) return;
  154. setState(() {
  155. board.status = HomeBoardStatus.dealing;
  156. });
  157. _dealingController.duration = Duration(milliseconds: _totalDealingDuration);
  158. _dealingController.forward(from: 0.0);
  159. _dealingCount = 0;
  160. audio.playSfx(SfxType.card);
  161. _dealingPeriodicTimer?.cancel();
  162. _dealingPeriodicTimer = Timer.periodic(const Duration(milliseconds: 130), (timer) {
  163. if (mounted) {
  164. _dealingCount++;
  165. if (_dealingCount >= (_totalDealingDuration / 130) - 2) {
  166. timer.cancel();
  167. } else {
  168. audio.playSfx(SfxType.card);
  169. }
  170. } else {
  171. timer.cancel();
  172. }
  173. });
  174. }
  175. ListItem? get currentCollectionItem {
  176. if (collection != null && collection!.isNotEmpty && data.currentCollectionIndex < collection!.length) {
  177. return collection![data.currentCollectionIndex];
  178. }
  179. return null;
  180. }
  181. @override
  182. void dispose() {
  183. MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (before)');
  184. board.isReadyNotifier.removeListener(_onBoardReady);
  185. _dealingPeriodicTimer?.cancel();
  186. _overlayEntry?.remove();
  187. _overlayEntry = null;
  188. board.dispose(); // 调用优化后的 dispose
  189. confettiLayer.dispose();
  190. _flipController.dispose();
  191. _unlockController.dispose();
  192. _dealingController.dispose();
  193. collectionSubscription?.cancel();
  194. MemoryMonitor.logMemoryUsage('HomeBoardPlay dispose (after)');
  195. super.dispose();
  196. }
  197. void _checkCollectionDone() async {
  198. if (_isCurCollectionDone) {
  199. if (mounted) {
  200. setState(() {
  201. board.status = HomeBoardStatus.done;
  202. board.invalidate();
  203. });
  204. }
  205. _startUnlockAnimation();
  206. }
  207. }
  208. void _startUnlockAnimation() {
  209. // 防御检查 1: 确保图片已加载,否则跳过动画直接发牌
  210. if (board.image == null) {
  211. _log.warning('Unlock image not ready, skipping animation.');
  212. widget.onCollectionDone?.call();
  213. _startDealingAnimation();
  214. return;
  215. }
  216. final RenderBox? targetRenderBox = widget.collectionKey.currentContext?.findRenderObject() as RenderBox?;
  217. final RenderBox? canvasRenderBox = context.findRenderObject() as RenderBox?;
  218. if (targetRenderBox == null || canvasRenderBox == null || !mounted) return;
  219. final targetPosition = targetRenderBox.localToGlobal(targetRenderBox.size.center(Offset.zero));
  220. final canvasGlobalTL = canvasRenderBox.localToGlobal(Offset.zero);
  221. final canvasCenter = canvasRenderBox.localToGlobal(canvasRenderBox.size.center(Offset.zero));
  222. final Offset delta = targetPosition - canvasCenter;
  223. board.setUnlockAnimationTarget(targetOffset: delta, targetScale: targetRenderBox.size.width * 0.2 / widget.canvasWidth);
  224. _overlayEntry = OverlayEntry(
  225. builder: (context) {
  226. return Positioned(
  227. left: canvasGlobalTL.dx,
  228. top: canvasGlobalTL.dy,
  229. child: SizedBox(
  230. width: widget.canvasWidth,
  231. height: widget.canvasHeight,
  232. child: CustomPaint(
  233. painter: CanvasPainter(
  234. board: board,
  235. level: data.currentLevel,
  236. collectionIndex: data.currentCollectionIndex,
  237. flipAnimation: _flipAnimation,
  238. unlockAnimation: _unlockAnimation,
  239. forceStatus: HomeBoardStatus.unlocking,
  240. dealingAnimation: _dealingAnimation,
  241. dealingPieceInterval: _dealingPieceInterval,
  242. dealingPieceDuration: _dealingPieceDuration,
  243. ),
  244. ),
  245. ),
  246. );
  247. },
  248. );
  249. Overlay.of(context).insert(_overlayEntry!);
  250. if (mounted) {
  251. setState(() {
  252. board.status = HomeBoardStatus.unlocking;
  253. });
  254. }
  255. _unlockController.forward(from: 0.0);
  256. }
  257. void startFlipAnimation() {
  258. MemoryMonitor.logMemoryUsage('Collection flip animation');
  259. _flipController.forward(from: 0.0);
  260. audio.playSfx(SfxType.flip);
  261. if (data.currentLevel != 0 && (data.currentCollectionIndex + 1) * 25 == data.currentLevel && currentCollectionItem != null) {
  262. data.collectionDone(currentCollectionItem!);
  263. audio.playSfx(SfxType.star);
  264. confettiLayer.play();
  265. // 合集完成时清理内存
  266. // MemoryMonitor().manualCleanup();
  267. }
  268. }
  269. void switchToNextCollection() {
  270. if (currentCollectionItem == null) {
  271. Fluttertoast.showToast(msg: AppLocalizations.of(context)!.noMorePicture);
  272. return;
  273. }
  274. board.switchToNextCollection(currentCollectionItem!);
  275. }
  276. @override
  277. Widget build(BuildContext context) {
  278. final isUnlocking = _unlockController.isAnimating || board.status == HomeBoardStatus.unlocking;
  279. return CustomPaint(
  280. size: Size(widget.canvasWidth, widget.canvasHeight),
  281. painter: isUnlocking && _overlayEntry != null
  282. ? null
  283. : CanvasPainter(
  284. board: board,
  285. level: data.currentLevel,
  286. collectionIndex: data.currentCollectionIndex,
  287. flipAnimation: _flipAnimation,
  288. unlockAnimation: _unlockAnimation,
  289. dealingAnimation: _dealingAnimation,
  290. dealingPieceDuration: _dealingPieceDuration,
  291. dealingPieceInterval: _dealingPieceInterval,
  292. ),
  293. child: board.status == HomeBoardStatus.loading
  294. ? Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(SkinHelper.slotBorderColor)))
  295. : Container(),
  296. );
  297. }
  298. }
  299. class CanvasPainter extends CustomPainter {
  300. final HomeBoard board;
  301. final int level;
  302. final int collectionIndex;
  303. final Animation<double> flipAnimation;
  304. final Animation<double> unlockAnimation;
  305. final HomeBoardStatus? forceStatus;
  306. final Animation<double> dealingAnimation;
  307. final int dealingPieceDuration;
  308. final int dealingPieceInterval;
  309. CanvasPainter({
  310. required this.board,
  311. required this.level,
  312. required this.collectionIndex,
  313. required this.flipAnimation,
  314. required this.unlockAnimation,
  315. this.forceStatus,
  316. required this.dealingAnimation,
  317. required this.dealingPieceDuration,
  318. required this.dealingPieceInterval,
  319. }) : super(repaint: Listenable.merge([board.boardNotifier, flipAnimation, unlockAnimation, dealingAnimation]));
  320. @override
  321. void paint(Canvas canvas, Size size) {
  322. final statusToPaint = forceStatus ?? board.status;
  323. // 根据不同状态执行绘制,每个方法内部现在都有 null 检查
  324. switch (statusToPaint) {
  325. case HomeBoardStatus.playing:
  326. _paintPlaying(canvas, size);
  327. break;
  328. case HomeBoardStatus.done:
  329. _paintSuccess(canvas, size);
  330. break;
  331. case HomeBoardStatus.unlocking:
  332. _paintUnlocking(canvas, size);
  333. break;
  334. case HomeBoardStatus.dealing:
  335. _paintDealing(canvas, size);
  336. break;
  337. case HomeBoardStatus.loading:
  338. break;
  339. }
  340. }
  341. void _paintDealing(Canvas canvas, Size size) {
  342. if (board.cardImage == null) return; // 防御性检查
  343. final totalPieces = board.rows * board.cols;
  344. final totalAnimationDuration = dealingPieceInterval * (totalPieces - 1) + dealingPieceDuration;
  345. final startX = board.cols - 1;
  346. final startY = board.rows - 1;
  347. final startPieceCenterX = startX * board.pieceLogicalWidth + board.pieceLogicalWidth / 2;
  348. final startPieceCenterY = startY * board.pieceLogicalHeight + board.pieceLogicalHeight / 2;
  349. for (var i = 0; i < board.rows; i++) {
  350. for (var j = 0; j < board.cols; j++) {
  351. final pieceIndex = i * board.cols + j;
  352. final animationStartTime = pieceIndex * dealingPieceInterval;
  353. final animationEndTime = animationStartTime + dealingPieceDuration;
  354. final currentGlobalTime = dealingAnimation.value * totalAnimationDuration;
  355. double progress;
  356. if (currentGlobalTime < animationStartTime) {
  357. progress = 0.0;
  358. } else if (currentGlobalTime >= animationEndTime) {
  359. progress = 1.0;
  360. } else {
  361. progress = (currentGlobalTime - animationStartTime) / dealingPieceDuration;
  362. }
  363. double startOffX = startPieceCenterX - (j * board.pieceLogicalWidth + board.pieceLogicalWidth / 2);
  364. double startOffY = startPieceCenterY - (i * board.pieceLogicalHeight + board.pieceLogicalHeight / 2);
  365. _drawDealingPiece(canvas, size, i, j, startOffX, startOffY, progress);
  366. }
  367. }
  368. }
  369. void _drawDealingPiece(Canvas canvas, Size size, int row, int col, double startOffsetX, double startOffsetY, double progress) {
  370. final cardImg = board.cardImage;
  371. if (cardImg == null) return;
  372. final curIndex = collectionIndex * board.count + row * board.cols + col;
  373. final w = board.pieceLogicalWidth;
  374. final h = board.pieceLogicalHeight;
  375. final targetLeft = col * w;
  376. final targetTop = row * h;
  377. final currentOffsetX = ui.lerpDouble(startOffsetX, 0.0, progress) ?? 0.0;
  378. final currentOffsetY = ui.lerpDouble(startOffsetY, 0.0, progress) ?? 0.0;
  379. canvas.save();
  380. canvas.translate(targetLeft + currentOffsetX, targetTop + currentOffsetY);
  381. final cardSourceRect = Rect.fromLTWH(0, 0, cardImg.width.toDouble(), cardImg.height.toDouble());
  382. final rect = Rect.fromLTWH(0, 0, w, h);
  383. canvas.drawImageRect(cardImg, cardSourceRect, rect, Paint()..isAntiAlias = true);
  384. final textStyle = TextStyle(
  385. color: Colors.white,
  386. fontSize: h * 0.25,
  387. fontWeight: FontWeight.bold,
  388. shadows: const [Shadow(offset: Offset(1, 1), blurRadius: 2, color: Colors.black38)],
  389. );
  390. final textPainter = TextPainter(
  391. text: TextSpan(text: (curIndex + 1).toString(), style: textStyle),
  392. textDirection: TextDirection.ltr,
  393. textAlign: TextAlign.center,
  394. );
  395. textPainter.layout(minWidth: 0, maxWidth: w);
  396. textPainter.paint(canvas, Offset((w - textPainter.width) / 2, (h - textPainter.height) / 2));
  397. canvas.restore();
  398. _drawBorders(canvas, targetLeft + currentOffsetX, targetTop + currentOffsetY, w, h);
  399. }
  400. void _paintUnlocking(Canvas canvas, Size size) {
  401. final img = board.image;
  402. if (img == null) return;
  403. final progress = unlockAnimation.value;
  404. final currentScale = ui.lerpDouble(1.0, board.unlockTargetScale, progress) ?? 1.0;
  405. final currentOffset = Offset(
  406. ui.lerpDouble(0.0, board.unlockTargetOffset.dx, progress) ?? 0.0,
  407. ui.lerpDouble(0.0, board.unlockTargetOffset.dy, progress) ?? 0.0,
  408. );
  409. canvas.save();
  410. final centerX = size.width / 2;
  411. final centerY = size.height / 2;
  412. canvas.translate(centerX + currentOffset.dx, centerY + currentOffset.dy);
  413. canvas.scale(currentScale);
  414. canvas.translate(-centerX, -centerY);
  415. final rect = Rect.fromLTWH(0, 0, size.width, size.height);
  416. final sourceRect = Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble());
  417. canvas.drawImageRect(img, sourceRect, rect, Paint()..isAntiAlias = true);
  418. canvas.restore();
  419. }
  420. void _paintSuccess(Canvas canvas, Size size) {
  421. final img = board.image;
  422. if (img == null) return;
  423. final rect = Rect.fromLTWH(0, 0, size.width, size.height);
  424. final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(4.0));
  425. final sourceRect = Rect.fromLTWH(0, 0, img.width.toDouble(), img.height.toDouble());
  426. canvas.save();
  427. canvas.clipRRect(rrect);
  428. canvas.drawImageRect(img, sourceRect, rect, Paint()..isAntiAlias = true);
  429. canvas.restore();
  430. _drawBorders(canvas, 0, 0, size.width, size.height);
  431. }
  432. void _paintPlaying(Canvas canvas, Size size) {
  433. for (var i = 0; i < board.rows; i++) {
  434. for (var j = 0; j < board.cols; j++) {
  435. final int curIndex = i * board.rows + j;
  436. bool flipped = level > collectionIndex * board.count + curIndex;
  437. _drawPiece(canvas, size, i, j, flipped);
  438. }
  439. }
  440. }
  441. void _drawPiece(Canvas canvas, Size size, int row, int col, bool flipped) {
  442. final img = board.image;
  443. final cardImg = board.cardImage;
  444. if (img == null || cardImg == null) return; // 双重检查
  445. final w = size.width / board.cols;
  446. final h = size.height / board.rows;
  447. final left = col * w;
  448. final top = row * h;
  449. final rect = Rect.fromLTWH(left, top, w, h);
  450. final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(4.0));
  451. final pieceWidth = img.width / board.cols;
  452. final pieceHeight = img.height / board.rows;
  453. final imageSourceRect = Rect.fromLTWH(pieceWidth * col, pieceHeight * row, pieceWidth, pieceHeight);
  454. final cardSourceRect = Rect.fromLTWH(0, 0, cardImg.width.toDouble(), cardImg.height.toDouble());
  455. final curIndex = collectionIndex * board.count + row * board.rows + col;
  456. double flipProgress = (flipAnimation.isAnimating && curIndex == level - 1) ? flipAnimation.value : 0.0;
  457. if (flipProgress > 0) flipped = flipProgress > 0.5;
  458. canvas.save();
  459. final centerX = left + w / 2;
  460. final centerY = top + h / 2;
  461. canvas.translate(centerX, centerY);
  462. if (flipProgress > 0.0) {
  463. double angle = flipProgress * pi;
  464. Matrix4 transform = Matrix4.identity()
  465. ..setEntry(3, 2, 0.0015)
  466. ..rotateY(angle);
  467. if (flipProgress > 0.5) transform.scale(-1.0, 1.0, 1.0);
  468. canvas.transform(transform.storage);
  469. }
  470. canvas.translate(-centerX, -centerY);
  471. canvas.clipRRect(rrect);
  472. if (flipped) {
  473. canvas.drawImageRect(img, imageSourceRect, rect, Paint()..isAntiAlias = true);
  474. } else {
  475. final targetImg = flipProgress > 0.5 ? img : cardImg;
  476. final sourceRect = flipProgress > 0.5 ? imageSourceRect : cardSourceRect;
  477. canvas.drawImageRect(targetImg, sourceRect, rect, Paint()..isAntiAlias = true);
  478. if (flipProgress <= 0.5) {
  479. final textStyle = TextStyle(
  480. color: Colors.white,
  481. fontSize: h * 0.25,
  482. fontWeight: FontWeight.bold,
  483. shadows: const [Shadow(offset: Offset(1, 1), blurRadius: 2, color: Colors.black38)],
  484. );
  485. final textPainter = TextPainter(
  486. text: TextSpan(text: (curIndex + 1).toString(), style: textStyle),
  487. textDirection: TextDirection.ltr,
  488. textAlign: TextAlign.center,
  489. );
  490. textPainter.layout(minWidth: 0, maxWidth: w);
  491. textPainter.paint(canvas, Offset(left + (w - textPainter.width) / 2, top + (h - textPainter.height) / 2));
  492. }
  493. }
  494. canvas.restore();
  495. _drawBorders(canvas, left, top, w, h);
  496. }
  497. void _drawBorders(Canvas canvas, double x, double y, double w, double h) {
  498. final rect = Rect.fromLTWH(x, y, w, h);
  499. canvas.drawRRect(
  500. RRect.fromRectAndRadius(rect.deflate(0.5), const Radius.circular(4.0)),
  501. Paint()
  502. ..color = SkinHelper.outLineBorderColor
  503. ..style = PaintingStyle.stroke
  504. ..strokeWidth = 1.0
  505. ..isAntiAlias = true,
  506. );
  507. canvas.drawRRect(
  508. RRect.fromRectAndRadius(rect.deflate(1.5), const Radius.circular(4.0)),
  509. Paint()
  510. ..color = SkinHelper.innerLineBorderColor
  511. ..style = PaintingStyle.stroke
  512. ..strokeWidth = 1.0
  513. ..isAntiAlias = true,
  514. );
  515. }
  516. @override
  517. bool shouldRepaint(covariant CanvasPainter oldDelegate) => true;
  518. }