delmin
delmin

Reputation: 2690

How to dynamically make same size of toggle buttons

Is there any way to make dynamically same size of each toggle button based on larger(longest) child? I made simple toggle button example with 2 texts. How to make these toggle buttons same width without hardcoding its size

enter image description here

ToggleButtons(
          children: [Text('long text'), Text('text')],
          onPressed: (int index) {
            setState(() {
              for (int buttonIndex = 0;
                  buttonIndex < isSelected.length;
                  buttonIndex++) {
                if (buttonIndex == index) {
                  isSelected[buttonIndex] = true;
                } else {
                  isSelected[buttonIndex] = false;
                }
              }
            });
          },
          isSelected: isSelected,
        )

Upvotes: 2

Views: 893

Answers (3)

PixelToast
PixelToast

Reputation: 965

This is difficult to do without some custom layout code and re-implementing the toggle button, here I am using the boxy package to make each button the same size:

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final isSelected = <bool>[false, false, false];

  @override
  Widget build(BuildContext context) {
    return EvenSizedToggleButtons(
      onPressed: (i) {
        setState(() {
          isSelected[i] = !isSelected[i];
        });
      },
      isSelected: isSelected,
      children: const [
        Text('Smol'),
        Text('Longer'),
        Text('__Longest__'),
      ],
    );
  }
}

class EvenSizedToggleButtons extends StatefulWidget {
  const EvenSizedToggleButtons({
    Key? key,
    required this.onPressed,
    required this.isSelected,
    required this.children,
  }) : super(key: key);

  final void Function(int index) onPressed;
  final List<bool> isSelected;
  final List<Widget> children;

  @override
  State<EvenSizedToggleButtons> createState() => _EvenSizedToggleButtonsState();
}

class _EvenSizedToggleButtonsState extends State<EvenSizedToggleButtons> {
  @override
  Widget build(BuildContext context) {
    return EvenSized(
      children: [
        for (var i = 0; i < widget.children.length; i++)
          ToggleButton(
            selected: widget.isSelected[i],
            onPressed: () => widget.onPressed(i),
            child: widget.children[i],
            isFirst: i == 0,
          ),
      ],
    );
  }
}

class ToggleButton extends StatelessWidget {
  const ToggleButton({
    Key? key,
    required this.onPressed,
    required this.selected,
    required this.child,
    required this.isFirst,
  }) : super(key: key);

  final VoidCallback onPressed;
  final bool selected;
  final Widget child;
  final bool isFirst;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;
    final borderSide = BorderSide(
      color: colorScheme.onSurface.withOpacity(0.12),
      width: 1,
    );
    return Container(
      decoration: BoxDecoration(
        border: Border(
          left: isFirst ? borderSide : BorderSide.none,
          top: borderSide,
          right: borderSide,
          bottom: borderSide,
        ),
      ),
      constraints: const BoxConstraints(
        minWidth: 50,
        minHeight: 50,
      ),
      child: TextButton(
        onPressed: onPressed,
        style: ButtonStyle(
          backgroundColor: MaterialStatePropertyAll(selected
              ? colorScheme.primary.withOpacity(0.12)
              : Colors.transparent),
          shape: const MaterialStatePropertyAll(RoundedRectangleBorder()),
          textStyle: const MaterialStatePropertyAll(TextStyle()),
          foregroundColor: MaterialStatePropertyAll(selected
              ? colorScheme.primary
              : theme.textTheme.bodyMedium!.color),
        ),
        child: child,
      ),
    );
  }
}

class EvenSized extends StatelessWidget {
  const EvenSized({
    Key? key,
    required this.children,
  }) : super(key: key);

  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return CustomBoxy(
      delegate: EvenSizedBoxy(),
      children: children,
    );
  }
}

