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

Flutter聊天APP打字机动画实现异常:旧消息也触发动画

问题描述

开发聊天APP时,希望收到新消息时展示打字机动画,但实现后所有历史消息都会触发该动画。尝试通过bool类型的isAnimated字段控制:新增消息时设为false触发动画,动画完成后设为true,但该方案未生效。

错误截图显示:聊天界面中所有历史消息气泡都在播放打字机动画,而非仅最新收到的消息。

相关Flutter代码:

class ChatMessage extends StatelessWidget {
  ChatMessage({super.key, required this.text, required this.isMe,required this.isAnimated});

  final String text;
  final bool isMe;
  bool isAnimated;
  
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Visibility(
          visible: isMe ? false : true,
          child: Padding(
            padding: const EdgeInsets.only(left: 10.0),
            child: CircleAvatar(
              child: Icon(Icons.abc),
              radius: 15,
            ),
          ),
        ),
        ChatBubble(
          clipper: ChatBubbleClipper5(
              type: isMe ? BubbleType.sendBubble : BubbleType.receiverBubble),
          alignment: isMe ? Alignment.topRight : Alignment.topLeft,
          margin: isMe
              ? EdgeInsets.only(bottom: 10, right: 10)
              : EdgeInsets.only(bottom: 10, left: 10),
          backGroundColor: isMe ? Colors.blue : Colors.grey.shade800,
          child: Container(
            constraints: BoxConstraints(
              maxWidth: MediaQuery.of(context).size.width * 0.65,
            ),
            child: isAnimated ? Text(
              text,
              style: TextStyle(color: Colors.white, fontSize: 15),
            ): AnimatedText(text),
          ),
        ),
      ],
    );
  }

  Widget AnimatedText(String text) {
    return DefaultTextStyle(
      style: GoogleFonts.varelaRound(
        fontSize: 15,
        color: Colors.white,
      ),
      child: AnimatedTextKit(
        totalRepeatCount: 1,
        animatedTexts: [TypewriterAnimatedText(text)],
      ),
    );
  }
}
解决方案

核心问题分析

  • StatelessWidget无法处理状态变化:ChatMessage是无状态组件,isAnimated作为可变字段,修改后不会触发组件重建,状态更新无法反映到UI上。
  • 动画完成后未正确更新状态:原代码没有监听动画结束事件,无法在动画播放完成后将isAnimated设为true。
  • 历史消息状态未持久化:列表刷新时所有消息的isAnimated可能被重置,导致历史消息重复播放动画。

修改步骤

  1. 将ChatMessage改为StatefulWidget,让每个消息组件能独立管理自身的动画状态:
class ChatMessage extends StatefulWidget {
  const ChatMessage({
    super.key,
    required this.text,
    required this.isMe,
    required this.playAnimation, // 新增:控制是否初始播放动画
  });

  final String text;
  final bool isMe;
  final bool playAnimation;

  @override
  State<ChatMessage> createState() => _ChatMessageState();
}

class _ChatMessageState extends State<ChatMessage> {
  late bool _isAnimated;

  @override
  void initState() {
    super.initState();
    // 初始化状态:如果需要播放动画则设为false,否则设为true
    _isAnimated = !widget.playAnimation;
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: widget.isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Visibility(
          visible: !widget.isMe,
          child: Padding(
            padding: const EdgeInsets.only(left: 10.0),
            child: const CircleAvatar(
              child: Icon(Icons.abc),
              radius: 15,
            ),
          ),
        ),
        ChatBubble(
          clipper: ChatBubbleClipper5(
              type: widget.isMe ? BubbleType.sendBubble : BubbleType.receiverBubble),
          alignment: widget.isMe ? Alignment.topRight : Alignment.topLeft,
          margin: widget.isMe
              ? const EdgeInsets.only(bottom: 10, right: 10)
              : const EdgeInsets.only(bottom: 10, left: 10),
          backGroundColor: widget.isMe ? Colors.blue : Colors.grey.shade800,
          child: Container(
            constraints: BoxConstraints(
              maxWidth: MediaQuery.of(context).size.width * 0.65,
            ),
            child: _isAnimated
                ? Text(
                    widget.text,
                    style: const TextStyle(color: Colors.white, fontSize: 15),
                  )
                : _buildAnimatedText(),
          ),
        ),
      ],
    );
  }

  Widget _buildAnimatedText() {
    return DefaultTextStyle(
      style: GoogleFonts.varelaRound(
        fontSize: 15,
        color: Colors.white,
      ),
      child: AnimatedTextKit(
        totalRepeatCount: 1,
        onFinished: () {
          // 动画结束后更新状态,切换为普通文本
          setState(() {
            _isAnimated = true;
          });
        },
        animatedTexts: [TypewriterAnimatedText(widget.text)],
      ),
    );
  }
}
  1. 在消息列表中控制动画触发:
    当新增消息时,给该消息的playAnimation参数传true,历史消息传false。例如在消息列表的构建中:
ListView.builder(
  itemCount: messages.length,
  itemBuilder: (context, index) {
    final message = messages[index];
    // 仅最新收到的对方消息播放动画
    final playAnimation = index == messages.length - 1 && !message.isMe;
    return ChatMessage(
      text: message.text,
      isMe: message.isMe,
      playAnimation: playAnimation,
    );
  },
)

关键改进点

  • 使用StatefulWidget管理每个消息的动画状态,确保状态变化能触发UI重建。
  • 通过onFinished回调监听动画结束事件,及时更新状态切换为普通文本。
  • 在列表层精准控制哪些消息需要播放动画,避免历史消息重复触发。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 09:07:47