距离上一次更新也过了一段时间了,期间忙着学车去了,现在有空再来写一篇。
为什么要用这个PopupMenu
呢,因为大多数消息界面的右上角,都会有一个设置的选项,而这个选项最常用的就是通过PopupMenu来实现。首先来看一下效果图:
类似于这样的情况,然后来看代码:
new PopupMenuButton(
icon: Icon(Icons.settings),
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
new PopupMenuItem<String>(
value: "2",child: new Text("全部已读",style: new TextStyle(fontSize: 14)),),
new PopupMenuItem(
value:"1",child: new Text("接受但不提醒",style: new TextStyle(fontSize: 14))),
new PopupMenuItem(
value:"0",child: new Text("接受并提醒",style: new TextStyle(fontSize: 14),)),
],
onSelected: (String value){
setState(() {
_model = value;
});
},
),
这个按钮的图标是可以自定义的,然后弹出的选项也可以有很多自定义的样式。注意在选中之后要通过setState()来改变值。
从上面的图种可以看出,这个消息界面的主题是一个ListView
,较为常用的除了普通的ListView,还有ListView.builder和ListView.separated,这几个的区别在于,ListView
这种方式适合只有少量的子组件的情况,因为这种方式需要将所有children都提前创建好;而ListView.builder
适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建;最后一个ListView.separated
可以在生成的列表项之间添加一个分割组件,它比ListView.builder
多了一个separatorBuilder
参数,该参数是一个分割组件生成器。
但是,仅仅是一个简单的列表还是不够的,我们需要给每个item绑定上一个点击事件,跳转到聊天的界面,具体的做法也很简单,就是将整个item包在一个GestureDetector
里,然后就可以调用它的onTap
,onDoubleTap
,onLongPress
等一系列跟手势有关的方法。
这还不够,flutter还提供了一个滑动删除item的方法,就是让整个ItemBuilder返回一个Dismissible
,然后根据传递的参数index,删除所选的这一行。
下面是实现的代码:
//分割线
Widget divider = Divider(color: Colors.black12, height: 1.0, indent: 18,);
......
//构造带有分割线的ListView
ListView.separated(
itemCount: _message.length,
separatorBuilder: (BuildContext context, int index) {
return divider;
},
itemBuilder: (context, index) => EachItem(_message[index],_message,index),)
//构造每一个Item的类
class EachItem extends StatelessWidget{
final Message message;
final List<Message> _message;
final int index;
const EachItem(this.message, this._message, this.index);
@override
Widget build(BuildContext context) {
//滑动删除功能
return Dismissible(
//key是必须的,而且是String类型
key: new Key(message.userName),
onDismissed: (direction){
_message.removeAt(index);
},
child:Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
//获取手势功能
child: GestureDetector(
child:Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
margin: const EdgeInsets.only(left: 14.0,right: 14.0),
child: new CircleAvatar(
backgroundImage: NetworkImage(message.imageUrl),
),
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(message.userName,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 17.0
)
),
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(message.firstMessage,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.grey,
),
),
)
]
),
),
]
) ,
onTap: (){
//跳转到聊天界面
Navigator.pushNamed(context,
"Chat",
arguments: new Chatter(message.userName,message.imageUrl)
);
},
)
) ,
);
}
这里还有一个小点,就是用命名路由进行传参时,还可以直接传递一个对象作为argument。这里跳转到聊天页面的时候,可以直接传递一个Chatter对象,在chat页面接收参数时,只需要使用
Chatter chatter = ModalRoute.of(context).settings.arguments;
就可以接收到传递的这个Chatter对象,并且使用其变量参数,比较方便。
但是我在写这段代码时遇到了一个问题,就是在使用滑动删除item的时候,那一行的消息的确删掉了,但是如果删掉的不是首尾而是中间行的话,上下的分割线divider就会重叠在一起,只有点击搜索框或其他触发ListView重新渲染的操作之后才恢复正常,我没有想到什么好的解决办法,麻烦看到的大佬帮助解决以下QAQ
先来看聊天布局的大体效果:
主体的代码就是一个ListView加上一个TextField。难度也不是很大,下面看代码:
///发送信息
void _handleSubmitted(String text) {
_textController.clear();//清空文本框
Message message = new Message(
text,true
);
setState((){
_messages.insert(0, message);
});
}
//构造输入框
Widget _buildTextComposer(){
return new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget> [
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(hintText: '发送消息'),
)
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 4.0),
child: new IconButton(
icon: new Icon(Icons.send,color: Colors.blue,),
onPressed: () => _handleSubmitted(_textController.text)
),
)
]
)
);
}
......
@override
Widget build(BuildContext context) {
Widget divider = Divider(color: Colors.white, height: 18.0, indent: 18,);
///获得聊天的对象
Chatter chatter = ModalRoute.of(context).settings.arguments;
//print(chatter.userName);
return new Scaffold(
appBar: AppBar(
//backgroundColor: Colors.white,
title: new Text("Chat with "+chatter.userName),
centerTitle: true,
),
body: new Column(
children: <Widget>[
new Flexible(
child:new ListView.separated(
separatorBuilder: (BuildContext context, int index) {
return divider;
},
padding: new EdgeInsets.all(8.0),
reverse: true,
itemBuilder: (context, int index) => EntryItem(_messages[index],chatter),
itemCount: _messages.length,
),
),
new Divider(height: 1.0),
new Container(
decoration: new BoxDecoration(
color: Theme.of(context).cardColor,
),
child: _buildTextComposer(),
)
],
),
);
}
......
///发送的信息类
class Message {
String text;//内容
bool isSender;//是否由自己发送
Message(this.text, this.isSender,);
}
///构造发送的信息
class EntryItem extends StatelessWidget{
final Chatter chatter;
final Message message;
const EntryItem(this.message,this.chatter);
Widget row(){
///由自己发送,在右边显示
if(message.isSender){
return new Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(message.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.black,
fontSize: 16
),
),
)
]
),
),
new Container(
margin: const EdgeInsets.only(left: 12.0,right: 12.0),
child: new CircleAvatar(
backgroundImage: AssetImage("assets/images/user.png"),
radius: 24.0,
) ,
),
],
);
}
else{
///对方发送,左边显示
return new Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(left: 12.0,right: 12.0),
child: new CircleAvatar(
backgroundImage: NetworkImage(chatter.imageUrl),
radius: 24.0,
) ,
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(message.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.black,
fontSize: 16
),
),
)
]
),
),
],
);
}
}
@override
Widget build(BuildContext context) {
return new Container(
child: row(),
);
}
}
crossAxisAlignment
参数可以决定一个Column的布局方式,以后有空会补充聊天气泡的部分。
参考资料: