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<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();
  }

}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

六毛六66

你的鼓励是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值