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

Flutter/Dart:如何触发单个Widget的AnimationController动画并实现互斥选中?

解决多Chip互斥选中与动画触发问题

一、先理清父子组件的职责划分

  • 子组件(自定义Chip):只管自己的动画逻辑(AnimationController、动画曲线)和UI渲染,接收外部传入的选中状态,当状态变化时自动触发对应动画。
  • 父组件:负责管理全局的选中状态(比如记录当前选中的Chip标识),处理Chip的点击事件,点击某个Chip时更新全局状态,让所有Chip同步感知状态变化。

二、具体实现步骤

1. 封装带动画的自定义Chip

把动画逻辑留在子组件,但让它能响应外部的状态变化自动执行动画:

class AnimatedChip extends StatefulWidget {
  final String label;
  final bool isSelected;
  final VoidCallback onTap;

  const AnimatedChip({
    super.key,
    required this.label,
    required this.isSelected,
    required this.onTap,
  });

  @override
  State<AnimatedChip> createState() => _AnimatedChipState();
}

class _AnimatedChipState extends State<AnimatedChip> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    // 初始化时匹配外部传入的选中状态
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
      value: widget.isSelected ? 1.0 : 0.0,
    );
    _scaleAnimation = Tween<double>(begin: 0.9, end: 1.1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
    );
  }

  @override
  void didUpdateWidget(covariant AnimatedChip oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 外部选中状态变化时,触发对应动画
    if (widget.isSelected != oldWidget.isSelected) {
      widget.isSelected ? _animationController.forward() : _animationController.reverse();
    }
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onTap,
      child: ScaleTransition(
        scale: _scaleAnimation,
        child: Chip(
          label: Text(widget.label),
          backgroundColor: widget.isSelected ? Colors.blue : Colors.grey[200],
          labelStyle: TextStyle(
            color: widget.isSelected ? Colors.white : Colors.black,
          ),
        ),
      ),
    );
  }
}

2. 父组件管理全局选中状态

场景一:简单页面用setState即可

class ChipGroup extends StatefulWidget {
  const ChipGroup({super.key});

  @override
  State<ChipGroup> createState() => _ChipGroupState();
}

class _ChipGroupState extends State<ChipGroup> {
  String? _selectedChipLabel;

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8.0,
      children: [
        AnimatedChip(
          label: "Chip 1",
          isSelected: _selectedChipLabel == "Chip 1",
          onTap: () => setState(() => _selectedChipLabel = "Chip 1"),
        ),
        AnimatedChip(
          label: "Chip 2",
          isSelected: _selectedChipLabel == "Chip 2",
          onTap: () => setState(() => _selectedChipLabel = "Chip 2"),
        ),
        AnimatedChip(
          label: "Chip 3",
          isSelected: _selectedChipLabel == "Chip 3",
          onTap: () => setState(() => _selectedChipLabel = "Chip 3"),
        ),
      ],
    );
  }
}

场景二:复杂跨组件状态用ChangeNotifier+Provider

先写状态管理类:

class ChipSelectionNotifier extends ChangeNotifier {
  String? _selectedLabel;

  String? get selectedLabel => _selectedLabel;

  void selectLabel(String label) {
    _selectedLabel = label;
    notifyListeners();
  }
}

再写父组件:

class ChipGroup extends StatelessWidget {
  const ChipGroup({super.key});

  @override
  Widget build(BuildContext context) {
    final selectionNotifier = Provider.of<ChipSelectionNotifier>(context);
    return Wrap(
      spacing: 8.0,
      children: [
        AnimatedChip(
          label: "Chip 1",
          isSelected: selectionNotifier.selectedLabel == "Chip 1",
          onTap: () => selectionNotifier.selectLabel("Chip 1"),
        ),
        AnimatedChip(
          label: "Chip 2",
          isSelected: selectionNotifier.selectedLabel == "Chip 2",
          onTap: () => selectionNotifier.selectLabel("Chip 2"),
        ),
        AnimatedChip(
          label: "Chip 3",
          isSelected: selectionNotifier.selectedLabel == "Chip 3",
          onTap: () => selectionNotifier.selectLabel("Chip 3"),
        ),
      ],
    );
  }
}

记得在顶层用ChangeNotifierProvider包裹:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => ChipSelectionNotifier(),
      child: const MyApp(),
    ),
  );
}

三、关键注意点

  • 子组件必须通过didUpdateWidget监听外部状态变化,这是触发动画的核心——声明式UI中组件只会在属性变化时重建,这个生命周期方法能精准捕捉状态切换并执行动画。
  • 全局选中状态必须由父组件统一管理,不能让每个Chip自己维护状态,这样才能保证互斥性:全局只有一个“当前选中项”,所有Chip的选中状态都基于这个值。
  • 之前用Provider没成功的话,大概率是没在子组件里实现didUpdateWidget触发动画,或者Provider的监听范围没覆盖到所有Chip组件。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 20:43:18