• Flutter高仿微信-第29篇-单聊


    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 with TickerProviderStateMixin {
    
      String _toChatId;
      _HomeChatPageState(this._toChatId);
      //好友账户
      UserBean? _otherUserBean;
      //我的账户
      UserBean? _meUserBean;
    
      List addTimeList = [];
      List 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) {
          if(mounted){
            setState(() {
              chatBean as ChatBean;
              items.add(chatBean);
            });
          }
        });
    
        chatEvent = eventBus.on((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: [
                  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: [
                        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 result = HashMap();
          result["from_account"] = _toChatId;
          eventBus.emit(BaseEvent(BaseEvent.TYPE_UPDATE_CHAT_STATUS, result: result));
        }
    
      }
    
      // 下拉刷新
      Future _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 result = HashMap();
        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 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 createState() => _ChatAddState();
    
    }
    
    class _ChatAddState extends State{
    
      @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: [
              BasicDialogAction(
                title: Text("知道了"),
                onPressed: () {
                  Navigator.pop(context);
                },
              ),
              BasicDialogAction(
                title: Text("去设置"),
                onPressed: () {
                  // 跳转到系统设置页
                  AppSettings.openAppSettings();
                },
              ),
            ],
          ),
        );
      }
    
      //打开相册
      void _openAblum() {
        LogUtils.d("打开相册");
        List selectedAssets = [];
        AssetPicker.pickAssets(
          context,
          pickerConfig: AssetPickerConfig(
            maxAssets: 1,
            selectedAssets: selectedAssets,
          ),
        ).then((imageList) {
          if(imageList == null){
            return;
          }
          imageList as List;
          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 getVideoFormat(String resultFilePath) async {
        String videoFormat = "";
        final FlutterFFprobe _flutterFFprobe =  FlutterFFprobe();
        MediaInformation info = await _flutterFFprobe.getMediaInformation(resultFilePath);
        if (info.getStreams() != null) {
          List? streams = info.getStreams();
          if (streams != null && streams.length > 0) {
            for (var stream in streams) {
              videoFormat =  stream.getAllProperties()['codec_tag_string'];
            }
          }
        }
        return videoFormat;
      }
    
      //获取视频时间
      Future 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 tags = info.getMediaProperties()?['tags'];
          /*if (tags != null) {
            tags.forEach((key, value) {
              LogUtils.d("Tag: " + key + ":" + value + "\n");
            });
          }*/
    
          if (info.getStreams() != null) {
            List? 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 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 items;
      final ChatBean chatBean;
      final int index;
      final UserBean? otherUserBean;
      final UserBean? meUserBean;
      final String account;
      final deleteCallback;
      final List? 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 createState() => _ChatContentViewState();
    
    }
    
    
    class _ChatContentViewState extends State {
      //判断是否已经存在转换好的时间
      @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: [
              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: [
              //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: [
            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: [
            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 _showDeleteDialog(ChatBean chatBean) async {
        return showDialog(
            context: context,
            barrierDismissible: false,
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text('确定要删除该消息吗?', style: new TextStyle(fontSize: 17.0)),
                actions: [
                  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 createState() =>  ChatVoiceState();
    
    }
    
    class ChatVoiceState extends State{
    
      // 倒计时总时长
      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: [
              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: [
                  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();
      }
    
    }
  • 相关阅读:
    python解释def __init__(self, *args, **kwargs)
    【GitLab私有仓库】在Linux上用Gitlab搭建自己的私有库并配置cpolar内网穿透
    冲刺备战金九银十,奉上万字面经[Java一线大厂高岗面试题解合集]
    CESM2代码下载
    【CSS3】media,伪类与伪元素,outline,font-face,resize,svg,多列布局
    Jenkins--部署--01--打包Maven项目为Docker镜像并运行
    (一)NanoPi m4v2 安装 Home Assistant(含 supervisor)
    SpringCloud-Gateway
    类注释规范
    【多线程与高并发原理篇:3_java内存模型】
  • 原文地址:https://blog.csdn.net/maoning20080808/article/details/128004347