GitHub:https://github.com/baiyuliang/Qrobot_Flutter

经过前几篇的学习,我们对Flutter基本的布局知识有了一定的了解(当然,这需要大家多练习,多动手,才能熟练掌握),那么本篇我们将实现一个简单的聊天界面!

用Flutter实现小Q聊天机器人(四)_Text


仍然先用最简单的代码实现:

class _MyHomePageState extends State<MyHomePage> {
  var textEditingController = TextEditingController();
  var messageList = List<Message>();

  @override
  void initState() {
    super.initState();
    for (var i = 0; i < 10; i++) {
      messageList.add(Message("你好啊$i"));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Column(
          children: <Widget>[
            Flexible(
                child: ListView.builder(
              itemBuilder: (context, index) => Text(messageList[index].content),
              itemCount: messageList.length,
            )),
            TextField(
              controller: textEditingController,
              decoration: new InputDecoration.collapsed(hintText: '请输入消息'),
            )
          ],
        ));
  }

}

Message 消息类:

class Message {
  var content;

  Message(content) {
    this.content = content;
  }
}

布局方式:Column[ListView,TextField],也就是ListView和输入框竖直排列,等同于LinearLayout android:orientation=“vertical”,我们再看其中的Flexible,Flexible和Expanded都是让组件有伸缩能力的工具,可以理解为比重吧,类似于安卓中的layout_weight,那么上述代码的意思就是让listview在竖直方向填满可用空间,我们可以对比一下安卓的实现方法就理解了:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

接着看TextField,我们在上图中看到谷歌键盘右下角有一个符号(其它键盘是回车按钮),这个是自动为输入框添加的一个监听事件,如果我们想让其作为发送按钮,那么就要实现TextField的onSubmitted属性:

onSubmitted: sendMessage,
sendMessage(String text) {
    if (text.isEmpty) return;
    print(text);
    setState(() {
      messageList.add(Message(text));
    });
    textEditingController.clear();
  }

注意:onSubmitted后面的方法可以省略掉sendMessage方法传入的参数text,系统会自动将输入框内容传入方法,更新UI则用到了setState方法,在setState方法中改变数据messageList后,listview便会得到更新!

接下来我们再做复杂一点,实现简单的对话功能,即我们发送一句话,让小Q模拟回答,一左一右布局:

用Flutter实现小Q聊天机器人(四)_List_02


仍然是最简单的代码实现:

class _MyHomePageState extends State<MyHomePage> {
  var textEditingController = TextEditingController();
  var messageList = List<Message>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Column(
          children: <Widget>[
            Flexible(
                child: ListView.builder(
              itemBuilder: (context, index) {
                if (messageList[index].username == '我') {
                  return buildRightItem(messageList[index].content);
                } else {
                  return buildLeftItem(messageList[index].content);
                }
              },
              itemCount: messageList.length,
            )),
            TextField(
              controller: textEditingController,
              decoration: new InputDecoration.collapsed(hintText: '请输入消息'),
              onSubmitted: sendMessage,
            )
          ],
        ));
  }

  //发送消息
  sendMessage(String text) {
    if (text.isEmpty) return;
    print(text);
    setState(() {
      messageList.add(Message("我", "我:" + text));
      messageList.add(Message("小Q", "小Q:" + text));
    });
    textEditingController.clear();
  }

  //回复消息布局
  buildLeftItem(content) {
    return Row(
      children: <Widget>[Text(content)],
    );
  }

  //发送消息布局
  buildRightItem(content) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[Text(content)],
    );
  }
}

为了区分是我们自己发送的消息还是小Q回复的消息,我们需要在Message类中添加一个username的属性:

class Message {
  String username;
  var content;

  Message(username,content) {
    this.username = username;
    this.content = content;
  }
}

那么在itemBuilder中,就可以根据username去区分消息发送方和回复方来返回不同的布局了,其中注意一下mainAxisAlignment: MainAxisAlignment.end表示水平方向右对齐。

这样一个最基本的聊天框架就实现了,接下来就是UI优化,达到第一篇博客中展示的最终效果,那么接下来,左右布局添加个头像吧,再来个气泡,文字颜色大小也调整下,修改buildLeftItem和buildRightItem返回值:
Left:

