上一篇: 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
- ListView 设置
@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 组件来显示
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<String> 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), ), ), ); }, ); } }
代码地址: gitee.com/shizidada/f…
列表 Mock 数据: gitee.com/shizidada/f…