@Nullable
  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) {
      metrics = isBoringLayout();
    }

    // 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;
  }
Builds and returns a {@link Layout}.

@return A {@link Layout} based on the parameters set. Returns null if no text was specified and
    empty text is not explicitly allowed (see {@link #setShouldLayoutZeroLengthText(boolean)}).
library/src/main/java/com/facebook/fbui/textlayoutbuilder/TextLayoutBuilder.java