3

I've been banging my head against this for several days now, so I really hope somebody can shed some light.

I need a custom button that looks a certain way and runs simple animations on itself (really simple, like cross-fading to a different color and then back upon being pressed). I have written such a button, and it suits me perfectly in terms of looks and behavior.

But! In order for the animations to work, I had to derive my button from the StatefulWidget class. The problem is this: no matter what I do, I can't get the page to rebuild the button anew, with updated parameters.

I have implemented a simple on/off switch with two buttons to show what I mean. On this page I have two buttons: "Drop anchor" and "Retract anchor". I want only one button to be enabled at any given time. Pressing one of the buttons should disable it and enable the other one. But that doesn't happen!

I have put some text on the screen to illustrate that the page does indeed update on setState(). The text is governed by the same variable the buttons are. But for some reason Flutter updates the text, but not the buttons.

I've tried just about every solution I could find here.

Here's what I've tried:

  1. I've added keys to my buttons and button's child. It helped with getting the AnimatedSwitch to do what I needed, but not with UI rebuilds.

  2. I've tried dispatching notifications and calling setState() upon receiving them. No effect.

  3. I've put a floating button to manually call setState() on the page to make sure it's called from the page widget, and not inside the button state. No effect.

  4. I've tried wrapping the whole app in an AppBuilder, as was suggested on one of the threads here, and rebuilding the whole app on the press of the buttons, which works even for changing the Theme of the app. But it doesn't update my stateful buttons... Duh!

  5. I've implemented the same example using ordinary stateless buttons, and it works exactly as I expect it to. So I'm about 75% sure the problem is that my custom button has its own state.

What else can I try?

Thank you for reading!

My example page code:

class _MyHomePageState extends State<MyHomePage> {
  bool anchorIsDown = false;

  void anchorUp() {
    anchorIsDown = false;
    setState(() {});
  }

  void anchorDown() {
    anchorIsDown = true;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: SafeArea(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Flexible(
                flex: 3,
                child: Container(
                  child: Center(
                    child: anchorIsDown ? Text('Anchor is down') : Text('Anchor is up'),
                  )
                ),
              ),
              Flexible(
                flex: 1,
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Row(
                    children: [
                      StatefulButton(
                        keyString: 'anchor_drop',
                        opacity: 0.2,
                        visible: true,
                        onPressedColor: Colors.greenAccent,
                        onPressed: !anchorIsDown ? anchorDown : null,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text('Drop anchor')
                          ],
                        ),
                      ),
                      Expanded(child: SizedBox(width: 0, height: 0)),
                      StatefulButton(
                        keyString: 'anchor_retract',
                        opacity: 0.2,
                        visible: true,
                        onPressedColor: Colors.greenAccent,
                        onPressed: anchorIsDown ? anchorUp : null,
                        child: Text('Retract anchor'),
                      ),
                    ],
                  ),
                ),
              ),
              Flexible(
                flex: 3,
                child: Container(),
              ),
            ],
          ),
        ),
            floatingActionButton: FloatingActionButton(
              onPressed: () { setState(() {}); },
    ),
    );
  }
}

My stateful button class:

import 'dart:async';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math';

class StatefulButton extends StatefulWidget {
  late final String keyString;
  late final bool visible;
  late final double? opacity;
  late final VoidCallback? onPressed;
  late final Color? onPressedColor;
  late final Widget child;

  StatefulButton(
      {required this.keyString,
      this.visible = true,
      this.opacity = 0.05,
      this.onPressedColor,
      this.onPressed,
      required this.child})
      : super(key: Key(keyString));

  @override
  State createState() => StatefulButtonState();
}

class StatefulButtonState extends State<StatefulButton> {
  late final bool visible;
  late double? opacity;
  VoidCallback? onPressed;
  late final Color? onPressedColor;
  late final Widget child;
  bool isAnimating = false;

  @override
  void initState() {
    visible = widget.visible;
    opacity = widget.opacity;
    onPressed = widget.onPressed;
    onPressedColor = widget.onPressedColor;
    child = widget.child;
    isAnimating = false;

    super.initState();
  }

  void onPressedWrapper() async {
    isAnimating = true;
    setState(() {});

    if (onPressed != null) onPressed!();

    await Future.delayed(Duration(milliseconds: 200));

    isAnimating = false;
    setState(() {});
  }

  Widget buildChildContainer() {
    if (!isAnimating)
      return Container(
        key: Key(widget.keyString + '_normalstate'),
        color: Theme.of(context).buttonColor,
        child: Center(
          child: child,
        ),
      );

    return Container(
      key: Key(widget.keyString + '_pressedstate'),
      decoration: BoxDecoration(
        color: onPressedColor,
        borderRadius: BorderRadius.all(Radius.circular(3.0))
      ),
      child: Center(
        child: child,
      ),
    );
  }

  //If the button is enabled, it should provide tap feedback.
  //This function will build such a button using AnimatedCrossFade
  Widget buildEnabledButton() {
    return OutlinedButton(
      style: ButtonStyle(
          padding: MaterialStateProperty.all(EdgeInsets.all(1)),
          side: MaterialStateProperty.all(BorderSide(
              color: Theme.of(context).dividerColor,
              width: 1,
              style: BorderStyle.solid)),
          enableFeedback: false,
          minimumSize: MaterialStateProperty.all(Size(150, 50)),
          elevation: MaterialStateProperty.all(4.0),
          shadowColor: MaterialStateProperty.all(Theme.of(context).shadowColor),
          overlayColor: MaterialStateProperty.all(Colors.transparent),
          backgroundColor:
              MaterialStateProperty.all(Theme.of(context).buttonColor)),
      onPressed: onPressed == null ? onPressed : onPressedWrapper,
      child: AnimatedSwitcher(
        duration: Duration(milliseconds: 200),
        child: buildChildContainer(),
        switchInCurve: Curves.bounceOut,
        switchOutCurve: Curves.easeOut,
      ),
    );
  }

  Widget buildDisabledButton() {
    if (!visible) opacity = 0;

    return Stack(
      children: [
        OutlinedButton(
            style: ButtonStyle(
                padding: MaterialStateProperty.all(EdgeInsets.all(1)),
                side: MaterialStateProperty.all(BorderSide(
                    color: Theme.of(context).dividerColor.withOpacity(sqrt(opacity!)),
                    width: 1,
                    style: visible ? BorderStyle.solid : BorderStyle.none)),
                enableFeedback: false,
                minimumSize: MaterialStateProperty.all(Size(1000, 1000)),
                elevation: MaterialStateProperty.all(4.0 * opacity!),
                shadowColor: visible
                    ? MaterialStateProperty.all(
                        Theme.of(context).shadowColor.withOpacity(opacity!))
                    : MaterialStateProperty.all(Colors.transparent),
                overlayColor: MaterialStateProperty.all(Colors.transparent),
                backgroundColor: visible
                    ? MaterialStateProperty.all(Theme.of(context)
                        .buttonColor
                        .withOpacity(sqrt(opacity!) / 2))
                    : MaterialStateProperty.all(Colors.transparent)),
            onPressed: visible ? onPressed : null,
            child: buildChildContainer()),
        Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.all(Radius.circular(3)),
              color: Theme.of(context).canvasColor.withOpacity(1 - opacity!),
            ),
            child: SizedBox(
              child: Center(),
            ))
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Expanded(
        child: ((onPressed != null) && visible)
            ? buildEnabledButton()
            : buildDisabledButton());
  }
}
Lev Tyrnov
  • 31
  • 3

0 Answers0