class EvenSizedBoxy extends BoxyDelegate {
  @override
  Size layout() {
    // Find the max intrinsic width of each child
    //
    // Intrinsics are a little scary but `getMaxIntrinsicWidth(double.infinity)`
    // just calculates the width of the child as if its maximum height is
    // infinite
    var childWidth = 0.0;
    for (final child in children) {
      childWidth = max(
        childWidth,
        child.render.getMaxIntrinsicWidth(double.infinity),
      );
    }

    // Clamp the width so children don't overflow
    childWidth = min(childWidth, constraints.maxWidth / children.length);

    // Find the max intrinsic height
    //
    // We calculate childHeight after childWidth because the height of text
    // depends on its width (i.e. wrapping), `getMinIntrinsicHeight(childWidth)`
    // calculates what the child's height would be if it's width is childWidth.
    var childHeight = 0.0;
    for (final child in children) {
      childHeight = max(
        childHeight,
        child.render.getMinIntrinsicHeight(childWidth),
      );
    }

    // Force each child to be the same size
    final childConstraints = BoxConstraints.tight(
      Size(childWidth, childHeight),
    );

    var x = 0.0;
    for (final child in children) {
      child.layout(childConstraints);

      // Space them out evenly
      child.position(Offset(x, 0));
      x += childWidth;
    }

    return Size(childWidth * children.length, childHeight);
  }
}

An interactive example can be found at https://dartpad.boxy.wiki/?id=1e1239c8e412750081a053e9f14f6d8d

Upvotes: 0

VMi
VMi

Reputation: 366

I had the similar issue today when I had to create custom toggle buttons, where toggle button items where loading from a list of various options of various text sizes and client was ok to wrap the longer text options.

With in built toggle button, it would just fix the size for all items. So when I created the custom toggle buttons,it calculates the size of each button based on the longest word in that item using getWidth() method.

  return Center(
        child: Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        SizedBox(
          height: 15.0,
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            for (int i = 0; i < responses.length; i++)
              InkWell(
                onTap: (() {
                  setState(() {
                    for (int j = 0; j < isSelected[index]!.length; j++) {
                      isSelected[index]![j] = j == i;

                      surveys[0]["feedbackQuestion"][index]["answer"] =
                          i.toString();
                    }
                  });
                  print(isSelected[index]![i]);
                  print(color.toString());
                }),
                child: SizedBox(
                  width: getWidth(
                      responses[i],
                      GoogleFonts.roboto(
                          fontWeight: FontWeight.bold,
                          fontSize: 12.0,
                          color: color)),
                  child: Text(responses[i],
                      style: GoogleFonts.roboto(
                          fontWeight: FontWeight.bold,
                          fontSize: 12.0,
                          color: isSelected[index]![i]
                              ? Colors.green
                              : Colors.orange)),
                ),
              ),

getWidth method

getWidth(String response, TextStyle textStyle) {
String result = "";

if (response.contains(" ")) {
  var values = <String>[];

  values = response.split(" ");

  for (String value in values) {
    if (value.length > result.length) {
      result = value;
    }
  }
} else {
  result = response;
}

final Size size = (TextPainter(
        text: TextSpan(text: result, style: textStyle),
        maxLines: 1,
        textScaleFactor: MediaQuery.of(context).textScaleFactor,
        textDirection: TextDirection.ltr)
      ..layout())
    .size;

return size.width + 5; //Extra  5 for padding
}

isSelected is a Map of <int, List<bool>?> to contain bool values for all buttons. isSelected should be loaded outside the buttons loading method, preferrably at class level to to avoid being reset with each set of buttons load.

Result(Green being the selected color):

Green being the selected color

Upvotes: 0

F Perroch
F Perroch

Reputation: 2215

Without calculating the largest text size, you can simply use a BoxConstraints with a minWidth and maxWidth properties like this :

ToggleButtons(
  children: [Text('long text'), Text('text')],
  constraints: BoxConstraints(minWidth: 70, maxWidth: 70, minHeight: kMinInteractiveDimension),
  isSelected: [true, false],
)

Larger text on ToggleButtons will be wrapped

I hope this will be helpful to you

Upvotes: 3

Related Questions