GitHub:https://github.com/baiyuliang/Qrobot_Flutter
经过前几篇的学习,我们对Flutter基本的布局知识有了一定的了解(当然,这需要大家多练习,多动手,才能熟练掌握),那么本篇我们将实现一个简单的聊天界面!
仍然先用最简单的代码实现:
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模拟回答,一左一右布局:
仍然是最简单的代码实现:
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水平方向排列),因为默认是居中布局,这样就可以避免下面的问题:
不设置对齐方式:
设置对齐方式:
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,
),
),
],
),
);
}
效果如图:
是不是有点意思了?啊,好像头像四四方方的,还有那个输入框差点意思,嗯,那我们先来实现一个类似QQ的圆形头像:
CircleAvatar(
backgroundImage: new NetworkImage(),
)
将Image修改为CircleAvatar,在其backgroundImage属性中传入NetworkImage(url)就可以了,很简单,但由于CircleAvatar并没有宽高属性,所以不能使用width和height来设置宽高,但其提供了radius属性:半径,跟宽高效果是一样的,减半即可!
输入框我们美化一下,再在其右侧加一个发送按钮,并给该按钮一个点击事件,点击按钮发送消息,说道这里我们就要来说一下Flutter的点击事件如何实现?
有这么一个组件:GestureDetector,手势控制,其包含了单击,双击,长按等一系列手势操作的监听:
详细的大家可以自行研究,这里我们只需要用一个点击事件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为按钮设置了边距,背景,以及文字,最终效果如下:
未完待续…