欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-02 23:21:09
...

用Flutter实现小Q聊天机器人(一)
用Flutter实现小Q聊天机器人(二)
用Flutter实现小Q聊天机器人(三)
用Flutter实现小Q聊天机器人(四)
用Flutter实现小Q聊天机器人(五)

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

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

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聊天机器人(四)
仍然是最简单的代码实现:

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聊天机器人(四)
设置对齐方式:
用Flutter实现小Q聊天机器人(四)
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聊天机器人(四)
是不是有点意思了?啊,好像头像四四方方的,还有那个输入框差点意思,嗯,那我们先来实现一个类似QQ的圆形头像:

CircleAvatar(
              backgroundImage: new NetworkImage(),
          )

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

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

有这么一个组件:GestureDetector,手势控制,其包含了单击,双击,长按等一系列手势操作的监听:
用Flutter实现小Q聊天机器人(四)
详细的大家可以自行研究,这里我们只需要用一个点击事件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聊天机器人(四)
未完待续…