Browse Source

feat(agent): 添加图片选项展开状态和键盘可见性控制

新增图片选择选项的展开/收缩功能,并添加键盘可见性状态管理
当键盘显示时自动收缩图片选项,避免界面重叠
newdev_shunjiawei
lxm 1 year ago
parent
commit
af6e7e7240
  1. 1
      .gitignore
  2. 58
      lib/modules/agent/controllers/agent_controller.dart
  3. 753
      lib/modules/agent/views/agent_view.dart

1
.gitignore

@ -222,3 +222,4 @@ fastlane/Preview.html
fastlane/screenshots/**/*.png fastlane/screenshots/**/*.png
fastlane/test_output fastlane/test_output
fastlane/readme.md fastlane/readme.md
.trae/rules/project_rules.md

58
lib/modules/agent/controllers/agent_controller.dart

@ -75,6 +75,12 @@ class AgentController extends GetxController {
final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式 final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式
final shouldShowExpandButton = false.obs; // 是否显示展开按钮 final shouldShowExpandButton = false.obs; // 是否显示展开按钮
// 图片选择选项展开状态
final isImageOptionsExpanded = false.obs; // 是否展开图片选择选项
// 键盘状态
final isKeyboardVisible = false.obs; // 键盘是否可见
// 流事件订阅 // 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription; StreamSubscription<AgentServiceEvent>? _eventSubscription;
@ -898,6 +904,58 @@ class AgentController extends GetxController {
isImageInputActive.value = false; isImageInputActive.value = false;
} }
// 切换图片选项展开状态
void toggleImageOptions() {
if (isImageOptionsExpanded.value) {
// 如果当前已展开,则收缩
collapseImageOptions();
} else {
// 如果当前未展开,则展开并关闭键盘
if (isKeyboardVisible.value) {
hideKeyboard();
// 延迟展开选项,等待键盘完全关闭
Future.delayed(const Duration(milliseconds: 200), () {
// 再次检查键盘状态,确保键盘已关闭
if (!isKeyboardVisible.value) {
isImageOptionsExpanded.value = true;
} else {
// 如果键盘还没关闭,再等一会儿
Future.delayed(const Duration(milliseconds: 100), () {
if (!isKeyboardVisible.value) {
isImageOptionsExpanded.value = true;
}
});
}
});
} else {
// 键盘未显示,直接展开
isImageOptionsExpanded.value = true;
}
}
}
// 收缩图片选项
void collapseImageOptions() {
isImageOptionsExpanded.value = false;
}
// 更新键盘可见状态
void updateKeyboardVisibility(bool isVisible) {
final wasVisible = isKeyboardVisible.value;
isKeyboardVisible.value = isVisible;
// 只有当键盘从隐藏变为显示时,才收缩图片选项
// 避免键盘关闭时误触发收缩逻辑
if (!wasVisible && isVisible && isImageOptionsExpanded.value) {
collapseImageOptions();
}
}
// 隐藏键盘
void hideKeyboard() {
FocusManager.instance.primaryFocus?.unfocus();
}
// 使用预设提示词处理图片 // 使用预设提示词处理图片
void useImagePrompt(String prompt) { void useImagePrompt(String prompt) {
// 确保selectedImagePath.value不为null // 确保selectedImagePath.value不为null

753
lib/modules/agent/views/agent_view.dart

@ -11,12 +11,45 @@ import '../../../core/theme/app_colors.dart';
import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../data/models/message_model.dart'; import '../../../data/models/message_model.dart';
class AgentView extends StatelessWidget { class AgentView extends StatefulWidget {
const AgentView({Key? key}) : super(key: key); const AgentView({Key? key}) : super(key: key);
@override
State<AgentView> createState() => _AgentViewState();
}
class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
late AgentController controller;
double _keyboardHeight = 0;
@override
void initState() {
super.initState();
controller = Get.find<AgentController>();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
final bottomInset = View.of(context).viewInsets.bottom;
final newKeyboardHeight = bottomInset / View.of(context).devicePixelRatio;
if (_keyboardHeight != newKeyboardHeight) {
_keyboardHeight = newKeyboardHeight;
// 更新键盘可见状态
controller.updateKeyboardVisibility(_keyboardHeight > 0);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = Get.find<AgentController>();
final theme = Theme.of(context); final theme = Theme.of(context);
final primaryColor = theme.primaryColor; final primaryColor = theme.primaryColor;
final backgroundColor = theme.scaffoldBackgroundColor; final backgroundColor = theme.scaffoldBackgroundColor;
@ -288,333 +321,391 @@ class AgentView extends StatelessWidget {
} }
}), }),
// 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 // 输入控件区域 - 包含输入行和展开的选项
IntrinsicHeight( Column(
child: Row( children: [
crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐
children: [ IntrinsicHeight(
// 键盘/语音切换按钮 child: Row(
Obx(() { crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐
final isTextMode = controller.isTextInputMode.value; children: [
return GestureDetector( // 键盘/语音切换按钮
onTap: () { Obx(() {
controller.toggleInputMode(); final isTextMode = controller.isTextInputMode.value;
},
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
),
child: Icon(
isTextMode ? Icons.mic_none : Icons.keyboard,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
),
),
);
}),
const SizedBox(width: 8),
// 文本输入框或语音波形图
Expanded(
child: Obx(() {
final isTextMode = controller.isTextInputMode.value;
if (isTextMode) {
// 文本输入模式 - 限制最大高度并只向上扩展
return Container(
constraints: const BoxConstraints(
minHeight: 44, // 最小高度
maxHeight: 120, // 最大高度限制,防止挤压按钮
),
padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: isDarkMode
? Colors.grey[
800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800]
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
child: Row(
crossAxisAlignment:
CrossAxisAlignment.end, // 对齐到底部,实现向上扩展
children: [
Expanded(
child: Obx(() {
// 图片模式下使用更高的输入框
final isImageActive =
controller.isImageInputActive.value;
return TextField(
controller: controller.textController,
maxLines:
isImageActive ? 3 : 5, // 设置合理的最大行数
minLines: isImageActive ? 2 : 1,
keyboardType:
TextInputType.multiline, // 启用多行键盘
textInputAction:
TextInputAction.newline, // 支持回车换行
style: TextStyle(
fontSize: 15,
color: isDarkMode
? Colors.white
: Colors.grey[800],
),
decoration: InputDecoration(
hintText: isImageActive
? '添加对图片的问题或描述...'
: '输入消息...',
hintStyle: TextStyle(
color: Colors.grey[500],
fontSize: 15,
),
filled: false,
border: InputBorder.none,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey[200]!,
width: 1.0,
),
),
contentPadding: EdgeInsets.symmetric(
vertical: isImageActive
? 12
: 8), // 调整垂直内边距
),
onSubmitted: (_) =>
controller.sendMessage(),
);
}),
),
],
),
);
} else {
// 语音输入模式 - 始终显示波形图
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (controller.isListening.value) { controller.toggleInputMode();
controller.stopVoiceInput();
} else {
controller.startVoiceInput();
}
}, },
child: Container( child: Container(
height: 50, width: 44,
padding: height: 44,
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.grey[800] ? Colors.grey[800]
: Colors.grey[200], : Colors.grey[200],
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
border: Border.all(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
), ),
child: Row( child: Icon(
children: [ isTextMode ? Icons.mic_none : Icons.keyboard,
// 语音波形图 - 始终显示 color: isDarkMode
Expanded( ? Colors.grey[300]
child: Row( : Colors.grey[700],
mainAxisAlignment: size: 22,
MainAxisAlignment.start,
children: [
...List.generate(10, (index) {
return _buildSoundBar(index);
}),
const Spacer(),
// "正在聆听"文字
Obx(() {
final isListening =
controller.isListening.value;
return Row(
children: [
Icon(
Icons.mic,
size: 14,
color: isListening
? primaryColor
: Colors.grey[500],
),
const SizedBox(width: 4),
Text(
'正在聆听',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: isListening
? primaryColor
: Colors.grey[500],
),
),
],
);
}),
],
),
),
],
), ),
), ),
); );
} }),
}),
),
const SizedBox(width: 8), const SizedBox(width: 8),
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 // 文本输入框或语音波形图
Obx(() { Expanded(
final hasText = controller.hasTextInput.value; child: Obx(() {
final hasImage = controller.isImageInputActive.value; final isTextMode =
final shouldShowSend = hasText || hasImage; controller.isTextInputMode.value;
final shouldShowExpand =
controller.shouldShowExpandButton.value;
if (shouldShowSend) { if (isTextMode) {
// 显示发送按钮(可能还有展开按钮) // 文本输入模式 - 限制最大高度并只向上扩展
return Column( return Container(
mainAxisSize: MainAxisSize.min, constraints: const BoxConstraints(
children: [ minHeight: 44, // 最小高度
// 展开按钮(在发送按钮上方) maxHeight: 120, // 最大高度限制,防止挤压按钮
if (shouldShowExpand && hasText)
Padding(
padding: const EdgeInsets.only(bottom: 40),
child: GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller.closeFullScreenEdit();
});
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
borderRadius: BorderRadius.circular(16),
),
child: Icon(
Icons.expand_less,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20,
),
),
), ),
), padding: const EdgeInsets.symmetric(
// 发送按钮 horizontal: 16),
GestureDetector(
onTap: () => controller.sendMessage(),
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: primaryColor, color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
border: Border.all(
color: isDarkMode
? Colors.grey[
800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800]
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
), ),
child: const Icon(
Icons.send,
color: Colors.white,
size: 22,
),
),
),
],
);
} else {
// 显示图片选择按钮
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
),
child: PopupMenuButton<String>(
icon: Icon(
Icons.add_photo_alternate_outlined,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
onSelected: (value) async {
if (value == 'camera') {
await controller.takePhoto();
} else if (value == 'gallery') {
await controller.pickImage();
}
},
itemBuilder: (context) => [
PopupMenuItem<String>(
value: 'camera',
child: Row( child: Row(
crossAxisAlignment:
CrossAxisAlignment.end, // 对齐到底部,实现向上扩展
children: [ children: [
Icon(Icons.camera_alt, Expanded(
color: primaryColor, size: 20), child: Obx(() {
const SizedBox(width: 8), // 图片模式下使用更高的输入框
const Text('拍照', final isImageActive = controller
style: TextStyle(fontSize: 14)), .isImageInputActive.value;
return TextField(
controller:
controller.textController,
maxLines: isImageActive
? 3
: 5, // 设置合理的最大行数
minLines: isImageActive ? 2 : 1,
keyboardType: TextInputType
.multiline, // 启用多行键盘
textInputAction: TextInputAction
.newline, // 支持回车换行
onTap: () {
// 当点击输入框时,如果图片选项已展开,则收缩
if (controller
.isImageOptionsExpanded
.value) {
controller
.collapseImageOptions();
}
},
style: TextStyle(
fontSize: 15,
color: isDarkMode
? Colors.white
: Colors.grey[800],
),
decoration: InputDecoration(
hintText: isImageActive
? '添加对图片的问题或描述...'
: '输入消息...',
hintStyle: TextStyle(
color: Colors.grey[500],
fontSize: 15,
),
filled: false,
border: InputBorder.none,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey[200]!,
width: 1.0,
),
),
contentPadding:
EdgeInsets.symmetric(
vertical: isImageActive
? 12
: 8), // 调整垂直内边距
),
onSubmitted: (_) =>
controller.sendMessage(),
);
}),
),
], ],
), ),
), );
PopupMenuItem<String>( } else {
value: 'gallery', // 语音输入模式 - 始终显示波形图
child: Row( return GestureDetector(
children: [ onTap: () {
Icon(Icons.photo_library, if (controller.isListening.value) {
color: primaryColor, size: 20), controller.stopVoiceInput();
const SizedBox(width: 8), } else {
const Text('从相册选择', controller.startVoiceInput();
style: TextStyle(fontSize: 14)), }
], },
child: Container(
height: 50,
padding: const EdgeInsets.symmetric(
horizontal: 16),
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: isDarkMode
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
child: Row(
children: [
// 语音波形图 - 始终显示
Expanded(
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: [
...List.generate(10, (index) {
return _buildSoundBar(index);
}),
const Spacer(),
// "正在聆听"文字
Obx(() {
final isListening = controller
.isListening.value;
return Row(
children: [
Icon(
Icons.mic,
size: 14,
color: isListening
? primaryColor
: Colors.grey[500],
),
const SizedBox(width: 4),
Text(
'正在聆听',
style: TextStyle(
fontSize: 13,
fontWeight:
FontWeight.w500,
color: isListening
? primaryColor
: Colors.grey[500],
),
),
],
);
}),
],
),
),
],
),
), ),
);
}
}),
),
const SizedBox(width: 8),
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Obx(() {
final hasText = controller.hasTextInput.value;
final hasImage =
controller.isImageInputActive.value;
final shouldShowSend = hasText || hasImage;
final shouldShowExpand =
controller.shouldShowExpandButton.value;
if (shouldShowSend) {
// 显示发送按钮(可能还有展开按钮)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText)
Padding(
padding:
const EdgeInsets.only(bottom: 40),
child: GestureDetector(
onTap: () {
// 打开底部弹出编辑模式
controller.openFullScreenEdit();
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) =>
const FullScreenEditor(),
).then((_) {
// 弹出页关闭时的回调
controller.closeFullScreenEdit();
});
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[700]
: Colors.grey[300],
borderRadius:
BorderRadius.circular(16),
),
child: Icon(
Icons.expand_less,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 20,
),
),
),
),
// 发送按钮
GestureDetector(
onTap: () => controller.sendMessage(),
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: primaryColor,
borderRadius: BorderRadius.circular(22),
),
child: const Icon(
Icons.send,
color: Colors.white,
size: 22,
),
),
),
],
);
} else {
// 显示'+'选择按钮
return Obx(() {
final isExpanded =
controller.isImageOptionsExpanded.value;
return GestureDetector(
onTap: () {
controller.toggleImageOptions();
},
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
),
child: Icon(
isExpanded ? Icons.close : Icons.add,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
),
),
);
});
}
}),
],
),
),
// 展开的'+'选择选项
Obx(() {
final isExpanded =
controller.isImageOptionsExpanded.value;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸
child: isExpanded
? Container(
margin: EdgeInsets.only(top: 8.h),
padding: EdgeInsets.symmetric(horizontal:16.w), // 添加左右边距
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 改为均匀分布
children: [
// 相机按钮
_buildPlusOptionButton(
context: context,
icon: Icons.camera_alt,
label: '拍照',
onTap: () async {
//controller.collapseImageOptions();
await controller.takePhoto();
},
isDarkMode: isDarkMode,
),
// 相册按钮
_buildPlusOptionButton(
context: context,
icon: Icons.photo_library,
label: '相册',
onTap: () async {
//controller.collapseImageOptions();
await controller.pickImage();
},
isDarkMode: isDarkMode,
),
// 文件按钮(预留位置)
// _buildPlusOptionButton(
// context: context,
// icon: Icons.folder,
// label: '文件',
// onTap: () {
// //controller.collapseImageOptions();
// // TODO: 实现文件选择功能
// },
// isDarkMode: isDarkMode,
// ),
// 通话按钮(预留位置)
_buildPlusOptionButton(
context: context,
icon: Icons.phone,
label: '打电话',
onTap: () {
//controller.collapseImageOptions();
// TODO: 实现通话功能
},
isDarkMode: isDarkMode,
),
],
), ),
], )
), : const SizedBox.shrink(),
); );
} }),
}), ],
],
),
), ),
], ],
), ),
@ -792,6 +883,62 @@ class AgentView extends StatelessWidget {
)), )),
); );
} }
// 构建"+"号展开选项按钮
Widget _buildPlusOptionButton({
required BuildContext context,
required IconData icon,
required String label,
required VoidCallback onTap,
required bool isDarkMode,
}) {
final primaryColor = Theme.of(context).primaryColor;
return GestureDetector(
onTap: onTap,
child: SizedBox(
// 调整为一行四个按钮的宽度,考虑屏幕宽度和间距
width: (1.sw - 100.w) / 4, // 屏幕宽度减去左右边距,除以4个按钮
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 正方形图标容器 - 增大尺寸
Container(
width: 50.w, // 调整为44.w以适应布局
height: 50.w, // 调整为44.w以适应布局
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r), // 调整为10.r
border: Border.all(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
width: 1,
),
),
child: Icon(
icon,
color: primaryColor,
size: 20.sp, // 调整为18.sp
),
),
SizedBox(height: 4.h), // 调整为3.h
// 文字标签
Text(
label,
style: TextStyle(
fontSize: 10.sp, // 调整为9.sp
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
} }
class _ReportForm extends StatelessWidget { class _ReportForm extends StatelessWidget {

Loading…
Cancel
Save