import 'package:flutter/material.dart'; import '../controller/yx_net_inspector_controller.dart'; import '../models/inspector_config.dart'; import '../models/inspector_theme.dart'; import 'inspector_panel.dart'; /// 悬浮调试球组件 class YxFloatingBall extends StatefulWidget { final YxNetInspectorConfig config; final YxNetInspectorTheme theme; final YxNetInspectorController controller; const YxFloatingBall({ super.key, required this.config, required this.theme, required this.controller, }); @override State createState() => _YxFloatingBallState(); } class _YxFloatingBallState extends State with TickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; late Animation _opacityAnimation; Offset _position = const Offset(20, 200); bool _isExpanded = false; OverlayEntry? _currentOverlayEntry; @override void initState() { super.initState(); // 初始化位置 if (widget.config.initialPosition != null) { _position = widget.config.initialPosition!; } // 初始化动画控制器 _animationController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _scaleAnimation = Tween(begin: 1.0, end: 1.2).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); _opacityAnimation = Tween(begin: 1.0, end: 0.8).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); } @override void dispose() { // 清理 overlay if (_currentOverlayEntry != null) { _currentOverlayEntry!.remove(); _currentOverlayEntry = null; } _animationController.dispose(); super.dispose(); } void _onTap() { if (_isExpanded) { _hideInspectorPanel(); } else { _showInspectorPanel(); } } void _showInspectorPanel() { setState(() { _isExpanded = true; }); _animationController.forward(); // 优先使用 Overlay,如果失败则使用 Navigator _showInspectorOverlay(); } void _showInspectorOverlay() { // 延迟到下一帧执行,确保Widget树已经完全构建 WidgetsBinding.instance.addPostFrameCallback((_) { _tryShowOverlay(); }); } void _tryShowOverlay() { // 尝试找到 Overlay OverlayState? overlay; try { overlay = Overlay.of(context, rootOverlay: true); print('YxNetInspector: 成功找到根 Overlay'); } catch (e) { print('YxNetInspector: 根 Overlay 查找失败: $e'); // 如果 Overlay.of 失败,尝试手动查找 overlay = _findOverlayInContext(context); if (overlay != null) { print('YxNetInspector: 手动查找 Overlay 成功'); } else { print('YxNetInspector: 手动查找 Overlay 也失败'); } } if (overlay == null) { // 如果仍然找不到 Overlay,使用备选方案 _showInspectorDialog(); return; } try { final overlayEntry = OverlayEntry( builder: (context) => Material( color: Colors.black54, child: Stack( children: [ // 背景遮罩 GestureDetector( onTap: _hideInspectorPanel, child: Container( color: Colors.transparent, width: double.infinity, height: double.infinity, ), ), // 检查器面板 Center( child: YxInspectorPanel( theme: widget.theme, controller: widget.controller, onClose: _hideInspectorPanel, ), ), ], ), ), ); overlay.insert(overlayEntry); _currentOverlayEntry = overlayEntry; print('YxNetInspector: 检查器面板显示成功'); } catch (e) { print('YxNetInspector: 插入 OverlayEntry 失败: $e'); // 重置状态 setState(() { _isExpanded = false; }); _animationController.reverse(); } } OverlayState? _findOverlayInContext(BuildContext context) { OverlayState? overlayState; context.visitAncestorElements((element) { if (element.widget is Overlay) { overlayState = Overlay.of(element); return false; // 停止遍历 } return true; // 继续向上查找 }); return overlayState; } void _showInspectorDialog() { // 作为最后的备选方案,创建一个自定义的全屏Overlay // 这里我们不依赖Navigator,而是手动管理显示状态 _createCustomOverlay(); } void _createCustomOverlay() { // 如果所有 Overlay 方法都失败,我们显示一个简单的调试信息 // 并重置状态,避免悬浮球卡在展开状态 print('YxNetInspector: 无法找到 Overlay 上下文,请确保 YxNetInspector 在 MaterialApp 内部使用'); // 显示一个简单的 SnackBar 或 print 提示 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { try { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('网络检查器暂时无法显示,请检查应用结构'), duration: Duration(seconds: 2), ), ); } catch (e) { // 如果 ScaffoldMessenger 也不可用,只打印日志 print('YxNetInspector: 无法显示检查器面板 - 请确保在 MaterialApp 内使用'); } // 重置状态 setState(() { _isExpanded = false; }); _animationController.reverse(); } }); } void _hideInspectorPanel() { setState(() { _isExpanded = false; }); _animationController.reverse(); // 如果有 overlay,移除它 if (_currentOverlayEntry != null) { _currentOverlayEntry!.remove(); _currentOverlayEntry = null; } // 注意:如果使用了 Navigator 的 PageRouteBuilder, // 对话框关闭会由 onClose 回调中的 Navigator.pop() 处理 } void _onPanStart(DragStartDetails details) { if (!widget.config.draggable) return; _animationController.forward(); } void _onPanUpdate(DragUpdateDetails details) { if (!widget.config.draggable) return; setState(() { _position += details.delta; }); } void _onPanEnd(DragEndDetails details) { if (!widget.config.draggable) return; _animationController.reverse(); // 确保悬浮球在屏幕边界内 final screenSize = MediaQuery.of(context).size; final maxX = screenSize.width - widget.config.ballSize; final maxY = screenSize.height - widget.config.ballSize; setState(() { if (_position.dx < 0) _position = Offset(0, _position.dy); if (_position.dx > maxX) _position = Offset(maxX, _position.dy); if (_position.dy < 0) _position = Offset(_position.dx, 0); if (_position.dy > maxY) _position = Offset(_position.dx, maxY); }); } @override Widget build(BuildContext context) { final ballColor = widget.theme.getFloatingBallColor( widget.config.ballColor, ); return Directionality( textDirection: TextDirection.ltr, child: Positioned( left: _position.dx, top: _position.dy, child: GestureDetector( onTap: _onTap, onPanStart: _onPanStart, onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, child: AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Opacity( opacity: _opacityAnimation.value, child: Container( width: widget.config.ballSize, height: widget.config.ballSize, decoration: BoxDecoration( color: ballColor, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Stack( alignment: Alignment.center, children: [ // 网络图标 Icon( Icons.network_check, color: Colors.white, size: widget.config.ballSize * 0.4, ), // 请求数量徽章 if (widget.config.showBadge) Positioned( right: 0, top: 0, child: ListenableBuilder( listenable: widget.controller, builder: (context, child) { final count = widget.controller.requestCount; if (count == 0) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: widget.theme.successColor, borderRadius: BorderRadius.circular(10), ), child: Text( count > 99 ? '99+' : count.toString(), style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ); }, ), ), // 错误数量徽章 if (widget.config.showBadge) Positioned( right: 0, bottom: 0, child: ListenableBuilder( listenable: widget.controller, builder: (context, child) { final count = widget.controller.errorCount; if (count == 0) return const SizedBox.shrink(); return Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: widget.theme.errorColor, borderRadius: BorderRadius.circular(10), ), child: Text( count > 99 ? '99+' : count.toString(), style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ); }, ), ), ], ), ), ), ); }, ), ), ), ); } }