buildLeftItem(content) {
    return new Container(
      margin: const EdgeInsets.only(left: 5.0, right: 10.0),
      padding: const EdgeInsets.symmetric(vertical: 10.0),
      child: new Row(
        crossAxisAlignment: CrossAxisAlignment.start, //对齐方式,左上对齐
        children: <Widget>[
          new Image.network(
            'https://pp.myapp.com/ma_icon/0/icon_42284557_1517984341/96',
            width: 40,
            height: 40,
            fit: BoxFit.cover,
          ),
          new Flexible(
              child: new Container(
            margin: const EdgeInsets.only(left: 10.0, right: 10, top: 10),
            padding: const EdgeInsets.all(8.0),
            child: new Text(
              content,
              style: new TextStyle(fontSize: 14, color: Colors.white),
            ),
            decoration: new BoxDecoration(
              color: Colors.blue,
              borderRadius:
                  new BorderRadius.only(bottomRight: new Radius.circular(10.0)),
            ),
          ))
        ],
      ),
    );
  }

外层Container包裹,好处是可以调节内容的margin和padding,注意:Row的对齐方式用了crossAxisAlignment: CrossAxisAlignment.start,意思是在垂直方向从左上排列(Row水平方向排列),因为默认是居中布局,这样就可以避免下面的问题:

不设置对齐方式:

用Flutter实现小Q聊天机器人(四)_Text_03


设置对齐方式:

用Flutter实现小Q聊天机器人(四)_List_04


Image的fit属性:类似于安卓ImageView的scale,BoxFit.cover即裁剪方式;

Container的decoration属性可以理解为给其设置一个背景、边框等,就相当于安卓中我们自定义一个shape然后给view设置背景,它可以设置背景颜色,背景四角弧度,边框大小及颜色等,弧度设置方法BorderRadius,跟边距使用方法类似,都有only设置指定某一角,all全部角的方法提供:

borderRadius:
                  new BorderRadius.only(bottomRight: new Radius.circular(10.0)),

意思为:给背景右下角设置一个大小为10的弧度;

Right:

buildRightItem(content) {
    return new Container(
      margin: const EdgeInsets.only(left: 10.0, right: 5.0),
      padding: const EdgeInsets.symmetric(vertical: 10.0),
      child: new Row(
        crossAxisAlignment: CrossAxisAlignment.end, //对齐方式,左上对齐
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          new Flexible(
              child: new Container(
            margin: const EdgeInsets.only(left: 10.0, right: 10, top: 10),
            padding: const EdgeInsets.all(8.0),
            child: new Text(
              content,
              style: new TextStyle(fontSize: 14, color: Colors.blue),
            ),
            decoration: new BoxDecoration(
              //设置背景
              color: Colors.white,
              borderRadius:
                  new BorderRadius.only(bottomLeft: new Radius.circular(10.0)),
            ),
          )),
          new Container(
            height: 40,
            width: 40,
            child: new Image.network(
              'https://ss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=2182894899,3428535748&fm=58&bpow=445&bpoh=605',
              width: 40,
              height: 40,
              fit: BoxFit.cover,
            ),
          ),
        ],
      ),
    );
  }

效果如图:

用Flutter实现小Q聊天机器人(四)_Text_05


是不是有点意思了?啊,好像头像四四方方的,还有那个输入框差点意思,嗯,那我们先来实现一个类似QQ的圆形头像:

CircleAvatar(
              backgroundImage: new NetworkImage(),
          )

将Image修改为CircleAvatar,在其backgroundImage属性中传入NetworkImage(url)就可以了,很简单,但由于CircleAvatar并没有宽高属性,所以不能使用width和height来设置宽高,但其提供了radius属性:半径,跟宽高效果是一样的,减半即可!

输入框我们美化一下,再在其右侧加一个发送按钮,并给该按钮一个点击事件,点击按钮发送消息,说道这里我们就要来说一下Flutter的点击事件如何实现?

有这么一个组件:GestureDetector,手势控制,其包含了单击,双击,长按等一系列手势操作的监听:

用Flutter实现小Q聊天机器人(四)_android_06


详细的大家可以自行研究,这里我们只需要用一个点击事件onTap,那么最终的实现代码:

