为什么要在具有另一个setState的子小部件内的回调中调用setState破坏程序?

时间:2019-12-09 18:01:09

标签: flutter flutter-animation

这是我使用动画实现的自定义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,因为我需要在父窗口小部件中设置另一个标志来触发Image更改。这是属于父窗口小部件的相关代码;

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

运行此代码时,动画不起作用。它检测到敲击并执行 onTap 回调,并且 onTap 中的所有代码都能正常工作(我用 print 方法进行了测试),但是正如我所说,动画没有发生。

我想了解为什么会这样,这是关于Flutter如何工作的吗?如果是,您能解释一下吗?

花点时间^。^!


编辑

我想知道为什么使用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),
          ],
        )
      ),
    );
  }
}

然后我在flagToControl中添加了DiamondSwitch,并在_DiamondSwitchState中将其用作bool get _isOn => widget.flagToControl;

在旧的方法中,如果我不执行或执行其他操作

setState() { _isOn = !_isOn; }

动画会自动发生。我想念什么?

1 个答案:

答案 0 :(得分:0)

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

要解决此问题,您可以将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的状态保持在两个位置(在父窗口小部件AND开关本身中)要好得多。进行此更改后,您的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子级并正确表示您的状态。