Improve BoringLayout#isBoring for PrecomputedText

Bug: 216736786
Test: atest PrecomputedTextTest
Test: perf score becomes 758,071 -> 110,755
Change-Id: I57d3eb9d1e896ed1c847733c4e96ac52dd97d2c4
diff --git a/core/api/current.txt b/core/api/current.txt
index 11e0a6b..3b3462c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -16412,6 +16412,7 @@
   public class MeasuredText {
     method public void getBounds(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull android.graphics.Rect);
     method @FloatRange(from=0.0f) @Px public float getCharWidthAt(@IntRange(from=0) int);
+    method public void getFontMetricsInt(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull android.graphics.Paint.FontMetricsInt);
     method @FloatRange(from=0.0) @Px public float getWidth(@IntRange(from=0) int, @IntRange(from=0) int);
   }
 
@@ -44939,6 +44940,7 @@
     method public char charAt(int);
     method public static android.text.PrecomputedText create(@NonNull CharSequence, @NonNull android.text.PrecomputedText.Params);
     method public void getBounds(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull android.graphics.Rect);
+    method public void getFontMetricsInt(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull android.graphics.Paint.FontMetricsInt);
     method @IntRange(from=0) public int getParagraphCount();
     method @IntRange(from=0) public int getParagraphEnd(@IntRange(from=0) int);
     method @IntRange(from=0) public int getParagraphStart(@IntRange(from=0) int);
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index 748d551..bedd409 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -287,6 +287,16 @@
     }
 
     /**
+     * Retrieves the font metrics for the given range.
+     *
+     * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
+     */
+    public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
+            @NonNull Paint.FontMetricsInt fmi) {
+        mMeasuredText.getFontMetricsInt(start, end, fmi);
+    }
+
+    /**
      * Returns a width of the character at the offset.
      *
      * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java
index be66db2..9307e56 100644
--- a/core/java/android/text/PrecomputedText.java
+++ b/core/java/android/text/PrecomputedText.java
@@ -21,6 +21,7 @@
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.text.LineBreakConfig;
 import android.graphics.text.MeasuredText;
@@ -697,6 +698,38 @@
     }
 
     /**
+     * Retrieves the text font metrics for the given range.
+     * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
+     * IllegalArgumentException will be thrown.
+     *
+     * @param start the inclusive start offset in the text
+     * @param end the exclusive end offset in the text
+     * @param outMetrics the output font metrics
+     * @throws IllegalArgumentException if start and end offset are in the different paragraph.
+     */
+    public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
+            @NonNull Paint.FontMetricsInt outMetrics) {
+        Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
+        Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
+        Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
+        Objects.requireNonNull(outMetrics);
+        if (start == end) {
+            mParams.getTextPaint().getFontMetricsInt(outMetrics);
+            return;
+        }
+        final int paraIndex = findParaIndex(start);
+        final int paraStart = getParagraphStart(paraIndex);
+        final int paraEnd = getParagraphEnd(paraIndex);
+        if (start < paraStart || paraEnd < end) {
+            throw new IllegalArgumentException("Cannot measured across the paragraph:"
+                    + "para: (" + paraStart + ", " + paraEnd + "), "
+                    + "request: (" + start + ", " + end + ")");
+        }
+        getMeasuredParagraph(paraIndex).getFontMetricsInt(start - paraStart,
+                end - paraStart, outMetrics);
+    }
+
+    /**
      * Returns a width of a character at offset
      *
      * @param offset an offset of the text.
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index 49e2111..e39231c 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -866,8 +866,12 @@
             wp.getFontMetricsInt(mChars, start, count, contextStart, contextCount, runIsRtl,
                     fmi);
         } else {
-            wp.getFontMetricsInt(mText, mStart + start, count, mStart + contextStart, contextCount,
-                    runIsRtl, fmi);
+            if (mComputed == null) {
+                wp.getFontMetricsInt(mText, mStart + start, count, mStart + contextStart,
+                        contextCount, runIsRtl, fmi);
+            } else {
+                mComputed.getFontMetricsInt(mStart + start, mStart + end, fmi);
+            }
         }
 
         updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java
index 6d691c1..3f7f088 100644
--- a/graphics/java/android/graphics/text/MeasuredText.java
+++ b/graphics/java/android/graphics/text/MeasuredText.java
@@ -34,6 +34,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 
 /**
  * Result of text shaping of the single paragraph string.
@@ -56,18 +57,22 @@
 public class MeasuredText {
     private static final String TAG = "MeasuredText";
 
-    private long mNativePtr;
-    private boolean mComputeHyphenation;
-    private boolean mComputeLayout;
-    private @NonNull char[] mChars;
+    private final long mNativePtr;
+    private final boolean mComputeHyphenation;
+    private final boolean mComputeLayout;
+    @NonNull private final char[] mChars;
+    private final int mTop;
+    private final int mBottom;
 
     // Use builder instead.
     private MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation,
-            boolean computeLayout) {
+            boolean computeLayout, int top, int bottom) {
         mNativePtr = ptr;
         mChars = chars;
         mComputeHyphenation = computeHyphenation;
         mComputeLayout = computeLayout;
+        mTop = top;
+        mBottom = bottom;
     }
 
     /**
@@ -124,6 +129,30 @@
     }
 
     /**
+     * Retrieves the font metrics of the given range
+     *
+     * @param start an inclusive start index of the range
+     * @param end an exclusive end index of the range
+     * @param outMetrics an output metrics object
+     */
+    public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
+            @NonNull Paint.FontMetricsInt outMetrics) {
+        Preconditions.checkArgument(0 <= start && start <= mChars.length,
+                "start(%d) must be 0 <= start <= %d", start, mChars.length);
+        Preconditions.checkArgument(0 <= end && end <= mChars.length,
+                "end(%d) must be 0 <= end <= %d", end, mChars.length);
+        Preconditions.checkArgument(start <= end,
+                "start(%d) is larger than end(%d)", start, end);
+        Objects.requireNonNull(outMetrics);
+
+        long packed = nGetExtent(mNativePtr, mChars, start, end);
+        outMetrics.ascent = (int) (packed >> 32);
+        outMetrics.descent = (int) (packed & 0xFFFFFFFF);
+        outMetrics.top = Math.min(outMetrics.ascent, mTop);
+        outMetrics.bottom = Math.max(outMetrics.descent, mBottom);
+    }
+
+    /**
      * Returns the width of the character at the given offset.
      *
      * @param offset an offset of the character.
@@ -160,6 +189,8 @@
     @CriticalNative
     private static native float nGetCharWidthAt(long nativePtr, int offset);
 
+    private static native long nGetExtent(long nativePtr, char[] buf, int start, int end);
+
     /**
      * Helper class for creating a {@link MeasuredText}.
      * <p>
@@ -189,6 +220,9 @@
         private boolean mFastHyphenation = false;
         private int mCurrentOffset = 0;
         private @Nullable MeasuredText mHintMt = null;
+        private int mTop = 0;
+        private int mBottom = 0;
+        private Paint.FontMetricsInt mCachedMetrics = new Paint.FontMetricsInt();
 
         /**
          * Construct a builder.
@@ -269,6 +303,10 @@
             nAddStyleRun(mNativePtr, paint.getNativeInstance(), lbStyle, lbWordStyle,
                     mCurrentOffset, end, isRtl);
             mCurrentOffset = end;
+
+            paint.getFontMetricsInt(mCachedMetrics);
+            mTop = Math.min(mTop, mCachedMetrics.top);
+            mBottom = Math.max(mBottom, mCachedMetrics.bottom);
             return this;
         }
 
@@ -419,7 +457,7 @@
                 long ptr = nBuildMeasuredText(mNativePtr, hintPtr, mText, mComputeHyphenation,
                         mComputeLayout, mFastHyphenation);
                 final MeasuredText res = new MeasuredText(ptr, mText, mComputeHyphenation,
-                        mComputeLayout);
+                        mComputeLayout, mTop, mBottom);
                 sRegistry.registerNativeAllocation(res, ptr);
                 return res;
             } finally {
diff --git a/libs/hwui/jni/text/MeasuredText.cpp b/libs/hwui/jni/text/MeasuredText.cpp
index 76ea2d5..c13c800 100644
--- a/libs/hwui/jni/text/MeasuredText.cpp
+++ b/libs/hwui/jni/text/MeasuredText.cpp
@@ -134,6 +134,21 @@
     GraphicsJNI::irect_to_jrect(ir, env, bounds);
 }
 
+// Regular JNI
+static jlong nGetExtent(JNIEnv* env, jobject, jlong ptr, jcharArray javaText, jint start,
+                        jint end) {
+    ScopedCharArrayRO text(env, javaText);
+    const minikin::U16StringPiece textBuffer(text.get(), text.size());
+    const minikin::Range range(start, end);
+
+    minikin::MinikinExtent extent = toMeasuredParagraph(ptr)->getExtent(textBuffer, range);
+
+    int32_t ascent = SkScalarRoundToInt(extent.ascent);
+    int32_t descent = SkScalarRoundToInt(extent.descent);
+
+    return (((jlong)(ascent)) << 32) | ((jlong)descent);
+}
+
 // CriticalNative
 static jlong nGetReleaseFunc(CRITICAL_JNI_PARAMS) {
     return toJLong(&releaseMeasuredParagraph);
@@ -153,12 +168,13 @@
 };
 
 static const JNINativeMethod gMTMethods[] = {
-    // MeasuredParagraph native functions.
-    {"nGetWidth", "(JII)F", (void*) nGetWidth},  // Critical Natives
-    {"nGetBounds", "(J[CIILandroid/graphics/Rect;)V", (void*) nGetBounds},  // Regular JNI
-    {"nGetReleaseFunc", "()J", (void*) nGetReleaseFunc},  // Critical Natives
-    {"nGetMemoryUsage", "(J)I", (void*) nGetMemoryUsage},  // Critical Native
-    {"nGetCharWidthAt", "(JI)F", (void*) nGetCharWidthAt},  // Critical Native
+        // MeasuredParagraph native functions.
+        {"nGetWidth", "(JII)F", (void*)nGetWidth},                             // Critical Natives
+        {"nGetBounds", "(J[CIILandroid/graphics/Rect;)V", (void*)nGetBounds},  // Regular JNI
+        {"nGetExtent", "(J[CII)J", (void*)nGetExtent},                         // Regular JNI
+        {"nGetReleaseFunc", "()J", (void*)nGetReleaseFunc},                    // Critical Natives
+        {"nGetMemoryUsage", "(J)I", (void*)nGetMemoryUsage},                   // Critical Native
+        {"nGetCharWidthAt", "(JI)F", (void*)nGetCharWidthAt},                  // Critical Native
 };
 
 int register_android_graphics_text_MeasuredText(JNIEnv* env) {