How to make column header text with 45 degree rotation?

Hello!
I am trying to make a table, but i have many column headers, and if you put them all next to each other, they take up too much horizontal space.
So i’m trying to rotate them 45 degrees to save space, like shown here:

So far, I have experimented with 3 solutions:

  • RotationTransition(turns: AlwaysStoppedAnimation(7/8)) and Transform.rotate keep the original width (it does layout calculations first, and only rotates before drawing). So the end result is that i get my rotations, but i don’t get the benefit of them, i still use way too much horizontal space
  • RotatedBox uses width/height after rotation, during layout - perfect for my needs - but only supports 90degree rotations, not 45! Using 45 degrees would mean having to rotate your head too much to try to read the text…

any other ideas? thank you

The first 2 solutions look like this (as a new user i wasn’t allowed to put 2 images in my opening post…):
image

The two options that come to mind here are:

  1. Using a very long Stack to position the rotated widgets just-so, and

  2. Writing your own RenderObject. This is probably not required, but if you can’t get the Stack working right (because you don’t have enough layout information in the build phase), then boy do I have a video for you.

Given that this looks like a spreadsheet, I suspect you may also need 2D scrolling. Luckily, Kate Lovett hosted an episode of The Flutter Build Show covering that, too.

Report back with progress!

One other idea is to combine your two approaches. First, rotate by 90 degrees so that the labels take less horizontal space. Then use Transform.rotate to make them diagonal (and therefore more readable).

2 Likes

I’ve been trying that out.
so something like the below:

Row(
      children: ProgramGroup.values
          .map((e) => Transform.rotate(
              alignment: Alignment.centerLeft,
              angle: pi / 4,
              child: RotatedBox(
                quarterTurns: 3,
                child: Expanded(
                  child: Container(
                    color: Colors.yellow,
                    child: Padding(
                      padding: const EdgeInsets.all(1),
                      child: Text(e.name.camelToSpace()),
                    ),
                  ),
                ),
              )))
          .toList());

This has a couple of issues however.
Because the text have varying lengths, they are never aligned when rotated.
See these images:

when rotated, it looks like this:

normally in a row you could solve this with Expanded() but in this case it doesn’t seem to work. ideally i want them all to be equally long, and equalling the length or whichever is the longest one (and no longer than needed, but also not shorter, to avoid wrapping. but this requires constraining the row to a very precise width.

the other issue is, you need to be able to calculate the correct padding to use left and right, not sure how to do that.

here’s an example of using Expanded() to try to get them to be the same width, but this causes wrapping, which i don’t want
meh

i seem to have found an incredible hacky solution to give them all the same “as-small-as-needed” width… every value becomes a stack where we draw on top of every other value (with opacity 0)…

I think you’re almost there. I’m not at a computer but I think there’s a way to define the pivot point around which to Transform.rotate(). You’re currently rotating around the center.

If there’s no such argument to Transform.rotate(), there’s always the manual way to do it. But first, just see if there a parameter you can provide, or search the internet for “Flutter rotate around point” or something like that.

i had the rotation around the bottom centerpoint working, but the issue was that the texts had differing lengths (which results in non-aligned rotation points). but i found an ugly workaround for that (see my above comments)
i pretty much got it working. hacky, but still…

this is the code i have right now:


Widget headers() {
  // when column headers are horizontal, they take too much space
  // when they are vertical, you have to twist your neck to read them
  // so we want 45 degree rotation to make them more compact horizontally, but still readable.
  // However:
  // * RotationTransition(turns: AlwaysStoppedAnimation(7/8)) and Transform.rotate keep the original width (making the rotation pointless)
  // * RotatedBox uses width/height afer rotation, during layout - perfect for what we need - but only supports 90degree rotations, not 45.
  // * OverflowBox(Transform.rotate) results in errors
  // So the solution is to first vertically rotate the with RotatedBox, so the layout engine uses the constraints of tall&narrow text boxes,
  // and after layouting, turn it by 45 degrees to make it more legible
//
// hack: create a stack of (invisible) text boxes on top of each other
// the resulting widget will have the width of whichever is the longest of the text boxes
// therefore, by stacking each entry on top of this stack, they all have the same length
// and therefore, consistent rotation origins
  final all = ProgramGroup.values
      .map((e) => Opacity(
            opacity: 0,
            child: Text(
              e.name.camelToSpace(),
            ),
          ))
      .toList();
  return Transform.translate(
    offset: const Offset(
        0, -10), // because we pivot around bottom center, raise text up a bit
    child: Row(
        children: ProgramGroup.values
            .map((e) => Transform.rotate(
                alignment: Alignment.bottomCenter,
                angle: pi / 4,
                child: RotatedBox(
                  quarterTurns: 3,
                  child: Container(
                    // color: Colors.yellow,
                    child: Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Stack(children: [
                        SizedBox(
                            height: 26, child: Text(e.name.camelToSpace())),
                        ...all
                      ]),
                    ),
                  ),
                )))
            .toList()),
  );
}

2 Likes

Does this not work?

Transform.rotate(
                alignment: Alignment.bottomCenter,
                angle: pi / 1.75,
                child:  DecoratedBox(
                            decoration: BoxDecoration(color: Colors.yellow),
                    child: Padding(

no. that looks like this:

Typo

Transform.rotate(
                alignment: Alignment.bottomCenter,
                angle: pi * 1.75,
                child:  DecoratedBox(
                            decoration: BoxDecoration(color: Colors.yellow),
                    child: Padding(

And the you don`t need the

RotatedBox(
                  quarterTurns: 3,

Transform.rotate is not a good solution. the OP explains this.

note for anyone else wanting to do this.
another thing to watch out is that gesture detection won’t work properly.
see GestureDetector doesn't respond correctly after transition with Transform widget · Issue #27587 · flutter/flutter · GitHub

1 Like

Yeah, if you need things like correct hitboxes and such, then @CraigLabenz’s approach might be a better fit. Namely, writing your own RenderObject. It’s a bit more involved but you have full control. His video on the topic is a great start.

3 Likes

I think a RenderObject is a perfect solution for this. Happy to throw it together if you like.

If you want to give it s go yourself first then try following what I did a couple of days ago on Humpday Q&A Show.

6 Likes

Hello!
making my own RenderObject seems a bit “scary”, i rather stick with more standard widgets rather than investing time into learning something i will rarely need and will probably forget how it works when i look at the code again in a year.
so i’m using the various techniques described above. the biggest downside is the incorrect hitboxes. but i can live with that for now.
if you’re curious what my current version looks like, you can see it in action on https://www.youtube.com/watch?v=aOri5XEApi0 (skip to 3:30)

of course, if you want to give it a shot, i won’t stop you and will be happy to check it out, but no pressure at all!

Check out this package: defer_pointer | Flutter package
I haven’t tried it for the rotate case but there’s a good chance it will work

1 Like