
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
TextPosition has an offset property which is used for cursor location or text selection.
Left-to-right text
Take the word Hello, for example. In the image below you can see where the cursor would be for each offset of the text position:

English text is written from left to right, but languages like Arabic and Hebrew are written from right to left.
Right-to-left text
If you take the Hebrew word שלום, the offsets go in the opposite direction from English. That is, they start at the right and move left:

Line wrapping
Now let’s say you have the text 0123456789. If you set the text position offset to 9
like this (the full code is at the end of the article):
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
Text affinity can be upstream or downstream. Since English text (and numbers like 0123456789) flows like a river from left to right, downstream means the next character to the right. When the cursor is sitting between 8 and 9, the downstream character is 9. And since downstream is the default, that’s why the cursor is on the second line:

Upstream text affinity
However, it’s also possible to set the affinity to upstream like this:
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
Another situation where you need text affinity is to choose the correct cursor location within bidirectional text. As already mentioned, English text is written left-to-right (LTR) while languages like Arabic and Hebrew are written right-to-left (RTL). Bidirectional text happens when LTR and RTL text are used together in the same string.
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
If you loop through all of the text position offsets using an affinity of TextAffinity.downsream
(the default), then you’ll get the following cursor positions:

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
If you set the affinity to upstream and loop through the string again, you’ll get the following result:

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
Again, this is downstream for offset 5
:

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
This article has talked quite a bit about bidirectional text so it’s worth mentioning TextDirection
, too. This enum has two values:
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
To summarize, text affinity has the purpose of disambiguating the text position in the following two circumstances:
- 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
You can try the code yourself by creating a new Flutter project and replacing main.dart with the following:
Follow Flutter Community on Twitter: https://www.twitter.com/FlutterComm