|
|
@@ -0,0 +1,370 @@
|
|
|
+import 'package:flutter/material.dart' hide ConnectionState;
|
|
|
+import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
+import 'dart:math' as math;
|
|
|
+import 'dart:async';
|
|
|
+import 'package:get/get.dart';
|
|
|
+import 'package:nomo/app/widgets/ix_image.dart';
|
|
|
+
|
|
|
+import '../../../constants/assets.dart';
|
|
|
+import '../../../../config/theme/theme_extensions/theme_extension.dart';
|
|
|
+import '../../../../config/translations/strings_enum.dart';
|
|
|
+import '../../../constants/enums.dart';
|
|
|
+
|
|
|
+class ConnectionRoundButton extends StatefulWidget {
|
|
|
+ final ConnectionState state;
|
|
|
+ final VoidCallback? onTap;
|
|
|
+
|
|
|
+ const ConnectionRoundButton({super.key, required this.state, this.onTap});
|
|
|
+
|
|
|
+ @override
|
|
|
+ State<ConnectionRoundButton> createState() => _ConnectionRoundButtonState();
|
|
|
+}
|
|
|
+
|
|
|
+class _ConnectionRoundButtonState extends State<ConnectionRoundButton>
|
|
|
+ with TickerProviderStateMixin {
|
|
|
+ late AnimationController _rotationController; // 旋转动画控制器
|
|
|
+ late AnimationController _fadeController; // 淡入淡出控制器
|
|
|
+ Timer? _connectingTimer; // 连接中状态的计时器
|
|
|
+ int _connectingTextIndex = 0; // 当前显示的连接文本索引(0-4)
|
|
|
+ ConnectionState? _previousState; // 保存前一个连接状态
|
|
|
+ bool _isStoppingRotation = false; // 是否正在停止旋转
|
|
|
+
|
|
|
+ @override
|
|
|
+ void initState() {
|
|
|
+ super.initState();
|
|
|
+ // 初始化旋转动画控制器
|
|
|
+ _rotationController = AnimationController(
|
|
|
+ duration: const Duration(milliseconds: 500),
|
|
|
+ vsync: this,
|
|
|
+ );
|
|
|
+
|
|
|
+ // 初始化淡入淡出控制器
|
|
|
+ _fadeController = AnimationController(
|
|
|
+ duration: const Duration(milliseconds: 600),
|
|
|
+ vsync: this,
|
|
|
+ value: 1.0,
|
|
|
+ );
|
|
|
+
|
|
|
+ // 如果初始状态是 connecting,启动动画
|
|
|
+ if (widget.state == ConnectionState.connecting) {
|
|
|
+ _rotationController.repeat();
|
|
|
+ _startConnectingTimer();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void didUpdateWidget(ConnectionRoundButton oldWidget) {
|
|
|
+ super.didUpdateWidget(oldWidget);
|
|
|
+ // 处理状态变化
|
|
|
+ if (oldWidget.state != widget.state) {
|
|
|
+ _handleStateChange(oldWidget.state);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void _handleStateChange(ConnectionState oldState) {
|
|
|
+ if (widget.state == ConnectionState.connecting) {
|
|
|
+ _isStoppingRotation = false;
|
|
|
+ if (!_rotationController.isAnimating) {
|
|
|
+ _rotationController.repeat();
|
|
|
+ }
|
|
|
+ if (_connectingTimer == null || !_connectingTimer!.isActive) {
|
|
|
+ _startConnectingTimer();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 从连接中切换到其他状态时,平滑停止旋转
|
|
|
+ if (_rotationController.isAnimating && !_isStoppingRotation) {
|
|
|
+ _isStoppingRotation = true;
|
|
|
+ // 计算剩余角度,让动画平滑停止在顶部(0度位置)
|
|
|
+ final currentValue = _rotationController.value;
|
|
|
+ // 停止重复,然后平滑减速到完整的一圈
|
|
|
+ _rotationController.stop();
|
|
|
+ _rotationController
|
|
|
+ .animateTo(
|
|
|
+ 1.0,
|
|
|
+ duration: Duration(
|
|
|
+ milliseconds: ((1.0 - currentValue) * 400).toInt().clamp(
|
|
|
+ 100,
|
|
|
+ 400,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ curve: Curves.easeOutCubic,
|
|
|
+ )
|
|
|
+ .then((_) {
|
|
|
+ if (mounted) {
|
|
|
+ _rotationController.reset();
|
|
|
+ _isStoppingRotation = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ _stopConnectingTimer();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ void dispose() {
|
|
|
+ _rotationController.dispose();
|
|
|
+ _fadeController.dispose();
|
|
|
+ _connectingTimer?.cancel();
|
|
|
+ super.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ void _onTap() {
|
|
|
+ if (widget.onTap != null) {
|
|
|
+ widget.onTap!();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 启动连接中状态的文本轮播计时器
|
|
|
+ void _startConnectingTimer() {
|
|
|
+ _connectingTimer?.cancel();
|
|
|
+ _connectingTextIndex = 0;
|
|
|
+ _connectingTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
|
+ if (mounted) {
|
|
|
+ setState(() {
|
|
|
+ // 每秒切换到下一个文本,循环显示0-4
|
|
|
+ _connectingTextIndex = (_connectingTextIndex + 1) % 5;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 停止连接中状态的文本轮播计时器
|
|
|
+ void _stopConnectingTimer() {
|
|
|
+ _connectingTimer?.cancel();
|
|
|
+ _connectingTimer = null;
|
|
|
+ _connectingTextIndex = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据索引获取对应的连接文本
|
|
|
+ String _getConnectingText() {
|
|
|
+ switch (_connectingTextIndex) {
|
|
|
+ case 0:
|
|
|
+ return Strings.securingData.tr;
|
|
|
+ case 1:
|
|
|
+ return Strings.encryptingTraffic.tr;
|
|
|
+ case 2:
|
|
|
+ return Strings.protectingPrivacy.tr;
|
|
|
+ case 3:
|
|
|
+ return Strings.safeConnection.tr;
|
|
|
+ case 4:
|
|
|
+ return Strings.yourDataIsSafe.tr;
|
|
|
+ default:
|
|
|
+ return Strings.connecting.tr;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建旋转的圆环
|
|
|
+ Widget _buildRoundRing(String svgPath, bool shouldRotate) {
|
|
|
+ final ringWidget = IXImage(
|
|
|
+ key: ValueKey('ring_$svgPath'),
|
|
|
+ source: svgPath,
|
|
|
+ sourceType: ImageSourceType.asset,
|
|
|
+ width: 170.w,
|
|
|
+ height: 170.w,
|
|
|
+ );
|
|
|
+
|
|
|
+ if (shouldRotate) {
|
|
|
+ return AnimatedBuilder(
|
|
|
+ key: const ValueKey('rotating_ring'),
|
|
|
+ animation: _rotationController,
|
|
|
+ builder: (context, child) {
|
|
|
+ return Transform.rotate(
|
|
|
+ angle: _rotationController.value * 2 * math.pi,
|
|
|
+ child: child,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ child: ringWidget,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return ringWidget;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建电源图标
|
|
|
+ Widget _buildPowerIcon(bool isConnected) {
|
|
|
+ return IXImage(
|
|
|
+ key: ValueKey('power_icon_$isConnected'),
|
|
|
+ source: isConnected ? Assets.connectedSwitch : Assets.disconnectedSwitch,
|
|
|
+ sourceType: ImageSourceType.asset,
|
|
|
+ width: 48.w,
|
|
|
+ height: 48.w,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return GestureDetector(onTap: _onTap, child: _buildMainButton());
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildMainButton() {
|
|
|
+ // 根据状态获取对应的资源和样式
|
|
|
+ String ringPath;
|
|
|
+ String statusImgPath;
|
|
|
+ String text;
|
|
|
+ Color textColor;
|
|
|
+ bool isConnected;
|
|
|
+ bool shouldRotate;
|
|
|
+
|
|
|
+ switch (widget.state) {
|
|
|
+ case ConnectionState.disconnected:
|
|
|
+ ringPath = Assets.disconnectedRound;
|
|
|
+ statusImgPath = Assets.disconnected;
|
|
|
+ text = Strings.disconnected.tr;
|
|
|
+ textColor = Get.reactiveTheme.hintColor;
|
|
|
+ isConnected = false;
|
|
|
+ shouldRotate = false;
|
|
|
+ break;
|
|
|
+ case ConnectionState.connecting:
|
|
|
+ ringPath = Assets.connectingRound;
|
|
|
+ statusImgPath = Assets.connecting;
|
|
|
+ text = _getConnectingText(); // 使用轮播文本
|
|
|
+ textColor = Get.reactiveTheme.hintColor;
|
|
|
+ isConnected = false;
|
|
|
+ shouldRotate = true;
|
|
|
+ break;
|
|
|
+ case ConnectionState.connected:
|
|
|
+ ringPath = Assets.connectedRound;
|
|
|
+ statusImgPath = Assets.connected;
|
|
|
+ text = Strings.connected.tr;
|
|
|
+ textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!;
|
|
|
+ isConnected = true;
|
|
|
+ shouldRotate = false;
|
|
|
+ break;
|
|
|
+ case ConnectionState.error:
|
|
|
+ ringPath = Assets.disconnectedRound;
|
|
|
+ statusImgPath = Assets.error;
|
|
|
+ text = Strings.error.tr;
|
|
|
+ textColor = Get.reactiveTheme.hintColor;
|
|
|
+ isConnected = false;
|
|
|
+ shouldRotate = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新前一个状态
|
|
|
+ if (_previousState != widget.state) {
|
|
|
+ WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
+ if (mounted) {
|
|
|
+ _previousState = widget.state;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ // 圆形按钮区域
|
|
|
+ SizedBox(
|
|
|
+ width: 170.w,
|
|
|
+ height: 170.w,
|
|
|
+ child: Stack(
|
|
|
+ alignment: Alignment.center,
|
|
|
+ children: [
|
|
|
+ // 圆环背景 - 使用纯淡入淡出,无缩放,更自然
|
|
|
+ AnimatedSwitcher(
|
|
|
+ duration: const Duration(milliseconds: 600),
|
|
|
+ switchInCurve: Curves.easeInOut,
|
|
|
+ switchOutCurve: Curves.easeInOut,
|
|
|
+ transitionBuilder: (Widget child, Animation<double> animation) {
|
|
|
+ return FadeTransition(
|
|
|
+ opacity: CurvedAnimation(
|
|
|
+ parent: animation,
|
|
|
+ curve: Curves.easeInOut,
|
|
|
+ ),
|
|
|
+ child: child,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ layoutBuilder: (currentChild, previousChildren) {
|
|
|
+ return Stack(
|
|
|
+ alignment: Alignment.center,
|
|
|
+ children: [
|
|
|
+ ...previousChildren,
|
|
|
+ if (currentChild != null) currentChild,
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ },
|
|
|
+ child: _buildRoundRing(ringPath, shouldRotate),
|
|
|
+ ),
|
|
|
+ // 电源图标 - 纯淡入淡出
|
|
|
+ AnimatedSwitcher(
|
|
|
+ duration: const Duration(milliseconds: 500),
|
|
|
+ switchInCurve: Curves.easeInOut,
|
|
|
+ switchOutCurve: Curves.easeInOut,
|
|
|
+ transitionBuilder: (Widget child, Animation<double> animation) {
|
|
|
+ return FadeTransition(
|
|
|
+ opacity: CurvedAnimation(
|
|
|
+ parent: animation,
|
|
|
+ curve: Curves.easeInOut,
|
|
|
+ ),
|
|
|
+ child: child,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ layoutBuilder: (currentChild, previousChildren) {
|
|
|
+ return Stack(
|
|
|
+ alignment: Alignment.center,
|
|
|
+ children: [
|
|
|
+ ...previousChildren,
|
|
|
+ if (currentChild != null) currentChild,
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ },
|
|
|
+ child: _buildPowerIcon(isConnected),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ 20.verticalSpaceFromWidth,
|
|
|
+ // 状态文字
|
|
|
+ AnimatedSwitcher(
|
|
|
+ duration: const Duration(milliseconds: 350),
|
|
|
+ switchInCurve: Curves.easeOutCubic,
|
|
|
+ switchOutCurve: Curves.easeInCubic,
|
|
|
+ transitionBuilder: (Widget child, Animation<double> animation) {
|
|
|
+ return FadeTransition(
|
|
|
+ opacity: CurvedAnimation(
|
|
|
+ parent: animation,
|
|
|
+ curve: Curves.easeInOut,
|
|
|
+ ),
|
|
|
+ child: SlideTransition(
|
|
|
+ position:
|
|
|
+ Tween<Offset>(
|
|
|
+ begin: const Offset(0, 0.15),
|
|
|
+ end: Offset.zero,
|
|
|
+ ).animate(
|
|
|
+ CurvedAnimation(
|
|
|
+ parent: animation,
|
|
|
+ curve: Curves.easeOutCubic,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ child: child,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ child: Row(
|
|
|
+ key: ValueKey('status_$text'), // 使用文本作为 key,确保文字改变时触发动画
|
|
|
+ mainAxisAlignment: MainAxisAlignment.center,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ IXImage(
|
|
|
+ source: statusImgPath,
|
|
|
+ sourceType: ImageSourceType.asset,
|
|
|
+ width: 14.w,
|
|
|
+ height: 14.w,
|
|
|
+ ),
|
|
|
+ 4.horizontalSpace,
|
|
|
+ Text(
|
|
|
+ text,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: 14.sp,
|
|
|
+ fontWeight: FontWeight.w500,
|
|
|
+ height: 1.4,
|
|
|
+ color: textColor,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|