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

如何基于TextField的controller值实现Text组件的动画效果?

实现TextField字符增删的差异化动画

之前的方案仅在添加字符时生效,问题出在删除字符时对应的组件会被Flutter直接销毁,根本没机会播放动画。要解决这个问题,得跟踪每个字符的生命周期,让删除操作先触发动画,等动画结束再移除字符。

具体实现步骤

1. 定义字符状态类

创建类存储每个字符的核心信息,包括字符内容、唯一ID(区分重复字符)和当前状态:

class CharItem {
  final String char;
  final String id;
  CharStatus status;

  CharItem({
    required this.char,
    required this.id,
    this.status = CharStatus.adding,
  });
}

enum CharStatus { adding, normal, removing }

2. 监听文本变化,更新字符状态

对比新旧文本内容,筛选出新增和待删除的字符,分别标记对应状态:

void _handleTextChange(String newText) {
  final oldChars = [..._charItems];
  final newChars = newText.split('').map((c) => CharItem(
        char: c,
        id: '${DateTime.now().microsecondsSinceEpoch}_$c', // 生成唯一ID
      )).toList();

  // 筛选新增字符
  final addedChars = newChars.where((newChar) => 
    !oldChars.any((old) => old.char == newChar.char && old.status != CharStatus.removing)
  ).toList();
  // 筛选待删除字符
  final removedChars = oldChars.where((old) => 
    !newChars.any((newChar) => newChar.char == old.char)
  ).toList();

  // 添加新字符并标记为添加状态
  setState(() => _charItems.addAll(addedChars));

  // 标记待删除字符状态,动画结束后移除
  for (var item in removedChars) {
    final index = _charItems.indexOf(item);
    if (index != -1) {
      setState(() => _charItems[index].status = CharStatus.removing);
      Future.delayed(const Duration(milliseconds: 300), () {
        if (mounted) setState(() => _charItems.removeAt(index));
      });
    }
  }

  // 新增字符动画结束后改为正常状态
  for (var item in addedChars) {
    Future.delayed(const Duration(milliseconds: 300), () {
      if (mounted) setState(() => item.status = CharStatus.normal);
    });
  }
}

3. 为每个字符绑定差异化动画

使用AnimatedBuilder根据字符状态渲染对应动画:添加时缩放淡入,删除时缩放淡出:

Row(
  mainAxisSize: MainAxisSize.min,
  children: _charItems.map((item) {
    return AnimatedBuilder(
      animation: _animController,
      builder: (context, child) {
        double scale = 1.0;
        double opacity = 1.0;

        if (item.status == CharStatus.adding) {
          scale = Tween(begin: 0.5, end: 1.0).animate(
            CurvedAnimation(parent: _animController, curve: Curves.easeOut)
          ).value;
          opacity = Tween(begin: 0.0, end: 1.0).animate(
            CurvedAnimation(parent: _animController, curve: Curves.easeOut)
          ).value;
        } else if (item.status == CharStatus.removing) {
          scale = Tween(begin: 1.0, end: 0.5).animate(
            CurvedAnimation(parent: _animController, curve: Curves.easeIn)
          ).value;
          opacity = Tween(begin: 1.0, end: 0.0).animate(
            CurvedAnimation(parent: _animController, curve: Curves.easeIn)
          ).value;
        }

        return Transform.scale(
          scale: scale,
          child: Opacity(
            opacity: opacity,
            child: Text(item.char, style: const TextStyle(fontSize: 24)),
          ),
        );
      },
    );
  }).toList(),
)

4. 初始化控制器与监听

在State类中完成控制器初始化,并绑定文本变化监听:

late TextEditingController _textController;
late AnimationController _animController;
List<CharItem> _charItems = [];

@override
void initState() {
  super.initState();
  _textController = TextEditingController();
  _animController = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 300),
  );
  _textController.addListener(() {
    _handleTextChange(_textController.text);
    _animController.forward(from: 0.0);
  });
}

@override
void dispose() {
  _textController.dispose();
  _animController.dispose();
  super.dispose();
}

关键注意事项

  • 必须为每个字符生成唯一ID,否则连续输入相同字符时会出现判断错误。
  • 删除字符时不能直接从列表移除,需先标记删除状态播放动画,动画结束后再执行移除操作。
  • 使用mounted判断组件是否存活,避免异步更新状态时出现崩溃。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 14:41:16