connection_theme_button.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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 ConnectingBorderPainter extends CustomPainter {
  13. final double rotationAngle;
  14. final List<Color> colors;
  15. ConnectingBorderPainter({required this.rotationAngle, required this.colors});
  16. @override
  17. void paint(Canvas canvas, Size size) {
  18. final center = Offset(size.width / 2, size.height / 2);
  19. final radius = size.width / 2 - 1;
  20. const strokeWidth = 2.0;
  21. final rect = Rect.fromCircle(center: center, radius: radius);
  22. // 连接中状态:绘制三色渐变流光效果
  23. final gradient = SweepGradient(
  24. startAngle: 0,
  25. endAngle: 2 * math.pi,
  26. colors: colors,
  27. stops: const [0.2, 0.6, 1.0],
  28. transform: GradientRotation(rotationAngle - math.pi / 2),
  29. );
  30. final paint = Paint()
  31. ..shader = gradient.createShader(rect)
  32. ..style = PaintingStyle.stroke
  33. ..strokeWidth = strokeWidth
  34. ..strokeCap = StrokeCap.round
  35. ..isAntiAlias = true;
  36. canvas.drawCircle(center, radius, paint);
  37. }
  38. @override
  39. bool shouldRepaint(covariant ConnectingBorderPainter oldDelegate) {
  40. return oldDelegate.rotationAngle != rotationAngle ||
  41. oldDelegate.colors != colors;
  42. }
  43. }
  44. class ConnectionThemeButton extends StatefulWidget {
  45. final ConnectionState state;
  46. final VoidCallback? onTap;
  47. const ConnectionThemeButton({super.key, required this.state, this.onTap});
  48. @override
  49. State<ConnectionThemeButton> createState() => _ConnectionThemeButtonState();
  50. }
  51. class _ConnectionThemeButtonState extends State<ConnectionThemeButton>
  52. with TickerProviderStateMixin {
  53. late AnimationController _rotationController; // 旋转动画控制器
  54. late AnimationController _fadeController; // 淡入淡出控制器
  55. Timer? _connectingTimer; // 连接中状态的计时器
  56. int _connectingTextIndex = 0; // 当前显示的连接文本索引(0-4)
  57. ConnectionState? _previousState; // 保存前一个连接状态
  58. @override
  59. void initState() {
  60. super.initState();
  61. // 初始化旋转动画控制器
  62. _rotationController = AnimationController(
  63. duration: const Duration(milliseconds: 350),
  64. vsync: this,
  65. );
  66. // 初始化淡入淡出控制器
  67. _fadeController = AnimationController(
  68. duration: const Duration(milliseconds: 450),
  69. vsync: this,
  70. value: 1.0,
  71. );
  72. // 如果初始状态是 connectingVirtual/connecting 或 disconnecting,启动动画
  73. if (widget.state == ConnectionState.connectingVirtual ||
  74. widget.state == ConnectionState.connecting ||
  75. widget.state == ConnectionState.disconnecting) {
  76. _rotationController.repeat();
  77. if (widget.state == ConnectionState.connectingVirtual ||
  78. widget.state == ConnectionState.connecting) {
  79. _startConnectingTimer();
  80. }
  81. }
  82. }
  83. @override
  84. void didUpdateWidget(ConnectionThemeButton oldWidget) {
  85. super.didUpdateWidget(oldWidget);
  86. // 处理状态变化
  87. if (oldWidget.state != widget.state) {
  88. _handleStateChange(oldWidget.state);
  89. }
  90. }
  91. void _handleStateChange(ConnectionState oldState) {
  92. if (widget.state == ConnectionState.connectingVirtual ||
  93. widget.state == ConnectionState.connecting ||
  94. widget.state == ConnectionState.disconnecting) {
  95. // 进入旋转状态时,立即开始旋转
  96. _rotationController.stop();
  97. _rotationController.repeat();
  98. if (widget.state == ConnectionState.connectingVirtual ||
  99. widget.state == ConnectionState.connecting) {
  100. if (_connectingTimer == null || !_connectingTimer!.isActive) {
  101. _startConnectingTimer();
  102. }
  103. } else {
  104. _stopConnectingTimer();
  105. }
  106. } else {
  107. // 从连接中/断开中切换到其他状态时
  108. // 延迟停止旋转动画,让流光在 AnimatedSwitcher 淡出过程中继续旋转
  109. // 避免看到停止的线
  110. _stopConnectingTimer();
  111. Future.delayed(const Duration(milliseconds: 650), () {
  112. if (mounted &&
  113. widget.state != ConnectionState.connectingVirtual &&
  114. widget.state != ConnectionState.connecting &&
  115. widget.state != ConnectionState.disconnecting) {
  116. _rotationController.stop();
  117. _rotationController.reset();
  118. }
  119. });
  120. }
  121. }
  122. @override
  123. void dispose() {
  124. _rotationController.dispose();
  125. _fadeController.dispose();
  126. _connectingTimer?.cancel();
  127. super.dispose();
  128. }
  129. void _onTap() {
  130. if (widget.onTap != null) {
  131. widget.onTap!();
  132. }
  133. }
  134. // 启动连接中状态的文本轮播计时器
  135. void _startConnectingTimer() {
  136. _connectingTimer?.cancel();
  137. _connectingTextIndex = -1;
  138. _connectingTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
  139. if (mounted) {
  140. setState(() {
  141. // 每秒切换到下一个文本,循环显示0-4
  142. _connectingTextIndex = (_connectingTextIndex + 1) % 5;
  143. });
  144. }
  145. });
  146. }
  147. // 停止连接中状态的文本轮播计时器
  148. void _stopConnectingTimer() {
  149. _connectingTimer?.cancel();
  150. _connectingTimer = null;
  151. _connectingTextIndex = 0;
  152. }
  153. // 根据索引获取对应的连接文本
  154. String _getConnectingText() {
  155. switch (_connectingTextIndex) {
  156. case 0:
  157. return Strings.securingData.tr;
  158. case 1:
  159. return Strings.encryptingTraffic.tr;
  160. case 2:
  161. return Strings.protectingPrivacy.tr;
  162. case 3:
  163. return Strings.safeConnection.tr;
  164. case 4:
  165. return Strings.yourDataIsSafe.tr;
  166. default:
  167. return Strings.connecting.tr;
  168. }
  169. }
  170. // 连接中状态的流光边框颜色(紫色 → 蓝色 → 浅蓝色)
  171. static const List<Color> _connectingColors = [
  172. Color(0xFFAC27FF), // 紫色
  173. Color(0xFF0EA5E9), // 蓝色
  174. Color(0xFF2ECBFF), // 浅蓝色
  175. ];
  176. // 断开中状态的流光边框颜色(浅黄色 → 橙色 → 红色)
  177. static const List<Color> _disconnectingColors = [
  178. Color(0xFFF5D89F), // 浅黄色
  179. Color(0xFFF19021), // 橙色
  180. Color(0xFFEF0000), // 红色
  181. ];
  182. // 连接成功状态的渐变色
  183. static const Color _connectedGradientStart = Color(0xFF0EA5E9);
  184. static const Color _connectedGradientEnd = Color(0xFF3B82F6);
  185. // 连接成功状态的阴影色
  186. static const Color _connectedShadowColor = Color(
  187. 0x660B84FE,
  188. ); // rgba(11, 132, 254, 0.40)
  189. // 断开状态的阴影色
  190. static const Color _disconnectedShadowColor = Color(
  191. 0x14000000,
  192. ); // rgba(0, 0, 0, 0.08)
  193. // 构建流光动画背景(连接中/断开中状态共用)
  194. Widget _buildAnimatingBackground({
  195. required double size,
  196. required ValueKey key,
  197. required List<Color> colors,
  198. }) {
  199. return Container(
  200. key: key,
  201. width: size,
  202. height: size,
  203. decoration: BoxDecoration(
  204. color: Get.reactiveTheme.highlightColor,
  205. shape: BoxShape.circle,
  206. ),
  207. child: Stack(
  208. alignment: Alignment.center,
  209. children: [
  210. // 流光边框
  211. AnimatedBuilder(
  212. animation: _rotationController,
  213. builder: (context, child) {
  214. return CustomPaint(
  215. size: Size(size, size),
  216. painter: ConnectingBorderPainter(
  217. rotationAngle: _rotationController.value * 2 * math.pi,
  218. colors: colors,
  219. ),
  220. );
  221. },
  222. ),
  223. // 内层背景圆(遮住边框内侧)
  224. Container(
  225. width: size - 4.w,
  226. height: size - 4.w,
  227. decoration: BoxDecoration(
  228. color: Get.reactiveTheme.highlightColor,
  229. shape: BoxShape.circle,
  230. ),
  231. ),
  232. ],
  233. ),
  234. );
  235. }
  236. // 构建圆形按钮背景
  237. Widget _buildButtonBackground(ConnectionState state, bool shouldRotate) {
  238. final size = 170.w;
  239. switch (state) {
  240. case ConnectionState.disconnected:
  241. case ConnectionState.error:
  242. // 断开状态:highlightColor 背景 + 4.w dividerColor 边框 + 阴影
  243. return Container(
  244. key: ValueKey('bg_disconnected_$state'),
  245. width: size,
  246. height: size,
  247. decoration: BoxDecoration(
  248. color: Get.reactiveTheme.highlightColor,
  249. shape: BoxShape.circle,
  250. border: Border.all(
  251. color: Get.reactiveTheme.dividerColor,
  252. width: 2.w,
  253. ),
  254. boxShadow: [
  255. BoxShadow(
  256. color: _disconnectedShadowColor,
  257. offset: const Offset(0, 8),
  258. blurRadius: 24,
  259. spreadRadius: 0,
  260. ),
  261. ],
  262. ),
  263. );
  264. case ConnectionState.connectingVirtual:
  265. case ConnectionState.connecting:
  266. // 连接中状态:highlightColor 背景 + 紫蓝渐变流光边框
  267. return _buildAnimatingBackground(
  268. size: size,
  269. key: const ValueKey('bg_connecting'),
  270. colors: _connectingColors,
  271. );
  272. case ConnectionState.disconnecting:
  273. // 断开中状态:highlightColor 背景 + 黄橙红渐变流光边框
  274. return _buildAnimatingBackground(
  275. size: size,
  276. key: const ValueKey('bg_disconnecting'),
  277. colors: _disconnectingColors,
  278. );
  279. case ConnectionState.connected:
  280. // 连接成功状态:渐变背景 + 蓝色阴影
  281. return Container(
  282. key: const ValueKey('bg_connected'),
  283. width: size,
  284. height: size,
  285. decoration: BoxDecoration(
  286. gradient: const LinearGradient(
  287. begin: Alignment.topLeft,
  288. end: Alignment.bottomRight,
  289. colors: [_connectedGradientStart, _connectedGradientEnd],
  290. ),
  291. shape: BoxShape.circle,
  292. boxShadow: [
  293. BoxShadow(
  294. color: _connectedShadowColor,
  295. offset: const Offset(0, 8),
  296. blurRadius: 20,
  297. spreadRadius: 0,
  298. ),
  299. ],
  300. ),
  301. );
  302. }
  303. }
  304. // 根据状态和主题获取中心图片路径
  305. String _getCenterImagePath(ConnectionState state) {
  306. final isDark = Get.isDarkMode;
  307. switch (state) {
  308. case ConnectionState.disconnected:
  309. case ConnectionState.error:
  310. return isDark ? Assets.darkDisconnected : Assets.lightDisconnected;
  311. case ConnectionState.connectingVirtual:
  312. case ConnectionState.connecting:
  313. return isDark ? Assets.darkConnecting : Assets.lightConnecting;
  314. case ConnectionState.disconnecting:
  315. return Assets.darkDisconnecting;
  316. case ConnectionState.connected:
  317. // 连接成功时使用白色图标(因为背景是蓝色渐变)
  318. return Assets.darkConnected;
  319. }
  320. }
  321. // 获取中心图片的 key(相同图片使用相同 key,避免不必要的动画)
  322. String _getCenterImageKey(ConnectionState state) {
  323. switch (state) {
  324. case ConnectionState.disconnected:
  325. case ConnectionState.error:
  326. return 'center_disconnected_${Get.isDarkMode}';
  327. case ConnectionState.connectingVirtual:
  328. case ConnectionState.connecting:
  329. case ConnectionState.disconnecting:
  330. return 'center_connecting_${Get.isDarkMode}';
  331. case ConnectionState.connected:
  332. return 'center_connected';
  333. }
  334. }
  335. // 构建中心图片
  336. Widget _buildCenterImage(ConnectionState state) {
  337. final imagePath = _getCenterImagePath(state);
  338. return IXImage(
  339. key: ValueKey(_getCenterImageKey(state)),
  340. source: imagePath,
  341. sourceType: ImageSourceType.asset,
  342. width: 40.w,
  343. height: 40.w,
  344. );
  345. }
  346. @override
  347. Widget build(BuildContext context) {
  348. return GestureDetector(onTap: _onTap, child: _buildMainButton());
  349. }
  350. Widget _buildMainButton() {
  351. // 根据状态获取对应的资源和样式
  352. String statusImgPath;
  353. String text;
  354. Color textColor;
  355. bool shouldRotate;
  356. switch (widget.state) {
  357. case ConnectionState.disconnected:
  358. statusImgPath = Assets.disconnected;
  359. text = Strings.disconnected.tr;
  360. textColor = Get.reactiveTheme.hintColor;
  361. shouldRotate = false;
  362. break;
  363. case ConnectionState.connectingVirtual:
  364. case ConnectionState.connecting:
  365. statusImgPath = Assets.connecting;
  366. text = _getConnectingText(); // 使用轮播文本
  367. textColor = Get.reactiveTheme.hintColor;
  368. shouldRotate = true;
  369. break;
  370. case ConnectionState.disconnecting:
  371. statusImgPath = Assets.connecting;
  372. text = Strings.disconnecting.tr;
  373. textColor = Get.reactiveTheme.hintColor;
  374. shouldRotate = true;
  375. break;
  376. case ConnectionState.connected:
  377. statusImgPath = Assets.connected;
  378. text = Strings.connected.tr;
  379. textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
  380. shouldRotate = false;
  381. break;
  382. case ConnectionState.error:
  383. statusImgPath = Assets.error;
  384. text = Strings.error.tr;
  385. textColor = Get.reactiveTheme.hintColor;
  386. shouldRotate = false;
  387. break;
  388. }
  389. // 更新前一个状态
  390. if (_previousState != widget.state) {
  391. WidgetsBinding.instance.addPostFrameCallback((_) {
  392. if (mounted) {
  393. _previousState = widget.state;
  394. }
  395. });
  396. }
  397. return Column(
  398. mainAxisSize: MainAxisSize.min,
  399. children: [
  400. // 圆形按钮区域
  401. SizedBox(
  402. width: 170.w,
  403. height: 170.w,
  404. child: Stack(
  405. alignment: Alignment.center,
  406. children: [
  407. // 按钮背景 - 使用纯淡入淡出
  408. AnimatedSwitcher(
  409. duration: const Duration(milliseconds: 600),
  410. switchInCurve: Curves.easeInOut,
  411. switchOutCurve: Curves.easeInOut,
  412. transitionBuilder: (Widget child, Animation<double> animation) {
  413. return FadeTransition(
  414. opacity: CurvedAnimation(
  415. parent: animation,
  416. curve: Curves.easeInOut,
  417. ),
  418. child: child,
  419. );
  420. },
  421. layoutBuilder: (currentChild, previousChildren) {
  422. return Stack(
  423. alignment: Alignment.center,
  424. children: [
  425. ...previousChildren,
  426. if (currentChild != null) currentChild,
  427. ],
  428. );
  429. },
  430. child: _buildButtonBackground(widget.state, shouldRotate),
  431. ),
  432. // 中心图片 - 纯淡入淡出
  433. AnimatedSwitcher(
  434. duration: const Duration(milliseconds: 500),
  435. switchInCurve: Curves.easeInOut,
  436. switchOutCurve: Curves.easeInOut,
  437. transitionBuilder: (Widget child, Animation<double> animation) {
  438. return FadeTransition(
  439. opacity: CurvedAnimation(
  440. parent: animation,
  441. curve: Curves.easeInOut,
  442. ),
  443. child: child,
  444. );
  445. },
  446. layoutBuilder: (currentChild, previousChildren) {
  447. return Stack(
  448. alignment: Alignment.center,
  449. children: [
  450. ...previousChildren,
  451. if (currentChild != null) currentChild,
  452. ],
  453. );
  454. },
  455. child: _buildCenterImage(widget.state),
  456. ),
  457. ],
  458. ),
  459. ),
  460. 20.verticalSpaceFromWidth,
  461. // 状态文字
  462. AnimatedSwitcher(
  463. duration: const Duration(milliseconds: 350),
  464. switchInCurve: Curves.easeOutCubic,
  465. switchOutCurve: Curves.easeInCubic,
  466. transitionBuilder: (Widget child, Animation<double> animation) {
  467. return FadeTransition(
  468. opacity: CurvedAnimation(
  469. parent: animation,
  470. curve: Curves.easeInOut,
  471. ),
  472. child: SlideTransition(
  473. position:
  474. Tween<Offset>(
  475. begin: const Offset(0, 0.15),
  476. end: Offset.zero,
  477. ).animate(
  478. CurvedAnimation(
  479. parent: animation,
  480. curve: Curves.easeOutCubic,
  481. ),
  482. ),
  483. child: child,
  484. ),
  485. );
  486. },
  487. child: SizedBox(
  488. key: ValueKey('status_$text'), // 使用文本作为 key,确保文字改变时触发动画
  489. height: 20.w,
  490. child: Row(
  491. mainAxisAlignment: MainAxisAlignment.center,
  492. crossAxisAlignment: CrossAxisAlignment.center,
  493. mainAxisSize: MainAxisSize.min,
  494. children: [
  495. IXImage(
  496. source: statusImgPath,
  497. sourceType: ImageSourceType.asset,
  498. width: 14.w,
  499. height: 14.w,
  500. ),
  501. 4.horizontalSpace,
  502. Text(
  503. text,
  504. style: TextStyle(
  505. fontSize: 14.sp,
  506. fontWeight: FontWeight.w500,
  507. height: 1.4,
  508. color: textColor,
  509. ),
  510. ),
  511. ],
  512. ),
  513. ),
  514. ),
  515. ],
  516. );
  517. }
  518. }