Vertical Text widget for Flutter
Asked Answered
N

6

38

The TextDirection docs say:

Flutter is designed to address the needs of applications written in any of the world's currently-used languages, whether they use a right-to-left or left-to-right writing direction. Flutter does not support other writing modes, such as vertical text or boustrophedon text, as these are rarely used in computer programs. (emphasis added)

Not only does Flutter not support vertical text, it won't be officially supported in the future. This was an early design decision (see here and here).

Even so, there is a real use for vertical script support today. In addition to certain Chinese and Japanese uses, the traditional Mongolian script requires it. This script is very commonly used in computer programs in Inner Mongolia (example, example, example, example, example, example).

It is written top to bottom, and lines wrap left to right:

enter image description here

Additionally, emoji and CJK characters retain the their orientation when displayed vertically (not absolutely necessary, but preferred). In the image below, the top paragraph shows the current implementation and the bottom paragraph shows the correct vertical rendering:

enter image description here

// sample text
ᠨᠢᠭᠡ ᠬᠣᠶᠠᠷ ᠭᠣᠷᠪᠠ ᠳᠥᠷᠪᠡ ᠲᠠᠪᠤ ᠵᠢᠷᠭᠤᠭ᠎ᠠ ᠳᠣᠯᠣᠭ᠎ᠠ ᠨᠠ‍ᠢᠮᠠ ᠶᠢᠰᠦ ᠠᠷᠪᠠ one two three four five six seven eight nine ten 😃🐐汉字 한국어 モンゴル語 English? ᠮᠣᠩᠭᠣᠯ︖

Since Flutter doesn't support vertical script, it must be implemented from scratch. All of the actual text layout and painting is done in the Flutter engine with LibTxt, so I can't change that.

What would it involve to create a top to bottom vertical Text widget?

Update

I'm still looking for an answer. Rémi's answer is good for single line text but doesn't work for multi-line text.

I'm currently researching three different possible solutions:

  • Create a RenderObject subclass (or perhaps a RenderParagraph subclass) that backs a custom StatelessWidget similar to a RichText widget.
  • Use a CustomPaint widget
  • Make a composited widget of a ListView (one row per text line), Text widgets and WidgetSpans.

All of those would involve measuring the text, laying it out, and getting an list of lines.

Another update

I'm currently leaning toward making a custom widget and render object that will mimic RichText and RenderParagraph. Under the hood RenderParagraph uses a TextPainter to layout and paint the text. My problem now is that TextPainter is tightly coupled to the underlying Skia LibTxt library. That is where all the actual layout and painting happens. Laying out the text in anything besides the default ltr and rtl is proving to be a big problem.

Yet another update

The accepted answer meets the criteria I put forth in this question. I think it will meet short term needs. I have some reservations for things like applying text style, but I will need to do more tests. Long term I may still try to do custom text layout and painting.

Nodose answered 27/6, 2019 at 1:30 Comment(1)
If someone is looking for English language, this question may helpNotions
A
25

This solution is based on a flex-box layout. It converts the string to a list of words and puts them in vertically rotated Text widgets. These are laid out with a Wrap widget set to Axis.vertical. The Wrap widget automatically handles words that need to wrap by putting them in the next column.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Wrap(
          direction: Axis.vertical,
          children: _getWords(),
        ),
      ),
    );
  }

  List<Widget> _getWords() {
    const text =
        "That's all 😉🐐 12345 One Two Three Four Five ᠸᠢᠺᠢᠫᠧᠳᠢᠶᠠ᠂ ᠴᠢᠯᠦᠭᠡᠲᠦ ᠨᠡᠪᠲᠡᠷᠬᠡᠢ ᠲᠣᠯᠢ ᠪᠢᠴᠢᠭ ᠪᠣᠯᠠᠢ᠃";
    var emoji = RegExp(r"([\u2200-\u3300]|[\uD83C-\uD83E].)");
    List<Widget> res = [];
    var words = text.split(" ");
    for (var word in words) {
      var matches = emoji.allMatches(word);
      if (matches.isEmpty) {
        res.add(RotatedBox(quarterTurns: 1, child: Text(word + ' ')));
      } else {
        var parts = word.split(emoji);
        int i = 0;
        for (Match m in matches) {
          res.add(RotatedBox(quarterTurns: 1, child: Text(parts[i++])));
          res.add(Text(m.group(0)));
        }
        res.add(RotatedBox(quarterTurns: 1, child: Text(parts[i] + ' ')));
      }
    }
    return res;
  }
}

enter image description here

Alisealisen answered 8/7, 2019 at 15:38 Comment(0)
M
57

Sideway text is possible using RotatedBox, which is enough for the text to correctly wrap as you'd expect.

Row(
  children: <Widget>[
    RotatedBox(
      quarterTurns: 1,
      child: Text(sample),
    ),
    Expanded(child: Text(sample)),
    RotatedBox(
      quarterTurns: -1,
      child: Text(sample),
    ),
  ],
),

