flutter 聊天界面+表情图片

网上找了找 零零碎碎有一些文章 没找到一个整体的 自己做完记录一下 防止忘了

大体就是这样

聊天气泡用的是 https://blog.csdn.net/oterminator12/article/details/105790961 这个文章看到的

然后表情用的是 https://blog.csdn.net/qq_36676433/article/details/104756685 这个文章看到的

 

整体结构及底部输入/表情选择部分

body下的结构主要为最外层Column,然后聊天部分用Flexible组件套住 为了保证聊天框部分可以保持在底部

                Flexible(
                  child: Container(
                    color: Color(0xfff5f5f7),
                    width: ScreenUtil().setWidth(750),
                    height: ScreenUtil().setHeight(1334),
                    child: EasyRefresh(
                      controller: _controller,
                      child: _comment(),
                      footer: comIsNone ? noneFooterStyle : footerStyle,
                      onRefresh: (){

                      },
                    ),
                  ),
                )
//_comment() 是自定义的一个widget 其实就是一个listview 用来承载聊天内容列表
//EasyRefresh是下拉加载 上拉刷新功能的插件 可用可不用

我的聊天输入部分是这么做的 与Flexible同级

                 Container(
                  padding: EdgeInsets.only(left: ScreenUtil().setWidth(15),top: ScreenUtil().setHeight(10),bottom: ScreenUtil().setHeight(10),right:ScreenUtil().setWidth(15) ),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    boxShadow: [
                      BoxShadow(
                        color: Color(0xFFF3F3F3),
                        offset: Offset(0.0, -5.0), //阴影xy轴偏移量
                        blurRadius: 15.0, //阴影模糊程度
                        spreadRadius: 0.4, //阴影扩散程度
                      ),
                    ],
                  ),
                  child:Row(
                    crossAxisAlignment: CrossAxisAlignment.end,
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: <Widget>[
                      ConstrainedBox(
                        constraints: BoxConstraints(
                          maxHeight: ScreenUtil().setHeight(500),
                          minHeight: ScreenUtil().setHeight(50),
                        ),
                        child: Container(
                          width: ScreenUtil().setWidth(565),
                          color: Color(0xfff5f5f7),
                          child:ConstrainedBox(
                            constraints: BoxConstraints(
                              maxHeight: ScreenUtil().setHeight(500),
                              minHeight: ScreenUtil().setHeight(50),
                            ),
                            child:TextField(
                              textInputAction: TextInputAction.send,
                              controller: _titleController,
                              focusNode: focusNode,
                              maxLines: null,
                              keyboardType: TextInputType.multiline,
                              decoration: InputDecoration(
                                isCollapsed:true,
                                hintStyle: TextStyle(fontSize: ScreenUtil().setSp(27)),
                                contentPadding:EdgeInsets.only(left: ScreenUtil().setWidth(10), right: ScreenUtil().setWidth(10),top: ScreenUtil().setWidth(10),bottom: ScreenUtil().setWidth(10)),
                                border: InputBorder.none
                              ),
                              onSubmitted: (text){
                                if( text.trim() == '' ){
                                  toast('请输入内容');
                                }else{
                                  _titleController.clear();
                                  widgetLists.add(
                                    BubbleShow(
                                      contentType:1,
                                      senderType:2,
                                      headPortrait:minePhoto,
                                      text:text,
                                      photo:'',
                                    ),
                                  );
                                  setState(() {});
                                }
                              },
                            ),
                          ),
                        ),
                      ),

                      //表情按钮
                      Container(
                        alignment: Alignment.center,
                        child: InkWell(
                          onTap: () {
                            focusNode.unfocus();
                            if( expShow ){
                              setState(() {
                                expShow = false;
                              });
                            }else{
                              setState(() {
                                expShow = true;
                              });
                            }
                          },
                          child:  Icon(
                            IconData(0xe62b, fontFamily: 'MyIcons'),
                            color: Color(0xff666666),
                            size: ScreenUtil().setSp(50),
                          )
                        ),
                      ),

                      //图片
                      Container(
                        alignment: Alignment.center,
                        child: InkWell(
                          onTap: () {
                            focusNode.unfocus();
                            if( functionShow ){
                              setState(() {
                                functionShow = false;
                              });
                            }else{
                              setState(() {
                                functionShow = true;
                              });
                            }
                          },
                          child:  Icon(
                            IconData(0xe7e1, fontFamily: 'MyIcons'),
                            color: Color(0xff666666),
                            size: ScreenUtil().setSp(50),
                          )
                        ),
                      ),
                    ],
                  ),
                ),

然后表情或者图片的下部扩展部分用Offstage组件嵌套上 他是靠offstage属性控制是否展示 true为隐藏 依旧是与flexible同级 图片功能也是类似写法

                 Container(
                  child: Offstage(
                    offstage: expShow,
                    child: Container(
                      height: ScreenUtil().setWidth(300),
                      child: WeChatExpression((Expression expression){
                        _selectText = _titleController.text;
                        _selectText += '[${expression.name}]';
                        _titleController.text = _selectText;
                        _titleController.selection = TextSelection(
                          baseOffset: _selectText.length,
                          extentOffset: _selectText.length,
                        );
                        setState(() {});
                      }),
                    ),
                  ),
                )

