上一篇: Flutter 实现钉钉消息列表效果(二)
依赖
flutter_slidable: ^0.5.5 # 包裹 ListView Item 实现可以左右滑动的扩展操作
实现效果如图
修改整体的 DTMessageScreen 布局
- 使用 SingleChildScrollView + Column
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: buildAppBar(context),
body: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: <Widget>[
DTMessageSearchDecoration(),
DTMessageTopQuick(),
DTMessageTopMask(),
DTMessageListView(),
],
),
)
);
}
实现一个 DTMessageListView 消息列表
- ListView.builder 来构建出 ListView
- 视图显示根据数据模型来显示不同的 Item
- 创建 DTMessageModel 数据模型
第三方 Slidable 添加扩展操作 (删除、取消置顶)
ListView 设置
- shrinkWrap: true 适应父容器
- physics: NeverScrollableScrollPhysics() ,禁止 ListView 自身的滚动效果,使用父组件的滚动,因为父组件是 SingleChildScrollView
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _messageList.length,
itemBuilder: (BuildContext context, int index) {
DTMessageModel model = _messageList[index];
return Slidable(
actionPane: SlidableDrawerActionPane(),
actionExtentRatio: 0.25,
secondaryActions: <Widget>[
IconSlideAction(
caption: '取消置顶',
color: Colors.black45,
icon: Icons.more_horiz,
onTap: () => _showSnackBar('More'),
),
IconSlideAction(
caption: '删除',
color: Colors.redAccent,
icon: Icons.delete,
onTap: () => _showSnackBar('Delete'),
),
],
child: Padding(
padding:
EdgeInsets.only(left: kSize36, bottom: kSize20, top: kSize20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(child: DTMessageItem(messageModel: model)),
model.isStick
? Container(
width: kSize48,
height: kSize48,
margin: EdgeInsets.only(top: kSize10, right: kSize10),
child:
SvgPicture.asset('assets/icons/icon_triangle.svg'),
)
: Padding(
padding: EdgeInsets.symmetric(horizontal: kSize24),
)
],
),
),
);
},
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
);
}
构建数据模型
enum DTMessageType { text, image, file, location, video, voice }
class DTMessageModel {
DTMessageModel({@required this.chatId,
@required this.userId,
@required this.userName,
@required this.chatName,
@required this.message,
this.messageType = DTMessageType.text,
this.time,
this.unReadCount = 0,
this.isSingle = false,
this.avatar,
this.isGroup = false,
this.groupAvatars,
this.isDisturbing = false,
this.isSpecialAttention = false,
this.isAtYou = false,
this.isAtAll = false,
this.isStick = false});
/// 聊天 Id
String chatId;
/// 用户名称
String userName;
/// 用户Id
String userId;
/// 聊天名称
String chatName;
/// 消息体
String message;
/// message type
DTMessageType messageType;
/// 时间
int time;
/// 未读数量
int unReadCount;
/// 单聊
bool isSingle;
String avatar;
/// 群聊信息
bool isGroup;
List<String> groupAvatars;
/// 消息免打扰
bool isDisturbing;
/// 是否为置顶
bool isStick;
/// 特别关注
bool isSpecialAttention;
/// 是否 @ 你
bool isAtYou;
/// 是否 @ 全部
bool isAtAll;
}
创建 DTMessageItem 视图
根据数据模型数据来显示
class DTMessageItem extends StatelessWidget {
final DTMessageModel messageModel;
DTMessageItem({@required this.messageModel});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
DTMessageAvatar(messageModel: messageModel),
Expanded(
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: kSize8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Text(
messageModel?.chatName ?? "",
style: TextStyle(color: kColor33, fontSize: kSize34),
overflow: TextOverflow.ellipsis,
),
),
Text(
_formatDate(),
style: TextStyle(color: kColor99, fontSize: kSize26),
overflow: TextOverflow.ellipsis,
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: RichText(
text: TextSpan(children: [
TextSpan(
text: messageModel.isAtYou ? "[@你]" : "",
style:
TextStyle(color: kColorF1A33A, fontSize: kSize28),
),
TextSpan(
text: messageModel.isSpecialAttention ? "[特别关注]" : "",
style:
TextStyle(color: kColorF1A33A, fontSize: kSize28),
),
TextSpan(
text: messageModel.isAtAll ? "[@所有人]" : "",
style:
TextStyle(color: kColorF1A33A, fontSize: kSize28),
),
TextSpan(
text: messageModel?.message ?? "",
style: TextStyle(color: kColor99, fontSize: kSize28),
)
]),
overflow: TextOverflow.ellipsis,
),
),
(messageModel.unReadCount != null &&
messageModel.unReadCount > 0 &&
!messageModel.isDisturbing)
? Container(
width: kSize32,
height: kSize32,
alignment: Alignment.center,
decoration: BoxDecoration(
color: kColorE86A3E,
borderRadius: BorderRadius.circular(kSize20)),
child: Text(
messageModel.unReadCount.toString(),
style: TextStyle(
color: Colors.white, fontSize: kSize26),
))
: Container(),
messageModel.isDisturbing
? Row(
children: <Widget>[
SvgPicture.asset(
'assets/icons/icon_disturbing.svg',
width: kSize36,
color: kColorECEDED,
),
messageModel.unReadCount != null &&
messageModel.unReadCount > 0
? SvgPicture.asset(
'assets/icons/icon_red_dot.svg',
width: kSize36,
)
: Container()
],
)
: Container()
],
)
],
),
)
],
);
}
String _formatDate() {
DateTime dateTime =
DateTime.fromMillisecondsSinceEpoch(messageModel?.time ?? 0);
return formatDate(dateTime, [HH, ':', nn]) ?? "";
}
}
构建头像
- 区分个人头像还是群组头像
- 群组头像使用 NineGridView 组件来显示 ```dart class DTMessageAvatar extends StatelessWidget { final DTMessageModel messageModel;
DTMessageAvatar({this.messageModel});
@override Widget build(BuildContext context) { return messageModel.isSingle ? _buildSingleAvatar() : _buildGroupAvatar(); }
Widget _buildSingleAvatar() { return Container(
margin: EdgeInsets.only(right: kSize24),
child: (messageModel.isSingle && messageModel.chatName != null)
? CircleAvatar(
radius: kSize50,
child: Text(
_getChatName(),
style: TextStyle(color: Colors.white, fontSize: kSize32),
),
backgroundColor: kPrimaryColor,
)
: Image.asset('assets/images/track_icon_attend.png',
fit: BoxFit.cover),
); }
String _getChatName() { String chatName = messageModel.chatName; if (chatName == null || chatName.isEmpty) {
return "(^_^)";
}
chatName = chatName.replaceAll(‘\(’, “”).replaceAll(‘\)’, “”);
/// RegExp regExp = RegExp(r’\(|\)’); /// chatName = chatName.replaceAll(regExp, “”); /// print(chatName);
if (chatName.length == 2) {
return chatName;
}
if (chatName.length > 2) {
return chatName.substring(chatName.length - 2, chatName.length);
} return “”; }
Widget _buildGroupAvatar() { List
groupAvatars = messageModel.groupAvatars; if (groupAvatars.length >= 4) { groupAvatars = groupAvatars.sublist(0, 4);
} return NineGridView(
width: kSize100,
height: kSize100,
type: NineGridType.dingTalkGp,
itemCount: groupAvatars.length,
space: kSizeDot5,
margin: EdgeInsets.only(right: kSize24),
itemBuilder: (BuildContext context, int index) {
String userName = groupAvatars[index];
return Container(
color: kPrimaryColor,
/// TODO: position ???
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.symmetric(horizontal: kSize6),
child: Text(
userName.length >= 2 ? userName.substring(1, 2) : userName,
style: TextStyle(color: Colors.white),
),
),
);
},
); } } ```
代码地址: https://gitee.com/shizidada/flutter_dingtalk/blob/master/lib/ui/screens/message/message_screen.dart
列表 Mock 数据: https://gitee.com/shizidada/flutter_dingtalk/blob/master/lib/mock/mock_message.dart