mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Fixed random styling for text nodes with many children (#36656)
Summary: Attempting to fix the issue https://github.com/facebook/react-native/issues/33418 ## Changelog <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [ANDROID] Fixed inconsistent styling for text nodes with many children Pull Request resolved: https://github.com/facebook/react-native/pull/36656 Test Plan: No test plan yet. I'd like to ask for help with creating one. ## Putting template aside, I'd like to ask for a review of the approach I'm suggesting. React Native as-is (at least in some cases) [messes up the styles](https://github.com/facebook/react-native/issues/33418#issuecomment-1485305556) for text nodes with more than 85 children, just like that.  All of this text should be blue. The root cause is that code (on Android) assumes it can assign as many `Spannable` span priority values as we'd like, while in reality, it has to be packed in an 8-bit-long section of the flags mask. So 255 possible values. In the scenario I produced, React generates three spans per text node, and for 85 text nodes, it sums up to 255. For each span, a new priority value is assigned. As I understand it, we don't need that many priority values. If I'm not mistaken, these priorities are crucial only for ensuring that nested styles have precedence over the outer ones. I'm proposing to calculate the priority value "vertically" (based on the node's depth in the tree) not "horizontally" (based on its position). It would be awesome if some core engineer familiar with `ReactAndroid` shared their experience with this module, especially if there are any known cases when we _know_ that we'd like to create overlapping spans fighting over the same aspects of the style. Reviewed By: cortinico Differential Revision: D46094200 Pulled By: NickGerleman fbshipit-source-id: aae195c71684fe50469a1ee1bd30625cbfc3622f
This commit is contained in:
committed by
Facebook GitHub Bot
parent
76d41864a2
commit
dcb4eb050a
+5
-30
@@ -70,31 +70,6 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
|
||||
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
|
||||
|
||||
private static class SetSpanOperation {
|
||||
protected int start, end;
|
||||
protected ReactSpan what;
|
||||
|
||||
SetSpanOperation(int start, int end, ReactSpan what) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.what = what;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildSpannedFromShadowNode(
|
||||
ReactBaseTextShadowNode textShadowNode,
|
||||
SpannableStringBuilder sb,
|
||||
@@ -281,8 +256,9 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
|
||||
// While setting the Spans on the final text, we also check whether any of them are inline views
|
||||
// or images.
|
||||
int priority = 0;
|
||||
for (SetSpanOperation op : ops) {
|
||||
for (int priorityIndex = 0; priorityIndex < ops.size(); priorityIndex++) {
|
||||
final SetSpanOperation op = ops.get(ops.size() - priorityIndex - 1);
|
||||
|
||||
boolean isInlineImage = op.what instanceof TextInlineImageSpan;
|
||||
if (isInlineImage || op.what instanceof TextInlineViewPlaceholderSpan) {
|
||||
int height;
|
||||
@@ -309,9 +285,8 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode {
|
||||
}
|
||||
|
||||
// Actual order of calling {@code execute} does NOT matter,
|
||||
// but the {@code priority} DOES matter.
|
||||
op.execute(sb, priority);
|
||||
priority++;
|
||||
// but the {@code priorityIndex} DOES matter.
|
||||
op.execute(sb, priorityIndex);
|
||||
}
|
||||
|
||||
textShadowNode.mTextAttributes.setHeightOfTallestInlineViewOrImage(
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.facebook.react.views.text;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import com.facebook.common.logging.FLog;
|
||||
|
||||
class SetSpanOperation {
|
||||
private static final String TAG = "SetSpanOperation";
|
||||
static final int SPAN_MAX_PRIORITY = Spanned.SPAN_PRIORITY >> Spanned.SPAN_PRIORITY_SHIFT;
|
||||
|
||||
protected int start, end;
|
||||
protected ReactSpan what;
|
||||
|
||||
SetSpanOperation(int start, int end, ReactSpan what) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.what = what;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sb builder
|
||||
* @param priorityIndex index of this operation in the topological sorting which puts operations
|
||||
* with higher priority before operations with lower priority.
|
||||
*/
|
||||
public void execute(SpannableStringBuilder sb, int priorityIndex) {
|
||||
assert priorityIndex >= 0;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Calculate priority, assigning the highest values to operations with the highest priority
|
||||
final int priority = SPAN_MAX_PRIORITY - priorityIndex;
|
||||
|
||||
if (priority < 0) {
|
||||
FLog.w(TAG, "Text tree size exceeded the limit, styling may become unpredictable");
|
||||
}
|
||||
|
||||
// If the computed priority doesn't fit in the flags, clamp it. The effect might not be correct
|
||||
// in 100% of cases, but doing nothing (as we did in the past) leads to totally random results.
|
||||
final int effectivePriority = Math.max(priority, 0);
|
||||
|
||||
spanFlags &= ~Spannable.SPAN_PRIORITY;
|
||||
spanFlags |= (effectivePriority << Spannable.SPAN_PRIORITY_SHIFT) & Spannable.SPAN_PRIORITY;
|
||||
|
||||
sb.setSpan(what, start, end, spanFlags);
|
||||
}
|
||||
}
|
||||
+5
-31
@@ -223,12 +223,12 @@ public class TextLayoutManager {
|
||||
|
||||
// TODO T31905686: add support for inline Images
|
||||
// While setting the Spans on the final text, we also check whether any of them are images.
|
||||
int priority = 0;
|
||||
for (SetSpanOperation op : ops) {
|
||||
for (int priorityIndex = 0; priorityIndex < ops.size(); ++priorityIndex) {
|
||||
final SetSpanOperation op = ops.get(ops.size() - priorityIndex - 1);
|
||||
|
||||
// Actual order of calling {@code execute} does NOT matter,
|
||||
// but the {@code priority} DOES matter.
|
||||
op.execute(sb, priority);
|
||||
priority++;
|
||||
// but the {@code priorityIndex} DOES matter.
|
||||
op.execute(sb, priorityIndex);
|
||||
}
|
||||
|
||||
if (reactTextViewManagerCallback != null) {
|
||||
@@ -561,30 +561,4 @@ public class TextLayoutManager {
|
||||
hyphenationFrequency);
|
||||
return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context);
|
||||
}
|
||||
|
||||
// TODO T31905686: This class should be private
|
||||
public static class SetSpanOperation {
|
||||
protected int start, end;
|
||||
protected ReactSpan what;
|
||||
|
||||
public SetSpanOperation(int start, int end, ReactSpan what) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.what = what;
|
||||
}
|
||||
|
||||
public void execute(Spannable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-31
@@ -264,12 +264,12 @@ public class TextLayoutManagerMapBuffer {
|
||||
|
||||
// TODO T31905686: add support for inline Images
|
||||
// While setting the Spans on the final text, we also check whether any of them are images.
|
||||
int priority = 0;
|
||||
for (SetSpanOperation op : ops) {
|
||||
for (int priorityIndex = 0; priorityIndex < ops.size(); ++priorityIndex) {
|
||||
final SetSpanOperation op = ops.get(ops.size() - priorityIndex - 1);
|
||||
|
||||
// Actual order of calling {@code execute} does NOT matter,
|
||||
// but the {@code priority} DOES matter.
|
||||
op.execute(sb, priority);
|
||||
priority++;
|
||||
// but the {@code priorityIndex} DOES matter.
|
||||
op.execute(sb, priorityIndex);
|
||||
}
|
||||
|
||||
if (reactTextViewManagerCallback != null) {
|
||||
@@ -588,30 +588,4 @@ public class TextLayoutManagerMapBuffer {
|
||||
hyphenationFrequency);
|
||||
return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context);
|
||||
}
|
||||
|
||||
// TODO T31905686: This class should be private
|
||||
public static class SetSpanOperation {
|
||||
protected int start, end;
|
||||
protected ReactSpan what;
|
||||
|
||||
public SetSpanOperation(int start, int end, ReactSpan what) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.what = what;
|
||||
}
|
||||
|
||||
public void execute(Spannable 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user