0

widget appearanceI have a Flutter code snippet where I'm using CustomPaint and Stack widgets to create a progress bar with steps. However, I am facing an issue with the labelText when it is too long; it doesn't fit within the SizedBox.

Here is my code:

import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';

class StepperProgressBar extends StatelessWidget {
  final List<Widget> stepUnits;

  StepperProgressBar({
    super.key,
    required this.stepUnits,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 300,
      width: 450,
      child: ListView(
        scrollDirection: Axis.horizontal,
        children: stepUnits,
      ),
    );
  }
}

class StepUnit extends StatelessWidget {
  final double progressBarHeight;
  double? progressBarWidth;
  double? circularStepSize;
  double? circularStepWidth;
  final double progressBarValue;
  final Icon? icon;
  bool isFirst = false;
  final Color? backgroundColor;
  final Color? progressColor;
  final String? labelText;

  StepUnit({
    super.key,
    this.progressBarHeight = 5,
    this.progressBarWidth,
    this.circularStepSize,
    this.circularStepWidth,
    this.progressBarValue = 0.0,
    this.icon,
    this.progressColor = const Color.fromARGB(255, 90, 116, 125),
    this.backgroundColor = const Color.fromARGB(255, 161, 169, 172),
    this.labelText,
  });

  @override
  Widget build(BuildContext context) {
    bool isDone = progressBarValue >= 1;
    bool isInProgress = progressBarValue > 0 && progressBarValue < 1;
    final stepUnitBoxWidth = SizedBox(width: 11);

    progressBarWidth ??= MediaQuery.of(context).size.width * 0.3;
    circularStepSize ??= MediaQuery.of(context).size.width * 0.11;
    circularStepWidth ??= MediaQuery.of(context).size.width * 0.1;

    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (!isFirst) ...[
          stepUnitBoxWidth,
          LinearPercentIndicator(
            width: progressBarWidth,
            animation: false,
            lineHeight: progressBarHeight,
            animationDuration: 2500,
            percent: progressBarValue,
            progressColor: progressColor,
            backgroundColor: backgroundColor,
            barRadius: Radius.circular(10),
            alignment: MainAxisAlignment.start,
          ),
        ],
        stepUnitBoxWidth,
        SizedBox(
          height: 300,
          width: 103,  // Increased width to fit longer label
          child: Stack(
            alignment: Alignment.center,
            children: [
              CustomPaint(
                painter: CirclePainter(
                  isDone: isDone,
                  width: circularStepWidth!,
                  isInProgress: isInProgress,
                  backgroundColor: backgroundColor!,
                  progressColor: progressColor!,
                ),
                size: Size(circularStepSize!, circularStepSize!),
              ),
              if (isDone)
                icon ?? const Icon(Icons.done, size: 25, color: Colors.white)
              else if (icon != null)
                icon!,
              if (labelText != null)
                Positioned(
                  bottom: 30,
                  child: Text(
                    labelText!,
                    style: TextStyle(fontSize: 15, color: Colors.blueGrey),
                  ),
                ),
            ],
          ),
        ),
      ],
    );
  }
}

class CirclePainter extends CustomPainter {
  final bool isDone;
  final double width;
  final bool isInProgress;
  final Color backgroundColor;
  final Color progressColor;

