使用Overlay来弹出聊天窗口

This commit is contained in:
2025-09-22 17:51:21 +08:00
parent 049cb7b5c4
commit b9ab384fde
3 changed files with 49 additions and 71 deletions

View File

@@ -37,7 +37,8 @@ class _MainScreenState extends State<MainScreen> {
), ),
), ),
// FloatingIcon(), // FloatingIcon(),
ChatPopup() ChatPopup(
)
], ],
), ),
); );

View File

@@ -27,7 +27,6 @@ class ChatWindowContent extends StatefulWidget {
class _ChatWindowContentState extends State<ChatWindowContent> { class _ChatWindowContentState extends State<ChatWindowContent> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
bool _isInitialized = false;
int _lastMessageCount = 0; int _lastMessageCount = 0;
@override @override
@@ -36,9 +35,6 @@ class _ChatWindowContentState extends State<ChatWindowContent> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final messageService = Provider.of<MessageService>(context, listen: false); final messageService = Provider.of<MessageService>(context, listen: false);
messageService.removeNonListeningMessages(); messageService.removeNonListeningMessages();
setState(() {
_isInitialized = true;
});
}); });
} }
@@ -73,10 +69,6 @@ class _ChatWindowContentState extends State<ChatWindowContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_isInitialized) {
return const SizedBox.shrink();
}
final minHeight = widget.minHeight ?? 0; final minHeight = widget.minHeight ?? 0;
final maxHeight = widget.maxHeight ?? double.infinity; final maxHeight = widget.maxHeight ?? double.infinity;
final chatWidth = widget.chatWidth ?? double.infinity; final chatWidth = widget.chatWidth ?? double.infinity;

View File

