connection_round_button.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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. class ConnectionRoundButton extends StatefulWidget {
  12. final ConnectionState state;
  13. final VoidCallback? onTap;
  14. const ConnectionRoundButton({super.key, required this.state, this.onTap});
  15. @override
  16. State<ConnectionRoundButton> createState() => _ConnectionRoundButtonState();
  17. }
  18. class _ConnectionRoundButtonState extends State<ConnectionRoundButton>
  19. with TickerProviderStateMixin {
  20. late AnimationController _rotationController; // 旋转动画控制器
  21. late AnimationController _fadeController; // 淡入淡出控制器
  22. Timer? _connectingTimer; // 连接中状态的计时器
  23. int _connectingTextIndex = 0; // 当前显示的连接文本索引(0-4)
  24. ConnectionState? _previousState; // 保存前一个连接状态
  25. bool _isStoppingRotation = false; // 是否正在停止旋转
  26. @override
  27. void initState() {
  28. super.initState();
  29. // 初始化旋转动画控制器
  30. _rotationController = AnimationController(
  31. duration: const Duration(milliseconds: 500),
  32. vsync: this,
  33. );
  34. // 初始化淡入淡出控制器
  35. _fadeController = AnimationController(
  36. duration: const Duration(milliseconds: 600),
  37. vsync: this,
  38. value: 1.0,
  39. );
  40. // 如果初始状态是 connecting,启动动画
  41. if (widget.state == ConnectionState.connecting) {
  42. _rotationController.repeat();
  43. _startConnectingTimer();
  44. }
  45. }
  46. @override
  47. void didUpdateWidget(ConnectionRoundButton oldWidget) {
  48. super.didUpdateWidget(oldWidget);
  49. // 处理状态变化
  50. if (oldWidget.state != widget.state) {
  51. _handleStateChange(oldWidget.state);
  52. }
  53. }
  54. void _handleStateChange(ConnectionState oldState) {
  55. if (widget.state == ConnectionState.connecting) {
  56. _isStoppingRotation = false;
  57. if (!_rotationController.isAnimating) {
  58. _rotationController.repeat();
  59. }
  60. if (_connectingTimer == null || !_connectingTimer!.isActive) {
  61. _startConnectingTimer();
  62. }
  63. } else {
  64. // 从连接中切换到其他状态时,平滑停止旋转
  65. if (_rotationController.isAnimating && !_isStoppingRotation) {
  66. _isStoppingRotation = true;
  67. // 计算剩余角度,让动画平滑停止在顶部(0度位置)
  68. final currentValue = _rotationController.value;
  69. // 停止重复,然后平滑减速到完整的一圈
  70. _rotationController.stop();
  71. _rotationController
  72. .animateTo(
  73. 1.0,
  74. duration: Duration(
  75. milliseconds: ((1.0 - currentValue) * 400).toInt().clamp(
  76. 100,
  77. 400,
  78. ),
  79. ),
  80. curve: Curves.easeOutCubic,
  81. )
  82. .then((_) {
  83. if (mounted) {
  84. _rotationController.reset();
  85. _isStoppingRotation = false;
  86. }
  87. });
  88. }
  89. _stopConnectingTimer();
  90. }
  91. }
  92. @override
  93. void dispose() {
  94. _rotationController.dispose();
  95. _fadeController.dispose();
  96. _connectingTimer?.cancel();
  97. super.dispose();
  98. }
  99. void _onTap() {
  100. if (widget.onTap != null) {
  101. widget.onTap!();
  102. }
  103. }
  104. // 启动连接中状态的文本轮播计时器
  105. void _startConnectingTimer() {
  106. _connectingTimer?.cancel();
  107. _connectingTextIndex = 0;
  108. _connectingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  109. if (mounted) {
  110. setState(() {
  111. // 每秒切换到下一个文本,循环显示0-4
  112. _connectingTextIndex = (_connectingTextIndex + 1) % 5;
  113. });
  114. }
  115. });
  116. }
  117. // 停止连接中状态的文本轮播计时器
  118. void _stopConnectingTimer() {
  119. _connectingTimer?.cancel();
  120. _connectingTimer = null;
  121. _connectingTextIndex = 0;
  122. }
  123. // 根据索引获取对应的连接文本
  124. String _getConnectingText() {
  125. switch (_connectingTextIndex) {
  126. case 0:
  127. return Strings.securingData.tr;
  128. case 1:
  129. return Strings.encryptingTraffic.tr;
  130. case 2:
  131. return Strings.protectingPrivacy.tr;
  132. case 3:
  133. return Strings.safeConnection.tr;
  134. case 4:
  135. return Strings.yourDataIsSafe.tr;
  136. default:
  137. return Strings.connecting.tr;
  138. }
  139. }
  140. // 构建旋转的圆环
  141. Widget _buildRoundRing(String svgPath, bool shouldRotate) {
  142. final ringWidget = IXImage(
  143. key: ValueKey('ring_$svgPath'),
  144. source: svgPath,
  145. sourceType: ImageSourceType.asset,
  146. width: 170.w,
  147. height: 170.w,
  148. );
  149. if (shouldRotate) {
  150. return AnimatedBuilder(
  151. key: const ValueKey('rotating_ring'),
  152. animation: _rotationController,
  153. builder: (context, child) {
  154. return Transform.rotate(
  155. angle: _rotationController.value * 2 * math.pi,
  156. child: child,
  157. );
  158. },
  159. child: ringWidget,
  160. );
  161. }
  162. return ringWidget;
  163. }
  164. // 构建电源图标
  165. Widget _buildPowerIcon(bool isConnected) {
  166. return IXImage(
  167. key: ValueKey('power_icon_$isConnected'),
  168. source: isConnected ? Assets.connectedSwitch : Assets.disconnectedSwitch,
  169. sourceType: ImageSourceType.asset,
  170. width: 48.w,
  171. height: 48.w,
  172. );
  173. }
  174. @override
  175. Widget build(BuildContext context) {
  176. return GestureDetector(onTap: _onTap, child: _buildMainButton());
  177. }
  178. Widget _buildMainButton() {
  179. // 根据状态获取对应的资源和样式
  180. String ringPath;
  181. String statusImgPath;
  182. String text;
  183. Color textColor;
  184. bool isConnected;
  185. bool shouldRotate;
  186. switch (widget.state) {
  187. case ConnectionState.disconnected:
  188. ringPath = Assets.disconnectedRound;
  189. statusImgPath = Assets.disconnected;
  190. text = Strings.disconnected.tr;
  191. textColor = Get.reactiveTheme.hintColor;
  192. isConnected = false;
  193. shouldRotate = false;
  194. break;
  195. case ConnectionState.connecting:
  196. ringPath = Assets.connectingRound;
  197. statusImgPath = Assets.connecting;
  198. text = _getConnectingText(); // 使用轮播文本
  199. textColor = Get.reactiveTheme.hintColor;
  200. isConnected = false;
  201. shouldRotate = true;
  202. break;
  203. case ConnectionState.connected:
  204. ringPath = Assets.connectedRound;
  205. statusImgPath = Assets.connected;
  206. text = Strings.connected.tr;
  207. textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
  208. isConnected = true;
  209. shouldRotate = false;
  210. break;
  211. case ConnectionState.error:
  212. ringPath = Assets.disconnectedRound;
  213. statusImgPath = Assets.error;
  214. text = Strings.error.tr;
  215. textColor = Get.reactiveTheme.hintColor;
  216. isConnected = false;
  217. shouldRotate = false;
  218. break;
  219. }
  220. // 更新前一个状态
  221. if (_previousState != widget.state) {
  222. WidgetsBinding.instance.addPostFrameCallback((_) {
  223. if (mounted) {
  224. _previousState = widget.state;
  225. }
  226. });
  227. }
  228. return Column(
  229. mainAxisSize: MainAxisSize.min,
  230. children: [
  231. // 圆形按钮区域
  232. SizedBox(
  233. width: 170.w,
  234. height: 170.w,
  235. child: Stack(
  236. alignment: Alignment.center,
  237. children: [
  238. // 圆环背景 - 使用纯淡入淡出,无缩放,更自然
  239. AnimatedSwitcher(
  240. duration: const Duration(milliseconds: 600),
  241. switchInCurve: Curves.easeInOut,
  242. switchOutCurve: Curves.easeInOut,
  243. transitionBuilder: (Widget child, Animation<double> animation) {
  244. return FadeTransition(
  245. opacity: CurvedAnimation(
  246. parent: animation,
  247. curve: Curves.easeInOut,
  248. ),
  249. child: child,
  250. );
  251. },
  252. layoutBuilder: (currentChild, previousChildren) {
  253. return Stack(
  254. alignment: Alignment.center,
  255. children: [
  256. ...previousChildren,
  257. if (currentChild != null) currentChild,
  258. ],
  259. );
  260. },
  261. child: _buildRoundRing(ringPath, shouldRotate),
  262. ),
  263. // 电源图标 - 纯淡入淡出
  264. AnimatedSwitcher(
  265. duration: const Duration(milliseconds: 500),
  266. switchInCurve: Curves.easeInOut,
  267. switchOutCurve: Curves.easeInOut,
  268. transitionBuilder: (Widget child, Animation<double> animation) {
  269. return FadeTransition(
  270. opacity: CurvedAnimation(
  271. parent: animation,
  272. curve: Curves.easeInOut,
  273. ),
  274. child: child,
  275. );
  276. },
  277. layoutBuilder: (currentChild, previousChildren) {
  278. return Stack(
  279. alignment: Alignment.center,
  280. children: [
  281. ...previousChildren,
  282. if (currentChild != null) currentChild,
  283. ],
  284. );
  285. },
  286. child: _buildPowerIcon(isConnected),
  287. ),
  288. ],
  289. ),
  290. ),
  291. 20.verticalSpaceFromWidth,
  292. // 状态文字
  293. AnimatedSwitcher(
  294. duration: const Duration(milliseconds: 350),
  295. switchInCurve: Curves.easeOutCubic,
  296. switchOutCurve: Curves.easeInCubic,
  297. transitionBuilder: (Widget child, Animation<double> animation) {
  298. return FadeTransition(
  299. opacity: CurvedAnimation(
  300. parent: animation,
  301. curve: Curves.easeInOut,
  302. ),
  303. child: SlideTransition(
  304. position:
  305. Tween<Offset>(
  306. begin: const Offset(0, 0.15),
  307. end: Offset.zero,
  308. ).animate(
  309. CurvedAnimation(
  310. parent: animation,
  311. curve: Curves.easeOutCubic,
  312. ),
  313. ),
  314. child: child,
  315. ),
  316. );
  317. },
  318. child: Row(
  319. key: ValueKey('status_$text'), // 使用文本作为 key,确保文字改变时触发动画
  320. mainAxisAlignment: MainAxisAlignment.center,
  321. crossAxisAlignment: CrossAxisAlignment.center,
  322. mainAxisSize: MainAxisSize.min,
  323. children: [
  324. IXImage(
  325. source: statusImgPath,
  326. sourceType: ImageSourceType.asset,
  327. width: 14.w,
  328. height: 14.w,
  329. ),
  330. 4.horizontalSpace,
  331. Text(
  332. text,
  333. style: TextStyle(
  334. fontSize: 14.sp,
  335. fontWeight: FontWeight.w500,
  336. height: 1.4,
  337. color: textColor,
  338. ),
  339. ),
  340. ],
  341. ),
  342. ),
  343. ],
  344. );
  345. }
  346. }