Skip to content

Commit

Permalink
Add caching of spannable text objects
Browse files Browse the repository at this point in the history
Summary: This diff adds support to cache the Spannable objects that are created during measure() and updateLocalData() for text

Reviewed By: shergin

Differential Revision: D13188599

fbshipit-source-id: 6547d8ce2bb8b1dfb3c91e64facff3ba0cd97472
  • Loading branch information
mdvacca authored and facebook-github-bot committed Nov 26, 2018
1 parent 3be2816 commit f341795
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ public Map<String, String> getNativeProps() {

public long measure(
ReactContext context,
T view,
ReadableNativeMap localData,
ReadableNativeMap props,
float width,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,9 @@ protected void onAfterUpdateTransaction(ReactTextView view) {
@Override
public Object updateLocalData(ReactTextView view, ReactStylesDiffMap props, ReactStylesDiffMap localData) {
ReadableMap attributedString = localData.getMap("attributedString");
ReadableArray fragments = attributedString.getArray("fragments");
String string = attributedString.getString("string");

Spannable spanned = TextLayoutManager.spannedFromTextFragments(view.getContext(),
fragments, string);
Spannable spanned = TextLayoutManager.getOrCreateSpannableForText(view.getContext(),
attributedString);
view.setSpanned(spanned);

TextAttributeProps textViewProps = new TextAttributeProps(props);
Expand Down Expand Up @@ -105,7 +103,6 @@ public Object updateLocalData(ReactTextView view, ReactStylesDiffMap props, Reac

public long measure(
ReactContext context,
ReactTextView view,
ReadableNativeMap localData,
ReadableNativeMap props,
float width,
Expand All @@ -114,7 +111,6 @@ public long measure(
YogaMeasureMode heightMode) {

return TextLayoutManager.measureText(context,
view,
localData,
props,
width,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,16 @@
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.UnderlineSpan;
import android.util.LruCache;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableNativeMap;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
import java.awt.font.TextAttribute;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -47,11 +46,17 @@ public class TextLayoutManager {
// The bug is that unicode emoticons aren't measured properly which causes text to be clipped.
private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG);

private static void buildSpannedFromShadowNode(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {
// Specifies the amount of spannable that are stored into the {@link sSpannableCache}.
private static final int spannableCacheSize = 100;

private static final Object sSpannableCacheLock = new Object();
private static LruCache<Double, Spannable> sSpannableCache = new LruCache<>(spannableCacheSize);

private static void buildSpannableFromFragment(
Context context,
ReadableArray fragments,
SpannableStringBuilder sb,
List<SetSpanOperation> ops) {

for (int i = 0, length = fragments.size(); i < length; i++) {
ReadableMap fragment = fragments.getMap(i);
Expand All @@ -64,7 +69,7 @@ private static void buildSpannedFromShadowNode(
// if (child instanceof ReactRawTextShadowNode) {
// sb.append(((ReactRawTextShadowNode) child).getText());
// } else if (child instanceof ReactBaseTextShadowNode) {
// buildSpannedFromShadowNode((ReactBaseTextShadowNode) child, sb, ops);
// buildSpannableFromFragment((ReactBaseTextShadowNode) child, sb, ops);
// } else if (child instanceof ReactTextInlineImageShadowNode) {
// // We make the image take up 1 character in the span and put a corresponding character into
// // the text so that the image doesn't run over any following text.
Expand Down Expand Up @@ -150,19 +155,39 @@ private static void buildSpannedFromShadowNode(
}
}

protected static Spannable spannedFromTextFragments(
Context context,
ReadableArray fragments, String text) {
SpannableStringBuilder sb = new SpannableStringBuilder();
protected static Spannable getOrCreateSpannableForText(
Context context,
ReadableMap attributedString) {

Double hash = attributedString.getDouble("hash");
Spannable preparedSpannableText;

// TODO(5837930): Investigate whether it's worth optimizing this part and do it if so
synchronized (sSpannableCacheLock) {
preparedSpannableText = sSpannableCache.get(hash);
if (preparedSpannableText != null) {
return preparedSpannableText;
}
}

preparedSpannableText = createSpannableFromAttributedString(context, attributedString);
synchronized (sSpannableCacheLock) {
sSpannableCache.put(hash, preparedSpannableText);
}
return preparedSpannableText;
}

private static Spannable createSpannableFromAttributedString(
Context context,
ReadableMap attributedString) {

SpannableStringBuilder sb = new SpannableStringBuilder();

// The {@link SpannableStringBuilder} implementation require setSpan operation to be called
// 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(context, fragments, sb, ops);
buildSpannableFromFragment(context, attributedString.getArray("fragments"), sb, ops);

// TODO T31905686: add support for inline Images
// textShadowNode.mContainsImages = false;
Expand Down Expand Up @@ -191,20 +216,17 @@ protected static Spannable spannedFromTextFragments(
}

public static long measureText(
ReactContext context,
ReactTextView view,
ReadableNativeMap attributedString,
ReadableNativeMap paragraphAttributes,
float width,
YogaMeasureMode widthYogaMeasureMode,
float height,
YogaMeasureMode heightYogaMeasureMode) {
ReactContext context,
ReadableNativeMap attributedString,
ReadableNativeMap paragraphAttributes,
float width,
YogaMeasureMode widthYogaMeasureMode,
float height,
YogaMeasureMode heightYogaMeasureMode) {

// TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic)
TextPaint textPaint = sTextPaintInstance;
Layout layout;

Spannable preparedSpannableText = spannedFromTextFragments(context, attributedString.getArray("fragments"), attributedString.getString("string"));
Spannable preparedSpannableText = getOrCreateSpannableForText(context, attributedString);

// TODO add these props to paragraph attributes
int textBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
Expand All @@ -221,6 +243,7 @@ public static long measureText(
// technically, width should never be negative, but there is currently a bug in
boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0;

Layout layout;
if (boring == null &&
(unconstrainedWidth ||
(!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) {
Expand Down Expand Up @@ -282,7 +305,10 @@ public static long measureText(
}
}

int maximumNumberOfLines = paragraphAttributes.hasKey("maximumNumberOfLines") ? paragraphAttributes.getInt("maximumNumberOfLines") : UNSET;
int maximumNumberOfLines =
paragraphAttributes.hasKey("maximumNumberOfLines")
? paragraphAttributes.getInt("maximumNumberOfLines")
: UNSET;

width = layout.getWidth();
if (maximumNumberOfLines != UNSET
Expand Down

0 comments on commit f341795

Please sign in to comment.