聊天功能是买卖双方沟通的核心,买家询问商品细节、协商价格、约定交易方式都在聊天中完成。今天我们来实现"闲置换"的聊天页面,包括消息气泡展示和消息发送功能。

聊天页面的设计思路

聊天页面的核心是消息列表和输入框。消息列表展示双方的对话,自己发的消息靠右显示绿色气泡,对方的消息靠左显示白色气泡。底部是输入框和发送按钮,支持快速发送文字消息。这种左右分布的布局是聊天界面的标准设计,用户一眼就能分清哪些是自己说的。

完整代码实现

import 'package:flutter/material.dart';

class ChatPage extends StatefulWidget {
  final int userId;
  final String userName;

  const ChatPage({super.key, required this.userId, required this.userName});

  
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  final TextEditingController _messageController = TextEditingController();
  final ScrollController _scrollController = ScrollController();
  
  final List<Map<String, dynamic>> _messages = [
    {'isMe': false, 'content': '你好,这个还在吗?', 'time': '10:30'},
    {'isMe': true, 'content': '在的,有什么问题吗?', 'time': '10:31'},
    {'isMe': false, 'content': '可以便宜点吗?', 'time': '10:32'},
  ];

聊天页面接收userIduserName两个参数,用于显示对方的信息和发送消息时标识接收者。定义了两个Controller:_messageController控制输入框,可以获取用户输入的内容和清空输入框;_scrollController控制消息列表滚动,发送新消息后需要自动滚动到底部让用户看到最新消息。_messages是消息列表,每条消息包含是否是自己发的、消息内容、发送时间,实际项目中还需要消息id、发送状态等字段。

  
  void dispose() {
    _messageController.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.userName),
        actions: [
          IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              controller: _scrollController,
              padding: const EdgeInsets.all(16),
              itemCount: _messages.length,
              itemBuilder: (context, index) => _buildMessageBubble(_messages[index]),
            ),
          ),
          _buildInputBar(),
        ],
      ),
    );
  }

dispose方法释放两个Controller,这是Flutter开发的好习惯,避免内存泄漏。页面结构用Column垂直排列消息列表和输入框,Expanded让消息列表占据除输入框外的所有空间。AppBar显示对方的用户名,让用户知道是在和谁聊天,右边放更多按钮可以弹出举报、拉黑等选项。消息列表用ListView.builder实现懒加载,传入_scrollController用于控制滚动,padding给列表加内边距让消息不会贴着屏幕边缘。

  Widget _buildMessageBubble(Map<String, dynamic> message) {
    final isMe = message['isMe'] as bool;
    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: Row(
        mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (!isMe) ...[
            CircleAvatar(
              radius: 18,
              backgroundColor: Colors.grey[300],
              child: Text(widget.userName[0], style: const TextStyle(fontSize: 14)),
            ),
            const SizedBox(width: 8),
          ],

消息气泡的布局根据是否是自己发的来决定对齐方式。自己的消息MainAxisAlignment.end靠右显示,对方的消息MainAxisAlignment.start靠左显示。crossAxisAlignment: CrossAxisAlignment.start让头像和气泡顶部对齐,如果消息很长换行了,头像不会跑到中间去。对方的消息前面显示头像,用CircleAvatar做成圆形,显示用户名的首字母,实际项目中应该显示真实头像图片。展开运算符...[]配合if实现条件渲染,只有对方的消息才显示头像。

          Flexible(
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
              decoration: BoxDecoration(
                color: isMe ? const Color(0xFF07C160) : Colors.white,
                borderRadius: BorderRadius.circular(16),
              ),
              child: Text(
                message['content'],
                style: TextStyle(color: isMe ? Colors.white : Colors.black87),
              ),
            ),
          ),
          if (isMe) ...[
            const SizedBox(width: 8),
            CircleAvatar(
              radius: 18,
              backgroundColor: const Color(0xFF07C160),
              child: const Text('我', style: TextStyle(fontSize: 14, color: Colors.white)),
            ),
          ],
        ],
      ),
    );
  }

