Showing
2 changed files
with
167 additions
and
7 deletions
| @@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; | @@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; | ||
| 3 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | 3 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; |
| 4 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 4 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 5 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | 5 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; |
| 6 | +import 'package:doublefeel_flutter/app/widget/marquee_text.dart'; | ||
| 6 | 7 | ||
| 7 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 8 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 8 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 9 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| @@ -417,13 +418,9 @@ class _DeleteAccountBottomSheet extends StatelessWidget { | @@ -417,13 +418,9 @@ class _DeleteAccountBottomSheet extends StatelessWidget { | ||
| 417 | children: [ | 418 | children: [ |
| 418 | Center( | 419 | Center( |
| 419 | child: Padding( | 420 | child: Padding( |
| 420 | - padding: EdgeInsets.symmetric(horizontal: 72), | ||
| 421 | - child: Text( | ||
| 422 | - AppLocalizations.of(context)! | ||
| 423 | - .areYouSureYouWantToDeleteYourAccount, | ||
| 424 | - maxLines: 1, | ||
| 425 | - overflow: TextOverflow.ellipsis, | ||
| 426 | - textAlign: TextAlign.center, | 421 | + padding: const EdgeInsets.symmetric(horizontal: 54), |
| 422 | + child: MarqueeText( | ||
| 423 | + text: context.l10n.areYouSureYouWantToDeleteYourAccount, | ||
| 427 | style: TextStyle( | 424 | style: TextStyle( |
| 428 | color: context.colors.textPrimary, | 425 | color: context.colors.textPrimary, |
| 429 | fontSize: 16, | 426 | fontSize: 16, |
lib/app/widget/marquee_text.dart
0 → 100644
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | + | ||
| 5 | +/// 无缝首尾相接的跑马灯文字组件: | ||
| 6 | +/// - 当文字宽度 <= 容器宽度时:正常居中/按 textAlign 静止展示 | ||
| 7 | +/// - 当文字宽度 > 容器宽度时:文字向左平滑连续滚动,尾部自带留白并首尾无缝衔接循环 | ||
| 8 | +class MarqueeText extends StatefulWidget { | ||
| 9 | + const MarqueeText({ | ||
| 10 | + super.key, | ||
| 11 | + required this.text, | ||
| 12 | + this.style, | ||
| 13 | + this.textAlign = TextAlign.center, | ||
| 14 | + this.velocity = 30.0, | ||
| 15 | + this.blankSpace = 48.0, | ||
| 16 | + this.startDelay = const Duration(seconds: 1), | ||
| 17 | + }); | ||
| 18 | + | ||
| 19 | + final String text; | ||
| 20 | + final TextStyle? style; | ||
| 21 | + final TextAlign textAlign; | ||
| 22 | + | ||
| 23 | + /// 滚动速度(像素/秒),数值越大滚动越快,建议 25~40 | ||
| 24 | + final double velocity; | ||
| 25 | + | ||
| 26 | + /// 首尾循环之间的间隔空白宽度(像素) | ||
| 27 | + final double blankSpace; | ||
| 28 | + | ||
| 29 | + /// 页面加载后首次滚动的初始延迟 | ||
| 30 | + final Duration startDelay; | ||
| 31 | + | ||
| 32 | + @override | ||
| 33 | + State<MarqueeText> createState() => _MarqueeTextState(); | ||
| 34 | +} | ||
| 35 | + | ||
| 36 | +class _MarqueeTextState extends State<MarqueeText> | ||
| 37 | + with SingleTickerProviderStateMixin { | ||
| 38 | + AnimationController? _controller; | ||
| 39 | + double _lastTextWidth = 0.0; | ||
| 40 | + Timer? _delayTimer; | ||
| 41 | + | ||
| 42 | + @override | ||
| 43 | + void dispose() { | ||
| 44 | + _delayTimer?.cancel(); | ||
| 45 | + _controller?.dispose(); | ||
| 46 | + super.dispose(); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + void _setupAnimation(double textWidth, double containerWidth) { | ||
| 50 | + if (textWidth <= containerWidth || containerWidth <= 0) { | ||
| 51 | + _delayTimer?.cancel(); | ||
| 52 | + _controller?.stop(); | ||
| 53 | + _controller?.dispose(); | ||
| 54 | + _controller = null; | ||
| 55 | + _lastTextWidth = 0; | ||
| 56 | + return; | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + final cycleDistance = textWidth + widget.blankSpace; | ||
| 60 | + if (_controller != null && (_lastTextWidth - textWidth).abs() < 1.0) { | ||
| 61 | + return; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + _lastTextWidth = textWidth; | ||
| 65 | + _delayTimer?.cancel(); | ||
| 66 | + _controller?.dispose(); | ||
| 67 | + | ||
| 68 | + final durationSeconds = cycleDistance / widget.velocity; | ||
| 69 | + final totalDuration = Duration( | ||
| 70 | + milliseconds: (durationSeconds * 1000).toInt().clamp(1000, 60000), | ||
| 71 | + ); | ||
| 72 | + | ||
| 73 | + final controller = AnimationController( | ||
| 74 | + vsync: this, | ||
| 75 | + duration: totalDuration, | ||
| 76 | + ); | ||
| 77 | + _controller = controller; | ||
| 78 | + | ||
| 79 | + _delayTimer = Timer(widget.startDelay, () { | ||
| 80 | + if (mounted && _controller == controller) { | ||
| 81 | + controller.repeat(); | ||
| 82 | + } | ||
| 83 | + }); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + @override | ||
| 87 | + Widget build(BuildContext context) { | ||
| 88 | + final effectiveStyle = | ||
| 89 | + DefaultTextStyle.of(context).style.merge(widget.style); | ||
| 90 | + | ||
| 91 | + return LayoutBuilder( | ||
| 92 | + builder: (context, constraints) { | ||
| 93 | + final textPainter = TextPainter( | ||
| 94 | + text: TextSpan(text: widget.text, style: effectiveStyle), | ||
| 95 | + textDirection: Directionality.of(context), | ||
| 96 | + maxLines: 1, | ||
| 97 | + )..layout(); | ||
| 98 | + | ||
| 99 | + final textWidth = textPainter.width; | ||
| 100 | + final maxWidth = constraints.maxWidth; | ||
| 101 | + | ||
| 102 | + // 文本未超出容器宽度:普通渲染 | ||
| 103 | + if (textWidth <= maxWidth || maxWidth.isInfinite) { | ||
| 104 | + _setupAnimation(0, 0); | ||
| 105 | + return Text( | ||
| 106 | + widget.text, | ||
| 107 | + style: widget.style, | ||
| 108 | + textAlign: widget.textAlign, | ||
| 109 | + maxLines: 1, | ||
| 110 | + ); | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + // 文本超出容器宽度:开启首尾无缝循环跑马灯 | ||
| 114 | + _setupAnimation(textWidth, maxWidth); | ||
| 115 | + final cycleDistance = textWidth + widget.blankSpace; | ||
| 116 | + final repeatCount = (maxWidth / cycleDistance).ceil() + 2; | ||
| 117 | + | ||
| 118 | + final controller = _controller; | ||
| 119 | + if (controller == null) { | ||
| 120 | + return Text( | ||
| 121 | + widget.text, | ||
| 122 | + style: widget.style, | ||
| 123 | + maxLines: 1, | ||
| 124 | + ); | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + return ClipRect( | ||
| 128 | + child: AnimatedBuilder( | ||
| 129 | + animation: controller, | ||
| 130 | + builder: (context, child) { | ||
| 131 | + final offset = controller.value * cycleDistance; | ||
| 132 | + return Transform.translate( | ||
| 133 | + offset: Offset(-offset, 0), | ||
| 134 | + child: child, | ||
| 135 | + ); | ||
| 136 | + }, | ||
| 137 | + child: OverflowBox( | ||
| 138 | + alignment: Alignment.centerLeft, | ||
| 139 | + minWidth: 0.0, | ||
| 140 | + maxWidth: double.infinity, | ||
| 141 | + minHeight: 0.0, | ||
| 142 | + maxHeight: constraints.hasBoundedHeight ? constraints.maxHeight : null, | ||
| 143 | + child: Row( | ||
| 144 | + mainAxisSize: MainAxisSize.min, | ||
| 145 | + children: List.generate( | ||
| 146 | + repeatCount, | ||
| 147 | + (_) => Padding( | ||
| 148 | + padding: EdgeInsets.only(right: widget.blankSpace), | ||
| 149 | + child: Text( | ||
| 150 | + widget.text, | ||
| 151 | + style: widget.style, | ||
| 152 | + maxLines: 1, | ||
| 153 | + ), | ||
| 154 | + ), | ||
| 155 | + ), | ||
| 156 | + ), | ||
| 157 | + ), | ||
| 158 | + ), | ||
| 159 | + ); | ||
| 160 | + }, | ||
| 161 | + ); | ||
| 162 | + } | ||
| 163 | +} |
-
Please register or login to post a comment