首页 > 解决方案 > 为什么在具有另一个 setState 的子小部件内的回调中调用 setState 会破坏程序?

问题描述

这是我使用动画实现的自定义 Switch 小部件。


enum SwitchType {
  LockToggle, EnableToggle
}

class DiamondSwitch extends StatefulWidget {
  final double width, height;
  final SwitchType switchType;
  final double switchThumbSize;
  final VoidCallback onTapCallback;

  DiamondSwitch({
    key, this.width, this.height,
    this.switchType, this.switchThumbSize,
    @required this.onTapCallback
  }) : super(key: key);

  @override
  _DiamondSwitchState createState() => _DiamondSwitchState(
    width: width, height: height,
    switchType: switchType, switchThumbSize: switchThumbSize,
    onTapCallback: onTapCallback
  );
}

class _DiamondSwitchState extends State<DiamondSwitch> {
  final double width, height;
  final int _toggleAnimationDuration = 1000;

  bool _isOn = false;

  final List<Color>
    _darkGradientShades = <Color>[
      Colors.black, Color.fromRGBO(10, 10, 10, 1.0)
    ],
    _lightGradientShades = <Color>[
      Colors.white, Color.fromRGBO(150, 150, 150, 1.0)
    ];

  final SwitchType switchType;

  final double switchThumbSize;

  List<Icon> _switchIcons = new List<Icon>();
  final double _switchIconSize = 35.0;

  final VoidCallback onTapCallback;

  _DiamondSwitchState({
    this.width = 100.0, this.height = 40.0,
    this.switchThumbSize = 40.0, @required this.switchType,
    @required this.onTapCallback
  });

  @override
  void initState() {
    _switchIcons.addAll(
      (switchType == SwitchType.LockToggle)?
        <Icon>[
          Icon(
            Icons.lock,
            color: Colors.black,
            size: _switchIconSize,
          ),
          Icon(
            Icons.lock_open,
            color: Colors.white,
            size: _switchIconSize,
          )
        ]
      :
        <Icon>[
          Icon(
            Icons.done,
            color: Colors.black,
            size: _switchIconSize,
          ),
          Icon(
            Icons.close,
            color: Colors.white,
            size: _switchIconSize,
          )
        ]
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: Duration(milliseconds: _toggleAnimationDuration),
      width: width, height: height,
      decoration: ShapeDecoration(
        shape: BeveledRectangleBorder(
          borderRadius: BorderRadius.circular(28.0),
          side: (_isOn)?
              BorderSide(
                color: Color.fromRGBO(45, 45, 45, 1.0),
                width: 0.5,
              )
            :
              BorderSide.none,
        ),
        gradient: LinearGradient(
          colors: <Color>[
            ...((_isOn)? _darkGradientShades : _lightGradientShades)
          ],
          begin: Alignment(1.0, -0.8), end: Alignment(-0.7, 1.0),
          stops: <double>[0.4, 1.0]
        ),
      ),
      alignment: Alignment(0.0, 0.0),
      child: Stack(
        alignment: Alignment(0.0, 0.0),
        children: <Widget>[
          AnimatedPositioned(
            duration: Duration(milliseconds: _toggleAnimationDuration),
            curve: Curves.easeIn,
            left: (_isOn)? 0.0 : ((width * 70) / 100),
            right: (_isOn)? ((width * 70) / 100) : 0.0,
            child: GestureDetector(
              onTap: () {
                onTapCallback();
                setState(() {
                  _isOn = !_isOn;
                });
              },
              child: AnimatedSwitcher(
                duration: Duration(milliseconds: _toggleAnimationDuration),
                transitionBuilder: (Widget child, Animation<double> animation) {
                  return FadeTransition(
                    opacity: animation,
                    child: child,
                  );
                },
                child: Transform.rotate(
                  alignment: Alignment.center,
                  angle: (math.pi / 4),
                  child: Container(
                    width: switchThumbSize, height: switchThumbSize,
                    alignment: Alignment.center,
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(3.0),
                      color: (_isOn)? Colors.white : Colors.black,
                      border: (_isOn)?
                        null
                      :
                        Border.all(
                          color: Color.fromRGBO(87, 87, 87, 1.0),
                          width: 1.0,
                        ),
                    ),
                    child: Transform.rotate(
                      alignment: Alignment.center,
                      angle: -(math.pi / 4),
                      child: (_isOn)?
                        _switchIcons[0]
                      :
                        _switchIcons[1],
                    ),
                  ),
                ),
              ),
            ),
          )
        ],
      ),
    );
  }
}

我添加了一个 onTapCallback,因为我需要在父小部件中设置另一个标志来触发图像更改。这是属于父小部件的相关代码;

DiamondSwitch(
  switchType: SwitchType.LockToggle,
  width: 186.0,
  height: 60.0,
  switchThumbSize: 41.0,
  onTapCallback: () {
    this.setState(() {
      this._isLockOn = !this._isLockOn;
    });
  },
  key: UniqueKey(),
),

当我运行此代码时,动画不起作用。它检测到点击并执行onTap回调,以及onTap Works 中的所有代码(我使用打印方法进行了测试),但正如我所说,动画没有发生。

我想了解为什么会发生这种情况,这是关于 Flutter 的工作原理吗?如果是,你能解释一下吗?

TY抽出时间^.^!


编辑

我想知道为什么使用 setState 获取方法会破坏我正在使用应答器@pulyaevskiy 实现的方法共享当前父小部件的动画。

