import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../pages/full_screen.dart'; import '../services/message_service.dart'; import 'chat/chat_window_content.dart'; import 'chat_floating_icon.dart'; class ChatPopup extends StatefulWidget { final double? iconSize; final Widget? icon; final Size chatSize; final ChatFloatingIcon? child; const ChatPopup({ super.key, this.child, this.iconSize, this.icon, this.chatSize = const Size(320, 450), }); @override State createState() => _ChatPopupState(); } class _ChatPopupState extends State with SingleTickerProviderStateMixin { OverlayEntry? _overlayEntry; Offset _iconPosition = const Offset(10, 120); final double _iconSizeDefault = 80.0; late AnimationController _partScreenAnimationController; @override void initState() { super.initState(); _partScreenAnimationController = AnimationController( duration: const Duration(milliseconds: 250), vsync: this, ); } @override void dispose() { _removeOverlay(); _partScreenAnimationController.dispose(); super.dispose(); } void _openFullScreen() async { final messageService = context.read(); _removeOverlay(); await Navigator.of(context).push( MaterialPageRoute( builder: (context) => ChangeNotifierProvider.value( value: messageService, // 传递同一个单例实例 child: const FullScreenPage(), ), ), ); } void _insertOverlay() { if (_overlayEntry != null) return; _overlayEntry = OverlayEntry( builder: (context) => _buildOverlayContent(), ); Overlay.of(context).insert(_overlayEntry!); _partScreenAnimationController.forward(); } void _removeOverlay() { if (_overlayEntry == null) return; _partScreenAnimationController.reverse().then((_) { _overlayEntry?.remove(); _overlayEntry = null; }); } Widget _buildOverlayContent() { return ChangeNotifierProvider.value( value: MessageService.instance, child: Material( color: Colors.transparent, child: LayoutBuilder( builder: (context, constraints) { final position = _calculatePosition(constraints); final double minHeight = constraints.maxHeight * 0.16; final double maxHeight = constraints.maxHeight * 0.4; final double chatWidth = constraints.maxWidth * 0.94; return Stack( children: [ _buildPopupBackground(), Positioned( left: position.left, right: position.right, bottom: position.bottom, child: ChatWindowContent( animationController: _partScreenAnimationController, minHeight: minHeight, maxHeight: maxHeight, chatWidth: chatWidth, ), ), ], ); }, ), ), ); } Widget _buildPopupBackground() { return Positioned.fill( child: GestureDetector( onTap: () { final messageService = MessageService.instance; _removeOverlay(); messageService.abortReply(); messageService.initializeEmpty(); }, child: Container( color: Colors.black.withOpacity(0.1), ), ), ); } Widget _buildFloatingIcon() { return ChatFloatingIcon( iconSize: widget.iconSize ?? _iconSizeDefault, icon: widget.icon, onLongPress: () async { final messageService = MessageService.instance; _insertOverlay(); await messageService.startVoiceInput(); }, onLongPressUp: () async { final messageService = MessageService.instance; await messageService.stopAndProcessVoiceInput(); final hasMessage = messageService.messages.isNotEmpty; if (!hasMessage) { _removeOverlay(); } }, onPositionChanged: (position) { setState(() { _iconPosition = position; }); // 重新构建 overlay 以更新弹窗位置 _overlayEntry?.markNeedsBuild(); }, ); } // 计算PartScreen的位置,使其显示在FloatingIcon附近 EdgeInsets _calculatePosition(BoxConstraints constraints) { final screenWidth = constraints.maxWidth; final screenHeight = constraints.maxHeight; final chatWidth = screenWidth * 0.94; final chatHeight = screenHeight * 0.4; // 1. 先将icon的right/bottom转为屏幕坐标(icon左下角) final iconLeft = screenWidth - _iconPosition.dx - (widget.iconSize ?? _iconSizeDefault); final iconBottom = _iconPosition.dy; final iconTop = iconBottom + (widget.iconSize ?? _iconSizeDefault); final iconCenterX = iconLeft + (widget.iconSize ?? _iconSizeDefault) / 2; // 判断FloatingIcon在屏幕的哪一边 final isOnRightSide = iconCenterX > screenWidth / 2; // 计算水平位置 double leftPadding; if (isOnRightSide) { // 图标在右边,聊天框显示在左边 leftPadding = (screenWidth - chatWidth) * 0.1; } else { // 图标在左边,聊天框显示在右边 leftPadding = screenWidth - chatWidth - (screenWidth - chatWidth) * 0.1; } // 优先尝试放在icon上方 double chatBottom = iconTop + 12; // 聊天框底部距离icon顶部12px double chatTop = screenHeight - chatBottom - chatHeight; // 如果上方空间不足,则放在icon下方 if (chatTop < 0) { chatBottom = iconBottom - 12 - chatHeight / 2; // 聊天框顶部距离icon底部12px // 如果下方也不足,则贴底 if (chatBottom < 0) { chatBottom = 20; // 距离底部20px } } return EdgeInsets.only( left: leftPadding, right: screenWidth - leftPadding - chatWidth, bottom: chatBottom, ); } @override Widget build(BuildContext context) { // 只渲染 FloatingIcon,Overlay 弹窗内容在长按时插入 return _buildFloatingIcon(); } }