A visual guide to text affinity in Flutter

Suragch
Suragch
Feb 4 · 7 min read

If you don’t crawl around the Flutter source code much you’ve probably never heard of text affinity, represented by the TextAffinity enum. It’s an interesting concept, though, and in this article I’ll try to make it easy to understand through the use of images.

The word affinity means attraction. This is referring to which part of the text the cursor (or caret as it’s sometimes called) is attracted to. To explain that, it’s first necessary to talk about text position, represented by the TextPosition class.

Text position

Left-to-right text

English text is written from left to right, but languages like Arabic and Hebrew are written from right to left.

Right-to-left text

Line wrapping

TextPosition(offset: 9),

you’d get the following result:

That’s the same left-to-right indexing that you saw with Hello.

This is all very nice, but what if you don’t have quite enough room to fit the text on one line, and the last character soft wraps to the next line like so:

Now if you put the text position at offset 9 , where does the cursor go? Does it go after the 8 on the first line or before the 9 on the second line?

Well, it turns out the default is to go before the 9 at the start of the second line:

But what if you want to make the cursor go after the 8 on the first line? It would be really annoying if you were trying to type something and you couldn’t do that.

This is where text affinity comes in.

Downstream text affinity

Upstream text affinity

TextPosition(offset: 9, affinity: TextAffinity.upstream),

Now when the cursor is wavering between 8 and 9, upstream means it goes against the natural direction (which in this case is right-to-left) and chooses the 8 like so:

Of course, if the text can all fit on one line then it doesn’t matter if the affinity is upstream or downstream. The cursor ends up in the same place either way:

Bidirectional text

Take the following string as an example:

const text = 'Helloשלום';

Try slowly selecting the part inside the quotes and notice how your browser handles bidirectional text. It can be a challenge to get the correct selection sometimes.

Downstream affinity

You’d think you could choose offset 5 to go to the end of Hello, but that’s not true because the affinity is downstream. When you are at the right side of Hello, you’d have to go upstream to get to o. Instead, downstream means the cursor has an affinity for (i.e., an attraction to) the beginning of שלום, which is on the right side of that Hebrew word.

Upstream affinity

The only difference is at offset 5, where the cursor has an upstream affinity toward the last character of Hello. It looks like offset 9 is the same, but there the cursor has an upstream affinity toward the last character of שלום.

Bidirectional text comparison

It’s at the beginning of שלום.

And this is upstream for offset 5:

It’s at the end of Hello.

If that still seems hard to get your mind around (as it was for me), it’s helpful to think about it in terms of the code unit order:

const text = 'Helloשלום';
final codeUnits = text.codeUnits;
for (var i = 0; i < codeUnits.length; i++) {
final char = String.fromCharCode(codeUnits[i]);
print('$i: $char');
}

This results in:

0: H
1: e
2: l
3: l
4: o
5: ש
6: ל
7: ו
8: ם

As you can see, the ש of שלום comes directly after o of Hello when observing then UTF-16 code unit order rather than the final display order. That is, ש is downstream from o, and o is upstream from ש.

A note about text direction

  • TextDirection.ltr (left-to-right)
  • TextDirection.rtl (right-to-left)

(I wish it had ttb (top-to-bottom) too, but that’s a problem for a different article.)

One could be forgiven for thinking that Hello as LTR text would be rendered olleH as RTL text, but that’s not what text direction means. TextDirection controls the order of text blocks in bidirectional text, that is, in a string with both LTR and RTL characters.

In the following string Hello is first and שלום is second. The two are separated by a space:

const text = 'Hello שלום';

In an LTR text direction environment, first is on the left and last is on the right:

But if you change the text direction by specifying TextDirection.rtl instead, then first is on the right and last is on the left.

As you can see, the character order of Hello or שלום didn’t change, but rather the order of the entire blocks changed in relation to each other. Interestingly the space remains in the middle.

You can find the code and more details about this in my Stack Overflow answer here.

Conclusion

  • At the end of soft line wraps
  • At the boundary between left-to-right and right-to-left bidirectional text

As a normal Flutter user, you almost never have to worry about text affinity because TextPainter does all the work for you. (You can read more about TextPainter in the article How text editing works internally in Flutter.) However, if you’re making your own custom text editor, text affinity is a useful concept to understand.

I didn’t use a lot of code in the explanation above, so feel free to check out the full example below.

Full code

Follow Flutter Community on Twitter: https://www.twitter.com/FlutterComm

Flutter Community

Articles and Stories from the Flutter Community

Suragch

Written by

Suragch

A Flutter and Dart developer. Follow me on Twitter @suragch1 to get updates of new articles.

Flutter Community

Articles and Stories from the Flutter Community

Suragch

Written by

Suragch

A Flutter and Dart developer. Follow me on Twitter @suragch1 to get updates of new articles.

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store