如何在Flutter中实现聊天应用的自定义聊天气泡组件?

Flutter 聊天气泡组件实现方案
核心实现思路
这种带侧边尖角的聊天气泡推荐用CustomPaint自定义绘制实现,灵活度最高,可适配各种尺寸、样式需求,不需要依赖第三方库。
具体实现步骤
1. 自定义气泡背景绘制类
通过CustomPainter绘制带尖角的气泡轮廓,支持左右两种方向适配发送/接收消息场景:
class ChatBubblePainter extends CustomPainter { final Color bgColor; final bool isSendByMe; // true为当前用户发送的气泡,显示在右侧 final double radius; final double arrowSize; ChatBubblePainter({ required this.bgColor, required this.isSendByMe, this.radius = 8, this.arrowSize = 10, }); @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = bgColor; final path = Path(); if (isSendByMe) { // 右侧气泡逻辑:尖角在右侧 path.moveTo(radius, 0); path.lineTo(size.width - arrowSize - radius, 0); path.quadraticBezierTo(size.width - arrowSize, 0, size.width - arrowSize, radius); path.lineTo(size.width - arrowSize, size.height - radius - arrowSize/2); path.lineTo(size.width, size.height - arrowSize); path.lineTo(size.width - arrowSize, size.height - arrowSize/2); path.lineTo(size.width - arrowSize, size.height - radius); path.quadraticBezierTo(size.width - arrowSize, size.height, size.width - arrowSize - radius, size.height); path.lineTo(radius, size.height); path.quadraticBezierTo(0, size.height, 0, size.height - radius); path.lineTo(0, radius); path.quadraticBezierTo(0, 0, radius, 0); } else { // 左侧气泡逻辑:尖角在左侧 path.moveTo(arrowSize + radius, 0); path.lineTo(size.width - radius, 0); path.quadraticBezierTo(size.width, 0, size.width, radius); path.lineTo(size.width, size.height - radius); path.quadraticBezierTo(size.width, size.height, size.width - radius, size.height); path.lineTo(arrowSize + radius, size.height); path.quadraticBezierTo(arrowSize, size.height, arrowSize, size.height - radius); path.lineTo(arrowSize, size.height - arrowSize/2); path.lineTo(0, size.height - arrowSize); path.lineTo(arrowSize, size.height - arrowSize/2); path.lineTo(arrowSize, radius); path.quadraticBezierTo(arrowSize, 0, arrowSize + radius, 0); } canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant ChatBubblePainter oldDelegate) { return bgColor != oldDelegate.bgColor || isSendByMe != oldDelegate.isSendByMe; } }
2. 封装完整聊天气泡组件
把自定义背景和文本、头像等内容组合,封装成可直接调用的组件:
class ChatBubble extends StatelessWidget { final String content; final bool isSendByMe; final TextStyle? textStyle; const ChatBubble({ super.key, required this.content, required this.isSendByMe, this.textStyle, }); @override Widget build(BuildContext context) { return Row( mainAxisAlignment: isSendByMe ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ // 对方消息左侧展示头像 if (!isSendByMe) const Padding( padding: EdgeInsets.only(right: 4), child: CircleAvatar(radius: 16, child: Icon(Icons.person, size: 16)), ), // 气泡主体 CustomPaint( painter: ChatBubblePainter( bgColor: isSendByMe ? Colors.blueAccent : Colors.grey[200]!, isSendByMe: isSendByMe, ), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), // 限制气泡最大宽度为屏幕70%,避免过长内容撑满全屏 constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.7), child: Text( content, style: textStyle ?? TextStyle( color: isSendByMe ? Colors.white : Colors.black87, fontSize: 14, ), ), ), ), // 自己发送的消息右侧展示头像 if (isSendByMe) const Padding( padding: EdgeInsets.only(left: 4), child: CircleAvatar(radius: 16, child: Icon(Icons.person, size: 16)), ), ], ); } }
3. 调用示例
直接在聊天列表中循环调用即可:
ListView.builder( itemCount: 10, itemBuilder: (context, index) { final isSendByMe = index % 2 == 0; return Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), child: ChatBubble( content: isSendByMe ? "这是我发送的消息" : "这是我收到的消息,内容可以很长,会自动换行显示", isSendByMe: isSendByMe, ), ); }, )
可扩展优化点
- 支持富内容:将气泡内的Text替换为图片、视频、卡片等组件即可,背景绘制逻辑无需修改
- 连续消息优化:同一用户连续发送的多条消息,可仅保留最后一条的尖角,更符合主流聊天APP交互习惯
- 附加信息扩展:通过Stack组件可在气泡右下角添加发送时间、已读状态等附加信息
内容的提问来源于stack exchange,提问作者murali
相关产品推荐
相关产品推荐

