上一篇: Flutter 实现钉钉消息列表效果(二)

依赖
  1. flutter_slidable: ^0.5.5 # 包裹 ListView Item 实现可以左右滑动的扩展操作

实现效果如图

Flutter 实现钉钉消息列表效果(三) - 图1

修改整体的 DTMessageScreen 布局
  • 使用 SingleChildScrollView + Column
  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.white,
  5. appBar: buildAppBar(context),
  6. body: SingleChildScrollView(
  7. controller: _scrollController,
  8. child: Column(
  9. children: <Widget>[
  10. DTMessageSearchDecoration(),
  11. DTMessageTopQuick(),
  12. DTMessageTopMask(),
  13. DTMessageListView(),
  14. ],
  15. ),
  16. )
  17. );
  18. }

实现一个 DTMessageListView 消息列表
  • ListView.builder 来构建出 ListView
  • 视图显示根据数据模型来显示不同的 Item
  • 创建 DTMessageModel 数据模型
  • 第三方 Slidable 添加扩展操作 (删除、取消置顶)

    • ListView 设置

      • shrinkWrap: true 适应父容器
      • physics: NeverScrollableScrollPhysics() ,禁止 ListView 自身的滚动效果,使用父组件的滚动,因为父组件是 SingleChildScrollView
  1. @override
  2. Widget build(BuildContext context) {
  3. return ListView.builder(
  4. itemCount: _messageList.length,
  5. itemBuilder: (BuildContext context, int index) {
  6. DTMessageModel model = _messageList[index];
  7. return Slidable(
  8. actionPane: SlidableDrawerActionPane(),
  9. actionExtentRatio: 0.25,
  10. secondaryActions: <Widget>[
  11. IconSlideAction(
  12. caption: '取消置顶',
  13. color: Colors.black45,
  14. icon: Icons.more_horiz,
  15. onTap: () => _showSnackBar('More'),
  16. ),
  17. IconSlideAction(
  18. caption: '删除',
  19. color: Colors.redAccent,
  20. icon: Icons.delete,
  21. onTap: () => _showSnackBar('Delete'),
  22. ),
  23. ],
  24. child: Padding(
  25. padding:
  26. EdgeInsets.only(left: kSize36, bottom: kSize20, top: kSize20),
  27. child: Row(
  28. crossAxisAlignment: CrossAxisAlignment.start,
  29. children: <Widget>[
  30. Expanded(child: DTMessageItem(messageModel: model)),
  31. model.isStick
  32. ? Container(
  33. width: kSize48,
  34. height: kSize48,
  35. margin: EdgeInsets.only(top: kSize10, right: kSize10),
  36. child:
  37. SvgPicture.asset('assets/icons/icon_triangle.svg'),
  38. )
  39. : Padding(
  40. padding: EdgeInsets.symmetric(horizontal: kSize24),
  41. )
  42. ],
  43. ),
  44. ),
  45. );
  46. },
  47. shrinkWrap: true,
  48. physics: NeverScrollableScrollPhysics(),
  49. );
  50. }

构建数据模型
  1. enum DTMessageType { text, image, file, location, video, voice }
  2. class DTMessageModel {
  3. DTMessageModel({@required this.chatId,
  4. @required this.userId,
  5. @required this.userName,
  6. @required this.chatName,
  7. @required this.message,
  8. this.messageType = DTMessageType.text,
  9. this.time,
  10. this.unReadCount = 0,
  11. this.isSingle = false,
  12. this.avatar,
  13. this.isGroup = false,
  14. this.groupAvatars,
  15. this.isDisturbing = false,
  16. this.isSpecialAttention = false,
  17. this.isAtYou = false,
  18. this.isAtAll = false,
  19. this.isStick = false});
  20. /// 聊天 Id
  21. String chatId;
  22. /// 用户名称
  23. String userName;
  24. /// 用户Id
  25. String userId;
  26. /// 聊天名称
  27. String chatName;
  28. /// 消息体
  29. String message;
  30. /// message type
  31. DTMessageType messageType;
  32. /// 时间
  33. int time;
  34. /// 未读数量
  35. int unReadCount;
  36. /// 单聊
  37. bool isSingle;
  38. String avatar;
  39. /// 群聊信息
  40. bool isGroup;
  41. List<String> groupAvatars;
  42. /// 消息免打扰
  43. bool isDisturbing;
  44. /// 是否为置顶
  45. bool isStick;
  46. /// 特别关注
  47. bool isSpecialAttention;
  48. /// 是否 @ 你
  49. bool isAtYou;
  50. /// 是否 @ 全部
  51. bool isAtAll;
  52. }

