home_board_play.dart 21 KB

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