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 'package:nomo/utils/misc.dart'; import '../../../constants/assets.dart'; import '../../../../config/theme/theme_extensions/theme_extension.dart'; import '../../../../config/translations/strings_enum.dart'; import '../../../constants/enums.dart'; /// 连接中状态的流光边框画笔 class ConnectingBorderPainter extends CustomPainter { final double rotationAngle; final List colors; ConnectingBorderPainter({required this.rotationAngle, required this.colors}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2 - 1; const strokeWidth = 2.0; final rect = Rect.fromCircle(center: center, radius: radius); // 连接中状态:绘制三色渐变流光效果 final gradient = SweepGradient( startAngle: 0, endAngle: 2 * math.pi, colors: colors, stops: const [0.2, 0.6, 1.0], transform: GradientRotation(rotationAngle - math.pi / 2), ); final paint = Paint() ..shader = gradient.createShader(rect) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round ..isAntiAlias = true; canvas.drawCircle(center, radius, paint); } @override bool shouldRepaint(covariant ConnectingBorderPainter oldDelegate) { return oldDelegate.rotationAngle != rotationAngle || oldDelegate.colors != colors; } } class ConnectionThemeButton extends StatefulWidget { final ConnectionState state; final VoidCallback? onTap; const ConnectionThemeButton({super.key, required this.state, this.onTap}); @override State createState() => _ConnectionThemeButtonState(); } class _ConnectionThemeButtonState extends State with TickerProviderStateMixin { late AnimationController _rotationController; // 旋转动画控制器 late AnimationController _fadeController; // 淡入淡出控制器 Timer? _connectingTimer; // 连接中状态的计时器 int _connectingTextIndex = 0; // 当前显示的连接文本索引(0-4) ConnectionState? _previousState; // 保存前一个连接状态 @override void initState() { super.initState(); // 初始化旋转动画控制器 _rotationController = AnimationController( duration: const Duration(milliseconds: 350), vsync: this, ); // 初始化淡入淡出控制器 _fadeController = AnimationController( duration: const Duration(milliseconds: 450), vsync: this, value: 1.0, ); // 如果初始状态是 connectingVirtual/connecting 或 disconnecting,启动动画 if (widget.state == ConnectionState.connectingVirtual || widget.state == ConnectionState.connecting || widget.state == ConnectionState.disconnecting) { _rotationController.repeat(); if (widget.state == ConnectionState.connectingVirtual || widget.state == ConnectionState.connecting) { _startConnectingTimer(); } } } @override void didUpdateWidget(ConnectionThemeButton oldWidget) { super.didUpdateWidget(oldWidget); // 处理状态变化 if (oldWidget.state != widget.state) { _handleStateChange(oldWidget.state); } } void _handleStateChange(ConnectionState oldState) { if (widget.state == ConnectionState.connectingVirtual || widget.state == ConnectionState.connecting || widget.state == ConnectionState.disconnecting) { // 进入旋转状态时,立即开始旋转 _rotationController.stop(); _rotationController.repeat(); if (widget.state == ConnectionState.connectingVirtual || widget.state == ConnectionState.connecting) { if (_connectingTimer == null || !_connectingTimer!.isActive) { _startConnectingTimer(); } } else { _stopConnectingTimer(); } } else { // 从连接中/断开中切换到其他状态时 // 延迟停止旋转动画,让流光在 AnimatedSwitcher 淡出过程中继续旋转 // 避免看到停止的线 _stopConnectingTimer(); Future.delayed(const Duration(milliseconds: 650), () { if (mounted && widget.state != ConnectionState.connectingVirtual && widget.state != ConnectionState.connecting && widget.state != ConnectionState.disconnecting) { _rotationController.stop(); _rotationController.reset(); } }); } } @override void dispose() { _rotationController.dispose(); _fadeController.dispose(); _connectingTimer?.cancel(); super.dispose(); } void _onTap() { if (widget.onTap != null) { widget.onTap!(); } } // 启动连接中状态的文本轮播计时器 void _startConnectingTimer() { _connectingTimer?.cancel(); _connectingTextIndex = -1; _connectingTimer = Timer.periodic(const Duration(seconds: 5), (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; } } // 连接中状态的流光边框颜色(紫色 → 蓝色 → 浅蓝色) static const List _connectingColors = [ Color(0xFFAC27FF), // 紫色 Color(0xFF0EA5E9), // 蓝色 Color(0xFF2ECBFF), // 浅蓝色 ]; // 断开中状态的流光边框颜色(浅黄色 → 橙色 → 红色) static const List _disconnectingColors = [ Color(0xFFF5D89F), // 浅黄色 Color(0xFFF19021), // 橙色 Color(0xFFEF0000), // 红色 ]; // 连接成功状态的渐变色 static const Color _connectedGradientStart = Color(0xFF0EA5E9); static const Color _connectedGradientEnd = Color(0xFF3B82F6); // 连接成功状态的阴影色 static const Color _connectedShadowColor = Color( 0x660B84FE, ); // rgba(11, 132, 254, 0.40) // 断开状态的阴影色 static const Color _disconnectedShadowColor = Color( 0x14000000, ); // rgba(0, 0, 0, 0.08) // 构建流光动画背景(连接中/断开中状态共用) Widget _buildAnimatingBackground({ required double size, required ValueKey key, required List colors, }) { return Container( key: key, width: size, height: size, decoration: BoxDecoration( color: Get.reactiveTheme.highlightColor, shape: BoxShape.circle, ), child: Stack( alignment: Alignment.center, children: [ // 流光边框 AnimatedBuilder( animation: _rotationController, builder: (context, child) { return CustomPaint( size: Size(size, size), painter: ConnectingBorderPainter( rotationAngle: _rotationController.value * 2 * math.pi, colors: colors, ), ); }, ), // 内层背景圆(遮住边框内侧) Container( width: size - 4.w, height: size - 4.w, decoration: BoxDecoration( color: Get.reactiveTheme.highlightColor, shape: BoxShape.circle, ), ), ], ), ); } // 构建圆形按钮背景 Widget _buildButtonBackground(ConnectionState state, bool shouldRotate) { final size = isDesktop ? 130.w : 170.w; switch (state) { case ConnectionState.disconnected: case ConnectionState.error: // 断开状态:highlightColor 背景 + 4.w dividerColor 边框 + 阴影 return Container( key: ValueKey('bg_disconnected_$state'), width: size, height: size, decoration: BoxDecoration( color: Get.reactiveTheme.highlightColor, shape: BoxShape.circle, border: Border.all( color: Get.reactiveTheme.dividerColor, width: 2.w, ), boxShadow: [ BoxShadow( color: _disconnectedShadowColor, offset: const Offset(0, 8), blurRadius: 24, spreadRadius: 0, ), ], ), ); case ConnectionState.connectingVirtual: case ConnectionState.connecting: // 连接中状态:highlightColor 背景 + 紫蓝渐变流光边框 return _buildAnimatingBackground( size: size, key: const ValueKey('bg_connecting'), colors: _connectingColors, ); case ConnectionState.disconnecting: // 断开中状态:highlightColor 背景 + 黄橙红渐变流光边框 return _buildAnimatingBackground( size: size, key: const ValueKey('bg_disconnecting'), colors: _disconnectingColors, ); case ConnectionState.connected: // 连接成功状态:渐变背景 + 蓝色阴影 return Container( key: const ValueKey('bg_connected'), width: size, height: size, decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_connectedGradientStart, _connectedGradientEnd], ), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: _connectedShadowColor, offset: const Offset(0, 8), blurRadius: 20, spreadRadius: 0, ), ], ), ); } } // 根据状态和主题获取中心图片路径 String _getCenterImagePath(ConnectionState state) { final isDark = Get.isDarkMode; switch (state) { case ConnectionState.disconnected: case ConnectionState.error: return isDark ? Assets.darkDisconnected : Assets.lightDisconnected; case ConnectionState.connectingVirtual: case ConnectionState.connecting: case ConnectionState.disconnecting: return isDark ? Assets.darkDisconnected : Assets.lightDisconnected; case ConnectionState.connected: // 连接成功时使用白色图标(因为背景是蓝色渐变) return Assets.darkConnected; } } // 获取中心图片的 key(相同图片使用相同 key,避免不必要的动画) String _getCenterImageKey(ConnectionState state) { switch (state) { case ConnectionState.disconnected: case ConnectionState.error: return 'center_disconnected_${Get.isDarkMode}'; case ConnectionState.connectingVirtual: case ConnectionState.connecting: case ConnectionState.disconnecting: return 'center_connecting_${Get.isDarkMode}'; case ConnectionState.connected: return 'center_connected'; } } // 构建中心图片 Widget _buildCenterImage(ConnectionState state) { final imagePath = _getCenterImagePath(state); return IXImage( key: ValueKey(_getCenterImageKey(state)), source: imagePath, sourceType: ImageSourceType.asset, width: 40.w, height: 40.w, ); } @override Widget build(BuildContext context) { return GestureDetector(onTap: _onTap, child: _buildMainButton()); } Widget _buildMainButton() { // 根据状态获取对应的资源和样式 String statusImgPath; String text; Color textColor; bool shouldRotate; switch (widget.state) { case ConnectionState.disconnected: statusImgPath = Assets.disconnected; text = Strings.disconnected.tr; textColor = Get.reactiveTheme.hintColor; shouldRotate = false; break; case ConnectionState.connectingVirtual: case ConnectionState.connecting: statusImgPath = Assets.connecting; text = _getConnectingText(); // 使用轮播文本 textColor = Get.reactiveTheme.hintColor; shouldRotate = true; break; case ConnectionState.disconnecting: statusImgPath = Assets.connecting; text = Strings.disconnecting.tr; textColor = Get.reactiveTheme.hintColor; shouldRotate = true; break; case ConnectionState.connected: statusImgPath = Assets.connected; text = Strings.connected.tr; textColor = Get.reactiveTheme.textTheme.bodyLarge!.color!; shouldRotate = false; break; case ConnectionState.error: statusImgPath = Assets.error; text = Strings.error.tr; textColor = Get.reactiveTheme.hintColor; shouldRotate = false; break; } // 更新前一个状态 if (_previousState != widget.state) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _previousState = widget.state; } }); } return Column( mainAxisSize: MainAxisSize.min, children: [ // 圆形按钮区域 SizedBox( width: isDesktop ? 130 : 170.w, height: isDesktop ? 130 : 170.w, child: Stack( alignment: Alignment.center, children: [ // 按钮背景 - 使用纯淡入淡出 AnimatedSwitcher( duration: const Duration(milliseconds: 600), switchInCurve: Curves.easeInOut, switchOutCurve: Curves.easeInOut, transitionBuilder: (Widget child, Animation 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: _buildButtonBackground(widget.state, shouldRotate), ), // 中心图片 - 纯淡入淡出 AnimatedSwitcher( duration: const Duration(milliseconds: 500), switchInCurve: Curves.easeInOut, switchOutCurve: Curves.easeInOut, transitionBuilder: (Widget child, Animation 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: _buildCenterImage(widget.state), ), ], ), ), 20.verticalSpaceFromWidth, // 状态文字 AnimatedSwitcher( duration: const Duration(milliseconds: 350), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeInCubic, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: CurvedAnimation( parent: animation, curve: Curves.easeInOut, ), child: SlideTransition( position: Tween( begin: const Offset(0, 0.15), end: Offset.zero, ).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, ), ), child: child, ), ); }, child: SizedBox( key: ValueKey('status_$text'), // 使用文本作为 key,确保文字改变时触发动画 height: 20.w, child: Row( 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, ), ), ], ), ), ), ], ); } }