body的最外层应该先放一个GestureDetector组件 点击事件里写好失去焦点 隐藏表情框等方法

 

聊天列表展示

聊天主要是两种 一种是照片的呈现 比较简单 就是image组件即可

另一种是文本与表情的配合展示,表情都是图片,他那个文章里有说到 也可以下载他的代码

我主要是将聊天气泡与那个文本表情结合在一起 重新写了个类

遇到的问题就是合成在一起内容不换行的问题 之后用ConstrainedBox嵌套住设置了最大宽度解决了  下面如果有不存在的类 需要到前面提到的两个文章 把他们的代码拿过来用下

//聊天展示类
class BubbleShow extends StatelessWidget {
  //contentType 内容类型 文字/表情-1  图片-2
  //senderType 发送人类型 对方-1 自己-2
  //headPortrait 头像url
  //text 聊天内容文本
  //photo 图片路径
  final contentType;
  final senderType;
  final headPortrait;
  final text;
  final photo;

  const BubbleShow({Key key, this.contentType,this.senderType,this.headPortrait,this.text,this.photo}):super(key: key);
  @override
  Widget build(BuildContext context) {
    return senderType==1?Container(
      margin: EdgeInsets.only(top:ScreenUtil().setWidth(20)),
      alignment: Alignment.topLeft,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.start,
        children:<Widget>[
          Container(
            child: ClipOval(
              child: SizedBox(
                width: ScreenUtil().setWidth(70),
                height: ScreenUtil().setWidth(70),
                child:
                    Image.network(headPortrait, fit: BoxFit.cover),
              ),
            ),
            margin: EdgeInsets.only(right: ScreenUtil().setWidth(15)),
            width: ScreenUtil().setWidth(70),
            height: ScreenUtil().setWidth(70),
          ),
          contentType==1?ConstrainedBox(
            constraints: BoxConstraints(
              maxWidth: ScreenUtil().setWidth(550),
            ),
            child:Bubble(
              direction:BubbleDirection.left,
              color: Colors.white,
              child:ExpressionText(text,null)
            ),
          ):InkWell(
            child:ConstrainedBox(
              constraints: BoxConstraints(
                maxHeight: ScreenUtil().setWidth(500),
              ),
              child:Image.file(
                File(photo),
                width: ScreenUtil().setWidth(150),
                fit: BoxFit.fitWidth,
              ),
            ),
            onTap: (){
              Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) =>
                        ArticleImage(src:photo)),
              );
            },
          ),
        ]
      ),
    ):Container(
      margin: EdgeInsets.only(top:ScreenUtil().setWidth(20)),
      alignment: Alignment.topRight,
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.end,
        children:<Widget>[
          contentType==1?ConstrainedBox(
            constraints: BoxConstraints(
              maxWidth: ScreenUtil().setWidth(550),
            ),
            child:Bubble(
              direction:BubbleDirection.right,
              color: Colors.lightBlue[100],
              child:ExpressionText(text,null)
            ),
          ):InkWell(
            child:ConstrainedBox(
              constraints: BoxConstraints(
                maxHeight: ScreenUtil().setWidth(500),
              ),
              child:Image.file(
                File(photo),
                width: ScreenUtil().setWidth(150),
                fit: BoxFit.fitWidth,
              ),
            ),
            onTap: (){
              Navigator.push(
                context,
                MaterialPageRoute(
                    builder: (context) =>
                        ArticleImage(src:photo)),
              );
            },
          ),
          Container(
            child: ClipOval(
              child: SizedBox(
                width: ScreenUtil().setWidth(70),
                height: ScreenUtil().setWidth(70),
                child:
                    Image.network(headPortrait, fit: BoxFit.cover),
              ),
            ),
            margin: EdgeInsets.only(left: ScreenUtil().setWidth(15)),
            width: ScreenUtil().setWidth(70),
            height: ScreenUtil().setWidth(70),
          ),
        ]
      ),
    );
  }
}

 

然后再说一下聊天展示部分,就是上面提到的_comment()

一开始为了做到聊天内容始终看到最新的 想到用了listview的reverse属性,但是如果不够一屏聊天记录,会将他们显示在下方而不是从上往下展示,之后靠测试查询发现是EasyRefresh导致的,去他的github上可以查到相关问题,官方说了解决方案,但是他说体验也不是很好,我没尝试那个,后来看到有人说可以用ScrollController,我就选用了这种方法对listview进行手动移动

ScrollController scrollController = ScrollController();

@override
  void initState() {
    super.initState();
    
    //build执行完后会执行
    var widgetsBinding=WidgetsBinding.instance;
    widgetsBinding.addPostFrameCallback((callback){
      time=Timer.periodic(
	      Duration(milliseconds: 3000),
	      (t){  
          if(scrollController.hasClients) {
            scrollController.jumpTo(scrollController.position.maxScrollExtent);
          }
	        t.cancel();
	      }
	    );
    });
  }

有一个很蠢的事,build完成可能不代表listview构建完成吧.. 如果直接执行jumpto操作,会报错scrollController没有用在widget上.. 然后我先暂时改成了等个三秒,在判断是否作用在了widget上,实际上可以有更好的写法,这样写也挺鸡肋的

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值