class _MyHomePageState extends State<MyHomePage> {
  var textEditingController = TextEditingController();
  var messageList = List<Message>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Column(
          children: <Widget>[
            Flexible(
                child: ListView.builder(
              itemBuilder: (context, index) {
                if (messageList[index].username == '我') {
                  return buildRightItem(messageList[index].content);
                } else {
                  return buildLeftItem(messageList[index].content);
                }
              },
              itemCount: messageList.length,
            )),
            Divider(height: 1.0),
            Container(
              height: 50,
              decoration: BoxDecoration(
                color: Theme.of(context).cardColor,
              ),
              child: buildEditText(),
            )
          ],
        ));
  }

  //发送消息
  sendMessage(String text) {
    if (text.isEmpty) return;
    print(text);
    setState(() {
      messageList.add(Message("我", text));
      messageList.add(Message("小Q", text));
    });
    textEditingController.clear();
  }

  Widget buildEditText() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 8.0),
      child: Row(
        children: <Widget>[
          Flexible(
              child: TextField(
            //输入框
            controller: textEditingController,
            onSubmitted: sendMessage,
            decoration: InputDecoration.collapsed(hintText: '请输入内容'),
          )),
          GestureDetector(
              onTap: () => sendMessage(textEditingController.text),
              child: Container(
                //发送按钮
                margin: const EdgeInsets.symmetric(horizontal: 4.0),
                padding: const EdgeInsets.only(
                    left: 10.0, top: 6, right: 10, bottom: 6),
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.all(Radius.circular(5.0)),
                ),
                child: Text(
                  "发送",
                  style: TextStyle(fontSize: 14, color: Colors.white),
                ),
              )),
        ],
      ),
    );
  }

  //回复消息布局
  buildLeftItem(content) {
    return Container(
      margin: const EdgeInsets.only(left: 5.0, right: 10.0),
      padding: const EdgeInsets.symmetric(vertical: 10.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start, //对齐方式,左上对齐
        children: <Widget>[
          CircleAvatar(
            backgroundImage: NetworkImage(
                'https://pp.myapp.com/ma_icon/0/icon_42284557_1517984341/96'),
            radius: 20,
          ),
          Flexible(
              child: Container(
            margin: const EdgeInsets.only(left: 10.0, right: 10, top: 10),
            padding: const EdgeInsets.all(8.0),
            child: Text(
              content,
              style: TextStyle(fontSize: 14, color: Colors.white),
            ),
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius:
                  BorderRadius.only(bottomRight: Radius.circular(10.0)),
            ),
          ))
        ],
      ),
    );
  }

  //发送消息布局
  buildRightItem(content) {
    return Container(
      margin: const EdgeInsets.only(left: 10.0, right: 5.0),
      padding: const EdgeInsets.symmetric(vertical: 10.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start, //对齐方式,右上对齐
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Flexible(
              child: Container(
            margin: const EdgeInsets.only(left: 10.0, right: 10, top: 10),
            padding: const EdgeInsets.all(8.0),
            child: Text(
              content,
              style: TextStyle(fontSize: 14, color: Colors.blue),
            ),
            decoration: BoxDecoration(
              //设置背景
              color: Colors.white,
              borderRadius:
                  BorderRadius.only(bottomLeft: Radius.circular(10.0)),
            ),
          )),
          CircleAvatar(
            backgroundImage: NetworkImage(
                'https://ss0.baidu.com/6ONWsjip0QIZ8tyhnq/it/u=2182894899,3428535748&fm=58&bpow=445&bpoh=605'),
            radius: 20,
          ),
        ],
      ),
    );
  }
}

注意发送按钮部分:

GestureDetector(
              onTap: () => sendMessage(textEditingController.text),
              child: Container(
                //发送按钮
                margin: const EdgeInsets.symmetric(horizontal: 4.0),
                padding: const EdgeInsets.only(
                    left: 10.0, top: 6, right: 10, bottom: 6),
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.all(Radius.circular(5.0)),
                ),
                child: Text(
                  "发送",
                  style: TextStyle(fontSize: 14, color: Colors.white),
                ),
              ))

GestureDetector包裹整个按钮,给其添加点击事件onTap,Container为按钮设置了边距,背景,以及文字,最终效果如下:

用Flutter实现小Q聊天机器人(四)_Text_07


未完待续…