import 'dart:async'; import 'package:bytedesk_kefu/ui/widget/emoji_picker_view.dart'; import 'package:bytedesk_kefu/ui/widget/image_button.dart'; import 'package:bytedesk_kefu/vendors/emoji_picker_flutter/emoji_picker_flutter.dart'; // import 'package:bytedesk_kefu/ui/widget/send_button_visibility_mode.dart'; // import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; // import 'package:emoji_picker_flutter/src/emoji_view_state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; // import 'package:flutter_chat_ui/src/widgets/inherited_chat_theme.dart'; // import 'package:imboy/config/init.dart'; // import 'package:imboy/service/websocket.dart'; // import 'package:imboy/store/model/message_model.dart'; /** * 部分代码来自该项目,感谢作者 CaiJingLong https://github.com/CaiJingLong/flutter_like_wechat_input */ enum InputType { text, voice, emoji, extra, } Widget _buildVoiceButton(BuildContext context) { return Container( width: double.infinity, child: TextButton( // color: Colors.white70, onPressed: () { // Get.snackbar('Tips', '语音输入功能暂无实现'); }, child: Text( 'chat_hold_down_talk', ), ), ); } typedef void OnSend(String text); InputType _initType = InputType.text; double _softKeyHeight = 210; class ChatInput extends StatefulWidget { const ChatInput({ Key? key, this.isAttachmentUploading, this.onAttachmentPressed, required this.onSendPressed, this.onTextChanged, this.onTextFieldTap, required this.sendButtonVisibilityMode, this.extraWidget, this.voiceWidget, }) : super(key: key); /// See [AttachmentButton.onPressed] final void Function()? onAttachmentPressed; /// Whether attachment is uploading. Will replace attachment button with a /// [CircularProgressIndicator]. Since we don't have libraries for /// managing media in dependencies we have no way of knowing if /// something is uploading so you need to set this manually. final bool? isAttachmentUploading; /// Will be called on [SendButton] tap. Has [types.PartialText] which can /// be transformed to [types.TextMessage] and added to the messages list. // final Future Function(types.PartialText) onSendPressed; final Future Function(String) onSendPressed; /// Will be called whenever the text inside [TextField] changes final void Function(String)? onTextChanged; /// Will be called on [TextField] tap final void Function()? onTextFieldTap; /// Controls the visibility behavior of the [SendButton] based on the /// [TextField] state inside the [Input] widget. /// Defaults to [SendButtonVisibilityMode.editing]. final SendButtonVisibilityMode sendButtonVisibilityMode; final Widget? extraWidget; final Widget? voiceWidget; @override _ChatInputState createState() => _ChatInputState(); } /// [Input] widget state class _ChatInputState extends State with TickerProviderStateMixin { InputType inputType = _initType; final _inputFocusNode = FocusNode(); bool _sendButtonVisible = false; final _textController = TextEditingController(); late AnimationController _bottomHeightController; bool emojiShowing = false; /** * https://stackoverflow.com/questions/60057840/flutter-how-to-insert-text-in-middle-of-text-field-text */ void _setText(String val) { String text = _textController.text; TextSelection textSelection = _textController.selection; int start = textSelection.start > -1 ? textSelection.start : 0; String newText = text.replaceRange( start, textSelection.end > -1 ? textSelection.end : 0, val, ); _textController.text = newText; int offset = start + val.length; _textController.selection = textSelection.copyWith( baseOffset: offset, extentOffset: offset, ); } @override void initState() { super.initState(); if (!mounted) { return; } if (widget.sendButtonVisibilityMode == SendButtonVisibilityMode.editing) { _sendButtonVisible = _textController.text.trim() != ''; _textController.addListener(_handleTextControllerChange); } else { _sendButtonVisible = true; } _bottomHeightController = AnimationController( vsync: this, duration: Duration( milliseconds: 150, ), ); // 解决"重新进入聊天页面的时候_bottomHeightController在开启状态"的问题 _bottomHeightController.animateBack(0); // 接收到新的消息订阅 // eventBus.on().listen((msg) async { // if (_textController.text.toString() != msg.text) { // _setText(msg.text); // } // }); //添加listener监听 //对应的TextField失去或者获取焦点都会回调此监听 _inputFocusNode.addListener(() { // debugPrint(">>> on chatinput ${_inputFocusNode.hasFocus}"); if (_inputFocusNode.hasFocus) { updateState(InputType.text); } else {} }); } @override void dispose() { // Get.delete(); _inputFocusNode.dispose(); _textController.dispose(); super.dispose(); } Future _handleSendPressed() async { final trimmedText = _textController.text.trim(); if (trimmedText != '') { // final _partialText = types.PartialText(text: trimmedText); // bool res = await widget.onSendPressed(_partialText); bool res = await widget.onSendPressed(trimmedText); if (res) { _textController.clear(); } else { // WSService.to.openSocket(); // 网络原因,发送失败 } } } void _handleTextControllerChange() { setState(() { _sendButtonVisible = _textController.text.trim() != ''; }); } void changeBottomHeight(final double height) { if (height > 0) { _bottomHeightController.animateTo(1); } else { _bottomHeightController.animateBack(0); } } /** * 语音按钮事件 */ Future _voiceBtnOnPressed(InputType type) async { if (type == this.inputType) { return; } if (type != InputType.text) { hideSoftKey(); } else { showSoftKey(); } setState(() { this.inputType = type; }); } Future updateState(InputType type) async { if (type == InputType.text || type == InputType.voice) { _initType = type; } if (type == inputType) { return; } this.inputType = type; // InputTypeNotification(type).dispatch(context); if (type != InputType.text) { hideSoftKey(); } else { showSoftKey(); } if (type == InputType.emoji || type == InputType.extra) { changeBottomHeight(1); hideSoftKey(); } else { changeBottomHeight(0); } setState(() { this.emojiShowing = type == InputType.emoji; this.inputType; }); } void showSoftKey() { FocusScope.of(context).requestFocus(_inputFocusNode); changeBottomHeight(0); // debugPrint(">>> on chatinput showSoftKey"); } void hideSoftKey() { _inputFocusNode.unfocus(); // 隐藏键盘而不丢失文本字段焦点:from https://developer.aliyun.com/article/763095 SystemChannels.textInput.invokeMethod('TextInput.hide'); } Widget _buildBottomContainer({required Widget child}) { return SizeTransition( sizeFactor: _bottomHeightController, child: Container( child: child, height: _softKeyHeight, ), ); } Widget _buildBottomItems() { if (this.inputType == InputType.extra) { return widget.extraWidget ?? Center(child: Text("其他item")); } else if (this.inputType == InputType.emoji) { return Offstage( offstage: !emojiShowing, child: SizedBox( height: 400, child: EmojiPicker( onEmojiSelected: (Category category, Emoji emoji) { _setText(emoji.emoji); }, onBackspacePressed: () { _textController ..text = _textController.text.characters.skipLast(1).toString() ..selection = TextSelection.fromPosition( TextPosition(offset: _textController.text.length), ); }, config: Config( columns: 7, // Issue: https://github.com/flutter/flutter/issues/28894 // emojiSizeMax: 24 * (GetPlatform.isIOS ? 1.30 : 1.0), verticalSpacing: 0, horizontalSpacing: 0, initCategory: Category.RECENT, bgColor: const Color(0xFFF2F2F2), indicatorColor: Colors.black87, iconColorSelected: Colors.black87, iconColor: Colors.grey, progressIndicatorColor: Colors.blue, backspaceColor: Colors.black54, showRecentsTab: true, recentsLimit: 19, // noRecentsText: 'No Recents'.tr, // noRecentsStyle: const TextStyle( // fontSize: 20, // color: Colors.black87, // ), tabIndicatorAnimDuration: kTabScrollDuration, categoryIcons: const CategoryIcons(), buttonMode: ButtonMode.MATERIAL, ), customWidget: (Config config, EmojiViewState state) => EmojiPickerView( config, state, _handleSendPressed, ), ), ), ); } else { return Container(); } } Widget _buildInputButton(BuildContext ctx) { final voiceButton = widget.voiceWidget ?? _buildVoiceButton(ctx); final inputButton = TextField( controller: _textController, // cursorColor: InheritedChatTheme.of(ctx).theme.inputTextCursorColor, // decoration: InheritedChatTheme.of(ctx).theme.inputTextDecoration.copyWith( // hintStyle: InheritedChatTheme.of(ctx).theme.inputTextStyle.copyWith( // color: InheritedChatTheme.of(ctx) // .theme // .inputTextColor // .withOpacity(0.5), // ), // hintText: '', // ), focusNode: _inputFocusNode, // maxLength: 400, maxLines: 6, minLines: 1, // 长按是否展示【剪切/复制/粘贴菜单LengthLimitingTextInputFormatter】 enableInteractiveSelection: true, keyboardType: TextInputType.multiline, textCapitalization: TextCapitalization.sentences, textInputAction: TextInputAction.newline, onChanged: widget.onTextChanged, onTap: () { updateState(inputType); widget.onTextFieldTap; }, // style: InheritedChatTheme.of(ctx).theme.inputTextStyle.copyWith( // color: InheritedChatTheme.of(ctx).theme.inputTextColor, // ), // 点击键盘的动作按钮时的回调,参数为当前输入框中的值 onSubmitted: (_) => _handleSendPressed(), ); return Stack( children: [ Offstage( child: inputButton, offstage: inputType == InputType.voice, ), Offstage( child: voiceButton, offstage: inputType != InputType.voice, ), ], ); } Widget buildLeftButton() { return ImageButton( onPressed: () { if (inputType == InputType.voice) { _voiceBtnOnPressed(InputType.text); } else { _voiceBtnOnPressed(InputType.voice); } changeBottomHeight(0); }, image: AssetImage( inputType != InputType.voice ? 'assets/images/chat/input_voice.png' : 'assets/images/chat/input_keyboard.png', ), ); } /** * */ Widget buildEmojiButton() { return ImageButton( image: AssetImage(inputType != InputType.emoji ? 'assets/images/chat/input_emoji.png' : 'assets/images/chat/input_keyboard.png'), onPressed: () { if (inputType != InputType.emoji) { updateState(InputType.emoji); } else { updateState(InputType.text); } }, ); } /** * 更多输入消息类型入口 * More input message types entries */ Widget buildExtra() { return ImageButton( image: AssetImage('assets/images/chat/input_extra.png'), onPressed: () { if (inputType != InputType.extra) { updateState(InputType.extra); } else { updateState(InputType.text); } }, ); } /** * 实现换行效果 * Implement line breaks */ void _handleNewLine() { final _newValue = '${_textController.text}\r\n'; _textController.value = TextEditingValue( text: _newValue, selection: TextSelection.fromPosition( TextPosition(offset: _newValue.length), ), ); } @override Widget build(BuildContext context) { final _query = MediaQuery.of(context); final isAndroid = Theme.of(context).platform == TargetPlatform.android; final isIOS = Theme.of(context).platform == TargetPlatform.iOS; return InkWell( child: Focus( autofocus: true, child: Padding( padding: EdgeInsets.fromLTRB(15, 0, 0, 0), //InheritedChatTheme.of(context).theme.inputPadding, child: Material( borderRadius: const BorderRadius.vertical(top: Radius.circular(10),), //InheritedChatTheme.of(context).theme.inputBorderRadius, color: Color(0xFFFAFAFA), //Color(0xff1d1c21),//InheritedChatTheme.of(context).theme.inputBackgroundColor, child: Container( padding: EdgeInsets.fromLTRB( _query.padding.left, 4, _query.padding.right, 4 + _query.viewInsets.bottom + _query.padding.bottom, ), child: Column( children: [ Row( children: [ // TODO: 录制语音 // buildLeftButton(), // input Expanded( child: Shortcuts( shortcuts: isAndroid || isIOS ? { LogicalKeySet(LogicalKeyboardKey.enter): const NewLineIntent(), LogicalKeySet(LogicalKeyboardKey.enter, LogicalKeyboardKey.alt): const NewLineIntent(), } : { LogicalKeySet(LogicalKeyboardKey.enter): const SendMessageIntent(), LogicalKeySet(LogicalKeyboardKey.enter, LogicalKeyboardKey.alt): const NewLineIntent(), LogicalKeySet(LogicalKeyboardKey.enter, LogicalKeyboardKey.shift): const NewLineIntent(), }, child: Actions( actions: { SendMessageIntent: CallbackAction( onInvoke: (SendMessageIntent intent) => _handleSendPressed(), ), NewLineIntent: CallbackAction( onInvoke: (NewLineIntent intent) => _handleNewLine(), ), }, child: _buildInputButton(context), ), ), ), // emoji //buildEmojiButton(), //extra _textController.text.isEmpty ? buildExtra() : IconButton( icon: Icon(Icons.send), onPressed: _handleSendPressed, padding: EdgeInsets.only(left: 0), ), ], ), this.inputType == InputType.emoji || this.inputType == InputType.extra ? Divider() : SizedBox.shrink(), // 横线 _buildBottomContainer(child: _buildBottomItems()), ], ), ), ), ), ), ); } }