connection_round_button.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. import 'package:flutter/material.dart' hide ConnectionState;
  2. import 'package:flutter_screenutil/flutter_screenutil.dart';
  3. import 'dart:math' as math;
  4. import 'dart:async';
  5. import 'package:get/get.dart';
  6. import 'package:nomo/app/widgets/ix_image.dart';
  7. import '../../../constants/assets.dart';
  8. import '../../../../config/theme/theme_extensions/theme_extension.dart';
  9. import '../../../../config/translations/strings_enum.dart';
  10. import '../../../constants/enums.dart';
  11. /// 自定义圆环画笔 - 绘制带渐变和发光效果的圆环
  12. class RingPainter extends CustomPainter {
  13. final ConnectionState state;
  14. final double rotationAngle;
  15. final double glowIntensity;
  16. final Color primaryColor;
  17. final Color secondaryColor;
  18. RingPainter({
  19. required this.state,
  20. this.rotationAngle = 0,
  21. this.glowIntensity = 0.5,
  22. required this.primaryColor,
  23. required this.secondaryColor,
  24. });
  25. @override
  26. void paint(Canvas canvas, Size size) {
  27. final center = Offset(size.width / 2, size.height / 2);
  28. final radius = size.width / 2 - 8;
  29. final strokeWidth = 2.5;
  30. // 根据状态选择绘制方式
  31. switch (state) {
  32. case ConnectionState.disconnected:
  33. case ConnectionState.error:
  34. _drawDashedRing(canvas, center, radius, strokeWidth);
  35. break;
  36. case ConnectionState.connecting:
  37. _drawGradientRing(canvas, center, radius, strokeWidth, true);
  38. break;
  39. case ConnectionState.connected:
  40. _drawGradientRing(canvas, center, radius, strokeWidth, false);
  41. _drawGlowEffect(canvas, center, radius);
  42. break;
  43. }
  44. }
  45. /// 绘制虚线圆环(断开/错误状态)
  46. void _drawDashedRing(
  47. Canvas canvas,
  48. Offset center,
  49. double radius,
  50. double strokeWidth,
  51. ) {
  52. final paint = Paint()
  53. ..color = secondaryColor.withValues(alpha: 0.4)
  54. ..style = PaintingStyle.stroke
  55. ..strokeWidth = strokeWidth
  56. ..strokeCap = StrokeCap.round
  57. ..isAntiAlias = true;
  58. const int dashCount = 60;
  59. const double dashRatio = 0.5; // 虚线占比
  60. final double dashAngle = (2 * math.pi / dashCount) * dashRatio;
  61. final double gapAngle = (2 * math.pi / dashCount) * (1 - dashRatio);
  62. for (int i = 0; i < dashCount; i++) {
  63. final startAngle = i * (dashAngle + gapAngle) - math.pi / 2;
  64. canvas.drawArc(
  65. Rect.fromCircle(center: center, radius: radius),
  66. startAngle,
  67. dashAngle,
  68. false,
  69. paint,
  70. );
  71. }
  72. }
  73. /// 绘制渐变圆环(连接中/已连接状态)
  74. void _drawGradientRing(
  75. Canvas canvas,
  76. Offset center,
  77. double radius,
  78. double strokeWidth,
  79. bool isAnimating,
  80. ) {
  81. final rect = Rect.fromCircle(center: center, radius: radius);
  82. if (isAnimating) {
  83. // 连接中状态:绘制平滑的流光尾巴效果
  84. final gradient = SweepGradient(
  85. startAngle: 0,
  86. endAngle: 2 * math.pi,
  87. colors: [
  88. primaryColor.withValues(alpha: 0.0),
  89. primaryColor.withValues(alpha: 0.05),
  90. primaryColor.withValues(alpha: 0.2),
  91. primaryColor.withValues(alpha: 0.5),
  92. primaryColor.withValues(alpha: 0.8),
  93. primaryColor,
  94. secondaryColor,
  95. secondaryColor.withValues(alpha: 0.8),
  96. secondaryColor.withValues(alpha: 0.4),
  97. secondaryColor.withValues(alpha: 0.1),
  98. primaryColor.withValues(alpha: 0.0),
  99. ],
  100. stops: const [
  101. 0.0,
  102. 0.1,
  103. 0.2,
  104. 0.35,
  105. 0.45,
  106. 0.5,
  107. 0.55,
  108. 0.65,
  109. 0.8,
  110. 0.9,
  111. 1.0,
  112. ],
  113. transform: GradientRotation(rotationAngle - math.pi / 2),
  114. );
  115. final paint = Paint()
  116. ..shader = gradient.createShader(rect)
  117. ..style = PaintingStyle.stroke
  118. ..strokeWidth = strokeWidth
  119. ..strokeCap = StrokeCap.round
  120. ..isAntiAlias = true;
  121. // 绘制完整圆环,渐变本身形成流光效果
  122. canvas.drawCircle(center, radius, paint);
  123. } else {
  124. // 已连接状态:绘制平滑渐变圆环
  125. final gradient = SweepGradient(
  126. startAngle: 0,
  127. endAngle: 2 * math.pi,
  128. colors: [
  129. primaryColor,
  130. primaryColor.withValues(alpha: 0.9),
  131. secondaryColor.withValues(alpha: 0.9),
  132. secondaryColor,
  133. secondaryColor,
  134. secondaryColor.withValues(alpha: 0.9),
  135. primaryColor.withValues(alpha: 0.9),
  136. primaryColor,
  137. ],
  138. stops: const [0.0, 0.15, 0.35, 0.5, 0.5, 0.65, 0.85, 1.0],
  139. transform: GradientRotation(rotationAngle),
  140. );
  141. final paint = Paint()
  142. ..shader = gradient.createShader(rect)
  143. ..style = PaintingStyle.stroke
  144. ..strokeWidth = strokeWidth
  145. ..strokeCap = StrokeCap.round
  146. ..isAntiAlias = true;
  147. canvas.drawCircle(center, radius, paint);
  148. }
  149. }
  150. /// 绘制发光效果(已连接状态)
  151. void _drawGlowEffect(Canvas canvas, Offset center, double radius) {
  152. // 外层发光 - 使用更多层次和更柔和的模糊
  153. for (int i = 4; i > 0; i--) {
  154. final glowPaint = Paint()
  155. ..color = primaryColor.withValues(alpha: glowIntensity * 0.12 / i)
  156. ..style = PaintingStyle.stroke
  157. ..strokeWidth = 2.0 + i * 2.5
  158. ..maskFilter = MaskFilter.blur(BlurStyle.normal, i * 1.5)
  159. ..isAntiAlias = true;
  160. canvas.drawCircle(center, radius, glowPaint);
  161. }
  162. }
  163. @override
  164. bool shouldRepaint(covariant RingPainter oldDelegate) {
  165. return oldDelegate.state != state ||
  166. oldDelegate.rotationAngle != rotationAngle ||
  167. oldDelegate.glowIntensity != glowIntensity ||
  168. oldDelegate.primaryColor != primaryColor ||
  169. oldDelegate.secondaryColor != secondaryColor;
  170. }
  171. }
  172. class ConnectionRoundButton extends StatefulWidget {
  173. final ConnectionState state;
  174. final VoidCallback? onTap;
  175. const ConnectionRoundButton({super.key, required this.state, this.onTap});
  176. @override
  177. State<ConnectionRoundButton> createState() => _ConnectionRoundButtonState();
  178. }
  179. class _ConnectionRoundButtonState extends State<ConnectionRoundButton>
  180. with TickerProviderStateMixin {
  181. late AnimationController _rotationController; // 旋转动画控制器
  182. late AnimationController _fadeController; // 淡入淡出控制器
  183. Timer? _connectingTimer; // 连接中状态的计时器
  184. int _connectingTextIndex = 0; // 当前显示的连接文本索引(0-4)
  185. ConnectionState? _previousState; // 保存前一个连接状态
  186. bool _isStoppingRotation = false; // 是否正在停止旋转
  187. @override
  188. void initState() {
  189. super.initState();
  190. // 初始化旋转动画控制器
  191. _rotationController = AnimationController(
  192. duration: const Duration(milliseconds: 500),
  193. vsync: this,
  194. );
  195. // 初始化淡入淡出控制器
  196. _fadeController = AnimationController(
  197. duration: const Duration(milliseconds: 600),
  198. vsync: this,
  199. value: 1.0,
  200. );
  201. // 如果初始状态是 connecting,启动动画
  202. if (widget.state == ConnectionState.connecting) {
  203. _rotationController.repeat();
  204. _startConnectingTimer();
  205. }
  206. }
  207. @override
  208. void didUpdateWidget(ConnectionRoundButton oldWidget) {
  209. super.didUpdateWidget(oldWidget);
  210. // 处理状态变化
  211. if (oldWidget.state != widget.state) {
  212. _handleStateChange(oldWidget.state);
  213. }
  214. }
  215. void _handleStateChange(ConnectionState oldState) {
  216. if (widget.state == ConnectionState.connecting) {
  217. _isStoppingRotation = false;
  218. if (!_rotationController.isAnimating) {
  219. _rotationController.repeat();
  220. }
  221. if (_connectingTimer == null || !_connectingTimer!.isActive) {
  222. _startConnectingTimer();
  223. }
  224. } else {
  225. // 从连接中切换到其他状态时,平滑停止旋转
  226. if (_rotationController.isAnimating && !_isStoppingRotation) {
  227. _isStoppingRotation = true;
  228. // 计算剩余角度,让动画平滑停止在顶部(0度位置)
  229. final currentValue = _rotationController.value;
  230. // 停止重复,然后平滑减速到完整的一圈
  231. _rotationController.stop();
  232. _rotationController
  233. .animateTo(
  234. 1.0,
  235. duration: Duration(
  236. milliseconds: ((1.0 - currentValue) * 400).toInt().clamp(
  237. 100,
  238. 400,
  239. ),
  240. ),
  241. curve: Curves.easeOutCubic,
  242. )
  243. .then((_) {
  244. if (mounted) {
  245. _rotationController.reset();
  246. _isStoppingRotation = false;
  247. }
  248. });
  249. }
  250. _stopConnectingTimer();
  251. }
  252. }
  253. @override
  254. void dispose() {
  255. _rotationController.dispose();
  256. _fadeController.dispose();
  257. _connectingTimer?.cancel();
  258. super.dispose();
  259. }
  260. void _onTap() {
  261. if (widget.onTap != null) {
  262. widget.onTap!();
  263. }
  264. }
  265. // 启动连接中状态的文本轮播计时器
  266. void _startConnectingTimer() {
  267. _connectingTimer?.cancel();
  268. _connectingTextIndex = -1;
  269. _connectingTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
  270. if (mounted) {
  271. setState(() {
  272. // 每秒切换到下一个文本,循环显示0-4
  273. _connectingTextIndex = (_connectingTextIndex + 1) % 5;
  274. });
  275. }
  276. });
  277. }
  278. // 停止连接中状态的文本轮播计时器
  279. void _stopConnectingTimer() {
  280. _connectingTimer?.cancel();
  281. _connectingTimer = null;
  282. _connectingTextIndex = 0;
  283. }
  284. // 根据索引获取对应的连接文本
  285. String _getConnectingText() {
  286. switch (_connectingTextIndex) {
  287. case 0:
  288. return Strings.securingData.tr;
  289. case 1:
  290. return Strings.encryptingTraffic.tr;
  291. case 2:
  292. return Strings.protectingPrivacy.tr;
  293. case 3:
  294. return Strings.safeConnection.tr;
  295. case 4:
  296. return Strings.yourDataIsSafe.tr;
  297. default:
  298. return Strings.connecting.tr;
  299. }
  300. }
  301. // 获取圆环主色调
  302. Color _getRingPrimaryColor(ConnectionState state) {
  303. switch (state) {
  304. case ConnectionState.disconnected:
  305. case ConnectionState.error:
  306. return Get.reactiveTheme.hintColor.withValues(alpha: 0.5);
  307. case ConnectionState.connecting:
  308. return const Color(0xFF4FC3F7); // 浅蓝色
  309. case ConnectionState.connected:
  310. return const Color(0xFF66BB6A); // 绿色
  311. }
  312. }
  313. // 获取圆环副色调
  314. Color _getRingSecondaryColor(ConnectionState state) {
  315. switch (state) {
  316. case ConnectionState.disconnected:
  317. case ConnectionState.error:
  318. return Get.reactiveTheme.hintColor.withValues(alpha: 0.3);
  319. case ConnectionState.connecting:
  320. return const Color(0xFF7E57C2); // 紫色
  321. case ConnectionState.connected:
  322. return const Color(0xFF26C6DA); // 青色
  323. }
  324. }
  325. // 构建圆环 - 使用自定义绘制
  326. Widget _buildRoundRing(ConnectionState state, bool shouldRotate) {
  327. final primaryColor = _getRingPrimaryColor(state);
  328. final secondaryColor = _getRingSecondaryColor(state);
  329. if (shouldRotate) {
  330. return AnimatedBuilder(
  331. key: ValueKey('rotating_ring_$state'),
  332. animation: _rotationController,
  333. builder: (context, child) {
  334. return CustomPaint(
  335. size: Size(170.w, 170.w),
  336. painter: RingPainter(
  337. state: state,
  338. rotationAngle: _rotationController.value * 2 * math.pi,
  339. glowIntensity: 0.5,
  340. primaryColor: primaryColor,
  341. secondaryColor: secondaryColor,
  342. ),
  343. );
  344. },
  345. );
  346. }
  347. return TweenAnimationBuilder<double>(
  348. key: ValueKey('static_ring_$state'),
  349. tween: Tween(begin: 0.0, end: 1.0),
  350. duration: const Duration(milliseconds: 800),
  351. curve: Curves.easeOutCubic,
  352. builder: (context, value, child) {
  353. return CustomPaint(
  354. size: Size(170.w, 170.w),
  355. painter: RingPainter(
  356. state: state,
  357. rotationAngle: 0,
  358. glowIntensity: state == ConnectionState.connected ? value : 0.3,
  359. primaryColor: primaryColor,
  360. secondaryColor: secondaryColor,
  361. ),
  362. );
  363. },
  364. );
  365. }
  366. // 构建电源图标
  367. Widget _buildPowerIcon(bool isConnected) {
  368. return IXImage(
  369. key: ValueKey('power_icon_$isConnected'),
  370. source: isConnected ? Assets.connectedSwitch : Assets.disconnectedSwitch,
  371. sourceType: ImageSourceType.asset,
  372. width: 48.w,
  373. height: 48.w,
  374. );
  375. }
  376. @override
  377. Widget build(BuildContext context) {
  378. return GestureDetector(onTap: _onTap, child: _buildMainButton());
  379. }
  380. Widget _buildMainButton() {
  381. // 根据状态获取对应的资源和样式
  382. String statusImgPath;
  383. String text;
  384. Color textColor;
  385. bool isConnected;
  386. bool shouldRotate;
  387. switch (widget.state) {
  388. case ConnectionState.disconnected:
  389. statusImgPath = Assets.disconnected;
  390. text = Strings.disconnected.tr;
  391. textColor = Get.reactiveTheme.hintColor;
  392. isConnected = false;
  393. shouldRotate = false;
  394. break;
  395. case ConnectionState.connecting:
  396. statusImgPath = Assets.connecting;
  397. text = _getConnectingText(); // 使用轮播文本
  398. textColor = Get.reactiveTheme.hintColor;
  399. isConnected = false;
  400. shouldRotate = true;
  401. break;
  402. case ConnectionState.connected:
  403. statusImgPath = Assets.connected;
  404. text = Strings.connected.tr;
  405. textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
  406. isConnected = true;
  407. shouldRotate = false;
  408. break;
  409. case ConnectionState.error:
  410. statusImgPath = Assets.error;
  411. text = Strings.error.tr;
  412. textColor = Get.reactiveTheme.hintColor;
  413. isConnected = false;
  414. shouldRotate = false;
  415. break;
  416. }
  417. // 更新前一个状态
  418. if (_previousState != widget.state) {
  419. WidgetsBinding.instance.addPostFrameCallback((_) {
  420. if (mounted) {
  421. _previousState = widget.state;
  422. }
  423. });
  424. }
  425. return Column(
  426. mainAxisSize: MainAxisSize.min,
  427. children: [
  428. // 圆形按钮区域
  429. SizedBox(
  430. width: 170.w,
  431. height: 170.w,
  432. child: Stack(
  433. alignment: Alignment.center,
  434. children: [
  435. // 圆环背景 - 使用纯淡入淡出,无缩放,更自然
  436. AnimatedSwitcher(
  437. duration: const Duration(milliseconds: 600),
  438. switchInCurve: Curves.easeInOut,
  439. switchOutCurve: Curves.easeInOut,
  440. transitionBuilder: (Widget child, Animation<double> animation) {
  441. return FadeTransition(
  442. opacity: CurvedAnimation(
  443. parent: animation,
  444. curve: Curves.easeInOut,
  445. ),
  446. child: child,
  447. );
  448. },
  449. layoutBuilder: (currentChild, previousChildren) {
  450. return Stack(
  451. alignment: Alignment.center,
  452. children: [
  453. ...previousChildren,
  454. if (currentChild != null) currentChild,
  455. ],
  456. );
  457. },
  458. child: _buildRoundRing(widget.state, shouldRotate),
  459. ),
  460. // 电源图标 - 纯淡入淡出
  461. AnimatedSwitcher(
  462. duration: const Duration(milliseconds: 500),
  463. switchInCurve: Curves.easeInOut,
  464. switchOutCurve: Curves.easeInOut,
  465. transitionBuilder: (Widget child, Animation<double> animation) {
  466. return FadeTransition(
  467. opacity: CurvedAnimation(
  468. parent: animation,
  469. curve: Curves.easeInOut,
  470. ),
  471. child: child,
  472. );
  473. },
  474. layoutBuilder: (currentChild, previousChildren) {
  475. return Stack(
  476. alignment: Alignment.center,
  477. children: [
  478. ...previousChildren,
  479. if (currentChild != null) currentChild,
  480. ],
  481. );
  482. },
  483. child: _buildPowerIcon(isConnected),
  484. ),
  485. ],
  486. ),
  487. ),
  488. 20.verticalSpaceFromWidth,
  489. // 状态文字
  490. AnimatedSwitcher(
  491. duration: const Duration(milliseconds: 350),
  492. switchInCurve: Curves.easeOutCubic,
  493. switchOutCurve: Curves.easeInCubic,
  494. transitionBuilder: (Widget child, Animation<double> animation) {
  495. return FadeTransition(
  496. opacity: CurvedAnimation(
  497. parent: animation,
  498. curve: Curves.easeInOut,
  499. ),
  500. child: SlideTransition(
  501. position:
  502. Tween<Offset>(
  503. begin: const Offset(0, 0.15),
  504. end: Offset.zero,
  505. ).animate(
  506. CurvedAnimation(
  507. parent: animation,
  508. curve: Curves.easeOutCubic,
  509. ),
  510. ),
  511. child: child,
  512. ),
  513. );
  514. },
  515. child: SizedBox(
  516. key: ValueKey('status_$text'), // 使用文本作为 key,确保文字改变时触发动画
  517. height: 20.w,
  518. child: Row(
  519. mainAxisAlignment: MainAxisAlignment.center,
  520. crossAxisAlignment: CrossAxisAlignment.center,
  521. mainAxisSize: MainAxisSize.min,
  522. children: [
  523. IXImage(
  524. source: statusImgPath,
  525. sourceType: ImageSourceType.asset,
  526. width: 14.w,
  527. height: 14.w,
  528. ),
  529. 4.horizontalSpace,
  530. Text(
  531. text,
  532. style: TextStyle(
  533. fontSize: 14.sp,
  534. fontWeight: FontWeight.w500,
  535. height: 1.4,
  536. color: textColor,
  537. ),
  538. ),
  539. ],
  540. ),
  541. ),
  542. ),
  543. ],
  544. );
  545. }
  546. }