365 lines
12 KiB
Dart
365 lines
12 KiB
Dart
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<YxFloatingBall> createState() => _YxFloatingBallState();
|
||
}
|
||
|
||
class _YxFloatingBallState extends State<YxFloatingBall>
|
||
with TickerProviderStateMixin {
|
||
late AnimationController _animationController;
|
||
late Animation<double> _scaleAnimation;
|
||
late Animation<double> _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<double>(begin: 1.0, end: 1.2).animate(
|
||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||
);
|
||
|
||
_opacityAnimation = Tween<double>(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);
|
||
} catch (e) {
|
||
debugPrint('YxNetInspector: 根 Overlay 查找失败: $e');
|
||
// 如果 Overlay.of 失败,尝试手动查找
|
||
overlay = _findOverlayInContext(context);
|
||
}
|
||
|
||
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;
|
||
} catch (e) {
|
||
debugPrint('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 方法都失败,我们显示一个简单的调试信息
|
||
// 并重置状态,避免悬浮球卡在展开状态
|
||
|
||
// 显示一个简单的 SnackBar 或 print 提示
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
try {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(
|
||
content: Text('网络检查器暂时无法显示,请检查应用结构'),
|
||
duration: Duration(seconds: 2),
|
||
),
|
||
);
|
||
} catch (e) {
|
||
// 如果 ScaffoldMessenger 也不可用,只打印日志
|
||
debugPrint('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,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|