微信的聊天输入框之前实现了一个版本(flutter 微信聊天输入框_flutter 聊天输入框-CSDN博客),
但是之前实现的不太优雅。这两天重写了一遍。效果如下:
1.页面拆分
这里我们把 聊天的页面进行 拆分:Scaffold ,
bottomNavigationBar:的高度增加 body会自动的缩小。这样软键盘 已经 表情 和有 文件的发送都可以放里面! 不用关系 body的尺寸 他会自动适配
2.页面实现逻辑
首先是要监听软键盘的弹起和收回:
flutter_keyboard_visibility | Flutter Package
然后我们缓存一个键盘高度的数值:
double _keyboardMaxHeight = AppCache().keyBordHeight;
一个设置和获取 键盘高度的值:
double get keyBordHeight { if (_keyBordHeight <= 0) { _keyBordHeight = _get<double>("keyBordHeight") ?? 0; } return _keyBordHeight; } set keyBordHeight(double height) { if (height > 0 && height > keyBordHeight) { _sharedPreferences.setDouble("keyBordHeight", height); } }
3.代码展示
---- page_chat_person.dart #单聊页面
---- chat_bottom.dart #聊天框输入
---- chat_input_box.dart #聊天时候输入框的封装
---- chat_utils.dart #配置和表情
3.1 page_chat_person
import 'package:flutter/material.dart';
import 'package:imspawn/model/relationship_model.dart';
import 'package:imspawn/pages/common/page_visiting_card.dart';
import 'package:imspawn/wrap/extension/extension.dart';
import '../../const/app_colors.dart';
import '../../const/app_icon.dart';
import '../../const/app_textStyle.dart';
import '../../wrap/navigator/app_navigator.dart';
import '../../wrap/navigator/page_hook.dart';
import '../../wrap/navigator/route_aware_state.dart';
import '../../wrap/widget/app_widget.dart';
import 'chat_bottom.dart';
import 'chat_utils.dart';
class PageChatPerson extends StatefulWidget {
final ContactInfo contacts;
const PageChatPerson({super.key, required this.contacts});
@override
State<PageChatPerson> createState() => _PageChatPersonState();
}
class _PageChatPersonState extends RouteAwareState<PageChatPerson>
implements PageHook {
final ScrollController _scrollController = ScrollController();
final ContentController _contentController = ContentController();
@override
void initState() {
// TODO: implement initState
super.initState();
_scrollController.addListener(() {
if (_scrollController.offset >
_scrollController.position.maxScrollExtent + 50) {
//_providerChatPersonMessages.chatMessageMore();
print("刷新。。。。。。。。。。。。");
}
});
}
@override
Widget build(BuildContext context) {
debugPrint("PageChatPerson build");
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: AppColor.colorEDEDED,
shadowColor: AppColor.colordddddd,
elevation: 1.cale,
leading: AppWidget.iconBack(() {
AppNavigator().navigateBack();
}),
centerTitle: true,
title: Text(
widget.contacts.nickName,
style: AppTextStyle.textStyle_34_000000,
),
actions: [
Padding(
padding: EdgeInsets.only(right: 24.cale),
child: AppWidget.inkWellEffectNone(
onTap: () {
AppNavigator().navigateTo(
PageVisitingCard(userInfo: widget.contacts),
);
},
child: Icon(
AppIcon.dot3,
size: 46.cale,
color: Colors.black,
),
),
)
],
),
body: AppWidget.inkWellEffectNone(
onTap: () {
_contentController.onContentClick();
},
child: Container(
color: Colors.white,
),
),
bottomNavigationBar: ChatBottom(
userInfoTarget: widget.contacts,
contentController: _contentController,
),
);
}
@override
onHide() {
print('PageChatPerson-------PageChatPerson onHide:${DateTime.now()}');
}
@override
onShow() {
print('PageChatPerson-------PageChatPerson onShow:${DateTime.now()}');
}
@override
void dispose() {
_scrollController.dispose();
// _textEditingController.dispose();
// _focusNode.dispose();
// TODO: implement dispose
super.dispose();
// print('PageChatPerson-------PageChatPerson dispose:${DateTime.now()}');
}
}
3.2 chat_bottom
import 'dart:async';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:imspawn/model/relationship_model.dart';
import 'package:imspawn/pages/tabbar0chat/page_chat_person.dart';
import 'package:imspawn/wrap/extension/extension.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
import '../../app/app_cache.dart';
import '../../app/app_overlay.dart';
import '../../const/app_colors.dart';
import '../../const/app_icon.dart';
import '../../const/app_textStyle.dart';
import '../../wrap/widget/app_widget.dart';
import 'chat_input_box.dart';
import 'chat_utils.dart';
class ChatBottom extends StatefulWidget {
final ContactInfo userInfoTarget;
final ContentController contentController;
const ChatBottom({
super.key,
required this.contentController,
required this.userInfoTarget,
});
@override
State<ChatBottom> createState() => _ChatBottomState();
}
class _ChatBottomState extends State<ChatBottom> {
final KeyboardVisibilityController _keyboardVisibilityController =
KeyboardVisibilityController();
late StreamSubscription _keyboardSubscription;
final TextEditingController _textEditingController = TextEditingController();
final FocusNode _focusNode = FocusNode();
ChatInputEnum _chatInputEnum = ChatInputEnum.inputText;
ChatMediaEnum _chatMediaEnum = ChatMediaEnum.none;
bool _showContent = false;
late OverlayEntry? _overlayEntry = null;
late OverlayState? _overlayState = null;
double _keyboardMaxHeight = AppCache().keyBordHeight;
double _keyboardHeight = 0.0;
@override
void initState() {
// TODO: implement initState
super.initState();
widget.contentController.onContentClick = () {
if (_focusNode.hasFocus) {
FocusScope.of(context).unfocus();
}
setState(() {
_showContent = false;
});
print('contentController.onContentClick');
};
_overlayState = Overlay.of(context);
// _textEditingController.addListener(() {
// setState(() {});
// });
_keyboardSubscription =
_keyboardVisibilityController.onChange.listen((bool visible) {
if (mounted) {
setState(() {
if (visible) {
_showContent = true;
} else {
if (_chatInputEnum != ChatInputEnum.inputText) {
_showContent = false;
}
}
});
}
});
}
@override
Widget build(BuildContext context) {
_keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
if (_keyboardMaxHeight < _keyboardHeight) {
_keyboardMaxHeight = _keyboardHeight;
AppCache().keyBordHeight = _keyboardMaxHeight;
}
return Container(
padding: EdgeInsets.symmetric(vertical: 20.cale),
decoration: BoxDecoration(
color: AppColor.colorF7F7F7,
border: Border(
top: BorderSide(width: 1.cale, color: AppColor.colordddddd),
),
),
// height: 110.cale,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 20),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
},
child: _inputWidget(),
),
Expanded(
child: _chatInputEnum == ChatInputEnum.inputText
? Padding(
padding: EdgeInsets.symmetric(
horizontal: 20.cale,
),
child: ChatInputBox(
style: AppTextStyle.textStyle_30_000000,
onEditingComplete: () {
print("onEditingComplete");
},
onSubmitted: (str) {
print("onSubmitted:$str");
},
controller: _textEditingController,
focusNode: _focusNode,
),
)
: GestureDetector(
behavior: HitTestBehavior.translucent,
onVerticalDragStart: (DragStartDetails details) {
print(
'-------------------------->onVerticalDragStart');
// widget.providerChatContent.updateRecordAudioState(
// RecordAudioState(
// recording: true,
// recordingState: 1,
// noticeMessage: '松开 发送'),
// );
_showAudioRecord();
},
onVerticalDragUpdate: (DragUpdateDetails details) {
// print(
// '-------------------------->onVerticalDragUpdate:${details.delta}');
// print(
// '-------------------------->onVerticalDragUpdate:${details.localPosition.dy}');
// if (details.localPosition.dy > -150) {
// widget.providerChatContent.updateRecordAudioState(
// RecordAudioState(
// recording: true,
// recordingState: 1,
// noticeMessage: '松开 发送'),
// );
// } else {
// widget.providerChatContent.updateRecordAudioState(
// RecordAudioState(
// recording: true,
// recordingState: -1,
// noticeMessage: '松开 取消'),
// );
// }
_updateAudioRecord();
},
onVerticalDragEnd: (DragEndDetails details) {
print('-------------------------->onVerticalDragEnd');
_hideAudioRecord();
// widget.providerChatContent.updateRecordAudioState(
// RecordAudioState(
// recording: false,
// recordingState: 1,
// noticeMessage: ''),
// );
},
onVerticalDragCancel: () {
_hideAudioRecord();
// widget.providerChatContent.updateRecordAudioState(
// RecordAudioState(
// recording: false,
// recordingState: 1,
// noticeMessage: ''),
// );
},
child: Container(
margin: EdgeInsets.symmetric(horizontal: 20.cale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(7.cale),
),
height: 80.cale,
child: Center(
child: Text(
'按住 说话',
style: AppTextStyle.textStyle_30_000000,
),
),
),
),
),
AppWidget.inkWellEffectNone(
onTap: () {
debugPrint('添加表情符号');
setState(() {
_showContent = true;
_chatInputEnum = ChatInputEnum.inputText;
_chatMediaEnum = ChatMediaEnum.mediaFace;
//收起小桌板
FocusScope.of(context).unfocus();
});
},
child: Padding(
padding: EdgeInsets.only(bottom: 15.cale),
child: Icon(
AppIcon.faceHappy,
size: 50.cale,
color: Colors.black,
),
),
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 50),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
alignment: Alignment.centerRight,
child: FadeTransition(
opacity: animation,
child: child,
),
);
},
child: _chatInputEnum == ChatInputEnum.inputText &&
_textEditingController.value.text.isNotEmpty
? AppWidget.inkWellEffectNone(
key: const ValueKey('发送'),
// onTap: () {
// print('发送');
// _controller.clear();
// },
onTap: _messageCharacters,
child: Container(
margin: EdgeInsets.only(
left: 20.cale, right: 24.cale, bottom: 10.cale),
padding: EdgeInsets.symmetric(
horizontal: 24.cale,
vertical: 10.cale,
),
decoration: BoxDecoration(
color: AppColor.color05C160,
borderRadius: BorderRadius.circular(12.cale),
),
child: Center(
child: Text(
'发送',
style: AppTextStyle.textStyle_30_FFFFFF,
)),
),
)
: AppWidget.inkWellEffectNone(
key: const ValueKey('AppIcon.add'),
onTap: () {
debugPrint('添加附件 图片视频');
setState(() {
_showContent = true;
_chatInputEnum = ChatInputEnum.inputText;
_chatMediaEnum = ChatMediaEnum.mediaFiles;
//收起小桌板
FocusScope.of(context).unfocus();
});
},
child: Padding(
padding: EdgeInsets.only(
left: 10.cale, right: 20.cale, bottom: 10.cale),
child: Icon(
AppIcon.add,
size: 58.cale,
color: Colors.black,
),
),
),
),
],
),
if (_showContent)
Container(
width: double.infinity,
height: _keyboardMaxHeight,
padding: EdgeInsets.only(
top: 20.cale,
),
// height: 240.cale,
// height:
// widget.providerChatContent.keyboardHeight > 0 ? 0 : 240.cale,
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.cale, color: AppColor.colordddddd),
),
),
child: _chatMediaEnum == ChatMediaEnum.mediaFace
? _contentFace()
: _contentMedia()))
],
),
);
}
Widget _contentFace() {
return Wrap(
runAlignment: WrapAlignment.center,
alignment: WrapAlignment.center,
//crossAxisAlignment: WrapCrossAlignment.center,
spacing: 30.cale,
runSpacing: 45.cale,
children: emojiList
.asMap()
.map(
(key, value) => MapEntry(
key,
AppWidget.inkWellEffectNone(
onTap: () {
// debugPrint(value);
_textEditingController.text =
'${_textEditingController.value.text}$value';
},
child: SizedBox(
width: 60.cale,
height: 60.cale,
child: Center(
child: Text(
value,
style: TextStyle(fontSize: 50.cale),
),
),
),
),
),
)
.values
.toList(),
);
}
Widget _contentMedia() {
return Wrap(
runAlignment: WrapAlignment.center,
alignment: WrapAlignment.center,
//crossAxisAlignment: WrapCrossAlignment.center,
spacing: 80.cale,
runSpacing: 80.cale,
children: mediaOption
.asMap()
.map(
(key, value) => MapEntry(
key,
AppWidget.inkWellEffectNone(
onTap: () {
if (value['type'] == 1) {
_mediaSelect();
} else if (value['type'] == 2) {
_mediaTake();
}
},
child: SizedBox(
width: 100.cale,
height: 150.cale,
child: Column(
children: [
Container(
width: 100.cale,
height: 100.cale,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25.cale),
),
child: Image.asset(
value['icon'],
width: 50.cale,
height: 50.cale,
),
),
Padding(
padding: EdgeInsets.only(top: 16.cale),
child: Text(
value['title'],
style: AppTextStyle.textStyle_20_656565,
),
)
],
),
),
),
),
)
.values
.toList(),
);
}
@override
void dispose() {
// TODO: implement dispose
_focusNode.dispose();
_textEditingController.dispose();
_keyboardSubscription.cancel();
super.dispose();
}
void _showAudioRecord() {
print("开始录音");
// Audio.startRecordAudio(AppUtils.generateUUid);
// _hideAudioRecord();
// _overlayEntry = OverlayEntry(builder: (BuildContext context) {
// return ChatAudioMask(recordAudioState: _recordState);
// });
_overlayState!.insert(_overlayEntry!);
}
void _updateAudioRecord() {
final overlayEntry = _overlayEntry;
if (overlayEntry != null) {
overlayEntry.markNeedsBuild();
}
}
void _hideAudioRecord() {
if (_overlayEntry != null) {
_overlayEntry?.remove();
_overlayEntry = null;
// if (_recordState.recording) {
// if (_recordState.recordingState == 1) {
// debugPrint("用户录音成功");
// _onRecordAudioDone();
// } else {
// debugPrint("用户取消录音");
// Audio.stopRecordAudio();
// }
// }
}
}
void _onRecordAudioDone() async {
// AudioInfo? audioInfo = await Audio.stopRecordAudio();
// if (DateTime.now().millisecondsSinceEpoch -
// _dateTime.millisecondsSinceEpoch <=
// 1500) {
// AppOverlay.showToast("说话时间太短");
// } else {
// if (audioInfo == null) {
// debugPrint("没有找到 录音音频文件");
// } else {
// _messageVoice(audioInfo);
// }
// }
}
//相册选取照片
void _mediaSelect() async {
final List<AssetEntity>? result = await AssetPicker.pickAssets(
context,
pickerConfig: const AssetPickerConfig(),
);
if (result != null) {
for (var asset in result) {
if (asset.type == AssetType.image) {
_messagePicture(asset);
} else if (asset.type == AssetType.video) {
_messageVideo(asset);
}
}
}
}
//拍照
void _mediaTake() async {
AndroidDeviceInfo info = await AppCache().androidInfo;
int version = int.parse(info.version.release);
if (version >= 11) {
PermissionStatus state = await Permission.manageExternalStorage.request();
if (state != PermissionStatus.granted) {
AppOverlay().showModal(
content: "请先同意权限",
onConfirm: () {
openAppSettings();
},
);
}
}
// Map<Permission, PermissionStatus> statuses = await [
// Permission.storage,
// Permission.manageExternalStorage,
// ].request();
//
// if (statuses.values
// .toList()
// .where((element) => !element.isGranted)
// .isNotEmpty) {
// AppOverlay().showModal(
// content: "请先同意权限",
// onConfirm: () {
// openAppSettings();
// },
// );
// return;
// }
print('_mediaTake');
final AssetEntity? asset = await CameraPicker.pickFromCamera(
context,
pickerConfig: const CameraPickerConfig(enableRecording: true),
);
if (asset != null) {
if (asset.type == AssetType.image) {
_messagePicture(asset);
} else if (asset.type == AssetType.video) {
_messageVideo(asset);
}
}
}
/// 发送消息
void _messageCharacters() {
// if (_controller.value.text.isNotEmpty) {
// AppMessage().sendMessageCharacters(
// chatType: IOEventEmit.chatPerson,
// senderId: AppCache().userInfo!.userId,
// target: widget.userInfoTarget,
// message: _controller.value.text,
// );
// _controller.clear();
// }
}
void _messagePicture(AssetEntity entity) async {
// AppMessage().sendMessagePicture(
// chatType: IOEventEmit.chatPerson,
// senderId: AppCache().userInfo!.userId,
// target: widget.userInfoTarget,
// entity: entity,
// );
}
void _messageVideo(AssetEntity entity) async {
// AppMessage().sendMessageVideo(
// chatType: IOEventEmit.chatPerson,
// senderId: AppCache().userInfo!.userId,
// target: widget.userInfoTarget,
// message: entity,
// );
}
Widget _inputWidget() {
if (_chatInputEnum == ChatInputEnum.inputAudio) {
return AppWidget.inkWellEffectNone(
key: const ValueKey("AppIcon.keyboard"),
onTap: () {
print("点击文字按钮 转向语音输入");
setState(() {
_chatInputEnum = ChatInputEnum.inputText;
_focusNode.requestFocus();
});
},
child: Padding(
padding: EdgeInsets.only(left: 20.cale, bottom: 15.cale),
child: Icon(
AppIcon.keyboard,
size: 50.cale,
color: Colors.black,
),
),
);
} else {
return AppWidget.inkWellEffectNone(
key: const ValueKey("AppIcon.audio"),
onTap: () {
setState(() {
debugPrint("点击音频按钮,转向问题");
_chatInputEnum = ChatInputEnum.inputAudio;
_showContent = false;
});
},
child: Padding(
padding: EdgeInsets.only(left: 20.cale, bottom: 15.cale),
child: Icon(
AppIcon.audio,
size: 50.cale,
color: Colors.black,
),
),
);
}
}
// void _messageVoice(AudioInfo audioInfo) {
// // AppMessage().sendMessageVoice(
// // chatType: IOEventEmit.chatPerson,
// // senderId: AppCache().userInfo!.userId,
// // target: widget.userInfoTarget,
// // audioInfo: audioInfo,
// // );
// }
}
3.3 chat_input_box
import 'package:flutter/material.dart';
import 'package:imspawn/wrap/extension/extension.dart';
import '../../const/app_colors.dart';
import '../../const/app_textStyle.dart';
class ChatInputBox extends StatelessWidget {
final String? hintText;
final int? maxLength;
final VoidCallback? onEditingComplete;
final ValueChanged<String>? onSubmitted;
final EdgeInsetsGeometry? contentPadding;
final TextEditingController? controller;
final String? errorText;
final Widget? prefixIcon;
final TextInputType? keyboardType;
final BoxConstraints? prefixIconConstraints;
final BoxDecoration? decoration;
final TextStyle? style;
final TextStyle? hintStyle;
final FocusNode? focusNode;
const ChatInputBox({
super.key,
this.maxLength = 200,
this.controller,
this.errorText,
this.prefixIcon,
this.prefixIconConstraints,
this.onEditingComplete,
this.onSubmitted,
this.contentPadding = EdgeInsets.zero,
this.decoration,
this.keyboardType,
this.style,
this.hintStyle,
this.focusNode,
this.hintText,
});
@override
Widget build(BuildContext context) {
return Container(
// height: 75.cale,
// margin: EdgeInsets.all(5.cale),
constraints: BoxConstraints(
minHeight: 75.cale,
maxHeight: 350.cale,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7.cale),
color: Colors.white,
),
child: TextField(
maxLength: maxLength,
focusNode: focusNode,
maxLines: null,
cursorColor: AppColor.color3BAB71,
controller: controller,
textAlignVertical: TextAlignVertical.center,
keyboardType: keyboardType,
onEditingComplete: onEditingComplete,
onSubmitted: onSubmitted,
style: style ?? AppTextStyle.textStyle_28_333333,
// inputFormatters: inputFormatters,
decoration: InputDecoration(
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(width: 0, color: Colors.transparent)),
disabledBorder: const OutlineInputBorder(
borderSide: BorderSide(width: 0, color: Colors.transparent)),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(width: 0, color: Colors.transparent)),
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(7.cale),
//borderSide: BorderSide(width: 0, color: Colors.transparent),
// borderSide: BorderSide(width: 0, color: Colors.transparent),
),
hintText: hintText,
prefixIcon: prefixIcon,
prefixIconConstraints: prefixIconConstraints,
hintStyle: hintStyle ?? AppTextStyle.textStyle_28_AAAAAA,
counterText: '', //取消文字计数器
// border: InputBorder.none,
isDense: true,
errorText: errorText,
contentPadding: EdgeInsets.symmetric(
horizontal: 16.cale,
vertical: 20.cale,
),
),
// contentPadding:
// EdgeInsets.only(left: 16.cale, right: 16.cale, top: 20.cale),
// errorText: "输入错误",
),
);
}
}
3.4 chat_utils
import 'dart:ui';
enum ChatInputEnum {
inputText,
inputAudio,
}
enum ChatMediaEnum {
none,
mediaFiles,
mediaFace,
}
class ContentController {
late VoidCallback onContentClick;
}
final List<Map> mediaOption = [
{
'title': '相册',
'icon': 'assets/common/chat/ic_details_photo.webp',
'type': 1
},
{
'title': '拍照',
'icon': 'assets/common/chat/ic_details_camera.webp',
'type': 2
},
{
'title': '视频通话',
'icon': 'assets/common/chat/ic_details_video.webp',
'type': 3
},
{
'title': '位置',
'icon': 'assets/common/chat/ic_details_localtion.webp',
'type': 4
},
{'title': '红包', 'icon': 'assets/common/chat/ic_details_red.webp', 'type': 5},
{
'title': '转账',
'icon': 'assets/common/chat/ic_details_transfer.webp',
'type': 6
},
{'title': '语音输入', 'icon': 'assets/common/chat/ic_chat_voice.webp', 'type': 7},
{
'title': '我的收藏',
'icon': 'assets/common/chat/ic_details_favorite.webp',
'type': 8
},
];
final List<String> emojiList = [
'😀',
'😃',
'😄',
'😁',
'😆',
'😅',
'😂',
'🤣',
'☺',
'😊',
'😇',
'🙂',
'🙃',
'😉',
'😌',
'😍',
'🥰',
'😘',
'😗',
'😙',
'😚',
'😋',
'😛',
'😝',
'😜',
'🤪',
'🤨',
'🧐',
'🤓',
'😎',
'🤩',
'🥳',
'😏',
'😒',
'😞',
'😔',
'😟',
'😕',
'🙁',
'☹',
];
基本上就OK,这里的思路就是 动态改变 bottomNavigationBar 的高度,聊天内容 body 会自适应高度