From 9f8182a15a418715a534b503001709e952407332 Mon Sep 17 00:00:00 2001 From: ChenNyan Date: Fri, 10 Apr 2026 14:14:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A9=AC=E7=94=B2=E5=8C=85=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Podfile.lock | 12 +- lib/pages/home/timeline_page.dart | 4 +- lib/pages/home/user_information_page.dart | 5 +- lib/widget/message/chat_input_bar.dart | 196 +++++++++++++--------- pubspec.yaml | 5 +- 5 files changed, 129 insertions(+), 93 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a37f9ab..627aa42 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -103,10 +103,10 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - - photo_manager (3.8.3): + - photo_manager (3.9.0): - Flutter - FlutterMacOS - - record_ios (1.1.0): + - record_ios (1.2.0): - Flutter - sensors_plus (0.0.1): - Flutter @@ -233,8 +233,8 @@ SPEC CHECKSUMS: AgoraRtcEngine_iOS: 5092a058c7b2842db39d8ca614d451af6f84969a AgoraRtm: d92cdfca825f3e6817c315d7dd6403742494f7ca app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2 - audioplayers_darwin: 4f9ca89d92d3d21cec7ec580e78ca888e5fb68bd - camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 + audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 + camera_avfoundation: 968a9a5323c79a99c166ad9d7866bfd2047b5a9b emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf @@ -248,8 +248,8 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: fe4cbb0808b96f8be4af7ce6ae18dcd9c9b983c6 - record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 + photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb + record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844 sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 diff --git a/lib/pages/home/timeline_page.dart b/lib/pages/home/timeline_page.dart index 3ee4562..59e829a 100644 --- a/lib/pages/home/timeline_page.dart +++ b/lib/pages/home/timeline_page.dart @@ -120,8 +120,8 @@ class _TimelinePageState extends State size: 19, ), ).onTap((){ - widget.goMessage(); - // Get.to(() => TimelineTrend()); + // widget.goMessage(); + Get.to(() => TimelineTrend()); }) ], ); diff --git a/lib/pages/home/user_information_page.dart b/lib/pages/home/user_information_page.dart index f4d312f..6473ea3 100644 --- a/lib/pages/home/user_information_page.dart +++ b/lib/pages/home/user_information_page.dart @@ -817,8 +817,9 @@ class UserInformationPage extends StatelessWidget { ], ), bottomNavigationBar: miId != GlobalData().userData!.id ? SafeArea( - child: SizedBox( - height: 48.h, + child: Container( + height: 58.h, + padding: EdgeInsets.symmetric(vertical: 5.w), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/widget/message/chat_input_bar.dart b/lib/widget/message/chat_input_bar.dart index 6f073cb..f60e688 100644 --- a/lib/widget/message/chat_input_bar.dart +++ b/lib/widget/message/chat_input_bar.dart @@ -1,5 +1,8 @@ import 'package:dating_touchme_app/extension/ex_widget.dart'; +import 'package:extended_text/extended_text.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../generated/assets.dart'; @@ -13,7 +16,7 @@ class ChatInputBar extends StatefulWidget { final ValueChanged>? onImageSelected; final Function(String filePath, int seconds)? onVoiceRecorded; final VoidCallback? onVoiceCall; // 语音通话回调 - final VoidCallback? onVideoCall; // 视频通话回调 + final Future Function()? onVideoCall; // 视频通话回调 final VoidCallback? onGiftTap; // 礼物按钮回调 const ChatInputBar({ @@ -28,6 +31,14 @@ class ChatInputBar extends StatefulWidget { @override State createState() => _ChatInputBarState(); + + // 静态方法:通过 key 关闭面板 + static void closePanels(GlobalKey? key) { + final state = key?.currentState; + if (state != null && state is _ChatInputBarState) { + state.closeAllPanels(); + } + } } class _ChatInputBarState extends State { @@ -106,6 +117,11 @@ class _ChatInputBarState extends State { } } + // 公开方法:从外部关闭所有面板 + void closeAllPanels() { + _closeAllPanels(); + } + @override void initState() { super.initState(); @@ -137,63 +153,6 @@ class _ChatInputBarState extends State { setState(() {}); // 刷新显示 } - /// 构建输入框内容(文本+表情) - List _buildInputContentWidgets() { - final List widgets = []; - final text = _textController.text; - final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); - - int lastMatchEnd = 0; - final matches = emojiRegex.allMatches(text); - - for (final match in matches) { - // 添加表情之前的文本 - if (match.start > lastMatchEnd) { - final textPart = text.substring(lastMatchEnd, match.start); - widgets.add( - Text( - textPart, - style: TextStyle(fontSize: 14.sp, color: Colors.black), - ), - ); - } - - // 添加表情图片 - final emojiId = match.group(1); - if (emojiId != null) { - final emoji = EmojiConfig.getEmojiById(emojiId); - if (emoji != null) { - widgets.add( - Padding( - padding: EdgeInsets.symmetric(horizontal: 2.w), - child: Image.asset( - emoji.path, - width: 24.w, - height: 24.w, - fit: BoxFit.contain, - ), - ), - ); - } - } - - lastMatchEnd = match.end; - } - - // 添加剩余的文本 - if (lastMatchEnd < text.length) { - final textPart = text.substring(lastMatchEnd); - widgets.add( - Text( - textPart, - style: TextStyle(fontSize: 14.sp, color: Colors.black), - ), - ); - } - - return widgets; - } - @override Widget build(BuildContext context) { return Column( @@ -218,7 +177,7 @@ class _ChatInputBarState extends State { child: Stack( children: [ // 真实的输入框 - TextField( + ExtendedTextField( controller: _textController, focusNode: _focusNode, decoration: InputDecoration( @@ -228,32 +187,23 @@ class _ChatInputBarState extends State { fontSize: 14.sp, color: Colors.grey, ), + contentPadding: EdgeInsets.only( + bottom: 10 + ), ), + inputFormatters: [ + // 可以添加其他格式化器,但不要添加过滤Unicode的规则 + FilteringTextInputFormatter.deny(RegExp(r'[\u200B]')), // 仅示例:过滤零宽空格 + ], + specialTextSpanBuilder: MySpecialTextSpanBuilder(), style: TextStyle( fontSize: 14.sp, - color: _textController.text.contains('[emoji:') - ? Colors.transparent - : Colors.black, + color: Colors.black, // 文字始终显示为黑色,specialTextSpanBuilder 会处理表情 ), onChanged: (value) { setState(() {}); // 刷新以更新表情显示 }, ), - // 表情显示层 - if (_textController.text.contains('[emoji:')) - Positioned.fill( - child: IgnorePointer( - child: Align( - alignment: Alignment.centerLeft, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: _buildInputContentWidgets(), - ), - ), - ), - ), - ), ], ), ), @@ -291,7 +241,7 @@ class _ChatInputBarState extends State { width: 24.w, height: 24.w, ).onTap(_toggleVoiceOptions), - // 语音通话按钮(暂时注释) + // 语音通话按钮(暂时隐藏) // Image.asset( // Assets.imagesSendCall, // width: 24.w, @@ -299,13 +249,13 @@ class _ChatInputBarState extends State { // ).onTap(() { // widget.onVoiceCall?.call(); // }), - // 视频通话按钮(暂时注释) + // 视频通话按钮(暂时隐藏) // Image.asset( - // Assets.imagesSendVideoCall, + // Assets.imagesVideoCall, // width: 24.w, // height: 24.w, - // ).onTap(() { - // widget.onVideoCall?.call(); + // ).onTap(() async { + // await widget.onVideoCall?.call(); // }), // 礼物按钮 // Image.asset(Assets.imagesGift, width: 24.w, height: 24.w).onTap(() { @@ -344,3 +294,85 @@ class _ChatInputBarState extends State { ); } } + + +/// 表情特殊文本构建器 - 参考 TIMUIKitTextField 的实现 +class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder { + @override + SpecialText? createSpecialText( + String flag, { + TextStyle? textStyle, + SpecialTextGestureTapCallback? onTap, + required int index, + }) { + if (flag.isEmpty) { + return null; + } + + // index 是 start flag 的结束位置,所以文本开始位置应该是 index - (flag.length - 1) + // 使用 '[' 作为 startFlag,参考 EmojiText.flag = '[' + if (isStart(flag, MyEmojiText.flag)) { + return MyEmojiText( + textStyle, + start: index - (MyEmojiText.flag.length - 1), + ); + } + return null; + } +} + +/// 表情特殊文本类 - 参考 TIMUIKitTextField 的 EmojiText 实现 +class MyEmojiText extends SpecialText { + static const String flag = '['; + final int start; + + MyEmojiText( + TextStyle? textStyle, { + required this.start, + }) : super(MyEmojiText.flag, ']', textStyle); + + @override + InlineSpan finishText() { + // toString() 返回完整的匹配文本,例如 "[emoji:11]" + final String key = toString(); + + // 检查是否是我们的表情格式 [emoji:数字] + final RegExp emojiPattern = RegExp(r'^\[emoji:(\d+)\]$'); + final match = emojiPattern.firstMatch(key); + + if (match != null && match.groupCount > 0) { + final emojiId = match.group(1); + if (emojiId != null && emojiId.isNotEmpty) { + final emoji = EmojiConfig.getEmojiById(emojiId); + if (emoji != null && emoji.path.isNotEmpty) { + // 使用 ImageSpan,完全参考 TIMUIKitTextField 的实现 + double size = 16; + final TextStyle ts = textStyle!; + if (ts.fontSize != null) { + // 参考 TIMUIKitTextField: size = ts.fontSize! * 1.44 + // 但我们使用 flutter_screenutil,所以需要适配 + // 如果 fontSize 已经是适配后的值(如 14.sp),则直接计算 + size = ts.fontSize! * 1.44; + // 将 sp 单位转换为实际像素值,因为 ImageSpan 需要像素值 + // flutter_screenutil 的 .sp 会返回像素值,所以这里直接使用 + } else { + // 如果没有设置 fontSize,使用默认值 14.sp * 1.44 + size = 14.w * 1.44; + } + + return ImageSpan( + AssetImage(emoji.path), + actualText: key, + imageWidth: size, + imageHeight: size, + start: start, + margin: const EdgeInsets.all(0), // 零边距避免空格问题 + ); + } + } + } + + // 如果匹配失败或表情不存在,显示原始文本 + return TextSpan(text: key, style: textStyle); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index c4eb228..9f1b329 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.2.0+1 environment: sdk: ^3.9.0 @@ -79,6 +79,9 @@ dependencies: im_flutter_sdk: 4.15.2 webview_flutter: ^4.13.0 emoji_picker_flutter: ^4.3.0 + extended_text: ^15.0.2 + extended_text_field: ^16.0.2 + emoji_text_field: ^1.0.0 dev_dependencies: flutter_test: