点击消息气泡显示发送时间功能开发笔记

December 28, 2025
4 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

1. 功能需求背景

在聊天界面中,需要查看消息的具体发送时间,但又不想让时间戳一直显示在界面上影响视觉效果。参考 Teams 等应用的设计,决定实现点击气泡显示发送时间的功能。

1.1 动机

  • 点击消息气泡后,显示该消息的精确发送时间
  • 时间显示在气泡下方,不影响原有布局
  • 再次点击可隐藏时间
  • 支持文本消息和多媒体消息(图片/视频)

1.2 设计考量

  • 视觉简洁性:时间戳默认隐藏,减少界面元素
  • 交互一致性:与现有消息气泡交互方式保持一致
  • 性能考虑:避免不必要的重构建

2. 技术方案设计

2.1 架构分析

原有消息气泡组件:

  • MessageBubble:文本消息气泡(StatelessWidget)
  • MediaMessageBubble:多媒体消息气泡(图片/视频,StatelessWidget)

需要将两个组件改为 StatefulWidget 以支持状态管理。

2.2 状态管理方案

  • _showTime:布尔状态,控制时间显示/隐藏
  • _toggleTime():切换时间显示状态的方法
  • _formatTime():时间格式化方法

2.3 UI 布局调整

  • 将 Row 布局改为 Column 布局
  • 气泡在上,时间在下
  • 保持原有的左右对齐逻辑

3. 实现细节

3.1 MessageBubble 组件改造

class MessageBubble extends StatefulWidget {
  // ... 原有参数
}
 
class _MessageBubbleState extends State<MessageBubble> {
  bool _showTime = false;
 
  void _toggleTime() {
    setState(() => _showTime = !_showTime);
    // 调用外部 onTap 回调
    widget.onTap?.call();
  }
 
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Column(
        crossAxisAlignment: widget.isSent
            ? CrossAxisAlignment.end
            : CrossAxisAlignment.start,
        children: [
          // 气泡内容
          Row(
            mainAxisAlignment: widget.isSent
                ? MainAxisAlignment.end
                : MainAxisAlignment.start,
            children: [
              if (widget.isSent) const Spacer(),
              Flexible(
                flex: 4,
                child: GestureDetector(
                  onTap: _toggleTime,  // 修改:使用内部方法
                  onLongPress: widget.onLongPress,
                  child: Container(
                    // ... 气泡样式
                    child: widget.customContent ?? 
                           _buildTextContent(widget.text ?? '', textColor, context),
                  ),
                ),
              ),
              if (!widget.isSent) const Spacer(),
            ],
          ),
          // 时间显示(条件渲染)
          if (_showTime)
            Padding(
              padding: EdgeInsets.only(
                top: 4,
                left: widget.isSent ? 0 : 16,
                right: widget.isSent ? 16 : 0,
              ),
              child: Text(
                _formatTime(widget.time),
                style: TextStyle(
                  fontSize: 11,
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ),
        ],
      ),
    );
  }
}

3.2 MediaMessageBubble 组件改造

class MediaMessageBubble extends StatefulWidget {
  // ... 原有参数
}
 
class _MediaMessageBubbleState extends State<MediaMessageBubble> {
  bool _showTime = false;
 
  void _toggleTime() {
    setState(() => _showTime = !_showTime);
    widget.onTap?.call();
  }
 
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Column(
        crossAxisAlignment: widget.isSent
            ? CrossAxisAlignment.end
            : CrossAxisAlignment.start,
        children: [
          // 媒体内容
          Row(
            mainAxisAlignment: widget.isSent
                ? MainAxisAlignment.end
                : MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              // ... 媒体内容
            ],
          ),
          // 时间显示(条件渲染)
          if (_showTime)
            Padding(
              padding: EdgeInsets.only(
                top: 4,
                left: widget.isSent ? 0 : 16,
                right: widget.isSent ? 16 : 0,
              ),
              child: Text(
                _formatTime(widget.time),
                style: TextStyle(
                  fontSize: 11,
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ),
        ],
      ),
    );
  }
}

3.3 时间格式化

String _formatTime(DateTime time) {
  final local = time.toLocal();
  final hour = local.hour.toString().padLeft(2, '0');
  final minute = local.minute.toString().padLeft(2, '0');
  return '$hour:$minute';
}

4. 遇到的问题及解决方案

4.1 状态访问问题

问题:在 StatefulWidget 的内部方法中无法直接访问 widget 的参数 解决方案:使用 widget.parameter 访问外部参数

4.2 事件处理链问题

问题:点击气泡时需要同时切换时间显示和触发外部事件 解决方案:在 _toggleTime 中先处理内部状态,再调用外部回调

4.3 布局对齐问题

问题:从 Row 改为 Column 后,左右对齐逻辑需要调整 解决方案:保持原有的 CrossAxisAlignment 对齐逻辑

5. 代码变更

5.1 文件变更

  • client/lib/ui/widgets/chat_widgets.dart:修改 MessageBubbleMediaMessageBubble

5.2 组件变更

  • MessageBubble:StatelessWidget → StatefulWidget
  • MediaMessageBubble:StatelessWidget → StatefulWidget
  • 添加状态管理逻辑
  • 调整布局结构

6. 测试验证

6.1 功能测试

  • 点击文本气泡显示时间
  • 点击文本气泡隐藏时间
  • 点击图片气泡显示时间
  • 点击图片气泡隐藏时间
  • 点击视频气泡显示时间
  • 点击视频气泡隐藏时间
  • 保持原有的长按菜单功能

6.2 性能测试

  • 确保无不必要的重构建
  • 滚动性能正常
  • 内存使用正常

6.3 代码验证

  • flutter analyze 无错误
  • 代码风格符合规范

7. 设计决策总结

7.1 为什么选择 StatefulWidget

  • 需要管理 showTime 状态
  • 避免将状态提升到父组件,保持组件独立性

7.2 为什么使用 Column 布局

  • 时间显示在气泡下方,自然的垂直布局
  • 保持左右对齐逻辑的一致性

7.3 为什么在气泡下方显示时间

  • 不影响原有气泡视觉效果
  • 时间信息与气泡内容关联性强
  • 避免时间戳覆盖在多媒体内容上

8. 未来优化方向

8.1 功能扩展

  • 支持显示完整日期时间
  • 添加时间显示动画效果
  • 支持批量时间显示

8.2 性能优化

  • 考虑使用 AnimatedSwitcher 实现平滑过渡
  • 优化状态管理,避免不必要的重建