消息气泡用Flexible包裹,这样长消息会自动换行而不是撑破布局超出屏幕。气泡用圆角矩形,自己的消息是绿色背景白色文字,对方的消息是白色背景黑色文字,这种配色方案和微信类似,用户很熟悉。自己的消息后面显示头像,绿色背景加"我"字,和对方的头像形成对比。padding给气泡内容加内边距,文字不会贴着边缘,阅读更舒适。

  Widget _buildInputBar() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: SafeArea(
        child: Row(
          children: [
            IconButton(
              icon: const Icon(Icons.add_circle_outline, color: Colors.grey),
              onPressed: () {},
            ),

输入栏用Container包裹,加上向上的阴影让它看起来悬浮在消息列表上方,有层次感。SafeArea确保在有底部安全区域的设备上输入框不会被遮挡,比如iPhone的Home Indicator区域。左边放一个加号按钮,点击可以展开更多功能,比如发送图片、位置、商品链接等,这些功能在二手交易场景中很实用,卖家可以发送商品图片,买家可以发送收货地址。

            Expanded(
              child: TextField(
                controller: _messageController,
                decoration: InputDecoration(
                  hintText: '输入消息...',
                  hintStyle: TextStyle(color: Colors.grey[400]),
                  filled: true,
                  fillColor: Colors.grey[100],
                  contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(20),
                    borderSide: BorderSide.none,
                  ),
                ),
              ),
            ),
            const SizedBox(width: 8),
            GestureDetector(
              onTap: _sendMessage,
              child: Container(
                padding: const EdgeInsets.all(10),
                decoration: const BoxDecoration(
                  color: Color(0xFF07C160),
                  shape: BoxShape.circle,
                ),
                child: const Icon(Icons.send, color: Colors.white, size: 20),
              ),
            ),
          ],
        ),
      ),
    );
  }

输入框用Expanded占据中间的空间,圆角胶囊形状,灰色背景,看起来简洁现代。contentPadding控制文字和边框的距离,让输入框看起来不会太挤。发送按钮是一个绿色圆形,里面放发送图标,用GestureDetector处理点击比IconButton更灵活,可以自定义按钮的样式和大小。点击发送按钮调用_sendMessage方法发送消息。

  void _sendMessage() {
    if (_messageController.text.trim().isEmpty) return;
    setState(() {
      _messages.add({
        'isMe': true,
        'content': _messageController.text,
        'time': '${DateTime.now().hour}:${DateTime.now().minute}',
      });
    });
    _messageController.clear();
    Future.delayed(const Duration(milliseconds: 100), () {
      _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    });
  }
}

发送消息的方法先检查输入是否为空,trim()去掉首尾空格,避免发送纯空格的无意义消息。然后往消息列表添加新消息,isMe: true表示是自己发的,时间用当前时间。_messageController.clear()清空输入框,方便用户继续输入下一条消息。Future.delayed延迟100毫秒后滚动到底部,这个延迟是为了等待setState完成UI更新,新消息渲染出来后再滚动。animateTo带动画地滚动到maxScrollExtent也就是列表底部,Curves.easeOut让滚动有一个减速的效果,看起来更自然流畅。

聊天功能的扩展

实际项目中聊天功能还需要这些能力:消息类型扩展支持图片、语音、位置、商品卡片等;消息状态显示发送中、已发送、已读等状态;历史消息加载打开聊天页面时加载最近的消息,往上滚动时加载更早的历史消息;实时消息接收用WebSocket保持长连接,对方发消息时实时显示在列表中。

小结

这篇实现了"闲置换"App的聊天页面,包括消息气泡的左右布局、输入框和发送功能。自己的消息绿色靠右,对方的消息白色靠左,发送后自动滚动到底部。这是一个基础的聊天功能,实际项目中还需要扩展更多消息类型和功能。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