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 {