本文完整呈现一个可落地的聊天页面:自定义渐变气泡、顺滑的滚动回到底部体验、基于 BLoC 的消息流转,以及 RenderObject 级别的文字/状态排版优化。代码已适配中文环境(
zh_CN)并提供轻量主题方案(FlexColorScheme)。(文末代码下载链接),
目录
- 在线效果预览
- 项目亮点
- 依赖与环境
- 目录结构
- 整体架构
- 页面与状态流转
- 自定义聊天气泡:渐变背景 & 状态布局
- 输入框系统:MessageInputController
- 消息列表与滚动体验
- 主题与系统栏适配
- 国际化与时间格式
- 可用性与性能优化
- 结语
- 仓库地址
在线效果预览
项目亮点
- 🎨 高质感聊天气泡:我的消息使用多段渐变(四色),对方为白底,圆角根据上下相邻消息动态变化。
- 🧮 RenderObject 级布局:
TextMessageWidget将“发送时间/已读状态”与最后一行文本进行智能贴合,减少冗余空白。 - 🧱 BLoC 状态管理:消息获取与发送事件分离,UI 与业务解耦,便于扩展(上拉加载/消息回执/撤回等)。
- 🧭 专业滚动体验:
ScrollablePositionedList+ 「回到底部」浮动按钮,支持反向列表与平滑滚动。 - 🌐 国际化时间:
intl+jiffy,统一中文环境时间格式。 - 🧰 工程化主题:
FlexColorScheme提供可扩展的浅色主题;系统状态栏/导航栏按平台分别适配。
依赖与环境
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.0
equatable: ^2.0.5
intl: ^0.19.0
jiffy: ^6.2.1
scrollable_positioned_list: ^0.3.8
flex_color_scheme: ^7.3.1
uuid: ^4.5.1
- 需要在
pubspec.yaml中声明图片资源(背景图、头像占位图、分享图等)。
目录结构
lib/
├─ main.dart
├─ app_ui/
│ └─ app_theme.dart
└─ chat/
├─ bloc/
│ ├─ chat_bloc.dart
│ ├─ chat_event.dart
│ └─ chat_state.dart
├─ model/
│ ├─ message.dart
│ └─ User.dart
├─ view/
│ └─ chat_page.dart
└─ widgets/
├─ message_bubble.dart
├─ message_bubble_background.dart
├─ message_conent.dart
├─ message_text.dart
├─ message_input_controller.dart
└─ message_text_field.dart
整体架构
MaterialApp (AppTheme)
└── ChatPage (BlocProvider<ChatBloc>)
└── ChatView (Stateful)
├── AppBar: ChatAppBar
├── Expanded: ChatMessagesListView
│ └── ScrollablePositionedList (reverse=true)
│ └── MessageBubble
│ ├── MessageBubbleBackground (CustomPainter 渐变)
│ └── MessageBubbleContent
│ ├── TextMessageWidget (RenderObject 文本+状态)
│ └── MessageSharePost (分享卡片)
└── ChatMessageTextField (输入框/发送按钮)
页面与状态流转
入口与主题
void main() {
WidgetsFlutterBinding.ensureInitialized();
initializeDateFormatting('zh_CN', null).then((_) => runApp(const MyApp()));
}
class MyApp extends StatelessWidget { ... }
- 要点:提前初始化
intl的zh_CN本地化,再启动应用。
BLoC:消息获取与发送
class ChatBloc extends Bloc<ChatEvent, ChatState> {
ChatBloc() : super(const ChatState.initial()) {
on<ChatMessagesRequested>(_onMessagesRequested);
on<ChatSendMessageRequested>(_onSendMessageRequested);
}
...
}
ChatMessagesRequested:初始化拉取Message.messageList(演示数据)。ChatSendMessageRequested:将输入框生成的Message追加到状态。
自定义聊天气泡:渐变背景 & 状态布局
渐变背景 MessageBubbleBackground
- 通过
CustomPainter+ui.Gradient.linear实现 随滚动位置变化的线性渐变:
class BubblePainter extends CustomPainter {
BubblePainter({ required ScrollableState scrollable, ... })
: super(repaint: scrollable.position); // 随滚动重绘
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..shader = ui.Gradient.linear(
scrollableRect.topCenter,
scrollableRect.bottomCenter,
_colors,
_colors.length == 4 ? [0.0, 0.5, 0.75, 1.0] : [1.0],
TileMode.clamp,
Matrix4.translationValues(-origin.dx, -origin.dy, 0).storage,
);
canvas.drawRect(Offset.zero & size, paint);
}
}
- 关键点:
repaint: scrollable.position能够在滚动时驱动渐变刷新,且每条气泡外层套了RepaintBoundary,避免全列表重绘。
文本与状态的排版 TextMessageWidget
- 自定义
RenderBox,将发送时间/已读图标作为child贴在最后一行文本旁边:
class RenderTextMessageWidget extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
@override
Size _performLayout({required BoxConstraints constraints, required bool dry}) {
textPainter = TextPainter(...); textPainter.layout(maxWidth: constraints.maxWidth);
final lines = textPainter.computeLineMetrics();
final lastLineWidth = lines.last.width;
// 若最后一行 + 状态宽度超出一行,则整体高度加半行作为“折行区”
if (child != null) {
final childSize = dry
? child!.getDryLayout(BoxConstraints(maxWidth: constraints.maxWidth))
: (child!..layout(BoxConstraints(maxWidth: constraints.maxWidth), parentUsesSize: true)).size;
if (lastLineWidth + spacing > constraints.maxWidth - childSize.width) {
height += childSize.height * 0.5;
} else if (lines.length == 1) {
width += childSize.width + spacing;
}
}
return Size(width, height);
}
@override
void paint(PaintingContext context, Offset offset) {
textPainter.paint(context.canvas, offset);
final parentData = child!.parentData! as BoxParentData;
context.paintChild(child!, offset + parentData.offset);
}
}
- 效果:单行文本时,时间/已读直接跟在文字后;多行时,尽量贴近最后一行,超出就下沉半行。
分享卡片样式 MessageSharePost
- 头像 + 用户名 + 图片 + 文字说明,右下角悬浮消息状态:
Positioned.fill(
right: 12, bottom: 4,
child: Align(
alignment: Alignment.bottomRight,
child: MessageStatuses(message: message),
),
)
输入框系统:MessageInputController
- 使用
ValueNotifier<Message>管理输入态,与TextEditingController双向同步:
class MessageInputController extends ValueNotifier<Message> {
MessageInputController._(...) : super(initialMessage) {
_textFieldController.addListener(_textFieldListener);
}
void _textFieldListener() {
message = message.copyWith(message: _textFieldController.text);
}
void reset({bool resetId = true}) { ... }
}
- 发送逻辑:回车/点击发送触发
_onSend(),派发ChatSendMessageRequested,然后重置输入并滚动回底部。
消息列表与滚动体验
- 使用
ScrollablePositionedList.separated,并设置reverse: true让 最新消息在底部:
ScrollablePositionedList.separated(
itemCount: messages.length,
reverse: true,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
...
)
- 「回到底部」按钮根据
UserScrollNotification的方向切换显示:
if (notification.direction == ScrollDirection.forward) {
_showScrollToBottom.value = false;
} else if (notification.direction == ScrollDirection.reverse) {
_showScrollToBottom.value = true;
}
- 点击后滚动:
itemScrollController.scrollTo(index: 0, duration: 150ms, curve: easeIn)。
主题与系统栏适配
- 采用
FlexColorScheme定义浅色主题,并通过扩展方法为 Android/iOS 设置不同的系统栏样式:
extension SystemNavigationBarTheme on Widget {
Widget withSystemNavigationBarTheme(BuildContext context) =>
AnnotatedRegion<SystemUiOverlayStyle>(value: ... , child: this);
}
国际化与时间格式
- 在
main()初始化:initializeDateFormatting('zh_CN', null)。 - 显示格式:
DateFormat.Hm('zh_CN').format(message.createAt)。
注意 API:
DateFormat.Hm('zh_CN')更直观;如需 24 小时制、日期拼接,可换用DateFormat('yyyy-MM-dd HH:mm')。
可用性与性能优化
- ✅
RepaintBoundary包裹气泡内容,减少渐变重绘影响。 - ✅ 列表项
key: ValueKey(message.id)提升复用稳定性。 - ✅ 控制
maxLines、overflow,避免极端长文本卡顿。 - ✅ 所有异步滚动与动画时长使用 毫秒 级(可读性更好)。
- ✅ 图片缩略图通过
ResizeImage.resizeIfNeeded控制分辨率。
结语
本文完整拆解了一个高质感的 Flutter 聊天页面:从 UI 细节到状态管理,再到滚动与绘制优化。你可以在此基础上扩展 图片/语音/撤回/已读回执/引用回复/消息搜索 等高级能力。