yx_net_inspector_flutter/lib/src/widgets/floating_ball.dart

365 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
),
),
);
},
),
),
],
),
),
),
);
},
),
),
),
);
}
}