  CirclePainter({
    this.isDone = false,
    this.width = 4,
    this.isInProgress = false,
    required this.progressColor,
    required this.backgroundColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = isDone ? progressColor : backgroundColor
      ..strokeWidth = 4.0
      ..style = isInProgress ? PaintingStyle.stroke : PaintingStyle.fill;

    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2;

    canvas.drawCircle(center, radius, paint);
    if (isInProgress) {
      paint.style = PaintingStyle.fill;
      canvas.drawCircle(center, radius - radius / 3, paint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Example usage:

   List<StepUnit> stepUnits = [
      StepUnit(
        progressBarValue: 1,
        labelText: 'STEP 1',
      ),
      StepUnit(
        progressBarValue: 1,
        labelText: 'TEXTTOOLONG',
      ),
      StepUnit(
        progressBarValue: 1,
        labelText: 'STEP 3',
      ),
      StepUnit(
        progressBarValue: 1,
        labelText: 'STEP 4',
      ),
    ];

When the labelText is a long string, it doesn't fit within the SizedBox. I increased the width of the surrounding SizedBox in the Stack:

SizedBox(
  height: 300,
  width: 103,
  child: Stack(
    alignment: Alignment.center,
    children: [
      CustomPaint(
        painter: CirclePainter(
          isDone: isDone,
          width: circularStepWidth!,
          isInProgress: isInProgress,
          backgroundColor: backgroundColor!,
          progressColor: progressColor!,
        ),
        size: Size(circularStepSize!, circularStepSize!),
      ),
      if (isDone)
        icon ?? const Icon(Icons.done, size: 25, color: Colors.white)
      else if (icon != null)
        icon!,
      if (labelText != null)
        Positioned(
          bottom: 30,
          child: Text(
            labelText!,
            style: TextStyle(fontSize: 15, color: Colors.blueGrey),
          ),
        ),
    ],
  ),
),

However, increasing the width of the SizedBox affects the layout of the widgets, and I can't achieve the desired look.

I need the Text widgets to expand to the right, regardless of the font size or length of the text, without disrupting the layout. An example of the desired appearance is shown in the second photo attached.desired widget appearance

1 Answer 1

0

You are trying to do a lot of things from scratch. This can be good but I wouldn't recommend it in this case as it is causing you problems that are hard to solve. And it is making a pretty straightforward task quite complicated. I hope you don't mind me suggesting a change in approach.

1. Use `Container` to draw simple circles

You have chosen to create a circle using `CustomPainter`. This works but is overkill as Flutter provides a much simpler way to do this: using `Container`:
Container(
  height: 30, // circularStepSize,
  width: 30, // circularStepSize,
  decoration: BoxDecoration(
    color: backgroundColor,
    shape: BoxShape.circle,
  ),
),

To place the icon inside, just add it as a child: of the Container we just created:

...
  decoration: BoxDecoration(
    color: backgroundColor,
    shape: BoxShape.circle,
    ),
   child: isDone ? icon ?? const Icon(Icons.done, size: 25, color: Colors.black) : icon,
...

2. Don't use Stack -- use Row and Column

It appears you are trying to use MediaQuery to get the screen width, then assign values to your widget based on percentages of that width. That is, in this case, the wrong approach. It makes you do a bunch of extra work and the result is not very good (as you have found out).

Flutter has an excellent range of widgets that are well suited to laying out the screen for you. You will do better to use them.

Based on your posted image, you want four elements, each containing text, an icon, and a progress bar. They take up all of the screen horizontally, with a bit of padding on each side and in between them.

|    ________     ________     ________     ________    |
|<->|        |<->|        |<->|        |<->|        |<->|
|   |________|   |________|   |________|   |________|   |

To lay these out you placed them inside a ListView. Do you really want this to be scrollable? If not, use a Row:

 child: Row(
        children: stepUnits,
      ),

Now, we need to lay out the elements in each item. Each item has an icon and a progress bar at the top, and text at the bottom:

(√) ---

TEXT

To create this layout you have chosen a Stack with SizedBox and Positioned. This creates problems for you because you have to lay things out using specific sizes and specific positions. (Which you tried to solve with MediaQuery).

But look at what you are trying to do: You have two top elements aligned horizontally, and then you have another element at the bottom. The top elements can be laid out with a Row widget. The top row and the bottom are aligned vertically, so we place both the Row and the Text inside a Column widget, using crossAxisAlignment: CrossAxisAlignment.start to tell the widgets to be on the left hand side.

3. Use Expanded

If you update the code as I have suggested, you will notice the 4 large elements will be lined up in a row, but they won't form a line that in total is the exact width of the screen -- they'll be either too long or too short. So now we need to use another widget to tell Flutter we want each element to take up as much space on the horizontal axis as possible: Expanded. Wrap the StepUnit widget in an Expanded. So now the widget starts like this:

 return Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ... // children come here

We're almost done! The final issue is you have a widget, LinearPercentIndicator which shows a percentage of the whole. How can you achieve this without providing it with a width? For this I suggest you use FractionallySizedBox. This widget sizes its child according to awidthFactor you provide. So just give it the percent loaded as the widthFactor.

Something like this:

return Container(
      height: 4,
      color: backgroundColor,
      child: FractionallySizedBox(
        widthFactor: percent,
        child: Container(
          color: Colors.red,
        ),
      ),
    );

Here is the updated code in full:

class StepperProgressBar extends StatelessWidget {
  final List<Widget> stepUnits;

  const StepperProgressBar({
    super.key,
    required this.stepUnits,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox( // This SizedBox give the row a fixed height
      height: 200,
      child: Row(
        children: stepUnits,
      ),
    );
  }
}

class StepUnit extends StatelessWidget {
  final double progressBarHeight;
  final double progressBarValue;
  final Icon? icon;
  final bool isFirst = false;
  final Color? backgroundColor;
  final Color? progressColor;
  final String? labelText;

  const StepUnit({
    super.key,
    this.progressBarHeight = 5,
    this.progressBarValue = 0.0,
    this.icon,
    this.progressColor = const Color.fromARGB(255, 90, 116, 125),
    this.backgroundColor = const Color.fromARGB(255, 161, 169, 172),
    this.labelText,
  });

  @override
  Widget build(BuildContext context) {
    bool isDone = progressBarValue >= 1;
    bool isInProgress = progressBarValue > 0 && progressBarValue < 1;
    final stepUnitBoxWidth = SizedBox(width: 11);

    return Expanded(
      child: Padding(// This adds padding around the four elements
        padding: const EdgeInsets.symmetric(horizontal: 18),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Container(
                  height: 30, // circularStepSize,
                  width: 30,
                  decoration: BoxDecoration(
                    color: backgroundColor,
                    shape: BoxShape.circle,
                  ),
                  child: isDone ? icon ?? const Icon(Icons.done, size: 25, color: Colors.black) : icon,
                ),
                stepUnitBoxWidth,
                if (!isFirst)
                  Expanded(
                    child: LinearPercentIndicator(
                      width: 200, // You shouldn't need to provide a width anymore!
                      animation: false,
                      lineHeight: progressBarHeight,
                      animationDuration: 2500,
                      percent: progressBarValue,
                      progressColor: progressColor,
                      backgroundColor: backgroundColor,
                      barRadius: Radius.circular(10),
                      alignment: MainAxisAlignment.start,
                    ),
                  ),
                stepUnitBoxWidth,
              ],
            ),
            const SizedBox(height: 50),
            if (labelText != null)
              Text(
                labelText!,
                style: TextStyle(fontSize: 15, color: Colors.blueGrey),
              ),
          ],
        ),
      ),
    );
  }
}

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.