Flutter 高仿微信 语音输入上划取消输入,网上的只有微信老版本的实现方式; 这边是最新的实现效果全网首发;
实现起来有几个麻烦的地方:
遮罩层的实现,刚开始用的不管是 普通的widget, 还是 showDialog 都会阻断 拖拽事件
遮罩内容的更新问题
遮罩弹出层的方案选择:
刚开始的时候,观察微信发现 从“长按 说话” 按钮开始;有一个全局遮罩 然后上抬圆弧;开始监听 上下滑动的事件。应为需要全局遮罩:
首先 想到的是 showDialog,但是显示 showDialog 会阻断“开始 说话” 按钮的拖拽事件(相当于打开了新的页面)。用过忽略事件(IgnorePointer / AbsorbPointer)都不好使。还想从 GestureDetector底层实现 入手 都以失败告终
然后勉强 使用 stack 实现 老版本的效果:
折腾了好几天(业余时间),本来就要妥协了 后来想到了 OverlayEntry, 还是试了一下没想到 这个 OverlayEntry 显示的时候 原来 按钮的拖拽事件还是可以继续监听的!所以姿势就对了!
下面就是显示 三件套
void _showAudioRecord() {
_hideAudioRecord();
_overlayEntry = OverlayEntry(builder: (BuildContext context) {
return ChatAudioMask(recordAudioState: _recordState);
});
_overlayState!.insert(_overlayEntry!);
}
void _updateAudioRecord() {
final overlayEntry = this._overlayEntry;
if (overlayEntry != null) {
overlayEntry.markNeedsBuild();
}
}
void _hideAudioRecord() {
if (_overlayEntry != null) {
_overlayEntry?.remove();
_overlayEntry = null;
if (_recordState.recording) {
if (_recordState.recordingState == 1) {
print("用户录音成功");
} else {
print("用户取消录音");
}
}
}
}
响应上下滑动事件:
这个还是再 GestureDetector 中实现的,老三样没有新姿势挖掘
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,
),
),
),
遮罩层的更新:
这个 OverlayEntry 是 insert 到 OverlayState 中的至于更新 OverlayEntry 内容;不可能每次更新都 重新 insert 到OverlayState 的;
后来使用 :
markNeedsBuild
void _updateAudioRecord() {
final overlayEntry = this._overlayEntry;
if (overlayEntry != null) {
overlayEntry.markNeedsBuild();
}
}
代码结构:
--- chatCommon
-------chat_audio_mask.dart 聊天语音遮罩具体实现
------ chat_bottom.dart 聊天底部输入框
------ chat_element_other.dart 聊天时别人信息的显示
------ chat_element_self.dart 聊天时自己信息的显示
------ chat_input_box.dart 聊天文本输入框封装
------ page_chat_group.dart 群聊
------ page_chat_person.dart 单聊
------ provider_chat_content.dart 聊天键盘显示 事件的传递 /键盘高度的处理
chat_audio_mask
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:imflutter/const/app_colors.dart';
import 'package:imflutter/const/app_icon.dart';
import 'package:imflutter/const/app_textStyle.dart';
import 'package:imflutter/pages/chatCommon/provider_chat_content.dart';
import 'package:imflutter/wrap/extension/extension.dart';
import 'package:lottie/lottie.dart';
class ChatAudioMask extends StatefulWidget {
final RecordAudioState recordAudioState;
const ChatAudioMask({Key? key, required this.recordAudioState})
: super(key: key);
@override
State createState() => _ChatAudioMaskState();
}
class _ChatAudioMaskState extends State {
double _height = 0;
bool _showAudioWave = false;
@override
void initState() {
// TODO: implement initState
super.initState();
Future.delayed(const Duration(milliseconds: 150)).then((value) {
if (mounted) {
setState(() {
_height = 250.cale;
});
}
});
Future.delayed(const Duration(milliseconds: 350)).then((value) {
if (mounted) {
setState(() {
_showAudioWave = true;
});
}
});
}
@override
Widget build(BuildContext context) {
return Positioned(
child: Material(
color: Colors.black.withOpacity(0.5),
child: Column(
children: [
Expanded(
child: Center(
child: _showAudioWave
? Container(
height: 200.cale,
width: 360.cale,
decoration: BoxDecoration(
color: widget.recordAudioState.recordingState == 1
? AppColor.color8FED6D
: Colors.red,
borderRadius: BorderRadius.circular(30.cale),
),
child: Center(
child: Container(
height: 200.cale,
width: 200.cale,
child: Lottie.asset(
'assets/lottieAnimation/record_auido.json'),
),
),
)
: Container(),
),
),
Container(
padding: EdgeInsets.symmetric(vertical: 50.cale),
child: Text(
widget.recordAudioState.noticeMessage,
style: AppTextStyle.textStyle_30_F7F7F7,
),
),
Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(250.cale),
topRight: Radius.circular(250.cale),
),
),
// color: Colors.grey,
width: double.infinity,
height: _height,
child: Icon(
AppIcon.audioSymbol,
color: AppColor.color7E7E7E,
size: 60.cale,
),
),
)
],
),
),
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
}
}
chat_bottom
import 'package:flutter/material.dart';
import 'package:imflutter/const/app_textStyle.dart';
import 'package:imflutter/pages/chatCommon/chat_audio_mask.dart';
import 'package:imflutter/pages/chatCommon/provider_chat_content.dart';
import 'package:imflutter/wrap/extension/extension.dart';
import '../../const/app_colors.dart';
import '../../const/app_icon.dart';
import '../../wrap/widget/app_widget.dart';
import 'chat_input_box.dart';
class ChatBottom extends StatefulWidget {
final ProviderChatContent providerChatContent;
const ChatBottom({Key? key, required this.providerChatContent})
: super(key: key);
@override
State createState() => _ChatBottomState();
}
class _ChatBottomState extends State with WidgetsBindingObserver {
// 0 语音 1 键盘 2 表情
int _inputType = 0;
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
late OverlayEntry? _overlayEntry = null;
late OverlayState? _overlayState = null;
bool get _keyboardShow => widget.providerChatContent.contentShow;
RecordAudioState get _recordState =>
widget.providerChatContent.recordAudioState;
final List
chat_element_other
import 'package:flutter/material.dart';
import 'package:imflutter/const/app_colors.dart';
import 'package:imflutter/wrap/extension/extension.dart';
import 'package:imflutter/wrap/widget/app_widget.dart';
class ChatElementOther extends StatefulWidget {
/// 用户信息
final Map userInfo;
/// 消息
final Map chatMessage;
const ChatElementOther(
{Key? key, required this.userInfo, required this.chatMessage})
: super(key: key);
@override
State createState() => _ChatElementOtherState();
}
class _ChatElementOtherState extends State {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 24.cale),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(bottom: 40.cale),
child: Text('11:25'),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 24.cale),
child: AppWidget.inkWellEffectNone(
onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(7.cale),
child: AppWidget.cachedImage(widget.userInfo['icon'],
width: 75.cale, height: 75.cale),
),
),
),
_chatContent(),
],
)
],
),
);
}
Widget _chatContent() {
/// 1 文本
/// 2 图片
/// 3 语音
/// 4 视频
/// 5 提示消息
/// 6 提示消息
switch (widget.chatMessage['type']) {
case 1:
return _chatType1();
break;
case 2:
return _chatType2();
break;
case 3:
return _chatType3();
break;
case 4:
return _chatType4();
break;
case 5:
return _chatType5();
break;
case 6:
return _chatType6();
break;
default:
return Container();
break;
}
}
Widget _chatType1() {
return Stack(
children: [
Container(
margin: EdgeInsets.only(left: 25.cale),
constraints: BoxConstraints(maxWidth: 500.cale),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.cale),
),
padding: EdgeInsets.symmetric(
vertical: 18.cale,
horizontal: 20.cale,
),
child: Text(
widget.chatMessage['content_1'],
softWrap: true,
),
),
Positioned(
top: 25.cale,
left: 10.cale,
child: CustomPaint(
size: Size(20.cale, 30.cale),
painter: TrianglePainter(),
),
),
],
);
}
Widget _chatType2() {
return Container(
constraints: BoxConstraints(
maxWidth: 320.cale,
maxHeight: 300.cale,
minHeight: 120.cale,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7.cale),
border: Border.all(width: 1.cale / 2, color: AppColor.color636363),
),
margin: EdgeInsets.only(left: 20.cale),
child: ClipRRect(
borderRadius: BorderRadius.circular(7.cale),
child: AppWidget.cachedImage(
widget.chatMessage['content_2']['picture_mini']['url'],
),
),
);
}
Widget _chatType3() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是语音"),
);
}
Widget _chatType4() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是视频"),
);
}
Widget _chatType5() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是提示5"),
);
}
Widget _chatType6() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是提示6"),
);
}
}
class TrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = Colors.white;
Path path = Path();
path.moveTo(0, size.height / 2);
path.lineTo(size.width, 0);
path.lineTo(size.width, size.height);
path.close();
canvas.drawPath(path, paint);
return;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return false;
}
}
chat_element_self
import 'package:flutter/material.dart';
import 'package:imflutter/const/app_colors.dart';
import 'package:imflutter/wrap/extension/extension.dart';
import 'package:imflutter/wrap/widget/app_widget.dart';
class ChatElementSelf extends StatefulWidget {
/// 用户信息
final Map userInfo;
/// 消息
final Map chatMessage;
const ChatElementSelf(
{Key? key, required this.userInfo, required this.chatMessage})
: super(key: key);
@override
State createState() => _ChatElementSelfState();
}
class _ChatElementSelfState extends State {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 24.cale),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(bottom: 40.cale),
child: Text('11:25'),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_chatContent(),
Padding(
padding: EdgeInsets.only(right: 24.cale),
child: AppWidget.inkWellEffectNone(
onTap: () {},
child: ClipRRect(
borderRadius: BorderRadius.circular(7.cale),
child: AppWidget.cachedImage(widget.userInfo['icon'],
width: 75.cale, height: 75.cale),
),
),
),
],
)
],
),
);
}
Widget _chatContent() {
/// 1 文本
/// 2 图片
/// 3 语音
/// 4 视频
/// 5 提示消息
/// 6 提示消息
switch (widget.chatMessage['type']) {
case 1:
return _chatType1();
break;
case 2:
return _chatType2();
break;
case 3:
return _chatType3();
break;
case 4:
return _chatType4();
break;
case 5:
return _chatType5();
break;
case 6:
return _chatType6();
break;
default:
return Container();
break;
}
}
Widget _chatType1() {
return Stack(
children: [
Container(
margin: EdgeInsets.only(right: 25.cale),
constraints: BoxConstraints(maxWidth: 500.cale),
decoration: BoxDecoration(
color: AppColor.color94ED6D,
borderRadius: BorderRadius.circular(12.cale),
),
padding: EdgeInsets.symmetric(
vertical: 18.cale,
horizontal: 20.cale,
),
child: Text(
widget.chatMessage['content_1'],
softWrap: true,
),
),
Positioned(
top: 25.cale,
right: 10.cale,
child: CustomPaint(
size: Size(20.cale, 30.cale),
painter: TrianglePainter(),
),
),
],
);
}
Widget _chatType2() {
return Container(
constraints: BoxConstraints(
maxWidth: 320.cale,
maxHeight: 300.cale,
minHeight: 120.cale,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7.cale),
border: Border.all(width: 1.cale / 2, color: AppColor.color636363),
),
margin: EdgeInsets.only(right: 20.cale),
child: ClipRRect(
borderRadius: BorderRadius.circular(7.cale),
child: AppWidget.cachedImage(
widget.chatMessage['content_2']['picture_mini']['url'],
),
),
);
}
Widget _chatType3() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是语音"),
);
}
Widget _chatType4() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是视频"),
);
}
Widget _chatType5() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是提示5"),
);
}
Widget _chatType6() {
return Container(
color: Colors.white,
padding: EdgeInsets.all(18.cale),
child: Text("这是提示6"),
);
}
}
class TrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = AppColor.color94ED6D;
Path path = Path();
// path.moveTo(0, 0);
// path.lineTo(0, size.height);
// path.lineTo(size.width, size.height);
// path.lineTo(size.width, 0);
path.moveTo(0, 0);
path.lineTo(0, size.height);
path.lineTo(size.width, size.height / 2);
path.close();
canvas.drawPath(path, paint);
return;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return false;
}
}
chat_input_box
import 'package:flutter/material.dart';
import 'package:imflutter/const/app_colors.dart';
import 'package:imflutter/wrap/extension/extension.dart';
import '../../const/app_textStyle.dart';
class ChatInputBox extends StatelessWidget {
final String? hintText;
final int? maxLength;
final VoidCallback? onEditingComplete;
final ValueChanged? 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({
Key? key,
this.maxLength = 20,
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,
}) : super(key: key);
@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,
maxLength: 200,
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: "输入错误",
),
);
}
}
page_chat_group
import 'package:flutter/cupertino.dart';
class PageChatGroup extends StatefulWidget {
const PageChatGroup({Key? key}) : super(key: key);
@override
State createState() => _PageChatGroupState();
}
class _PageChatGroupState extends State {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
page_chat_person
import 'package:flutter/material.dart';
import 'package:imflutter/wrap/extension/extension.dart';
import 'package:imflutter/wrap/navigator/app_navigator.dart';
import 'package:imflutter/pages/chatCommon/chat_element_other.dart';
import 'package:provider/provider.dart';
import '../../const/app_colors.dart';
import '../../const/app_icon.dart';
import '../../const/app_textStyle.dart';
import '../../wrap/widget/app_widget.dart';
import 'provider_chat_content.dart';
import 'chat_bottom.dart';
import 'chat_element_self.dart';
class PageChatPerson extends StatefulWidget {
final Map userInfoOther;
const PageChatPerson({Key? key, required this.userInfoOther})
: super(key: key);
@override
State createState() => _PageChatPersonState();
}
class _PageChatPersonState extends State {
/// 1 文本
/// 2 图片
/// 3 语音
/// 4 视频
/// 5 提示消息
/// 6 提示消息
final List
provider_chat_content
import 'package:flutter/cupertino.dart';
import 'package:imflutter/wrap/extension/extension.dart';
///用于 软键盘区/发送附件 域显示控制
class ProviderChatContent extends ChangeNotifier {
bool _contentShow = false;
double _keyboardHeight = 200;
RecordAudioState _recordAudioState = RecordAudioState(
recording: false,
recordingState: -1,
noticeMessage: '',
);
/// 是否显示 附件区域
bool get contentShow => _contentShow;
/// 键盘高度
double get keyboardHeight => _keyboardHeight - 20.cale;
RecordAudioState get recordAudioState => _recordAudioState;
///更新区域 展示
void updateContentShow(bool isShow) {
_contentShow = isShow;
notifyListeners();
}
void updateKeyboardHeight(double height) {
_keyboardHeight = height;
notifyListeners();
}
void updateRecordAudioState(RecordAudioState state) {
_recordAudioState = state;
notifyListeners();
}
}
class RecordAudioState {
final bool recording;
final int recordingState;
final String noticeMessage;
RecordAudioState(
{required this.recording,
required this.recordingState,
required this.noticeMessage});
}