@@ -1,6 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../pages/full_screen.dart';
import '../services/message_service.dart'; import '../services/message_service.dart';
import 'chat/chat_window_content.dart'; import 'chat/chat_window_content.dart';
import 'chat_floating_icon.dart'; import 'chat_floating_icon.dart';
@@ -10,8 +11,11 @@ class ChatPopup extends StatefulWidget {
final Widget? icon; final Widget? icon;
final Size chatSize; final Size chatSize;
final ChatFloatingIcon? child;
const ChatPopup({ const ChatPopup({
super.key, super.key,
this.child,
this.iconSize, this.iconSize,
this.icon, this.icon,
this.chatSize = const Size(320, 450), this.chatSize = const Size(320, 450),
@@ -25,7 +29,6 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
OverlayEntry? _overlayEntry; OverlayEntry? _overlayEntry;
Offset _iconPosition = const Offset(10, 120); Offset _iconPosition = const Offset(10, 120);
final double _iconSizeDefault = 80.0; final double _iconSizeDefault = 80.0;
bool _isShowingPopup = false;
late AnimationController _partScreenAnimationController; late AnimationController _partScreenAnimationController;
@override @override
@@ -35,9 +38,6 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
vsync: this, vsync: this,
); );
WidgetsBinding.instance.addPostFrameCallback((_) {
_insertOverlay();
});
} }
@override @override
@@ -47,35 +47,34 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
super.dispose(); super.dispose();
} }
void _openFullScreen() async {
final messageService = context.read<MessageService>();
_removeOverlay();
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeNotifierProvider.value(
value: messageService, // 传递同一个单例实例
child: const FullScreenPage(),
),
),
);
}
void _insertOverlay() { void _insertOverlay() {
if (_overlayEntry != null) return; if (_overlayEntry != null) return;
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (context) => _buildOverlayContent(), builder: (context) => _buildOverlayContent(),
); );
Overlay.of(context).insert(_overlayEntry!); Overlay.of(context).insert(_overlayEntry!);
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
void _showPopup() {
if (_isShowingPopup) return;
setState(() {
_isShowingPopup = true;
});
_partScreenAnimationController.forward(); _partScreenAnimationController.forward();
} }
void _hidePopup() { void _removeOverlay() {
if (!_isShowingPopup) return; if (_overlayEntry == null) return;
_partScreenAnimationController.reverse().then((_) { _partScreenAnimationController.reverse().then((_) {
if (mounted) { _overlayEntry?.remove();
setState(() { _overlayEntry = null;
_isShowingPopup = false;
});
}
}); });
} }
@@ -84,39 +83,29 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
value: MessageService.instance, value: MessageService.instance,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Stack( child: LayoutBuilder(
children: [ builder: (context, constraints) {
// 1. 底层:弹窗内容(只有在显示时才渲染) final position = _calculatePosition(constraints);
if (_isShowingPopup) ...[ final double minHeight = constraints.maxHeight * 0.16;
_buildPopupBackground(), final double maxHeight = constraints.maxHeight * 0.4;
LayoutBuilder( final double chatWidth = constraints.maxWidth * 0.94;
builder: (context, constraints) { return Stack(
final position = _calculatePosition(constraints); children: [
final double minHeight = constraints.maxHeight * 0.16; _buildPopupBackground(),
final double maxHeight = constraints.maxHeight * 0.4; Positioned(
final double chatWidth = constraints.maxWidth * 0.94; left: position.left,
// 只在 isInitialized 时返回弹窗,否则返回空 right: position.right,
final chatWindow = ChatWindowContent( bottom: position.bottom,
child: ChatWindowContent(
animationController: _partScreenAnimationController, animationController: _partScreenAnimationController,
minHeight: minHeight, minHeight: minHeight,
maxHeight: maxHeight, maxHeight: maxHeight,
chatWidth: chatWidth, chatWidth: chatWidth,
); ),
if ((chatWindow as dynamic)._isInitialized == false) { ),
return const SizedBox.shrink(); ],
} );
return Positioned( },
left: position.left,
right: position.right,
bottom: position.bottom,
child: chatWindow,
);
},
),
],
// 2. 顶层FloatingIcon始终在最上层事件不被遮挡
_buildFloatingIcon(),
],
), ),
), ),
); );
@@ -127,7 +116,7 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
final messageService = MessageService.instance; final messageService = MessageService.instance;
_hidePopup(); _removeOverlay();
messageService.abortReply(); messageService.abortReply();
messageService.initializeEmpty(); messageService.initializeEmpty();
}, },
@@ -142,12 +131,9 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
return ChatFloatingIcon( return ChatFloatingIcon(
iconSize: widget.iconSize ?? _iconSizeDefault, iconSize: widget.iconSize ?? _iconSizeDefault,
icon: widget.icon, icon: widget.icon,
onTap: () {
// 默认点击弹出全屏
},
onLongPress: () async { onLongPress: () async {
final messageService = MessageService.instance; final messageService = MessageService.instance;
_showPopup(); _insertOverlay();
await messageService.startVoiceInput(); await messageService.startVoiceInput();
}, },
onLongPressUp: () async { onLongPressUp: () async {
@@ -155,7 +141,7 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
await messageService.stopAndProcessVoiceInput(); await messageService.stopAndProcessVoiceInput();
final hasMessage = messageService.messages.isNotEmpty; final hasMessage = messageService.messages.isNotEmpty;
if (!hasMessage) { if (!hasMessage) {
_hidePopup(); _removeOverlay();
} }
}, },
onPositionChanged: (position) { onPositionChanged: (position) {
@@ -168,7 +154,6 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
); );
} }
// 计算PartScreen的位置使其显示在FloatingIcon附近 // 计算PartScreen的位置使其显示在FloatingIcon附近
EdgeInsets _calculatePosition(BoxConstraints constraints) { EdgeInsets _calculatePosition(BoxConstraints constraints) {
final screenWidth = constraints.maxWidth; final screenWidth = constraints.maxWidth;
@@ -217,7 +202,7 @@ class _ChatPopupState extends State<ChatPopup> with SingleTickerProviderStateMix
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 只返回空容器,实际内容都在 Overlay 中 // 只渲染 FloatingIconOverlay 弹窗内容在长按时插入
return const SizedBox.shrink(); return _buildFloatingIcon();
} }
} }