前言
我们来完成上一节剩下的任务右边菜单
- 1.导航栏右边菜单按钮(扫一扫等)
完成✅ - 2.导航栏下方的搜索框
完成✅ - 3.聊天列表
完成✅ - 4.消息表左滑
未完成✅ - 5.消息未读数量小红点
完成✅
预览
PopupMenuButton简介
PopupMenuButton构造器:
const PopupMenuButton({
super.key,
required this.itemBuilder,
this.initialValue,
this.onOpened,
this.onSelected,
this.onCanceled,
this.tooltip,
this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.padding = const EdgeInsets.all(8.0),
this.child,
this.splashRadius,
this.icon,
this.iconSize,
this.offset = Offset.zero,
this.enabled = true,
this.shape,
this.color,
this.enableFeedback,
this.constraints,
this.position,
this.clipBehavior = Clip.none,
})
其中常用的属性为:
-
itemBuilder: 每个条目 -
initialValue: 初始的value , 就是打开的时候, 在这个value 里面会有不同的样式,让你知道当前值 -
onSelected: 选择时的回调 -
onCanceled: 点击返回键,或者是点击外部, popupmenu关闭时的回调 -
elevation: 弹出来的时候的阴影高度 -
padding: 内边距 -
child: icon 和 child 不能同时设置, 否则报错 -
icon: icon 和 child 不能同时设置, 否则报错 -
offset: 偏移量,根据当前控件,向右向下平移。默认是跟当前控件的左上角对齐 -
shape: 边框装饰器
实现
自定义数据格式:
class PopupMenuData {
const PopupMenuData({required this.name, required this.icon});
/// 名称
final String name;
/// 图标
final IconData icon;
}
准备数据
List<PopupMenuData> popList = const [
PopupMenuData(name: "发起群聊", icon: WeChatFont.group_chat),
PopupMenuData(name: "添加朋友", icon: WeChatFont.person_outlined),
PopupMenuData(name: "扫一扫", icon: WeChatFont.sweep),
PopupMenuData(name: "收付款", icon: WeChatFont.payment_received),
];
修改AppBar
AppBar(
elevation: 0,
title: const Text(
"微信",
style: TextStyle(color: Style.appBarTextColor),
),
backgroundColor: Style.appBarBackgroundColor,
//导航右侧按钮组
actions: [
//外部container可以用来调整间距,不放到里面的Container
//是因为这个margin会增加点击区域,毕竟外面包了一层TextButton一样的东西
Container(
margin: const EdgeInsets.only(right: 10),
//使用 PopupMenuButton 来定义右侧点击弹层功能
child: PopupMenuButton(
color: Colors.black87,
//弹层实物位置,相对于当前组件的偏移
offset: const Offset(0, 56),
//我们看到的按钮的信息,组件给其默认添加点击事件
shape: const TooltipShape(),
child: Container(
width: 40,
height: 40,
alignment: Alignment.center,
child: const Icon(
Icons.add,
color: Colors.black,
),
),
//返回内部组件信息列表,单行 item 使用 PopupMenuItem
//使用 .map<PopupMenuItem> 的原因可以动态生成多个 item
itemBuilder: (BuildContext context) {
return popList.map<PopupMenuItem>((item) {
return PopupMenuItem(
//水平布局,左侧图片,右侧问题,中间间隔使用 Sizebox即可
child: Row(
children: [
Icon(item.icon),
const SizedBox(width: 10),
Text(item.name,
style: const TextStyle(color: Colors.white)),
],
),
);
}).toList();
},
),
)
],
)
自定义边框
import 'package:flutter/material.dart';
/// I'm using [RoundedRectangleBorder] as my reference...
class TooltipShape extends ShapeBorder {
const TooltipShape();
final BorderSide _side = BorderSide.none;
final BorderRadiusGeometry _borderRadius = BorderRadius.zero;
@override
EdgeInsetsGeometry get dimensions => EdgeInsets.all(_side.width);
@override
Path getInnerPath(
Rect rect, {
TextDirection? textDirection,
}) {
final Path path = Path();
path.addRRect(
_borderRadius.resolve(textDirection).toRRect(rect).deflate(_side.width),
);
return path;
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
final Path path = Path();
final RRect rrect = _borderRadius.resolve(textDirection).toRRect(rect);
path.moveTo(0, 10);
path.quadraticBezierTo(0, 0, 10, 0);
path.lineTo(rrect.width - 30, 0);
path.lineTo(rrect.width - 20, -10);
path.lineTo(rrect.width - 10, 0);
path.quadraticBezierTo(rrect.width, 0, rrect.width, 10);
path.lineTo(rrect.width, rrect.height - 10);
path.quadraticBezierTo(
rrect.width, rrect.height, rrect.width - 10, rrect.height);
path.lineTo(10, rrect.height);
path.quadraticBezierTo(0, rrect.height, 0, rrect.height - 10);
return path;
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
@override
ShapeBorder scale(double t) => RoundedRectangleBorder(
side: _side.scale(t),
borderRadius: _borderRadius * t,
);
}
完整代码
import 'package:date_format/date_format.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:pseudo_we_chat/ui/badge_avatar.dart';
import 'package:pseudo_we_chat/ui/font/WeChatFont.dart';
import 'package:pseudo_we_chat/ui/tooltip_shape.dart';
import '../../constant/style.dart';
class MessagePage extends StatefulWidget {
const MessagePage({Key? key}) : super(key: key);
@override
State<MessagePage> createState() => _MessagePageState();
}
class _MessagePageState extends State<MessagePage> {
//定义列表数据
final List<MessageData> _messageList = [
MessageData(
id: BigInt.from(1),
avatar:
"https://d36tnp772eyphs.cloudfront.net/blogs/1/2018/02/Taj-Mahal.jpg",
name: "张三123",
message:
"ZFLEX对UIKit的一层封装,主要包含一个数据驱动的列表框架、和UIKit中常用控件的链式拓展,ZZFLEX相关资料正在整理中,目前已经开源,ZFLEX对UIKit的一层封装,主要包含一个数据驱动的列表框架、和UIKit中常用控件的链式拓展,ZZFLEX相关资料正在整理中,目前已经开源",
lastTime: DateTime.now(),
unReadNum: 13),
MessageData(
id: BigInt.from(2),
avatar: "https://www.itying.com/images/flutter/3.png",
name: "云淡风轻",
message: "[图片]",
lastTime: DateTime.now().subtract(const Duration(minutes: 30)),
unReadNum: 0),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/2.png",
name: "魅力人生",
message: "今天是个好日子。",
lastTime: DateTime.now().subtract(const Duration(hours: 1)),
unReadNum: 9),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/1.png",
name: "随访飘逸",
message: "你好啊",
lastTime: DateTime.now().subtract(const Duration(hours: 2)),
unReadNum: 66),
MessageData(
id: BigInt.from(1),
avatar:
"https://d36tnp772eyphs.cloudfront.net/blogs/1/2018/02/Taj-Mahal.jpg",
name: "张三",
message: "Hello word!",
lastTime: DateTime.now(),
unReadNum: 120),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/3.png",
name: "云淡风轻",
message: "[图片]",
lastTime: DateTime.now().subtract(const Duration(minutes: 30)),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/2.png",
name: "魅力人生",
message: "今天是个好日子。",
lastTime: DateTime.now().subtract(const Duration(hours: 1)),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/1.png",
name: "随访飘逸",
message: "你好啊",
lastTime: DateTime.now().subtract(const Duration(hours: 2)),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar:
"https://d36tnp772eyphs.cloudfront.net/blogs/1/2018/02/Taj-Mahal.jpg",
name: "张三",
message: "Hello word!",
lastTime: DateTime.now(),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/3.png",
name: "云淡风轻",
message: "[图片]",
lastTime: DateTime.now().subtract(const Duration(minutes: 30)),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/2.png",
name: "魅力人生",
message: "今天是个好日子。",
lastTime: DateTime.now().subtract(const Duration(hours: 1)),
unReadNum: 10),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/1.png",
name: "随访飘逸",
message: "你好啊",
lastTime: DateTime.now().subtract(const Duration(hours: 2)),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar:
"https://d36tnp772eyphs.cloudfront.net/blogs/1/2018/02/Taj-Mahal.jpg",
name: "张三",
message: "Hello word!",
lastTime: DateTime.now(),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/3.png",
name: "云淡风轻",
message: "[图片]",
lastTime: DateTime.now().subtract(const Duration(minutes: 30)),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/2.png",
name: "魅力人生",
message: "今天是个好日子。",
lastTime: DateTime.now().subtract(const Duration(hours: 1)),
unReadNum: 1),
MessageData(
id: BigInt.from(1),
avatar: "https://www.itying.com/images/flutter/1.png",
name: "随访飘逸",
message: "你好啊",
lastTime: DateTime.now().subtract(const Duration(hours: 2)),
unReadNum: 1)
];
List<PopupMenuData> popList = const [
PopupMenuData(name: "发起群聊", icon: WeChatFont.group_chat),
PopupMenuData(name: "添加朋友", icon: WeChatFont.person_outlined),
PopupMenuData(name: "扫一扫", icon: WeChatFont.sweep),
PopupMenuData(name: "收付款", icon: WeChatFont.payment_received),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: const Text(
"微信",
style: TextStyle(color: Style.appBarTextColor),
),
backgroundColor: Style.appBarBackgroundColor,
//导航右侧按钮组
actions: [
//外部container可以用来调整间距,不放到里面的Container
//是因为这个margin会增加点击区域,毕竟外面包了一层TextButton一样的东西
Container(
margin: const EdgeInsets.only(right: 10),
//使用 PopupMenuButton 来定义右侧点击弹层功能
child: PopupMenuButton(
color: Colors.black87,
//弹层实物位置,相对于当前组件的偏移
offset: const Offset(0, 56),
//我们看到的按钮的信息,组件给其默认添加点击事件
shape: const TooltipShape(),
child: Container(
width: 40,
height: 40,
alignment: Alignment.center,
child: const Icon(
Icons.add,
color: Colors.black,
),
),
//返回内部组件信息列表,单行 item 使用 PopupMenuItem
//使用 .map<PopupMenuItem> 的原因可以动态生成多个 item
itemBuilder: (BuildContext context) {
return popList.map<PopupMenuItem>((item) {
return PopupMenuItem(
//水平布局,左侧图片,右侧问题,中间间隔使用 Sizebox即可
child: Row(
children: [
Icon(item.icon),
const SizedBox(width: 10),
Text(item.name,
style: const TextStyle(color: Colors.white)),
],
),
);
}).toList();
},
),
)
],
),
body: Container(
color: Style.contentBackgroundColor,
child: Column(
children: [
//聊天列表
Expanded(
flex: 1,
child: SlidableAutoCloseBehavior(
child: ListView.builder(
itemCount: _messageList.length,
itemBuilder: (BuildContext context, int index) {
var item = _messageList[index];
return Column(
children: [
//搜索按钮
index == 0
? Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 10,
right: 10,
top: 5,
bottom: 5),
decoration: const BoxDecoration(
color: Style
.messageSearchBackgroundColor),
child: ElevatedButton(
style: ButtonStyle(
elevation:
MaterialStateProperty.all(0),
backgroundColor:
MaterialStateProperty.all(
Colors.white)),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: const [
Icon(
Icons.search,
color: Colors.grey,
),
Text(
"搜索",
style: TextStyle(
fontSize: 20,
color: Colors.grey),
)
],
),
onPressed: () {},
),
)
: const SizedBox(),
Column(
children: [
Slidable(
// Specify a key if the Slidable is dismissible.
key: ValueKey("$index"),
groupTag: "ccc",
// 把所有item归类到一个组, 保证同时只出现一个滑动的效果
// The end action pane is the one at the right or the bottom side.
endActionPane: ActionPane(
extentRatio: 0.7,
dragDismissible: false,
motion: ScrollMotion(),
children: [
item.unReadNum == 0
? SlidableAction(
// An action can be bigger than the others.
flex: 2,
onPressed:
(BuildContext context) {},
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
label: '标记未读',
)
: SlidableAction(
// An action can be bigger than the others.
flex: 2,
onPressed:
(BuildContext context) {},
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
label: '标记已读',
),
SlidableAction(
flex: 1,
onPressed: (BuildContext context) {},
backgroundColor: Colors.amber,
foregroundColor: Colors.white,
label: '不显示',
),
SlidableAction(
autoClose: false,
flex: 1,
onPressed: (BuildContext context) {},
backgroundColor: Colors.deepOrange,
foregroundColor: Colors.white,
label: '删除',
),
],
),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
//头像
leading: BadgeAvatar(
avatar: item.avatar,
num: item.unReadNum,
),
//名称和时间
title: Row(
children: [
//名称
Expanded(
flex: 17,
child: Text(item.name,
style: const TextStyle(
fontSize: 18,
fontWeight:
FontWeight.w500))),
//右侧时间
Expanded(
flex: 3,
child: Text(
formatDate(item.lastTime,
[HH, ':', nn]),
style: const TextStyle(
fontSize: 12,
color: Colors.grey),
)),
],
),
//消息内容
subtitle: Padding(
padding: const EdgeInsets.only(top: 10),
child: Text(item.message,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(
fontSize: 16,
color: Colors.grey)),
),
),
),
//下划线
Padding(
padding: index == _messageList.length - 1
? const EdgeInsets.only(left: 0)
: const EdgeInsets.only(left: 80),
child: const Divider(),
)
],
)
],
);
}),
),
),
],
)));
}
}
class MessageData {
const MessageData(
{required this.id,
required this.avatar,
required this.name,
required this.message,
required this.unReadNum,
required this.lastTime});
/// id
final BigInt id;
/// 头像
final String avatar;
/// 名称
final String name;
/// 消息
final String message;
/// 消息未读数量
final int unReadNum;
/// 消息时间
final DateTime lastTime;
}
class PopupMenuData {
const PopupMenuData({required this.name, required this.icon});
/// 名称
final String name;
/// 图标
final IconData icon;
}