class _SettingsState extends State<Settings> {
  bool
    _isLockOn = false,
    _isPassiconOn = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: Column(
          children: <Widget>[
            /*Lock Toggle Graphic*/
            Align(
              alignment: Alignment(-0.1, 0.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.end,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  /*Lock Toggle Text*/
                  RotatedBox(
                    quarterTurns: 3,
                    child: Column(
                      children: <Widget>[
                        Text(
                          (_isLockOn)? "locked" : "unlocked",
                          style: TextStyle(
                            color: Colors.white,
                            fontFamily: "Philosopher",
                            fontSize: 25.0,
                            shadows: <Shadow>[
                              Shadow(
                                color: Color.fromRGBO(184, 184, 184, 0.68),
                                offset: Offset(2.0, 2.0),
                                blurRadius: 4.0,
                              ),
                            ],
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.only(top: 5.5),
                          child: Container(
                            color: Color.fromRGBO(204, 204, 204, 1.0),
                            width: 30.0, height: 1.0,
                          ),
                        ),
                      ],
                    ),
                  ),
                  /*Lock Toggle Image*/
                  Image.asset(
                    "assets/images/settings_screen/crystal_"
                        "${(_isLockOn)? "white_light_up" : "black_outline"}.png",
                    scale: 5.0,
                    alignment: Alignment.center,
                  ),
                ],
              ),
            ),
            /*Lock Toggle Switch*/
            Padding(
              padding: const EdgeInsets.only(top: 12.5),
              child: DiamondSwitch(
                switchType: SwitchType.LockToggle,
                width: 186.0,
                height: 60.0,
                switchThumbSize: 41.0,
                flagToControl: this._isLockOn,
                onTapCallback: () {
                  this.setState(() {
                    this._isLockOn = !this._isLockOn;
                  });
                },
                key: UniqueKey(),
              ),
            ),
            /*Separator*/
            WhiteDiamondSeparator(paddingAmount: 36.5),
            /*Passicon Toggle Graphic*/
            Align(
              alignment: Alignment(-0.32, 0.0),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.end,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  /*Lock Toggle Text*/
                  RotatedBox(
                    quarterTurns: 3,
                    child: Column(
                      children: <Widget>[
                        Text(
                          "passicon",
                          style: TextStyle(
                            color: Colors.white,
                            fontFamily: "Philosopher",
                            fontSize: 25.0,
                            shadows: <Shadow>[
                              Shadow(
                                color: Color.fromRGBO(184, 184, 184, 0.68),
                                offset: Offset(2.0, 2.0),
                                blurRadius: 4.0,
                              ),
                            ],
                          ),
                        ),
                        Padding(
                          padding: const EdgeInsets.only(top: 5.5),
                          child: Container(
                            color: Color.fromRGBO(204, 204, 204, 1.0),
                            width: 30.0, height: 1.0,
                          ),
                        ),
                      ],
                    ),
                  ),
                  /*Passicon Toggle Image*/
                  Padding(
                    padding: const EdgeInsets.only(left: 40),
                    child: Image.asset(
                      "assets/images/settings_screen/emote_"
                          "${(_isPassiconOn)? "winking" : "nervous"}.png",
                      scale: 3.25,
                      alignment: Alignment.center,
                    ),
                  ),
                ],
              ),
            ),
            /*Passicon Toggle Switch*/
            Padding(
              padding: const EdgeInsets.only(top: 42.5),
              child: DiamondSwitch(
                switchType: SwitchType.PassiconToggle,
                width: 186.0,
                height: 60.0,
                switchThumbSize: 41.0,
                flagToControl: this._isPassiconOn,
                onTapCallback: () {
                  this.setState(() {
                    this._isPassiconOn = !this._isPassiconOn;
                  });
                },
                key: UniqueKey(),
              ),
            ),
            /*Separator*/
            WhiteDiamondSeparator(paddingAmount: 36.5),
          ],
        )
      ),
    );
  }
}

我添加并在asflagToControlDiamondSwitch使用它。_DiamondSwitchStatebool get _isOn => widget.flagToControl;

在旧方法中,如果我不执行其他事情

setState() { _isOn = !_isOn; }

动画按原样发生。我错过了什么?

标签: flutterflutter-animation

解决方案


在您onTapCallback的情况下,您正在更改父小部件的状态,这确实有效。但是它不会影响DiamondSwitch小部件本身的状态(我看到在 GestureDetector 中您还设置了开关小部件的状态,但这种方法存在一些问题)。

要解决此问题,您可以将this._isLockOn父小部件状态的值传递给子小部件DiamondSwitch。这意味着您需要开关小部件上的另一个属性。例如

class DiamondSwitch extends StatefulWidget {
  final bool isOn;
  // ... remaining fields go here
  DiamondSwitch({this.isOn, ...});
}

然后_DiamondSwitchState也改变。可以简单地代理_isOn小部件的值:

class _DiamondSwitchState extends State<DiamondSwitch> {
  bool get _isOn => widget.isOn;
}

isOn这比您现在在两个地方(在父小部件和开关本身中)保持状态要好得多。通过此更改,您的isLockOn状态仅保留在父小部件上,您只需将其传递给切换子小部件即可使用。

这意味着对于 GestureDetector 的onTap属性,您也只需传递onTapCallback父小部件的 ,无需用另一个函数包装它。

最后一部分:在您的父小部件的构建方法中:

DiamondSwitch(
  isOn: this._isLockOn, // <-- new line here to pass the value down
  switchType: SwitchType.LockToggle,
  width: 186.0,
  height: 60.0,
  switchThumbSize: 41.0,
  onTapCallback: () {
    this.setState(() {
      this._isLockOn = !this._isLockOn;
    });
  },
  key: UniqueKey(),
),

这样做的另一个好处是,现在您可以根据需要使用不同的默认值初始化您的开关(现在硬编码为始终以 开头false)。因此,如果您从数据库加载值isLockOn并将其设置为,true您可以立即将此值传递给 switch 子节点并正确表示您的状态。


推荐阅读