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

Flutter动画按钮子组件切换失效:需二次触发的解决问询

问题分析与解决方案

你的问题核心在于状态更新没有触发UI重建,以及动画与状态切换的时序匹配问题。以下是具体修复方案:

1. 核心问题拆解

  • 用静态类_LocalVariables存储isLoading:即便值改变,StatelessWidget不会自动重建,UI无法更新;就算改成StatefulWidget,没调用setState也不会触发刷新。
  • 动画回调仅修改静态变量,未触发父组件的状态更新,导致首次切换时UI无变化,第二次触发才可能因其他逻辑间接触发重建。

2. 修改后的代码实现

动画组件类(优化时序)

typedef LoadingStatusCallback = void Function(bool isLoading);

class AMASOpacityAndScale extends StatefulWidget {
  const AMASOpacityAndScale(
      {super.key,
      required this.animationDurationInMiliSec,
      required this.child,
      required this.loadingStatusCallback});

  final int animationDurationInMiliSec;
  final Widget child;
  final LoadingStatusCallback loadingStatusCallback;

  @override
  State<AMASOpacityAndScale> createState() => _AMASOpacityAndScaleState();
}

class _AMASOpacityAndScaleState extends State<AMASOpacityAndScale>
    with TickerProviderStateMixin {
  late final AnimationController _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: widget.animationDurationInMiliSec));

  final Curve _curve = Curves.easeIn;
  late final Animation<double> _animation =
      CurvedAnimation(curve: _curve, parent: _controller);

  bool _isLoading = false;

  void _animate() async {
    // 先执行淡出缩小动画
    await _controller.reverse();
    // 切换状态并通知父组件
    _isLoading = !_isLoading;
    widget.loadingStatusCallback(_isLoading);
    // 等待父组件完成UI重建后,再执行淡入放大动画
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _controller.forward();
    });
  }

  @override
  void didUpdateWidget(covariant AMASOpacityAndScale oldWidget) {
    _animate();
    super.didUpdateWidget(oldWidget);
  }

  @override
  void initState() {
    _controller.forward();
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) => Opacity(
          opacity: _controller.value,
          child: ScaleTransition(
            scale: _animation,
            child: widget.child,
          )),
    );
  }
}

按钮组件(改为StatefulWidget管理状态)

class AMASButton extends StatefulWidget {
  final String? text;
  final Color? textColor;
  final double? fontSize;
  final String? fontFamily;
  final FontWeight? fontWeight;
  final Color? gradientBackgroundColor;
  final bool hasOutLines;
  final double? height;

  const AMASButton({
    super.key,
    this.text,
    this.textColor,
    this.fontSize,
    this.fontFamily,
    this.fontWeight,
    this.gradientBackgroundColor,
    this.hasOutLines = false,
    this.height,
  });

  @override
  State<AMASButton> createState() => _AMASButtonState();
}

class _AMASButtonState extends State<AMASButton> {
  bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      // 点击触发动画
      onTap: () {
        setState(() {});
      },
      child: AMASOpacityAndScale(
        animationDurationInMiliSec: 200,
        loadingStatusCallback: (isLoading) {
          // 触发按钮组件重建,切换子组件
          setState(() {
            _isLoading = isLoading;
          });
        },
        child: !_isLoading
            ? Text(
                widget.text ?? 'AMASButton',
                style: TextStyle(
                    color: widget.textColor ??
                        (widget.gradientBackgroundColor == null && !widget.hasOutLines
                            ? Colors.white
                            : Colors.black),
                    fontSize: widget.fontSize,
                    fontFamily: widget.fontFamily,
                    fontWeight: widget.fontWeight),
              )
            : SizedBox(
                height: widget.height != null
                    ? (widget.height! - widget.height! / 2.2)
                    : 50 - 22,
                width: widget.height != null
                    ? (widget.height! - widget.height! / 2.2)
                    : 50 - 50 / 2.2,
                child: CircularProgressIndicator(
                  strokeWidth: 2,
                  color: widget.textColor ?? Colors.white,
                ),
              ),
      ),
    );
  }
}

3. 关键修改说明

  • 替换静态状态:用StatefulWidget的内部状态_isLoading替代静态类,确保状态变化能触发UI重建。
  • 触发父组件刷新:在loadingStatusCallback中调用setState,让按钮组件重建,从而切换文本和加载指示器。
  • 动画时序优化:用WidgetsBinding.instance.addPostFrameCallback确保父组件完成UI更新后,再执行淡入放大动画,避免动画执行时子组件尚未切换。
  • 触发动画逻辑:点击按钮时调用setState(),触发AMASOpacityAndScale的didUpdateWidget方法,启动动画流程。

内容的提问来源于stack exchange,提问作者Amir Mohammad Ahmadian

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 07:37:10