connection_round_button.dart 20 KB

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