Text to Spannable conversion is now using PRIORITY flag to enforce the order of spans

Summary:
When we convert nested <Text> components to Spannable object we must enforce the order of spans somehow,
otherwise we will have Spannable object with unpredictable order of spans, which will produce unpredictalbe text layout.
We can do it only using `Spannable.SPAN_PRIORITY` feature because Spannable objects do not maintain the order of spans internally.

We also have to fix this to implement autoexpandable <TextInput>.

Reviewed By: achen1

Differential Revision: D5811172

fbshipit-source-id: 5bc68b869e58aba27d6986581af9fe3343d116a7
This commit is contained in:
Valentin Shergin 2017-09-17 21:59:31 -07:00 committed by Facebook Github Bot
parent 3ff463f6a0
commit 7efd4fabfd
2 changed files with 42 additions and 13 deletions

View File

@ -231,6 +231,27 @@ class TextExample extends React.Component<{}> {
<RNTesterBlock title="Nested">
<Text onPress={() => console.log('1st')}>
(Normal text,
<Text style={{color: 'red', fontWeight: 'bold'}}>
(R)red
<Text style={{color: 'green', fontWeight: 'normal'}}>
(G)green
<Text style={{color: 'blue', fontWeight: 'bold'}}>
(B)blue
<Text style={{color: 'cyan', fontWeight: 'normal'}}>
(C)cyan
<Text style={{color: 'magenta', fontWeight: 'bold'}}>
(M)magenta
<Text style={{color: 'yellow', fontWeight: 'normal'}}>
(Y)yellow
<Text style={{color: 'black', fontWeight: 'bold'}}>
(K)black
</Text>
</Text>
</Text>
</Text>
</Text>
</Text>
</Text>
<Text style={{fontWeight: 'bold'}} onPress={() => console.log('2nd')}>
(and bold
<Text style={{fontStyle: 'italic', fontSize: 11, color: '#527fe4'}} onPress={() => console.log('3rd')}>

View File

@ -65,13 +65,17 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
this.what = what;
}
public void execute(SpannableStringBuilder sb) {
public void execute(SpannableStringBuilder sb, int priority) {
// All spans will automatically extend to the right of the text, but not the left - except
// for spans that start at the beginning of the text.
int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE;
if (start == 0) {
spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE;
}
spanFlags &= ~Spannable.SPAN_PRIORITY;
spanFlags |= (priority << Spannable.SPAN_PRIORITY_SHIFT) & Spannable.SPAN_PRIORITY;
sb.setSpan(what, start, end, spanFlags);
}
}
@ -167,6 +171,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
// up-to-bottom, otherwise all the spannables that are withing the region for which one may set
// a new spannable will be wiped out
List<SetSpanOperation> ops = new ArrayList<>();
buildSpannedFromShadowNode(textShadowNode, sb, ops);
if (text != null) {
@ -174,22 +179,20 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
}
if (textShadowNode.mFontSize == UNSET) {
sb.setSpan(
new AbsoluteSizeSpan(
textShadowNode.mAllowFontScaling
? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
: (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP))),
0,
sb.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
int defaultFontSize =
textShadowNode.mAllowFontScaling
? (int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP))
: (int) Math.ceil(PixelUtil.toPixelFromDIP(ViewDefaults.FONT_SIZE_SP));
ops.add(new SetSpanOperation(0, sb.length(), new AbsoluteSizeSpan(defaultFontSize)));
}
textShadowNode.mContainsImages = false;
textShadowNode.mHeightOfTallestInlineImage = Float.NaN;
// While setting the Spans on the final text, we also check whether any of them are images
for (int i = ops.size() - 1; i >= 0; i--) {
SetSpanOperation op = ops.get(i);
// While setting the Spans on the final text, we also check whether any of them are images.
int priority = 0;
for (SetSpanOperation op : ops) {
if (op.what instanceof TextInlineImageSpan) {
int height = ((TextInlineImageSpan) op.what).getHeight();
textShadowNode.mContainsImages = true;
@ -198,8 +201,13 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
textShadowNode.mHeightOfTallestInlineImage = height;
}
}
op.execute(sb);
// Actual order of calling {@code execute} does NOT matter,
// but the {@code priority} DOES matter.
op.execute(sb, priority);
priority++;
}
return sb;
}