使用Overlay来弹出聊天窗口
This commit is contained in:
@@ -37,7 +37,8 @@ class _MainScreenState extends State<MainScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
// FloatingIcon(),
|
// FloatingIcon(),
|
||||||
ChatPopup()
|
ChatPopup(
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 中
|
// 只渲染 FloatingIcon,Overlay 弹窗内容在长按时插入
|
||||||
return const SizedBox.shrink();
|
return _buildFloatingIcon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user