Improve TextView.onMeasure() for multiline text.

Previously, measures all paragraph for deciding view width.
However, in case AT_MOST measurment, we can stop calculating measure if
the measured paragraph already exceeds the upper limit.

Bug: 64389125
Test: bit CtsWidgetTestCases:*
Test: CorePerfTest. Here is a result before and after this patch.

Before:
INSTRUMENTATION_STATUS: measure_AtMost_median=50332114
INSTRUMENTATION_STATUS: measure_Exactly_median=28276317
INSTRUMENTATION_STATUS: measure_Unspecified_median=50193036

After:
INSTRUMENTATION_STATUS: measure_AtMost_median=28475187
INSTRUMENTATION_STATUS: measure_Exactly_median=26944710
INSTRUMENTATION_STATUS: measure_Unspecified_median=50880088

Change-Id: I2a39eb39817a8f845c4fa2e174a905a2d057096e
diff --git a/apct-tests/perftests/core/src/android/widget/TextViewOnMeasurePerfTest.java b/apct-tests/perftests/core/src/android/widget/TextViewOnMeasurePerfTest.java
new file mode 100644
index 0000000..a14dd25
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/widget/TextViewOnMeasurePerfTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.widget;
+
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Typeface;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.TextAppearanceSpan;
+import android.view.LayoutInflater;
+
+import com.android.perftests.core.R;
+
+import java.util.Random;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TextViewOnMeasurePerfTest {
+    private static final String MULTILINE_TEXT =
+        "Lorem ipsum dolor sit amet, \n"+
+        "consectetur adipiscing elit, \n" +
+        "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \n" +
+        "Ut enim ad minim veniam, \n" +
+        "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
+        "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat " +
+        "nulla pariatur.\n" +
+        "Excepteur sint occaecat cupidatat non proident, \n" +
+        "sunt in culpa qui officia deserunt mollit anim id est laborum.\n";
+
+    private static final int VIEW_WIDTH = 1000;
+    private static final int VIEW_HEIGHT = 1000;
+    private static final CharSequence COMPLEX_MULTILINE_TEXT;
+    static {
+        final SpannableStringBuilder ssb = new SpannableStringBuilder();
+
+        // To emphasize, append multiline text 10 times.
+        for (int i = 0; i < 10; ++i) {
+            ssb.append(MULTILINE_TEXT);
+        }
+
+        final ColorStateList[] COLORS = {
+                ColorStateList.valueOf(0xFFFF0000),  // RED
+                ColorStateList.valueOf(0xFF00FF00),  // GREEN
+                ColorStateList.valueOf(0xFF0000FF),  // BLUE
+        };
+
+        final int[] STYLES = {
+                Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC
+        };
+
+        final String[] FAMILIES = { "sans-serif", "serif", "monospace" };
+
+        // Append random span to text.
+        final Random random = new Random(0);
+        for (int pos = 0; pos < ssb.length();) {
+            final TextAppearanceSpan span = new TextAppearanceSpan(
+                FAMILIES[random.nextInt(FAMILIES.length)],
+                STYLES[random.nextInt(STYLES.length)],
+                24 + random.nextInt(32),  // text size. minimum 24
+                COLORS[random.nextInt(COLORS.length)],
+                COLORS[random.nextInt(COLORS.length)]);
+            int spanLength = 1 + random.nextInt(9);  // Up to 9 span length.
+            if (pos + spanLength > ssb.length()) {
+                spanLength = ssb.length() - pos;
+            }
+            ssb.setSpan(span, pos, pos + spanLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+            pos += spanLength;
+        }
+        COMPLEX_MULTILINE_TEXT = ssb;
+    }
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void testMeasure_AtMost() throws Throwable {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        final TextView textView = new TextView(context);
+        textView.setText(COMPLEX_MULTILINE_TEXT);
+
+        while (state.keepRunning()) {
+            // Changing locale to invalidate internal layout.
+            textView.setTextLocale(Locale.UK);
+            textView.setTextLocale(Locale.US);
+
+            textView.measure(AT_MOST | VIEW_WIDTH, AT_MOST | VIEW_HEIGHT);
+        }
+    }
+
+    @Test
+    public void testMeasure_Exactly() throws Throwable {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        final TextView textView = new TextView(context);
+        textView.setText(COMPLEX_MULTILINE_TEXT);
+
+        while (state.keepRunning()) {
+            // Changing locale to invalidate internal layout.
+            textView.setTextLocale(Locale.UK);
+            textView.setTextLocale(Locale.US);
+
+            textView.measure(EXACTLY | VIEW_WIDTH, EXACTLY | VIEW_HEIGHT);
+        }
+    }
+
+    @Test
+    public void testMeasure_Unspecified() throws Throwable {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        final TextView textView = new TextView(context);
+        textView.setText(COMPLEX_MULTILINE_TEXT);
+
+        while (state.keepRunning()) {
+            // Changing locale to invalidate internal layout.
+            textView.setTextLocale(Locale.UK);
+            textView.setTextLocale(Locale.US);
+
+            textView.measure(UNSPECIFIED, UNSPECIFIED);
+        }
+    }
+}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index ecefce9..d612fc7 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -152,6 +152,17 @@
      */
     public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint,
             TextDirectionHeuristic textDir) {
+        return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE);
+    }
+    /**
+     * Return how wide a layout must be in order to display the
+     * specified text slice with one line per paragraph.
+     *
+     * If the measured width exceeds given limit, returns limit value instead.
+     * @hide
+     */
+    public static float getDesiredWidthWithLimit(CharSequence source, int start, int end,
+            TextPaint paint, TextDirectionHeuristic textDir, float upperLimit) {
         float need = 0;
 
         int next;
@@ -163,6 +174,9 @@
 
             // note, omits trailing paragraph char
             float w = measurePara(paint, source, i, next, textDir);
+            if (w > upperLimit) {
+                return upperLimit;
+            }
 
             if (w > need)
                 need = w;
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 4c7039b..1291671 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -8117,6 +8117,8 @@
 
         int des = -1;
         boolean fromexisting = false;
+        final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
+                ?  (float) widthSize : Float.MAX_VALUE;
 
         if (widthMode == MeasureSpec.EXACTLY) {
             // Parent has told us how big to be. So be it.
@@ -8137,8 +8139,8 @@
 
             if (boring == null || boring == UNKNOWN_BORING) {
                 if (des < 0) {
-                    des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, 0,
-                            mTransformed.length(), mTextPaint, mTextDir));
+                    des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
+                            mTransformed.length(), mTextPaint, mTextDir, widthLimit));
                 }
                 width = des;
             } else {
@@ -8168,8 +8170,8 @@
 
                 if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                     if (hintDes < 0) {
-                        hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, 0, mHint.length(),
-                                mTextPaint, mTextDir));
+                        hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
+                                mHint.length(), mTextPaint, mTextDir, widthLimit));
                     }
                     hintWidth = hintDes;
                 } else {