| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- 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 RingPainter extends CustomPainter {
- final ConnectionState state;
- final double rotationAngle;
- final double glowIntensity;
- final Color primaryColor;
- final Color secondaryColor;
- RingPainter({
- required this.state,
- this.rotationAngle = 0,
- this.glowIntensity = 0.5,
- required this.primaryColor,
- required this.secondaryColor,
- });
- @override
- void paint(Canvas canvas, Size size) {
- final center = Offset(size.width / 2, size.height / 2);
- final radius = size.width / 2 - 8;
- final strokeWidth = 2.5;
- // 根据状态选择绘制方式
- switch (state) {
- case ConnectionState.disconnected:
- case ConnectionState.error:
- _drawDashedRing(canvas, center, radius, strokeWidth);
- break;
- case ConnectionState.connectingVirtual:
- case ConnectionState.connecting:
- case ConnectionState.disconnecting:
- _drawGradientRing(canvas, center, radius, strokeWidth, true);
- break;
- case ConnectionState.connected:
- _drawGradientRing(canvas, center, radius, strokeWidth, false);
- _drawGlowEffect(canvas, center, radius);
- break;
- }
- }
- /// 绘制虚线圆环(断开/错误状态)
- void _drawDashedRing(
- Canvas canvas,
- Offset center,
- double radius,
- double strokeWidth,
- ) {
- final paint = Paint()
- ..color = secondaryColor.withValues(alpha: 0.4)
- ..style = PaintingStyle.stroke
- ..strokeWidth = strokeWidth
- ..strokeCap = StrokeCap.round
- ..isAntiAlias = true;
- const int dashCount = 60;
- const double dashRatio = 0.5; // 虚线占比
- final double dashAngle = (2 * math.pi / dashCount) * dashRatio;
- final double gapAngle = (2 * math.pi / dashCount) * (1 - dashRatio);
- for (int i = 0; i < dashCount; i++) {
- final startAngle = i * (dashAngle + gapAngle) - math.pi / 2;
- canvas.drawArc(
- Rect.fromCircle(center: center, radius: radius),
- startAngle,
- dashAngle,
- false,
- paint,
- );
- }
- }
- /// 绘制渐变圆环(连接中/已连接状态)
- void _drawGradientRing(
- Canvas canvas,
- Offset center,
- double radius,
- double strokeWidth,
- bool isAnimating,
- ) {
- final rect = Rect.fromCircle(center: center, radius: radius);
- if (isAnimating) {
- // 连接中状态:绘制平滑的流光尾巴效果
- final gradient = SweepGradient(
- startAngle: 0,
- endAngle: 2 * math.pi,
- colors: [
- primaryColor.withValues(alpha: 0.0),
- primaryColor.withValues(alpha: 0.05),
- primaryColor.withValues(alpha: 0.2),
- primaryColor.withValues(alpha: 0.5),
- primaryColor.withValues(alpha: 0.8),
- primaryColor,
- secondaryColor,
- secondaryColor.withValues(alpha: 0.8),
- secondaryColor.withValues(alpha: 0.4),
- secondaryColor.withValues(alpha: 0.1),
- primaryColor.withValues(alpha: 0.0),
- ],
- stops: const [
- 0.0,
- 0.1,
- 0.2,
- 0.35,
- 0.45,
- 0.5,
- 0.55,
- 0.65,
- 0.8,
- 0.9,
- 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);
- } else {
- // 已连接状态:绘制平滑渐变圆环
- final gradient = SweepGradient(
- startAngle: 0,
- endAngle: 2 * math.pi,
- colors: [
- primaryColor,
- primaryColor.withValues(alpha: 0.9),
- secondaryColor.withValues(alpha: 0.9),
- secondaryColor,
- secondaryColor,
- secondaryColor.withValues(alpha: 0.9),
- primaryColor.withValues(alpha: 0.9),
- primaryColor,
- ],
- stops: const [0.0, 0.15, 0.35, 0.5, 0.5, 0.65, 0.85, 1.0],
- transform: GradientRotation(rotationAngle),
- );
- final paint = Paint()
- ..shader = gradient.createShader(rect)
- ..style = PaintingStyle.stroke
- ..strokeWidth = strokeWidth
- ..strokeCap = StrokeCap.round
- ..isAntiAlias = true;
- canvas.drawCircle(center, radius, paint);
- }
- }
- /// 绘制发光效果(已连接状态)
- void _drawGlowEffect(Canvas canvas, Offset center, double radius) {
- // 外层发光 - 使用更多层次和更柔和的模糊
- for (int i = 4; i > 0; i--) {
- final glowPaint = Paint()
- ..color = primaryColor.withValues(alpha: glowIntensity * 0.12 / i)
- ..style = PaintingStyle.stroke
- ..strokeWidth = 2.0 + i * 2.5
- ..maskFilter = MaskFilter.blur(BlurStyle.normal, i * 1.5)
- ..isAntiAlias = true;
- canvas.drawCircle(center, radius, glowPaint);
- }
- }
- @override
- bool shouldRepaint(covariant RingPainter oldDelegate) {
- return oldDelegate.state != state ||
- oldDelegate.rotationAngle != rotationAngle ||
- oldDelegate.glowIntensity != glowIntensity ||
- oldDelegate.primaryColor != primaryColor ||
- oldDelegate.secondaryColor != secondaryColor;
- }
- }
- 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,
- );
- // 如果初始状态是 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(ConnectionRoundButton 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) {
- _isStoppingRotation = false;
- if (!_rotationController.isAnimating) {
- _rotationController.repeat();
- }
- if (widget.state == ConnectionState.connectingVirtual ||
- widget.state == ConnectionState.connecting) {
- if (_connectingTimer == null || !_connectingTimer!.isActive) {
- _startConnectingTimer();
- }
- } else {
- _stopConnectingTimer();
- }
- } 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 = -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;
- }
- }
- // 获取圆环主色调
- Color _getRingPrimaryColor(ConnectionState state) {
- switch (state) {
- case ConnectionState.disconnected:
- case ConnectionState.error:
- return Get.reactiveTheme.hintColor.withValues(alpha: 0.5);
- case ConnectionState.connectingVirtual:
- case ConnectionState.connecting:
- return const Color(0xFF4FC3F7); // 浅蓝色
- case ConnectionState.disconnecting:
- return const Color(0xFFFF7043); // 橙色
- case ConnectionState.connected:
- return const Color(0xFF66BB6A); // 绿色
- }
- }
- // 获取圆环副色调
- Color _getRingSecondaryColor(ConnectionState state) {
- switch (state) {
- case ConnectionState.disconnected:
- case ConnectionState.error:
- return Get.reactiveTheme.hintColor.withValues(alpha: 0.3);
- case ConnectionState.connectingVirtual:
- case ConnectionState.connecting:
- return const Color(0xFF7E57C2); // 紫色
- case ConnectionState.disconnecting:
- return const Color(0xFFFFCA28); // 黄色
- case ConnectionState.connected:
- return const Color(0xFF26C6DA); // 青色
- }
- }
- // 构建圆环 - 使用自定义绘制
- Widget _buildRoundRing(ConnectionState state, bool shouldRotate) {
- final primaryColor = _getRingPrimaryColor(state);
- final secondaryColor = _getRingSecondaryColor(state);
- if (shouldRotate) {
- return AnimatedBuilder(
- key: ValueKey('rotating_ring_$state'),
- animation: _rotationController,
- builder: (context, child) {
- return CustomPaint(
- size: Size(170.w, 170.w),
- painter: RingPainter(
- state: state,
- rotationAngle: _rotationController.value * 2 * math.pi,
- glowIntensity: 0.5,
- primaryColor: primaryColor,
- secondaryColor: secondaryColor,
- ),
- );
- },
- );
- }
- return TweenAnimationBuilder<double>(
- key: ValueKey('static_ring_$state'),
- tween: Tween(begin: 0.0, end: 1.0),
- duration: const Duration(milliseconds: 800),
- curve: Curves.easeOutCubic,
- builder: (context, value, child) {
- return CustomPaint(
- size: Size(170.w, 170.w),
- painter: RingPainter(
- state: state,
- rotationAngle: 0,
- glowIntensity: state == ConnectionState.connected ? value : 0.3,
- primaryColor: primaryColor,
- secondaryColor: secondaryColor,
- ),
- );
- },
- );
- }
- // 根据状态和主题获取中心图片路径
- 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 isDark ? Assets.darkConnected : Assets.lightConnected;
- }
- }
- // 构建中心图片
- Widget _buildCenterImage(ConnectionState state) {
- final imagePath = _getCenterImagePath(state);
- return IXImage(
- key: ValueKey('center_image_${state}_${Get.isDarkMode}'),
- 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: 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(widget.state, 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: _buildCenterImage(widget.state),
- ),
- ],
- ),
- ),
- 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: 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,
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- );
- }
- }
|