You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter聊天气泡Read more/Read less功能无法动态调整大小问题

解决聊天气泡Read More/Read Less后无法动态调整大小的问题

问题核心原因

  1. showFullText变量定义在_buildChatBubble方法内部,每次Widget重建都会被重置为false,setState无法持久化状态
  2. Text组件未根据showFullText状态切换显示内容,始终固定显示截断文本
  3. 状态变化后未触发聊天气泡容器的自适应重绘

解决方案

将聊天气泡封装为独立的StatefulWidget,让每个气泡拥有自己的展开状态,同时修改文本显示逻辑以响应状态变化。

修改后的完整代码

独立聊天气泡组件

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class ChatBubble extends StatefulWidget {
  final String text;
  final bool isQuestion;
  final ThemeProvider themeProvider;
  final Function(String) onCopy;
  final Function(String) onSpeak;

  const ChatBubble({
    super.key,
    required this.text,
    required this.isQuestion,
    required this.themeProvider,
    required this.onCopy,
    required this.onSpeak,
  });

  @override
  State<ChatBubble> createState() => _ChatBubbleState();
}

class _ChatBubbleState extends State<ChatBubble> {
  final double borderRadiusValue = 20.0;
  final int maxTextLengthToShow = 200;
  late bool isTextTooLong;
  bool showFullText = false;

  @override
  void initState() {
    super.initState();
    isTextTooLong = widget.text.length > maxTextLengthToShow;
  }

  @override
  Widget build(BuildContext context) {
    final String avatarImage = widget.themeProvider.isIconSelected
        ? 'images/chatbot_icon4.jpg'
        : 'images/chatbot_icon4.jpg';

    if (!widget.isQuestion) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          SizedBox(height: MediaQuery.of(context).size.height * 0.01),
          Container(
            margin: const EdgeInsets.symmetric(vertical: 5.0),
            alignment: Alignment.centerLeft,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Padding(
                  padding: const EdgeInsets.only(left: 12.0, right: 5.0),
                  child: CircleAvatar(
                    radius: 20,
                    backgroundImage: AssetImage(avatarImage),
                    backgroundColor: Colors.transparent,
                  ),
                ),
                SizedBox(
                  width: MediaQuery.of(context).size.width * 0.7,
                  child: Container(
                    constraints: const BoxConstraints(
                      maxWidth: 230.0,
                    ),
                    padding: const EdgeInsets.all(12.0),
                    decoration: BoxDecoration(
                      color: widget.themeProvider.themeData.colorScheme.onError,
                      borderRadius: BorderRadius.only(
                        topLeft: Radius.circular(borderRadiusValue),
                        bottomRight: Radius.circular(borderRadiusValue),
                        topRight: Radius.circular(borderRadiusValue),
                      ),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          showFullText || !isTextTooLong
                              ? widget.text
                              : '${widget.text.substring(0, maxTextLengthToShow)}...',
                          style: const TextStyle(
                            color: CustomColor.whiteColor,
                            fontSize: 16.0,
                          ),
                        ),
                        if (isTextTooLong)
                          Row(
                            mainAxisAlignment: MainAxisAlignment.end,
                            children: [
                              TextButton(
                                onPressed: () {
                                  setState(() {
                                    showFullText = !showFullText;
                                  });
                                },
                                child: Text(
                                  showFullText ? 'Read Less' : 'Read More',
                                  style: const TextStyle(
                                    color: Colors.white,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                              ),
                              IconButton(
                                onPressed: () {
                                  widget.onCopy(widget.text);
                                },
                                icon: const Icon(
                                  size: 20.0,
                                  Icons.content_copy_rounded,
                                  color: Colors.white,
                                ),
                              ),
                              IconButton(
                                onPressed: () {
                                  widget.onSpeak(widget.text);
                                },
                                icon: const Icon(
                                  size: 20.0,
                                  Icons.volume_up_sharp,
                                  color: Colors.white,
                                ),
                              ),
                            ],
                          ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
          Row(
            children: [
              SizedBox(width: MediaQuery.of(context).size.width * 0.12),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 12.0),
                child: Text(
                  DateFormat.yMd().add_jm().format(DateTime.now()),
                  style: const TextStyle(
                    fontWeight: FontWeight.normal,
                    color: Colors.black,
                    fontSize: 12.0,
                  ),
                ),
              ),
            ],
          ),
          SizedBox(height: MediaQuery.of(context).size.height * 0.02),
        ],
      );
    } else {
      return Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          SizedBox(height: MediaQuery.of(context).size.height * 0.01),
          Container(
            margin: const EdgeInsets.symmetric(vertical: 5.0),
            alignment: Alignment.centerRight,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 10.0),
                  child: SizedBox(
                    width: MediaQuery.of(context).size.width * 0.75,
                    child: Container(
                      constraints: const BoxConstraints(
                        maxWidth: 230.0,
                      ),
                      padding: const EdgeInsets.all(12.0),
                      decoration: BoxDecoration(
                        color: widget.themeProvider.themeData.colorScheme.onBackground,
                        borderRadius: BorderRadius.only(
                          topRight: Radius.circular(borderRadiusValue),
                          bottomRight: Radius.circular(borderRadiusValue * 0.1),
                          bottomLeft: Radius.circular(borderRadiusValue),
                          topLeft: Radius.circular(borderRadiusValue),
                        ),
                      ),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Text(
                            widget.text,
                            style: TextStyle(
                              color: widget.themeProvider.themeData.colorScheme.onPrimary,
                              fontSize: 16.0,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12.0),
            child: Text(
              DateFormat.yMd().add_jm().format(DateTime.now()),
              style: const TextStyle(
                fontWeight: FontWeight.normal,
                color: Colors.black,
                fontSize: 12.0,
              ),
            ),
          ),
        ],
      );
    }
  }
}

调用聊天气泡组件

// 在原页面的build方法中替换原_buildChatBubble调用
Consumer<ThemeProvider>(
  builder: (context, themeProvider, _) {
    return ChatBubble(
      text: yourText, // 传入聊天文本
      isQuestion: isQuestion, // 标记是否为用户提问
      themeProvider: themeProvider,
      onCopy: _copyToClipboard, // 原页面的复制方法
      onSpeak: _speak, // 原页面的朗读方法
    );
  },
)

关键修改说明

  • 将聊天气泡拆分为独立StatefulWidget,每个气泡维护自己的showFullText状态,避免状态重置
  • 在initState中初始化isTextTooLong,减少重复计算
  • Text组件根据showFullText状态动态切换显示完整/截断文本
  • 通过回调函数传递复制、朗读方法,保持组件逻辑解耦

内容的提问来源于stack exchange,提问作者Alex Aaron

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 00:55:54