enter image description here

Similarly, Flutter now supports inline widgets inside text. This can be used to rotate smileys inside a text.

RotatedBox(
  quarterTurns: 1,
  child: RichText(
    text: TextSpan(
      text: 'Hello World',
      style: DefaultTextStyle.of(context).style,
      children: [
        WidgetSpan(
          child: RotatedBox(quarterTurns: -1, child: Text('😃')),
        )
      ],
    ),
  ),
),

enter image description here

Malchy answered 2/7, 2019 at 8:53 Comment(4)
Excellent idea about the new WidgetSpan. That should work well for single line text. The same concept could be used for supporting CJK characters by using a list of widget spans.Nodose
Indeed. A small script should be able to easily that convertion.Montague
For multiline text, simply rotating the paragraph isn't enough. Mongolian text is written top to bottom and columns wrap from left to right. In your first example, the text in the first child of the Row wraps from right to left. This would be like reading an English book starting at the bottom of the book and working up. In the third child of the Row, the text is upside-down.Nodose
Side note: For vertical Chinese/Japanese layout, columns usually do have right to left line wrapping. So simply rotating the paragraph (quarterTurns: 1) could be a valid solution. Since widget creation is cheap I suppose having a WidgetSpan for every single character might work, but that would need to be tested.Nodose
A
25

This solution is based on a flex-box layout. It converts the string to a list of words and puts them in vertically rotated Text widgets. These are laid out with a Wrap widget set to Axis.vertical. The Wrap widget automatically handles words that need to wrap by putting them in the next column.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Wrap(
          direction: Axis.vertical,
          children: _getWords(),
        ),
      ),
    );
  }

  List<Widget> _getWords() {
    const text =
        "That's all 😉🐐 12345 One Two Three Four Five ᠸᠢᠺᠢᠫᠧᠳᠢᠶᠠ᠂ ᠴᠢᠯᠦᠭᠡᠲᠦ ᠨᠡᠪᠲᠡᠷᠬᠡᠢ ᠲᠣᠯᠢ ᠪᠢᠴᠢᠭ ᠪᠣᠯᠠᠢ᠃";
    var emoji = RegExp(r"([\u2200-\u3300]|[\uD83C-\uD83E].)");
    List<Widget> res = [];
    var words = text.split(" ");
    for (var word in words) {
      var matches = emoji.allMatches(word);
      if (matches.isEmpty) {
        res.add(RotatedBox(quarterTurns: 1, child: Text(word + ' ')));
      } else {
        var parts = word.split(emoji);
        int i = 0;
        for (Match m in matches) {
          res.add(RotatedBox(quarterTurns: 1, child: Text(parts[i++])));
          res.add(Text(m.group(0)));
        }
        res.add(RotatedBox(quarterTurns: 1, child: Text(parts[i] + ' ')));
      }
    }
    return res;
  }
}

enter image description here

Alisealisen answered 8/7, 2019 at 15:38 Comment(0)
S
7

It's simple use RotatedBox

RotatedBox(
  quarterTurns: 1,
  child: //your text
),
Shelli answered 7/6, 2021 at 10:14 Comment(0)
A
2

If you make custom font with all symbols rotated by 180° (hard part), then you can simple change textDirection to TextDirection.rtl (right-to-left) and rotate text by one quarter (not easy too).

Alisealisen answered 6/7, 2019 at 11:46 Comment(3)
The idea is good, though I don't think this particular implementation would work since changing the text direction doesn't change the order the glyphs are painted in. A similar idea is to vertically mirror the glyphs in the font and then rotate and mirror the Text widget (see the second image here). This has the disadvantage that any glyphs that aren't included in the font end up being mirrored, but this could perhaps be handled using a WidgetSpan as mentioned in Remi's answer. Anyway, this is a viable option.Nodose
Yes, double flip idea looks perfect, but how it will work in edit mode? Btw. my experiments with right-to-left mode disappointment my too. Good luck with this great challenge.Alisealisen
I used to use the double flip method in Android, but I eventually abandoned it in favor of custom layout and painting. In the end that seems to be more flexible. Thanks for your input.Nodose
N
2

I ended up creating a custom widget to handle text rendering.

enter image description here

It wasn't a trivial task, but it is flexible. Other visitors to this question can follow a similar pattern if the other answers don't meet your needs.

If you would like more control over text rendering in Flutter, read My First Disappointment with Flutter and add a comment to this GitHub issue.

Nodose answered 11/2, 2020 at 0:52 Comment(0)
B
-2

Its Simple draw what you want

Examples flutter-widget-positioning-guide

then

Column(
    children: [
        MyWidget(),
        MyWidget(),
        MyWidget()
    ]
);

// or swap a Column for a Row to flip the axis

Row(children: [ ]);

// and this is all just sugar for the Flex widget
Flex(
    direction: Axis.vertical<--------------
)
Ballot answered 8/7, 2019 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.