Flutter高仿微信系列共59篇,从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。
详情请查看
效果图:

实现代码:
单聊包含:文本、表情、语音、图片、小视频、红包、转账、视频通话、语音通话功能,有4个widget:
home_chat_page.dart、chat_add_view.dart、chat_content_view.dart、chat_voice_view.dart
home_chat_page.dart实现:
/**
* Author : wangning
* Email : maoning20080809@163.com
* Date : 2022/8/24 14:48
* Description : 单聊页面
*/
class HomeChatPage extends StatefulWidget {
String toChatId;
String account = SpUtils.getString(CommonUtils.LOGIN_ACCOUNT);
HomeChatPage({required this.toChatId});
@override
_HomeChatPageState createState() => _HomeChatPageState(toChatId);
}
class _HomeChatPageState extends State<HomeChatPage> with TickerProviderStateMixin {
String _toChatId;
_HomeChatPageState(this._toChatId);
//好友账户
UserBean? _otherUserBean;
//我的账户
UserBean? _meUserBean;
List<String> addTimeList = [];
List<ChatBean> items = [];
ScrollController _controller = ScrollController();
var chatEvent;
//每页13条
static const PAGE_SIZE = 13;
//当前页
var PAGE_NUM = 1;
//从那一条开始(为保证最新的先显示, 先查询最后的,并且不能用desc查询)
var startNum = 0;
//总共多少条
var CHAT_TOTAL = 0;
@override
void initState() {
super.initState();
AppManager.getInstance().toChatId = _toChatId;
_checkAvailable();
_updateChatStatus();
chatEvent = eventBus.on<ChatBean>((chatBean) {
if(mounted){
setState(() {
chatBean as ChatBean;
items.add(chatBean);
});
}
});
chatEvent = eventBus.on<RedPacketBean>((redPacketBean) {
setState(() {
_updateRedpacketBalance(redPacketBean);
});
});
loadUserBean();
loadAllChat();
jumpToBottom(400);
// 监听滚动事件
_controller.addListener((){
if(_controller.position.pixels>_controller.position.maxScrollExtent-40){
}
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_initScreenUtil(context);
});
}
//更新红包金额
void _updateRedpacketBalance(RedPacketBean redPacketBean){
LogUtils.d("home_chat_page 的金额:${redPacketBean?.position}");
ChatBean chatBean = items[redPacketBean.position??0];
String messageId = chatBean.messageId??"";
int isClick = 1;
chatBean.isClick = isClick;
ChatRepository.getInstance().updateChatRedPacketStatus(messageId, isClick);
setState(() {
});
Navigator.push(context, MaterialPageRoute(builder: (context) => ReceiveRedpacketSuccess(fromUser: widget.account, toUser: widget.toChatId, balance: "${chatBean.content}", addTime: chatBean.addTime??"",)));
}
@override
void dispose() {
super.dispose();
eventBus.off(chatEvent);
AppManager.getInstance().toChatId = "";
}
final controller = TextEditingController();
final FocusNode _chatContentFocus = FocusNode();
@override
Widget build(BuildContext context) {
if(!isLoadMore){
//每次发送消息滚动到底部0.1秒
jumpToBottom(100);
}
return Scaffold(
appBar: WnAppBar.getAppBar(context, Text("${_otherUserBean?.nickName??""}")),
bottomNavigationBar: Text(""),//占用底部位置
body: GestureDetector(
onTap: (){
_processClickBlank();
},
child: Container(
child: Column(
children: <Widget>[
Expanded(
child: RefreshIndicator(
displacement: 2,
onRefresh: _onRefresh,
child: ListView.builder(
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return ChatContentView(items: items,account: widget.account, chatBean: items[index], index: index, meUserBean: _meUserBean,addTimeList: addTimeList,
otherUserBean: _otherUserBean, deleteCallback: (data){
setState(() {
items.remove(items[index]);
});
},clickVoiceCallback: (data){
//点击播放录音,暂停后再播放
for(int i = 0; i < items.length; i++){
if(i == index){
items[i].isPlayVoice = true;
} else {
items[i].isPlayVoice = false;
}
}
setState(() {
});
}, refreshTransfer: (position){
LogUtils.d("回调刷新:${position}");
_refreshTransfer(position);
},);
},
itemCount: items.length,
)
),
),
Divider(height: 12.0, color: Color(0xFFF7F8F8),),
Container(
padding: EdgeInsets.only(top: 5.0, bottom: 5.0, right: 2.0, left: 2.0),
color: Color(0xFFF3F3F3),
width: double.infinity,
child: Row(
children: <Widget>[
Container(
margin: EdgeInsets.symmetric(horizontal: 2.0),
child: IconButton(
//按下语音说活
icon: isPressVoice ? Image.asset("assets/chat/button_keyboard.png"):Image.asset("assets/chat/button_voice.png"),
onPressed: () =>{
_processPressVoice()
}
), //触发发送消息事件执行的函数_handleSubmitted
),
Expanded(
child: Stack(
children: [
Offstage(
offstage: !isPressVoice,
child: ChatVoiceView(
refreshMediaCallback: (type, mediaURL, thumbnailFileName, second, messageId){
_refreshMedia(type, mediaURL, thumbnailFileName, mediaSecond: second, messageId: messageId);
},
sendMedialCallback: (type, mediaURL, second, messageId){
_sendMedia(type, mediaURL, mediaSecond: second, messageId: messageId);
},
stopPlayVoiceCallback: (){
hidePlayVoiceList();
},
),
),
Offstage(
offstage: isPressVoice,
child: Container(
padding: EdgeInsets.only(top: 8.0, bottom: 8.0, left: 8.0),
decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Colors.white),
child: TextField(
controller: controller,
focusNode: _chatContentFocus,
decoration: InputDecoration.collapsed(hintText: null),
autocorrect: true,
//是否自动更正
autofocus: false,
maxLines: 5,
minLines: 1,
textAlign: TextAlign.start,
style: TextStyle(color: Colors.black, fontSize: 20),
cursorColor: Colors.green,
onTap: (){
//点击编辑框
jumpToBottom(400);
hideEmoji = true;
hideAdd = true;
setState(() {
});
},
onChanged: (value){
//录入文字
setState(() {
if(value.length>0){
hideSend = false;
hideAddIcon = true;
} else {
hideSend = true;
hideAddIcon = false;
}
});
},
onSubmitted: _handleSubmitted,
enabled: true, //是否禁用
),
),
),
],
),
),
Container(
child: IconButton(
icon: Image.asset("assets/chat/button_emoji.png"),
onPressed: () => _processEmoji()),
),
Offstage(
offstage: hideAddIcon,
child: Container(
//margin: EdgeInsets.only(right: 4.0),
child: IconButton(
//添加按钮
icon: Image.asset("assets/chat/button_add.png"),
onPressed: () => {
_processAdd()
}
),
),
),
Offstage(
offstage: hideSend,
child: Container(
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
//发送按钮
icon: new Icon(Icons.send), //发送按钮图标
onPressed: () => _handleSubmitted(
controller.text)), //触发发送消息事件执行的函数_handleSubmitted
),
),
],
),
),
Offstage(
offstage: hideAdd,
child: ChatAddView(
viewType: CommonUtils.VIEW_TYPE_SINGLE_CHAT,
toChatId: widget.toChatId,
refreshMediaCallback: (type, mediaURL, thumbnailFileName, second, messageId){
_refreshMedia(type, mediaURL, thumbnailFileName, mediaSecond: second , messageId: messageId);
},
sendMedialCallback: (type, mediaURL, second, messageId){
_sendMedia(type, mediaURL, mediaSecond: second, messageId: messageId);
},
refreshRedpacketAndTransfer: (type, text){
_refreshRedpacketAndTransfer(type, text);
},
),
),
Offstage(
offstage: hideEmoji,
child: getEmojiWidget(),
),
],
),
),
),
);
}
//进入聊天页面,把聊天状态更新为已读
void _updateChatStatus() async{
int newMessageCount = await ChatRepository.getInstance().getAllChatUnReadByAccount(_toChatId)??0;
if(newMessageCount >= 0){
await ChatRepository.getInstance().updateChatReadByAccount(_toChatId);
Map<String, Object> result = HashMap<String, Object>();
result["from_account"] = _toChatId;
eventBus.emit(BaseEvent(BaseEvent.TYPE_UPDATE_CHAT_STATUS, result: result));
}
}
// 下拉刷新
Future<void> _onRefresh() async{
//延迟0.02秒
await Future.delayed(Duration(milliseconds:20),(){
if(startNum >= PAGE_SIZE){
startNum = CHAT_TOTAL - PAGE_SIZE * PAGE_NUM;
_loadMoreData(widget.account, widget.toChatId, startNum, PAGE_SIZE);
} else if(startNum > 0 && startNum < PAGE_SIZE){
//不够1页数据,查询全部,然后就不能下一页
_loadMoreData(widget.account, widget.toChatId, 0, startNum);
startNum = 0;
}
});
}
bool isLoadMore = false;
//上拉加载更多数据
void _loadMoreData(String fromAccount, String toAccount, int sNum , int pageSize){
isLoadMore = true;
ChatRepository.getInstance().findAllChatByAccountPage(fromAccount, toAccount, sNum, pageSize).then((chatList) {
if(startNum > 0){
PAGE_NUM++;
}
Timer(Duration(milliseconds: 100),() => _controller.jumpTo(AppManager.getInstance().getHeight(context)/3));
setState(() {
items.insertAll(0, chatList??[]);
});
Timer(Duration(milliseconds: 100),() => isLoadMore = false);
});
}
//检查状态, 如果不可以,先登录
void _checkAvailable() async{
var isAvailable = await XmppManager.getInstance().isAvailable();
if(!isAvailable){
String account = SpUtils.getString(CommonUtils.LOGIN_ACCOUNT);
String password = SpUtils.getString(CommonUtils.LOGIN_PASSWORD);
XmppManager.getInstance().connect(account, password);
}
}
//加载聊天信息
void loadAllChat() async {
CHAT_TOTAL = await ChatRepository.getInstance().getChatCountByAccount(widget.account, widget.toChatId)??0;
startNum = CHAT_TOTAL - PAGE_SIZE * PAGE_NUM;
ChatRepository.getInstance().findAllChatByAccountPage(widget.account, widget.toChatId, startNum, CHAT_TOTAL).then((chatList) {
if(startNum > 0){
PAGE_NUM++;
}
setState(() {
items = chatList??[];
});
});
}
//加载我的、好友信息
void loadUserBean() async {
_otherUserBean = await UserRepository.getInstance().findUserByAccount(_toChatId);
_meUserBean = await UserRepository.getInstance().findUserByAccount(widget.account);
}
//发送消息
_sendMessage(var message){
int id = DateTime.now().millisecondsSinceEpoch;
String account = SpUtils.getString(CommonUtils.LOGIN_ACCOUNT);
String toJid = "${widget.toChatId}@wangning";
XmppManager.getInstance().sendMessageWithType(toJid, message, "$account", id);
Map<String, Object> result = HashMap<String, Object>();
eventBus.emit(BaseEvent(BaseEvent.TYPE_NEW_MESSAGE, result: result));
}
//默认滚动到底部
void jumpToBottom(int milliseconds){
if (items.length > 0) {
Timer(Duration(milliseconds: milliseconds),
() => _controller.jumpTo(_controller.position.maxScrollExtent));
}
}
//隐藏播放列表,停止播放录音
hidePlayVoiceList(){
for(int i = 0; i < items.length;i++){
items[i].isPlayVoice = false;
}
AudioPlayer.getInstance().stop();
setState(() {
});
}
//刷新多媒体(图片、语音、小视频) (先刷新本地,然后小视频压缩完成再慢慢发送)
void _refreshMedia(int type, String mediaURL, String thumbnailFileName, {int mediaSecond=0, String messageId = "" }) async {
bool isNetwork = await CommonNetwork.isNetwork();
if(!isNetwork) {
CommonUtils.showNetworkError(context);
return;
}
bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId);
if(deleteContacts){
WnBaseDialog.showAddFriendsDialog(context, widget.toChatId);
return;
}
String addTime = WnDateUtils.getCurrentTime();
//先刷新本地聊天
ChatBean chatBean = ChatBean(fromAccount: widget.account, toAccount: widget.toChatId,addTime:addTime,messageId: messageId,isRead: 1);
chatBean.contentType = type;
if(type == CommonUtils.CHAT_CONTENT_TYPE_VOICE){
chatBean.voiceLocal = mediaURL;
chatBean.second = mediaSecond;
//状态变更,向聊天记录中插入新记录
setState(() {
items.add(chatBean);
});
ChatRepository.getInstance().insertChat(chatBean);
} else if(type == CommonUtils.CHAT_CONTENT_TYPE_IMG){
chatBean.imgPathLocal = mediaURL;
//状态变更,向聊天记录中插入新记录
setState(() {
items.add(chatBean);
});
ChatRepository.getInstance().insertChat(chatBean);
} else if(type == CommonUtils.CHAT_CONTENT_TYPE_VIDEO){
//小视频会刷新本地2次。但messageId都是一样的
chatBean.videoLocal = mediaURL;
chatBean.imgPathLocal = thumbnailFileName;
chatBean.second = mediaSecond;
ChatBean? localChatBean = await ChatRepository.getInstance().findChatByMessageId(messageId);
//状态变更,向聊天记录中插入新记录
if(localChatBean == null){
items.add(chatBean);
ChatRepository.getInstance().insertChat(chatBean);
} else {
chatBean.id = localChatBean.id;
ChatRepository.getInstance().updateChat(chatBean);
//如果已经存在,先删除在添加
for(int i = 0; i < items.length; i++){
ChatBean item = items[i];
if(item.messageId == messageId){
items.remove(item);
break;
}
}
items.add(chatBean);
}
setState(() {
});
}
//LogUtils.d("滚动到底部3");
jumpToBottom(100);
}
//发送多媒体(图片、语音、小视频)
void _sendMedia(int type, String mediaURL, {int mediaSecond = 0, String messageId = ""}) async {
bool isNetwork = await CommonNetwork.isNetwork();
if(!isNetwork) {
return;
}
bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId);
if(deleteContacts){
return;
}
//上传文件
ChatBean serverChatBean;
String message = "";
ChatSendBean chatSendBean = ChatSendBean();
chatSendBean.contentType = type;
chatSendBean.messageId = messageId;
chatSendBean.addTime = WnDateUtils.getCurrentTime();
if(type == CommonUtils.CHAT_CONTENT_TYPE_IMG){
//图片
serverChatBean = await UploadUtils.getInstance().uploadChatImage(widget.account, widget.toChatId, mediaURL);
chatSendBean.content = serverChatBean.imgPath??"";
} else if(type == CommonUtils.CHAT_CONTENT_TYPE_VOICE){
//语音
serverChatBean = await UploadUtils.getInstance().uploadChatVoice(widget.account, widget.toChatId, mediaURL);
chatSendBean.content = serverChatBean.voice??"";
chatSendBean.second = mediaSecond;
} else if(type == CommonUtils.CHAT_CONTENT_TYPE_VIDEO){
//小视频
serverChatBean = await UploadUtils.getInstance().uploadChatVideo(widget.account, widget.toChatId, mediaURL);
message = "${type}${CommonUtils.CHAT_MESSAGE_SPILE}${serverChatBean.video}";
chatSendBean.content = serverChatBean.video??"";
chatSendBean.second = mediaSecond;
} else {
return ;
}
message = jsonEncode(chatSendBean);
_sendMessage(message);
}
//是否隐藏文件
bool hideAdd = true;
//是否隐藏emoji表情
bool hideEmoji = true;
//是否隐藏发送按钮
bool hideSend = true;
//是否隐藏添加按钮
bool hideAddIcon = false;
//是否按下语音说话
bool isPressVoice = false;
//点击空白地方,隐藏文件、emoji
void _processClickBlank(){
setState(() {
hideAdd = true;
hideEmoji = true;
_chatContentFocus.unfocus(); // 失去焦点
});
}
//按下录音
void _processPressVoice(){
setState(() {
isPressVoice = !isPressVoice;
hideEmoji = true;
hideAdd = true;
_processFocus();
});
}
//点击emoji表情
void _processEmoji(){
setState(() {
hideEmoji = !hideEmoji;
isPressVoice = false;
hideAdd = true;
_processFocus();
});
}
//点击+按钮
void _processAdd(){
setState(() {
hideAdd = !hideAdd;
isPressVoice = false;
hideEmoji = true;
_processFocus();
});
}
//处理焦点
void _processFocus(){
if(!hideAdd || !hideEmoji || isPressVoice){
_chatContentFocus.unfocus(); // 失去焦点
} else {
FocusScope.of(context).requestFocus(_chatContentFocus); // 获取焦点
}
}
emoticonClick(String name){
controller.text = name;
}
///选中表情
_onEmojiSelected(Emoji emoji) {
controller
..text += emoji.emoji
..selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length));
hideAddIcon = true;
hideSend = false;
setState(() {
});
}
///表情删除按钮
_onBackspacePressed() {
controller
..text = controller.text.characters.skipLast(1).toString()
..selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
if (controller.text.isNotEmpty) {
setState(() {
});
}
}
//是否已经删除联系人
Future<bool> isDeleteContacts(String fromAccount, String toAccount) async {
bool delete = false;
ContactsBean? contactsBean = await ContactsRepository.getInstance().findContactByFromOrToAccount(fromAccount, toAccount);
if(contactsBean != null){
delete = (contactsBean.type == ContactsBean.typeDelete);
}
return Future.value(delete);
}
//定义发送文本事件的处理函数
void _handleSubmitted(String text) async {
if (text.length > 0) {
bool isNetwork = await CommonNetwork.isNetwork();
if(!isNetwork) {
CommonUtils.showNetworkError(context);
return;
}
bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId);
if(deleteContacts){
WnBaseDialog.showAddFriendsDialog(context, widget.toChatId);
return;
}
int contentType = CommonUtils.CHAT_CONTENT_TYPE_TEXT;
String addTime = WnDateUtils.getCurrentTime();
String messageId = UUID.getUUID();
ChatSendBean chatSendBean = ChatSendBean();
chatSendBean.contentType = contentType;
chatSendBean.content = text;
chatSendBean.addTime = addTime;
chatSendBean.second = 0;
chatSendBean.messageId = messageId;
String message = jsonEncode(chatSendBean);
_sendMessage(message);
controller.clear(); //清空输入框
ChatBean chatBean = ChatBean(fromAccount: widget.account, toAccount: widget.toChatId, content: text,contentType: contentType, addTime: addTime, isRead: 1, messageId: messageId);
LogUtils.d("插入数据:${chatBean.toJson()}");
//状态变更,向聊天记录中插入新记录
setState(() {
hideAddIcon = false;
hideSend = true;
items.add(chatBean);
});
await ChatRepository.getInstance().insertChat(chatBean);
jumpToBottom(100);
}
}
//Emoji表情控件
Widget getEmojiWidget(){
return SizedBox(
height: 200.0,
width: 1000.0,
child: EmojiPicker(
onEmojiSelected: (Category category, Emoji emoji) {
_onEmojiSelected(emoji);
},
onBackspacePressed: _onBackspacePressed,
config: const Config(
columns: 7,
emojiSizeMax: 25.0,
verticalSpacing: 0,
horizontalSpacing: 0,
initCategory: Category.RECENT,
bgColor: Color(0xFFF2F2F2),
indicatorColor: Color(0xff65DAC5),
iconColor: Colors.orange,
iconColorSelected: Color(0xff65DAC5),
progressIndicatorColor: Color(0xff65DAC5),
backspaceColor: Color(0xff65DAC5),
showRecentsTab: true,
recentsLimit: 28,
categoryIcons: CategoryIcons(),
buttonMode: ButtonMode.MATERIAL)),
);
}
/**刷新红包、转账
*@contentType 类型
*@text 内容
*/
void _refreshRedpacketAndTransfer(int contentType, String text) async {
if (text.length > 0) {
bool isNetwork = await CommonNetwork.isNetwork();
if(!isNetwork) {
CommonUtils.showNetworkError(context);
return;
}
String messageId = UUID.getUUID();
String addTime = WnDateUtils.getCurrentTime();
ChatSendBean chatSendBean = ChatSendBean();
chatSendBean.contentType = contentType;
chatSendBean.content = text;
chatSendBean.addTime = addTime;
chatSendBean.second = 0;
chatSendBean.messageId = messageId;
String message = jsonEncode(chatSendBean);
_sendMessage(message);
controller.clear(); //清空输入框
ChatBean chatBean = ChatBean(fromAccount: widget.account, toAccount: widget.toChatId, content: text,contentType: contentType, addTime: addTime, isRead: 1, messageId: messageId);
await ChatRepository.getInstance().insertChat(chatBean);
//状态变更,向聊天记录中插入新记录
setState(() {
items.add(chatBean);
});
jumpToBottom(100);
}
}
//刷新转账
void _refreshTransfer(int position) async {
ChatBean chatBean = items[position];
chatBean.isClick = 1;
setState(() {
});
}
void _initScreenUtil(BuildContext context) {
ScreenUtil.init(
BoxConstraints(
maxWidth: MediaQuery.of(context).size.width,
maxHeight: MediaQuery.of(context).size.height),
designSize: const Size(375, 812),
context: context);
}
}
chat_add_view.dart实现:
/**
* Author : wangning
* Email : maoning20080809@163.com
* Date : 2022/9/24 14:46
* Description : 聊天点击+按钮
* 1单聊支持:相册、拍照、视频通话、语音通话、红包、转账
* 2群聊支持:相册、拍照
*/
class ChatAddView extends StatefulWidget{
//刷新列表
final refreshMediaCallback;
//发送信息
final sendMedialCallback;
//聊天id
final String toChatId;
//刷新红包、转账
final refreshRedpacketAndTransfer;
//1单聊, 2群聊
final int viewType;
ChatAddView({required this.viewType, required this.toChatId, required this.refreshMediaCallback, required this.sendMedialCallback, required this.refreshRedpacketAndTransfer});
@override
State<StatefulWidget> createState() => _ChatAddState();
}
class _ChatAddState extends State<ChatAddView>{
@override
Widget build(BuildContext context) {
return getAddWidget();
}
//相册
List ablums = [CommonUtils.getBaseIconUrlPng("wc_chat_album_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_album_selected")];
int ablumsPosition = 0;
//拍照
List takePhotos = [CommonUtils.getBaseIconUrlPng("wc_chat_video_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_video_selected")];
int takePhotosPosition = 0;
//视频通话
List videoCalls = [CommonUtils.getBaseIconUrlPng("wc_chat_video_call_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_video_call_selected")];
int videoCallsPosition = 0;
//语音通话
List voiceCalls = [CommonUtils.getBaseIconUrlPng("wc_chat_voice_call_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_voice_call_selected")];
int voiceCallsPosition = 0;
//红包
List redPackets = [CommonUtils.getBaseIconUrlPng("wc_chat_redpacket_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_redpacket_selected")];
int redPacketsPosition = 0;
//转账
List transfers = [CommonUtils.getBaseIconUrlPng("wc_chat_transfer_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_transfer_selected")];
int transfersPosition = 0;
//相册
final TYPE_ABLUM = 1;
//拍照
final TYPE_TAKE_PHOTO = 2;
//视频通话
final TYPE_VIDEO_CALL = 3;
//语音通话
final TYPE_VOICE_CALL = 4;
//红包
final TYPE_RED_PACKET = 5;
//转账
final TYPE_TRANSFER = 6;
//改变状态
void _changeStatus(int type, int position){
if(type == TYPE_ABLUM){
ablumsPosition = position;
ablums[ablumsPosition];
} else if(type == TYPE_TAKE_PHOTO){
takePhotosPosition = position;
takePhotos[takePhotosPosition];
} else if(type == TYPE_VIDEO_CALL){
videoCallsPosition = position;
videoCalls[videoCallsPosition];
} else if(type == TYPE_VOICE_CALL){
voiceCallsPosition = position;
voiceCalls[voiceCallsPosition];
} else if(type == TYPE_RED_PACKET){
redPacketsPosition = position;
redPackets[redPacketsPosition];
} else if(type == TYPE_TRANSFER){
transfersPosition = position;
transfers[transfersPosition];
}
setState(() {
});
}
Widget getAddWidget(){
return Container(
//margin: EdgeInsets.only(top: 40, bottom: AppManager.getInstance().getBottom(context) + 20),
alignment: Alignment.center,
child: Center(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
//交叉轴的布局方式,对于column来说就是水平方向的布局方式
crossAxisAlignment: CrossAxisAlignment.center,
//就是字child的垂直布局方向,向上还是向下
verticalDirection: VerticalDirection.down,
children: [
_buildBottomItem(TYPE_ABLUM),
_buildBottomItem(TYPE_TAKE_PHOTO),
Offstage(
offstage: widget.viewType == CommonUtils.VIEW_TYPE_GROUP_CHAT,
child: _buildBottomItem(TYPE_VIDEO_CALL),
),
Offstage(
offstage: widget.viewType == CommonUtils.VIEW_TYPE_GROUP_CHAT,
child: _buildBottomItem(TYPE_VOICE_CALL),
),
],
),
Offstage(
offstage: widget.viewType == CommonUtils.VIEW_TYPE_GROUP_CHAT,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
//交叉轴的布局方式,对于column来说就是水平方向的布局方式
crossAxisAlignment: CrossAxisAlignment.center,
//就是字child的垂直布局方向,向上还是向下
verticalDirection: VerticalDirection.down,
children: [
_buildBottomItem(TYPE_RED_PACKET),
_buildBottomItem(TYPE_TRANSFER),
_buildBottomItem(-1),
_buildBottomItem(-1),
],
),
),
],
),
),
);
}
Widget _buildBottomItem(int type){
return Container(
alignment: Alignment.center,
margin: EdgeInsets.only(top: 10, bottom: 10),
child: GestureDetector(
child: _getBottomWidget(type),
onTap: (){
_changeStatus(type,0);
if(type == TYPE_ABLUM){
_openAblumPermission();
} else if(type == TYPE_TAKE_PHOTO){
_takePhotoPermission();
} else if (type == TYPE_VIDEO_CALL){
_openVideoCall();
} else if (type == TYPE_VOICE_CALL){
_openVoiceCall();
} else if (type == TYPE_RED_PACKET){
_openRedPacket();
} else if (type == TYPE_TRANSFER){
_openTransfer();
}
},
onTapCancel: (){
_changeStatus(type,0);
},
onTapDown: (data){
_changeStatus(type,1);
},
),
);
}
Widget _getBottomWidget(int type){
if(type == TYPE_ABLUM){
return Column(
children: [
Image.asset(ablums[ablumsPosition], width: 50, height: 50,),
const Text("相册"),
],
);
} else if(type == TYPE_TAKE_PHOTO){
return Column(
children: [
Image.asset(takePhotos[takePhotosPosition], width: 50, height: 50,),
const Text("拍照"),
],
);
} else if(type == TYPE_VIDEO_CALL){
return Column(
children: [
Image.asset(videoCalls[videoCallsPosition], width: 50, height: 50,),
const Text("视频通话"),
],
);
} else if(type == TYPE_VOICE_CALL){
return Column(
children: [
Image.asset(voiceCalls[voiceCallsPosition], width: 50, height: 50,),
const Text("语音通话"),
],
);
} else if(type == TYPE_RED_PACKET){
return Column(
children: [
Image.asset(redPackets[redPacketsPosition], width: 50, height: 50,),
const Text("红包"),
],
);
} else if(type == TYPE_TRANSFER){
return Column(
children: [
Image.asset(transfers[transfersPosition], width: 50, height: 50,),
const Text("转账"),
],
);
} else {
//空白占位符
return Column(
children: [
Container(
width: 50,
height: 50,
),
Text(""),
],
);
}
}
//打开相册权限
void _openAblumPermission() async {
bool isPhotosGranted = await Permission.photos.isGranted;
bool isPhotosDenied = await Permission.photos.isDenied;
if(isPhotosGranted){
_openAblum();
} else {
if(isPhotosDenied){
_openAblum();
} else {
//跳转到设置页面提示
_showPhotosConfirmationAlert(context);
}
}
}
// 为正常拍摄,请前往设备中的【设置】> 【隐私】> 【相机】中允许无他相机使用
_showPhotosConfirmationAlert(BuildContext context) {
showPlatformDialog(
context: context,
builder: (_) => BasicDialogAlert(
title: Text("无法使用相册"),
content: Text("为编辑照片,请前往设备中的【设置】> 【隐私】> 【照片】中允许${AppManager.getInstance().appName}使用"),
actions: <Widget>[
BasicDialogAction(
title: Text("知道了"),
onPressed: () {
Navigator.pop(context);
},
),
BasicDialogAction(
title: Text("去设置"),
onPressed: () {
// 跳转到系统设置页
AppSettings.openAppSettings();
},
),
],
),
);
}
//打开相册
void _openAblum() {
LogUtils.d("打开相册");
List<AssetEntity> selectedAssets = [];
AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: 1,
selectedAssets: selectedAssets,
),
).then((imageList) {
if(imageList == null){
return;
}
imageList as List<AssetEntity>;
for(int i = 0; i < imageList.length; i++){
AssetEntity ae = imageList[i];
ae.file.then((file) async {
String resultFilePath = file?.path??"";
_processVideoAndPicture(resultFilePath);
});
}
});
}
//拍照权限
_takePhotoPermission() async{
bool isCameraGranted = await Permission.camera.isGranted;
bool isCameraDenied = await Permission.camera.isDenied;
bool isMicrophoneGranted = await Permission.microphone.isGranted;
bool isMicrophoneDenied = await Permission.microphone.isDenied;
LogUtils.d("拍照:${isCameraGranted}, ${isCameraDenied} , ${isMicrophoneGranted} , ${isMicrophoneDenied}");
//如果2个权限都同意,直接打开
if(isCameraGranted && isMicrophoneGranted){
_takePhoto();
} else if(isCameraDenied && isMicrophoneDenied){
//如果2个权限都拒绝,直接打开
_takePhoto();
} else if(!isCameraGranted && isMicrophoneGranted){
_takePhoto();
} else if(isCameraGranted && !isCameraDenied){
//提示设置麦克风权限
String title = "无法使用麦克风";
String content = "为正常录制声音,请前往设备中的【设置】> 【隐私】> 【麦克风】中允许${AppManager.getInstance().appName}使用";
WnBaseDialog.showPermissionDialog(context, title: title, content: content);
} else if(!isCameraDenied){
String title = "无法使用相机";
String content = "为正常拍摄,请前往设备中的【设置】> 【隐私】> 【相机】中允许${AppManager.getInstance().appName}使用";
WnBaseDialog.showPermissionDialog(context, title: title, content: content);
} else if(!isMicrophoneDenied){
//提示设置麦克风权限
LogUtils.d("拍照7");
String title = "无法使用麦克风";
String content = "为正常录制声音,请前往设备中的【设置】> 【隐私】> 【麦克风】中允许${AppManager.getInstance().appName}使用";
WnBaseDialog.showPermissionDialog(context, title: title, content: content);
}
}
//拍照
void _takePhoto(){
LogUtils.d("拍照");
Feedback.forTap(context);
CameraPicker.pickFromCamera(
context,
pickerConfig: const CameraPickerConfig(enableRecording: true, textDelegate: CameraPickerTextDelegate()),
useRootNavigator: false
).then((resultAssetEntity) {
resultAssetEntity?.file.then((resultFile) {
LogUtils.d("2拍照返回:${resultFile?.path}");
_processVideoAndPicture(resultFile?.path??"");
});
});
}
//打开红包
void _openRedPacket() async {
var balanceStr = await Navigator.push(context, MaterialPageRoute(builder: (context) => RedPacketWidget()));
if(balanceStr == null){
return;
}
widget.refreshRedpacketAndTransfer(CommonUtils.CHAT_CONTENT_TYPE_REDPACKET, balanceStr);
}
//打开转账
void _openTransfer() async {
var balanceStr = await Navigator.push(context, MaterialPageRoute(builder: (context) => PaymentTransfer(toUser: widget.toChatId,)));
if(balanceStr == null){
return;
}
widget.refreshRedpacketAndTransfer(CommonUtils.CHAT_CONTENT_TYPE_TRANSFER, balanceStr);
}
//打开语音通话
void _openVoiceCall() async{
Navigator.push(context, MaterialPageRoute(builder: (context) => VideoCallWidget(videoPeerId: widget.toChatId, mediaFlag: CommonUtils.MEDIA_FLAG_VOICE,)));
}
//打开视频通话
void _openVideoCall(){
Navigator.push(context, MaterialPageRoute(builder: (context) => VideoCallWidget(videoPeerId: widget.toChatId, mediaFlag: CommonUtils.MEDIA_FLAG_VIDEO,)));
}
//处理图片和小视频(相册、拍照)
void _processVideoAndPicture(String resultFilePath) async {
if(resultFilePath == null || "" == resultFilePath){
return;
}
String messageId = UUID.getUUID();
if(CommonUtils.isImage(resultFilePath)){
//压缩图片完成再发送
String compressImagePath = await CompressImageUtils.compressFile(fileName: resultFilePath);
widget.sendMedialCallback(CommonUtils.CHAT_CONTENT_TYPE_IMG, compressImagePath,0 ,messageId);
widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_IMG, compressImagePath, "",0, messageId);
} else if(CommonUtils.isVideo(resultFilePath)){
/**
* 小视频发送流程,因为小视频比较大,压缩时间比较长。发送的视频,先本地优先显示,查看播放小视频
* 1、先复制一份小视频
* 2、生成缩略图显示
* 3、压缩小视频
* 4、删除复制的小视频
*/
//_testmp4(resultFilePath);
//最大100M的视频, 不是1024*1024*500 , 使用1000*100*500
int maxSize = 100000000;
int fileSize = File(resultFilePath).lengthSync();
if(fileSize > maxSize){
CommonToast.show(context, "上传视频大小不能超过100M", duration: 3);
return ;
}
//小视频生成缩略图大概5秒左右,先刷新站位图
widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, resultFilePath, "", 0, messageId);
String videoFormat = await getVideoFormat(resultFilePath);
File srcFile = File(resultFilePath);
String newVideoFileName = await FileUtils.getBaseFile("new_${DateUtil.getNowDateMs()}.mp4");
srcFile.copySync(newVideoFileName);
String thumbnailFileName = await FileUtils.getBaseFile("thum_${DateUtil.getNowDateMs()}.png");
//生成缩略图
await VideoThumbnail.thumbnailFile(video: resultFilePath, thumbnailPath: thumbnailFileName);
//获取视频时间
int second = await getVideoTime(resultFilePath);
//int size = File(resultFilePath).lengthSync();
//先刷新
widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, resultFilePath, thumbnailFileName, second, messageId);
//压缩完成再发送
MediaInfo? mediaInfo = await CompressVideoUtils.compressVideo(newVideoFileName);
String compressVideoPath = mediaInfo?.path??"";
//int csecond = await getVideoTime(resultFilePath);
//int csize = File(compressVideoPath).lengthSync();
widget.sendMedialCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, compressVideoPath, second, messageId);
}
}
//获取视频格式
Future<String> getVideoFormat(String resultFilePath) async {
String videoFormat = "";
final FlutterFFprobe _flutterFFprobe = FlutterFFprobe();
MediaInformation info = await _flutterFFprobe.getMediaInformation(resultFilePath);
if (info.getStreams() != null) {
List<StreamInformation>? streams = info.getStreams();
if (streams != null && streams.length > 0) {
for (var stream in streams) {
videoFormat = stream.getAllProperties()['codec_tag_string'];
}
}
}
return videoFormat;
}
//获取视频时间
Future<int> getVideoTime(String resultFilePath) async {
int time = 0;
final FlutterFFprobe _flutterFFprobe = FlutterFFprobe();
MediaInformation info = await _flutterFFprobe.getMediaInformation(resultFilePath);
if (info.getStreams() != null) {
String duration = info.getMediaProperties()?['duration'];
String size = info.getMediaProperties()?['size'];
double durationDouble = double.parse(duration);
time = durationDouble.toInt();
LogUtils.d("多媒体文件大小:${size}");
}
return time;
}
void _testmp4(String resultFilePath){
final FlutterFFprobe _flutterFFprobe = new FlutterFFprobe();
_flutterFFprobe.getMediaInformation(resultFilePath).then((info) {
LogUtils.d("测试视频信息:Media Information");
LogUtils.d("测试视频信息:Path: ${info.getMediaProperties()?['filename']}");
LogUtils.d("测试视频信息:Format: ${info.getMediaProperties()?['format_name']}");
LogUtils.d("测试视频信息:Duration: ${info.getMediaProperties()?['duration']}");
LogUtils.d("测试视频信息:Start time: ${info.getMediaProperties()?['start_time']}");
LogUtils.d("测试视频信息:Bitrate: ${info.getMediaProperties()?['bit_rate']}");
//LogUtils.d("测试视频信息:CodecTagString: ${info.getMediaProperties()?['codec_tag_string']}");
Map<dynamic, dynamic> tags = info.getMediaProperties()?['tags'];
/*if (tags != null) {
tags.forEach((key, value) {
LogUtils.d("Tag: " + key + ":" + value + "\n");
});
}*/
if (info.getStreams() != null) {
List<StreamInformation>? streams = info.getStreams();
if (streams != null && streams.length > 0) {
for (var stream in streams) {
LogUtils.d("测试视频信息:Stream id: ${stream.getAllProperties()['index']}");
LogUtils.d("Stream type: ${stream.getAllProperties()['codec_type']}");
LogUtils.d("Stream codec: ${stream.getAllProperties()['codec_name']}");
LogUtils.d("Stream full codec: ${stream.getAllProperties()['codec_long_name']}");
LogUtils.d("Stream format: ${stream.getAllProperties()['pix_fmt']}");
LogUtils.d("Stream width: ${stream.getAllProperties()['width']}");
LogUtils.d("Stream height: ${stream.getAllProperties()['height']}");
LogUtils.d("Stream bitrate: ${stream.getAllProperties()['bit_rate']}");
LogUtils.d("Stream sample rate: ${stream.getAllProperties()['sample_rate']}");
LogUtils.d("Stream sample format: ${stream.getAllProperties()['sample_fmt']}");
LogUtils.d("Stream channel layout: ${stream.getAllProperties()['channel_layout']}");
LogUtils.d("Stream sar: ${stream.getAllProperties()['sample_aspect_ratio']}");
LogUtils.d("Stream dar: ${stream.getAllProperties()['display_aspect_ratio']}");
LogUtils.d("Stream average frame rate: ${stream.getAllProperties()['avg_frame_rate']}");
LogUtils.d("Stream real frame rate: ${stream.getAllProperties()['r_frame_rate']}");
LogUtils.d("Stream time base: ${stream.getAllProperties()['time_base']}");
LogUtils.d("测试视频信息:Stream codec time base: ${stream.getAllProperties()['codec_time_base']}");
LogUtils.d("A测试视频信息:Stream codec_tag_string: ${stream.getAllProperties()['codec_tag_string']}");
/*Map<dynamic, dynamic> tags = stream.getAllProperties()['tags'];
if (tags != null) {
tags.forEach((key, value) {
LogUtils.d("Stream tag: " + key + ":" + value + "\n");
});
}*/
}
}
}
});
}
}
chat_content_view.dart实现:
/**
* Author : wangning
* Email : maoning20080809@163.com
* Date : 2022/9/24 12:09
* Description : 单聊内容控件
*/
class ChatContentView extends StatefulWidget {
final List<ChatBean> items;
final ChatBean chatBean;
final int index;
final UserBean? otherUserBean;
final UserBean? meUserBean;
final String account;
final deleteCallback;
final List<String>? addTimeList;
//点击语音播放回调
final clickVoiceCallback;
//点击领取转账,刷新页面
final refreshTransfer;
ChatContentView({required this.items, required this.account, required this.chatBean, required this.index,
required this.meUserBean, required this.otherUserBean, this.addTimeList, required this.deleteCallback, required this.clickVoiceCallback, required this.refreshTransfer});
@override
State<ChatContentView> createState() => _ChatContentViewState();
}
class _ChatContentViewState extends State<ChatContentView> {
//判断是否已经存在转换好的时间
@override
void initState() {
super.initState();
}
void goNewFriends(String account) async{
UserBean? userBean = await UserRepository.getInstance().findUserByAccount(account);
if(userBean != null){
Navigator.push(context,MaterialPageRoute(builder: (context)=>AddFriends(userBean: userBean!,)));
} else {
userBean = await UserRepository.getInstance().getUserServer(account);
Navigator.push(context,MaterialPageRoute(builder: (context)=>AddFriends(userBean: userBean!,)));
}
}
@override
Widget build(BuildContext context) {
String addTimeResult = _getAddTime("${widget.chatBean.addTime}");
bool isExistTime = isExistAddTime(addTimeResult);
if(!isExistTime){
widget.addTimeList?.add(addTimeResult);
}
//如果是最后一个,清除标志
if(widget.index == widget.items.length -1){
widget.addTimeList?.clear();
}
return Column(
children: [
Offstage(
offstage: isExistTime,
child: Container(
margin: EdgeInsets.only(top: 12),
child: Text("${addTimeResult}"),
),
),
Container(
child: widget.account == widget.chatBean.fromAccount
? fromAccountWidget()
: toAccountWidget(),
)
],
);
}
//小视频缩略图
Widget getCommonThumbnail(int second){
return CommonThumbnailWidget(
padding: EdgeInsets.only(
top: 0.0,
right: (widget.account == widget.chatBean.fromAccount ? 0.0 : 5.0),
left: (widget.account == widget.chatBean.toAccount ? 2.0 : 0.0)),
image: widget.chatBean.imgPathLocal??"",
second: second,
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => VideoPlayLocalPreview(widget.chatBean.videoLocal!)));
});
}
//显示我的
Widget fromAccountWidget(){
return Container(
margin: EdgeInsets.only(top: 8.0, left: 68.0, right: 8),
padding: EdgeInsets.all(2.0),
child: Row(
children: <Widget>[
Expanded(
child: GestureDetector(
onLongPress: (){
_showDeleteDialog(widget.chatBean);
},
onTap: () {
},
child: Stack(
alignment: AlignmentDirectional.bottomEnd,
children: [
//文本
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TEXT?meTextWidget():Container(),
//语言
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VOICE?meVoiceWidget():Container(),
//图片
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_IMG?CommonUtils.showBaseImage(widget.chatBean.imgPathLocal??"", width:100, height:200, angle:1, onPressed: (data){
Navigator.push(context,MaterialPageRoute(builder: (context)=>CommonImagePreview(fileName: data)));
}):Container(),
//小视频
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VIDEO?getCommonThumbnail(widget.chatBean.second??0):Container(),
//红包
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_REDPACKET?meRedpacketWidget():Container(),
//转账
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TRANSFER?meTransferWidget():Container(),
],
),
),
),
//userImage
Container(
padding: EdgeInsets.only(left: 6, right: 6),
child: GestureDetector(
onTap: (){
Navigator.push(context,MaterialPageRoute(builder: (context)=>ContactsDetails(toChatId: widget.chatBean.fromAccount??"")));
},
child: CommonAvatarView.showBaseImage(widget.meUserBean?.avatar??"", 38, 38),
),
),
],
),
);
}
//显示好友
Widget toAccountWidget(){
return Container(
margin: EdgeInsets.only(top: 8.0, right: 68.0),
padding: EdgeInsets.all(2.0),
child: Row(
//crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//userImage,
Container(
margin: EdgeInsets.only(left: 6, right: 6),
child: GestureDetector(
onTap : (){
Navigator.push(context,MaterialPageRoute(builder: (context)=>ContactsDetails(toChatId: widget.otherUserBean?.account??"")));
},
child: CommonAvatarView.showBaseImage(widget.otherUserBean?.avatar??"", 38, 38),
),
),
Expanded(
child: GestureDetector(
onLongPress: (){
_showDeleteDialog(widget.chatBean);
},
onTap: () {
},
child: Stack(
alignment: AlignmentDirectional.centerStart,
children: [
//文本
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TEXT?toTextWidget():Container(),
//语音
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VOICE?toVoiceWidget():Container(),
//图片
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_IMG?CommonUtils.showBaseImage(widget.chatBean.imgPathLocal??"", width:100, height:200, angle:1, onPressed: (data){
Navigator.push(context,MaterialPageRoute(builder: (context)=>CommonImagePreview(fileName: data)));
}):Container(),
//小视频
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VIDEO?getCommonThumbnail(widget.chatBean.second??0):Container(),
//红包
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_REDPACKET?toRedpacketWidget():Container(),
//转账
widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TRANSFER?toTransferWidget():Container(),
],
)
),
),
/**/
],
),
);
}
//打开红包对话框
void _onOpenRedpacket(){
}
//朋友的文本
Widget toTextWidget(){
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 10.0),
decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Color(0xFFEDEDED)),
child: Text(
widget.chatBean.content??"",
textAlign: TextAlign.left,
style: TextStyle(color: Colors.black, fontSize: 20.0),
),
)
],
);
}
//朋友的语音
Widget toVoiceWidget(){
return InkWell(
onTap: () {
setState(() {
widget.chatBean.isPlayVoice = true;
});
LogUtils.d("点击语音");
AudioPlayer.getInstance().playLocal(widget.chatBean.voiceLocal??"", callback: (data){
LogUtils.d("录音回调:${data}");
setState(() {
widget.chatBean.isPlayVoice = false;
});
});
},
child : Container(
width: 120,
height: 40,
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 2.0),
decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Color(0xFFEDEDED)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
widget.chatBean.isPlayVoice?Image.asset("assets/chat/wn_chat_other_animator.gif", height: 34,):Image.asset("assets/chat/wn_chat_other_volume_3.png", height: 34,),
SizedBox(width: 4,),
Text("${widget.chatBean.second}''"),
],
),
)
);
}
//朋友的红包
Widget toRedpacketWidget(){
return GestureDetector(
onTap: (){
if(widget.chatBean.isClick == 1){
Navigator.push(context, MaterialPageRoute(builder: (context) => ReceiveRedpacketSuccess(fromUser: widget.meUserBean?.account??"", toUser: widget.otherUserBean?.account??"", balance: widget.chatBean?.content??"", addTime: widget.chatBean.addTime??"",)));
} else {
showRedPacket(context, _onOpenRedpacket, widget.otherUserBean?.account, widget.chatBean?.content??"", widget.index);
}
},
child: Opacity(
opacity: widget.chatBean.isClick == 1 ? 0.6 :1,
child: Container(
child: Stack(
children: [
toRedpacketBackground(),
Positioned(
left: 38, top: 20,
child:CommonUtils.getBaseIconPng("wc_redpacket_icon", width: 40, height: 40),
),
Positioned(
left: 88, top: 30,
child: Text("恭喜发财,大吉大利", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),),
),
Positioned(
left: 88, top: 50,
child: Container(
margin: EdgeInsets.only(top:10),
width: 120,
height: 1,
color: Colors.white,
),
),
Positioned(
left: 38, bottom: 14,
child:Text("私人红包", style: TextStyle(fontSize:12, color: Colors.white38),),
),
],
),
),
),
);
}
//处理转账
void _processTransferDetails() async{
var data = await Navigator.push(context, MaterialPageRoute(builder: (context) => TransferDetails(toUser: widget.chatBean?.toAccount??"", balance: double.parse(widget.chatBean?.content??""), messageId: widget.chatBean?.messageId??"")));
if(data != null && data > 0){
widget.refreshTransfer(widget.index);
}
}
//朋友的转账
Widget toTransferWidget(){
return GestureDetector(
onTap: (){
_processTransferDetails();
},
child: Opacity(
opacity: widget.chatBean.isClick == 1 ? 0.6 :1,
child: Container(
child: Stack(
children: [
toRedpacketBackground(),
Positioned(
left: 42, top: 20,
child:CommonUtils.getBaseIconPng("wc_chat_transfer_icon", width: 40, height: 40),
),
Positioned(
left: 98, top: 14,
child: Text("¥${double.parse(widget.chatBean.content??'0').toStringAsFixed(2)}", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),),
),
Positioned(
left: 98, top: 40,
child: Text("请收款", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),),
),
Positioned(
left: 98, top: 54,
child: Container(
margin: EdgeInsets.only(top:10),
width: 120,
height: 1,
color: Colors.white,
),
),
Positioned(
left: 38, bottom: 14,
child:Text("私人转账", style: TextStyle(fontSize: 12, color: Colors.white38),),
),
],
),
),
),
);
}
Widget toRedpacketBackground(){
return CustomPaint(
painter: RedPacketOther(
strokeColor: Color(0xFFf58220),
paintingStyle:
PaintingStyle.fill,
),
child: Container(
height: 100,
width: 280,
),
);
}
//我的文本
Widget meTextWidget(){
return Column(
// Column被Expanded包裹起来,使其内部文本可自动换行
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 10.0),
decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Color(0xFF9EEA6A),),
child: Text(
widget.chatBean.content??"",
textAlign: TextAlign.left,
style: TextStyle(color: Colors.black, fontSize: 20.0),
),
)
],
);
}
//我的语言
Widget meVoiceWidget(){
return InkWell(
onTap: () {
widget.clickVoiceCallback(true);
setState(() {
widget.chatBean.isPlayVoice = true;
});
//点击语音
AudioPlayer.getInstance().playLocal(widget.chatBean.voiceLocal??"", callback: (data){
//录音回调
setState(() {
widget.chatBean.isPlayVoice = false;
});
});
},
child : Container(
width: 120,
height: 40,
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 2.0),
decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(1.0),),color: Color(0xFF9EEA6A),),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("${widget.chatBean.second}''"),
SizedBox(width: 4,),
widget.chatBean.isPlayVoice?Image.asset("assets/chat/wn_chat_me_animator.gif", height: 24,):Image.asset("assets/chat/wn_chat_me_volume_3.png", height: 24,),
],
),
)
);
}
//我的红包
Widget meRedpacketWidget(){
return GestureDetector(
onTap: (){
//点击红包
Navigator.push(context, MaterialPageRoute(builder: (context) => ReceiveRedpacketSuccess(fromUser: widget.meUserBean?.account??"", toUser: widget.otherUserBean?.account??"", balance: widget.chatBean?.content??"", addTime: widget.chatBean.addTime??"",)));
},
child: Container(
child: Stack(
children: [
meRedpacketBackground(),
Positioned(
left: 20, top: 20,
child:CommonUtils.getBaseIconPng("wc_redpacket_icon", width: 40, height: 40),
),
Positioned(
left: 70, top: 30,
child: Text("恭喜发财,大吉大利", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),),
),
Positioned(
left: 70, top: 50,
child: Container(
margin: EdgeInsets.only(top:10),
width: 120,
height: 1,
color: Colors.white,
),
),
Positioned(
left: 20, bottom: 14,
child:Text("私人红包", style: TextStyle(fontSize: 12, color: Colors.white38),),
),
],
),
),
);
}
//我的转账
Widget meTransferWidget(){
return GestureDetector(
onTap: (){
//点击转账
Navigator.push(context, MaterialPageRoute(builder: (context) => TransferDetails(toUser: widget.otherUserBean?.account??"", balance: double.parse(widget.chatBean?.content??""), messageId: widget.chatBean?.messageId??"")));
},
child: Container(
child: Stack(
children: [
meRedpacketBackground(),
Positioned(
left: 20, top: 20,
child:CommonUtils.getBaseIconPng("wc_chat_transfer_icon", width: 40, height: 40),
),
Positioned(
left: 70, top: 14,
child: Text("¥${double.parse(widget.chatBean.content??'0').toStringAsFixed(2)}", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),),
),
Positioned(
left: 70, top: 40,
child: Text("你发起了一笔转账", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),),
),
Positioned(
left: 70, top: 54,
child: Container(
margin: EdgeInsets.only(top:10),
width: 120,
height: 1,
color: Colors.white,
),
),
Positioned(
left: 20, bottom: 14,
child:Text("私人转账", style: TextStyle(fontSize: 12, color: Colors.white38),),
),
],
),
),
);
}
Widget meRedpacketBackground(){
return CustomPaint(
painter: RedPacketMe(
strokeColor: Color(0xFFf58220),
paintingStyle:
PaintingStyle.fill,
),
child: Container(
height: 100,
width: 280,
),
);
}
bool isExistAddTime(String addTimeResult){
return widget.addTimeList?.contains(addTimeResult)??false;
}
String _getAddTime(String addTime){
return WnTimeUtils.timeUtils(startTime: addTime);
}
//删除对话框
Future<void> _showDeleteDialog(ChatBean chatBean) async {
return showDialog<Null>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('确定要删除该消息吗?', style: new TextStyle(fontSize: 17.0)),
actions: <Widget>[
MaterialButton(
child: Text('取消'),
onPressed: (){
LogUtils.d("确定取消");
Navigator.of(context).pop();
},
),
MaterialButton(
child: Text('确定'),
onPressed: (){
LogUtils.d("确定删除");
Navigator.pop(context);
//_deleteContacts(contactsBeanComb);
_deleteChatBean(chatBean);
},
)
],
);
}
);
}
//删除消息
_deleteChatBean(ChatBean chatBean) async{
int id = chatBean.id??0;
await ChatRepository.getInstance().deleteChatById(id);
widget.deleteCallback(true);
}
}
chat_voice_view.dart实现:
/**
* Author : wangning
* Email : maoning20080809@163.com
* Date : 2022/9/24 12:21
* Description : 语音动画
*/
class ChatVoiceView extends StatefulWidget{
//刷新列表
final refreshMediaCallback;
//发送信息
final sendMedialCallback;
//停止语音播放
final stopPlayVoiceCallback;
ChatVoiceView({required this.refreshMediaCallback, required this.sendMedialCallback, required this.stopPlayVoiceCallback});
@override
State<StatefulWidget> createState() => ChatVoiceState();
}
class ChatVoiceState extends State<ChatVoiceView>{
// 倒计时总时长
int _countTotal = 60;
double starty = 0.0;
double offset = 0.0;
bool isUp = false;
String textShow = "按住 说话";
String toastShow = "手指上滑,取消发送";
String voiceIco = CommonUtils.getChatUrlPng("voice_volume_1");
///默认隐藏状态
bool voiceState = true;
Timer? _timer;
int _count = 0;
//录音总数
int _soundSecond = 0;
OverlayEntry? overlayEntry;
final _audioRecorder = Record();
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
_audioRecorder.dispose();
_timer?.cancel();
}
@override
Widget build(BuildContext context) {
return getPressVoiceWidget();
}
//打开录音权限
void _openMicrophonePermission(details) async {
bool isMicrophoneGranted = await Permission.microphone.isGranted;
bool isMicrophoneDenied = await Permission.microphone.isDenied;
if(isMicrophoneGranted){
_onLongPressStart(details);
} else {
if(isMicrophoneDenied){
PermissionUtils.requestMicrophonePermission();
} else {
//跳转到设置页面提示
_showMicrophoneConfirmationAlert(context);
}
}
}
//无法使用相机
// 为正常拍摄,请前往设备中的【设置】> 【隐私】> 【相机】中允许无他相机使用
_showMicrophoneConfirmationAlert(BuildContext context) {
showPlatformDialog(
context: context,
builder: (_) => BasicDialogAlert(
title: Text("无法使用麦克风"),
content: Text("为正常录制声音,请前往设备中的【设置】> 【隐私】> 【麦克风】中允许${AppManager.getInstance().appName}使用"),
actions: <Widget>[
BasicDialogAction(
title: Text("知道了"),
onPressed: () {
Navigator.pop(context);
},
),
BasicDialogAction(
title: Text("去设置"),
onPressed: () {
// 跳转到系统设置页
AppSettings.openAppSettings();
},
),
],
),
);
}
void _onLongPressStart(details){
starty = details.globalPosition.dy;
_timer = Timer.periodic(Duration(milliseconds: 1000), (t) {
_count++;
if (_count == _countTotal) {
hideVoiceView();
}
});
showVoiceView();
}
void _onLongPressEnd() async {
bool isMicrophoneGranted = await Permission.microphone.isGranted;
if(isMicrophoneGranted){
hideVoiceView();
}
}
void _onLongPressMoveUpdate(details) async {
bool isMicrophoneGranted = await Permission.microphone.isGranted;
if(isMicrophoneGranted){
offset = details.globalPosition.dy;
moveVoiceView();
}
}
Widget getPressVoiceWidget() {
return GestureDetector(
onLongPressStart: (details) {
_openMicrophonePermission(details);
},
onLongPressEnd: (details) {
_onLongPressEnd();
},
onLongPressMoveUpdate: (details) {
_onLongPressMoveUpdate(details);
},
child: Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(top: 10.0, bottom: 10.0),
decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Colors.black12),
width: double.infinity,
child: Text("按住 说话"),
),
);
}
///显示录音悬浮布局
buildOverLayView(BuildContext context) {
if (overlayEntry == null) {
overlayEntry = new OverlayEntry(builder: (content) {
return CustomOverlay(
icon: Column(
children: <Widget>[
Container(
margin: const EdgeInsets.only(top: 10),
child: _countTotal - _count < 11
? Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 15.0),
child: Text(
(_countTotal - _count).toString(),
style: TextStyle(
fontSize: 70.0,
color: Colors.white,
),
),
),
)
: new Image.asset(
voiceIco,
width: 100,
height: 100,
//package: 'flutter_plugin_record',
),
),
Container(
// padding: const EdgeInsets.only(right: 20, left: 20, top: 0),
child: Text(
toastShow,
style: TextStyle(
fontStyle: FontStyle.normal,
color: Colors.white,
fontSize: 14,
),
),
)
],
),
);
});
Overlay.of(context)!.insert(overlayEntry!);
}
}
showVoiceView() {
setState(() {
textShow = "松开结束";
voiceState = false;
});
///显示录音悬浮布局
buildOverLayView(context);
hidePlayVoiceList();
start();
playAnimation();
}
//隐藏播放列表,停止播放录音
hidePlayVoiceList(){
widget.stopPlayVoiceCallback();
}
hideVoiceView() async {
if (_timer!.isActive) {
if (_count < 1) {
CommonToast.showView(
context: context,
msg: '说话时间太短',
icon: Text(
'!',
style: TextStyle(fontSize: 80, color: Colors.white),
));
isUp = true;
}
_timer?.cancel();
_soundSecond = _count;
_count = 0;
}
setState(() {
textShow = "按住 说话";
voiceState = true;
});
stop();
if (overlayEntry != null) {
overlayEntry?.remove();
overlayEntry = null;
}
if (isUp) {
LogUtils.d("取消发送");
File file = File(fileName);
if(fileName != null && file.existsSync()){
file.deleteSync();
}
} else {
LogUtils.d("进行发送 ${_soundSecond}");
await _audioRecorder.stop();
String messageId = UUID.getUUID();
widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VOICE, fileName, "", _soundSecond, messageId);
widget.sendMedialCallback(CommonUtils.CHAT_CONTENT_TYPE_VOICE, fileName, _soundSecond, messageId);
}
}
moveVoiceView() {
setState(() {
isUp = starty - offset > 100 ? true : false;
if (isUp) {
textShow = "松开手指,取消发送";
toastShow = textShow;
} else {
textShow = "松开结束";
toastShow = "手指上滑,取消发送";
}
});
}
String fileName = "";
///开始语音录制的方法
void start() async {
try {
fileName = await FileUtils.getBaseFile("voice_${DateUtil.getNowDateMs()}.m4a");
File file = File(fileName);
if(!file.existsSync()){
file.createSync();
}
if (await _audioRecorder.hasPermission()) {
final isSupported = await _audioRecorder.isEncoderSupported(
AudioEncoder.aacLc,
);
LogUtils.d("录音文件:${fileName}");
await _audioRecorder.start(path: fileName, encoder: AudioEncoder.aacLc, bitRate: 262144, samplingRate: 48000);
}
} catch (e) {
LogUtils.d("${e}");
}
}
Timer? periodicTimer;
//播放录音动画
void playAnimation(){
int i =0;
periodicTimer = Timer.periodic(
const Duration(milliseconds: 150),(timer) {
i++;
if (i == 1) {
voiceIco = CommonUtils.getChatUrlPng("voice_volume_2");
} else if (i == 2) {
voiceIco = CommonUtils.getChatUrlPng("voice_volume_3");
} else if (i == 3) {
voiceIco = CommonUtils.getChatUrlPng("voice_volume_4");
} else if (i == 4) {
voiceIco = CommonUtils.getChatUrlPng("voice_volume_5");
} else if (i == 5) {
voiceIco = CommonUtils.getChatUrlPng("voice_volume_6");
} else if (i == 6) {
voiceIco = CommonUtils.getChatUrlPng("voice_volume_7");
} else {
i = 0;
}
if (overlayEntry != null) {
overlayEntry!.markNeedsBuild();
}
},
);
}
///停止语音录制的方法
void stop() async{
await _audioRecorder.stop();
periodicTimer?.cancel();
}
}


















