in library/src/main/java/com/facebook/fbui/textlayoutbuilder/TextLayoutBuilder.java [1031:1185]
public Layout build() {
// Return the cached layout if no property changed.
if (mShouldCacheLayout && mSavedLayout != null) {
return mSavedLayout;
}
if (mParams.text == null
|| (mParams.text.length() == 0 && !mParams.shouldLayoutZeroLengthText)) {
return null;
}
boolean hasClickableSpans = false;
int hashCode = -1;
if (mShouldCacheLayout && mParams.text instanceof Spannable) {
ClickableSpan[] spans =
((Spannable) mParams.text).getSpans(0, mParams.text.length() - 1, ClickableSpan.class);
hasClickableSpans = spans.length > 0;
}
// If the text has ClickableSpans, it will be bound to different
// click listeners each time. It is unsafe to cache these text Layouts.
// Hence they will not be in cache.
if (mShouldCacheLayout && !hasClickableSpans) {
hashCode = mParams.hashCode();
Layout cachedLayout = sCache.get(hashCode);
if (cachedLayout != null) {
return cachedLayout;
}
}
BoringLayout.Metrics metrics = null;
int numLines = mParams.singleLine ? 1 : mParams.maxLines;
// Try creating a boring layout only if singleLine is requested.
if (numLines == 1) {
try {
metrics = BoringLayout.isBoring(mParams.text, mParams.paint);
} catch (NullPointerException e) {
// On older Samsung devices (< M), we sometimes run into a NPE here where a FontMetricsInt
// object created within BoringLayout is not properly null-checked within TextLine.
// Its ok to ignore this exception since we'll create a regular StaticLayout later.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// If we see this on M or above, then its something else.
throw e;
}
}
}
// getDesiredWidth here is used to ensure we layout text at the same size which it is measured.
// If we used a large static value it would break RTL due to drawing text at the very end of the
// large value.
int width;
switch (mParams.measureMode) {
case MEASURE_MODE_UNSPECIFIED:
width = (int) Math.ceil(Layout.getDesiredWidth(mParams.text, mParams.paint));
break;
case MEASURE_MODE_EXACTLY:
width = mParams.width;
break;
case MEASURE_MODE_AT_MOST:
width =
Math.min(
(int) Math.ceil(Layout.getDesiredWidth(mParams.text, mParams.paint)),
mParams.width);
break;
default:
throw new IllegalStateException("Unexpected measure mode " + mParams.measureMode);
}
final int lineHeight = mParams.getLineHeight();
if (mMaxWidthMode == EMS) {
width = Math.min(width, mMaxWidth * lineHeight);
} else {
width = Math.min(width, mMaxWidth);
}
if (mMinWidthMode == EMS) {
width = Math.max(width, mMinWidth * lineHeight);
} else {
width = Math.max(width, mMinWidth);
}
Layout layout;
if (metrics != null) {
layout =
BoringLayout.make(
mParams.text,
mParams.paint,
width,
mParams.alignment,
mParams.spacingMult,
mParams.spacingAdd,
metrics,
mParams.includePadding,
mParams.ellipsize,
width);
} else {
while (true) {
try {
layout =
StaticLayoutHelper.make(
mParams.text,
0,
mParams.text.length(),
mParams.paint,
width,
mParams.alignment,
mParams.spacingMult,
mParams.spacingAdd,
mParams.includePadding,
mParams.ellipsize,
width,
numLines,
mParams.textDirection,
mParams.breakStrategy,
mParams.hyphenationFrequency,
mParams.justificationMode,
mParams.leftIndents,
mParams.rightIndents,
mParams.useLineSpacingFromFallbacks);
} catch (IndexOutOfBoundsException e) {
// Workaround for https://code.google.com/p/android/issues/detail?id=35412
if (!(mParams.text instanceof String)) {
// remove all Spannables and re-try
Log.e("TextLayoutBuilder", "Hit bug #35412, retrying with Spannables removed", e);
mParams.text = mParams.text.toString();
continue;
} else {
// If it still happens with all Spannables removed we'll bubble the exception up
throw e;
}
}
break;
}
}
// Do not cache if the text has ClickableSpans.
if (mShouldCacheLayout && !hasClickableSpans) {
mSavedLayout = layout;
sCache.put(hashCode, layout);
}
// Force a new paint.
mParams.mForceNewPaint = true;
if (mShouldWarmText && mGlyphWarmer != null) {
// Draw the text in a background thread to warm the cache.
mGlyphWarmer.warmLayout(layout);
}
return layout;
}