创建 DTMessageItem 视图
  • 根据数据模型数据来显示

    1. class DTMessageItem extends StatelessWidget {
    2. final DTMessageModel messageModel;
    3. DTMessageItem({@required this.messageModel});
    4. @override
    5. Widget build(BuildContext context) {
    6. return Row(
    7. mainAxisAlignment: MainAxisAlignment.spaceBetween,
    8. children: <Widget>[
    9. DTMessageAvatar(messageModel: messageModel),
    10. Expanded(
    11. child: Column(
    12. children: <Widget>[
    13. Padding(
    14. padding: EdgeInsets.symmetric(vertical: kSize8),
    15. child: Row(
    16. mainAxisAlignment: MainAxisAlignment.spaceBetween,
    17. children: <Widget>[
    18. Expanded(
    19. child: Text(
    20. messageModel?.chatName ?? "",
    21. style: TextStyle(color: kColor33, fontSize: kSize34),
    22. overflow: TextOverflow.ellipsis,
    23. ),
    24. ),
    25. Text(
    26. _formatDate(),
    27. style: TextStyle(color: kColor99, fontSize: kSize26),
    28. overflow: TextOverflow.ellipsis,
    29. ),
    30. ],
    31. ),
    32. ),
    33. Row(
    34. mainAxisAlignment: MainAxisAlignment.spaceBetween,
    35. children: <Widget>[
    36. Expanded(
    37. child: RichText(
    38. text: TextSpan(children: [
    39. TextSpan(
    40. text: messageModel.isAtYou ? "[@你]" : "",
    41. style:
    42. TextStyle(color: kColorF1A33A, fontSize: kSize28),
    43. ),
    44. TextSpan(
    45. text: messageModel.isSpecialAttention ? "[特别关注]" : "",
    46. style:
    47. TextStyle(color: kColorF1A33A, fontSize: kSize28),
    48. ),
    49. TextSpan(
    50. text: messageModel.isAtAll ? "[@所有人]" : "",
    51. style:
    52. TextStyle(color: kColorF1A33A, fontSize: kSize28),
    53. ),
    54. TextSpan(
    55. text: messageModel?.message ?? "",
    56. style: TextStyle(color: kColor99, fontSize: kSize28),
    57. )
    58. ]),
    59. overflow: TextOverflow.ellipsis,
    60. ),
    61. ),
    62. (messageModel.unReadCount != null &&
    63. messageModel.unReadCount > 0 &&
    64. !messageModel.isDisturbing)
    65. ? Container(
    66. width: kSize32,
    67. height: kSize32,
    68. alignment: Alignment.center,
    69. decoration: BoxDecoration(
    70. color: kColorE86A3E,
    71. borderRadius: BorderRadius.circular(kSize20)),
    72. child: Text(
    73. messageModel.unReadCount.toString(),
    74. style: TextStyle(
    75. color: Colors.white, fontSize: kSize26),
    76. ))
    77. : Container(),
    78. messageModel.isDisturbing
    79. ? Row(
    80. children: <Widget>[
    81. SvgPicture.asset(
    82. 'assets/icons/icon_disturbing.svg',
    83. width: kSize36,
    84. color: kColorECEDED,
    85. ),
    86. messageModel.unReadCount != null &&
    87. messageModel.unReadCount > 0
    88. ? SvgPicture.asset(
    89. 'assets/icons/icon_red_dot.svg',
    90. width: kSize36,
    91. )
    92. : Container()
    93. ],
    94. )
    95. : Container()
    96. ],
    97. )
    98. ],
    99. ),
    100. )
    101. ],
    102. );
    103. }
    104. String _formatDate() {
    105. DateTime dateTime =
    106. DateTime.fromMillisecondsSinceEpoch(messageModel?.time ?? 0);
    107. return formatDate(dateTime, [HH, ':', nn]) ?? "";
    108. }
    109. }
  • 构建头像

    • 区分个人头像还是群组头像
    • 群组头像使用 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(

    1. margin: EdgeInsets.only(right: kSize24),
    2. child: (messageModel.isSingle && messageModel.chatName != null)
    3. ? CircleAvatar(
    4. radius: kSize50,
    5. child: Text(
    6. _getChatName(),
    7. style: TextStyle(color: Colors.white, fontSize: kSize32),
    8. ),
    9. backgroundColor: kPrimaryColor,
    10. )
    11. : Image.asset('assets/images/track_icon_attend.png',
    12. fit: BoxFit.cover),

    ); }

    String _getChatName() { String chatName = messageModel.chatName; if (chatName == null || chatName.isEmpty) {

    1. return "(^_^)";

    }

    chatName = chatName.replaceAll(‘\(’, “”).replaceAll(‘\)’, “”);

    /// RegExp regExp = RegExp(r’\(|\)’); /// chatName = chatName.replaceAll(regExp, “”); /// print(chatName);

    if (chatName.length == 2) {

    1. return chatName;

    }

    if (chatName.length > 2) {

    1. return chatName.substring(chatName.length - 2, chatName.length);

    } return “”; }

    Widget _buildGroupAvatar() { List groupAvatars = messageModel.groupAvatars; if (groupAvatars.length >= 4) {

    1. groupAvatars = groupAvatars.sublist(0, 4);

    } return NineGridView(

    1. width: kSize100,
    2. height: kSize100,
    3. type: NineGridType.dingTalkGp,
    4. itemCount: groupAvatars.length,
    5. space: kSizeDot5,
    6. margin: EdgeInsets.only(right: kSize24),
    7. itemBuilder: (BuildContext context, int index) {
    8. String userName = groupAvatars[index];
    9. return Container(
    10. color: kPrimaryColor,
    11. /// TODO: position ???
    12. alignment: Alignment.center,
    13. child: Container(
    14. margin: EdgeInsets.symmetric(horizontal: kSize6),
    15. child: Text(
    16. userName.length >= 2 ? userName.substring(1, 2) : userName,
    17. style: TextStyle(color: Colors.white),
    18. ),
    19. ),
    20. );
    21. },

    ); } } ```

代码地址: 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