Refactor MeasuredText
This is 2nd attempt of I58d3020a3fa560d05576e18888fbfe46e2975e8f
The root cause of the crash is passing end offset instead of passing
length. This CL contains that fix and also has a test case for that.
This refactoring contains:
- Add lots of comments.
- Mark private the internal fields and introduce accessors and helper
methods.
- Factor out the auto grow array implementation to another class.
- Use SynchronizedPool for pool implementation.
- Introduce three build methods for each use case.
- Hide addStyleRun and compute all necessary informations in build method.
Locally verified that this doesn't cause performance regressions.
Here is a raw performance test result on walleye-userdebug.
StaticLayoutPerfTest (median, N=100):
createRandom: 7,846,449 -> 8,003,903 (+2.01%)
createRandom Balanced: 7,810,436 -> 7,919,200 (+1.40%)
TextViewOnMeasurePerfTest (median, N=100):
measure_AtMost: 94,276,376 -> 94,124,658 (-0.16%)
measure_Exactly: 91,629,352 -> 91,617,639 (-0.01%)
measure_Unspecified: 151,006,181 -> 150,957,598 (-0.03%)
Test: bit CtsTextTestCases:*
Test: bit CtsGraphicsTestCases:*
Test: bit CtsWidgetTestCases:*
Test: bit FrameworksCoreTests:android.text.StaticLayoutTest
Bug: 65024629
Bug: 70146381
Change-Id: I772f60444d0fe87f59609977af5ef712cab3eb37
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index c0fc44f..81c82c9 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -21,10 +21,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Paint;
+import android.text.AutoGrowArray.FloatArray;
import android.text.style.LeadingMarginSpan;
import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
import android.text.style.LineHeightSpan;
-import android.text.style.MetricAffectingSpan;
import android.text.style.TabStopSpan;
import android.util.Log;
import android.util.Pools.SynchronizedPool;
@@ -99,8 +99,6 @@
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
-
- b.mMeasuredText = MeasuredText.obtain();
return b;
}
@@ -111,8 +109,6 @@
private static void recycle(@NonNull Builder b) {
b.mPaint = null;
b.mText = null;
- MeasuredText.recycle(b.mMeasuredText);
- b.mMeasuredText = null;
b.mLeftIndents = null;
b.mRightIndents = null;
b.mLeftPaddings = null;
@@ -128,7 +124,6 @@
mRightIndents = null;
mLeftPaddings = null;
mRightPaddings = null;
- mMeasuredText.finish();
}
public Builder setText(CharSequence source) {
@@ -444,9 +439,6 @@
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
- // This will go away and be subsumed by native builder code
- private MeasuredText mMeasuredText;
-
private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
}
@@ -618,11 +610,7 @@
TextUtils.TruncateAt ellipsize = b.mEllipsize;
final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs
- // store span end locations
- int[] spanEndCache = new int[4];
- // store fontMetrics per span range
- // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
- int[] fmCache = new int[4 * 4];
+ FloatArray widths = new FloatArray();
mLineCount = 0;
mEllipsized = false;
@@ -634,8 +622,6 @@
Paint.FontMetricsInt fm = b.mFontMetricsInt;
int[] chooseHtv = null;
- MeasuredText measured = b.mMeasuredText;
-
Spanned spanned = null;
if (source instanceof Spanned)
spanned = (Spanned) source;
@@ -662,6 +648,7 @@
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
indents, mLeftPaddings, mRightPaddings);
+ MeasuredText measured = null;
try {
int paraEnd;
for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
@@ -721,13 +708,6 @@
}
}
- measured.setPara(source, paraStart, paraEnd, textDir);
- char[] chs = measured.mChars;
- float[] widths = measured.mWidths;
- byte[] chdirs = measured.mLevels;
- int dir = measured.mDir;
- boolean easy = measured.mEasy;
-
// tab stop locations
int[] variableTabStops = null;
if (spanned != null) {
@@ -743,50 +723,16 @@
}
}
+ measured = MeasuredText.buildForStaticLayout(
+ paint, source, paraStart, paraEnd, textDir, nativePtr, measured);
+ final char[] chs = measured.getChars();
+ final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
+ final int[] fmCache = measured.getFontMetrics().getRawArray();
+ widths.resize(chs.length);
+
// measurement has to be done before performing line breaking
// but we don't want to recompute fontmetrics or span ranges the
// second time, so we cache those and then use those stored values
- int fmCacheCount = 0;
- int spanEndCacheCount = 0;
- for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
- if (fmCacheCount * 4 >= fmCache.length) {
- int[] grow = new int[fmCacheCount * 4 * 2];
- System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
- fmCache = grow;
- }
-
- if (spanEndCacheCount >= spanEndCache.length) {
- int[] grow = new int[spanEndCacheCount * 2];
- System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
- spanEndCache = grow;
- }
-
- if (spanned == null) {
- spanEnd = paraEnd;
- int spanLen = spanEnd - spanStart;
- measured.addStyleRun(paint, spanLen, fm, nativePtr);
- } else {
- spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
- MetricAffectingSpan.class);
- int spanLen = spanEnd - spanStart;
- MetricAffectingSpan[] spans =
- spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, spanned,
- MetricAffectingSpan.class);
- measured.addStyleRun(paint, spans, spanLen, fm, nativePtr);
- }
-
- // the order of storage here (top, bottom, ascent, descent) has to match the
- // code below where these values are retrieved
- fmCache[fmCacheCount * 4 + 0] = fm.top;
- fmCache[fmCacheCount * 4 + 1] = fm.bottom;
- fmCache[fmCacheCount * 4 + 2] = fm.ascent;
- fmCache[fmCacheCount * 4 + 3] = fm.descent;
- fmCacheCount++;
-
- spanEndCache[spanEndCacheCount] = spanEnd;
- spanEndCacheCount++;
- }
int breakCount = nComputeLineBreaks(
nativePtr,
@@ -809,7 +755,7 @@
lineBreaks.ascents,
lineBreaks.descents,
lineBreaks.flags,
- widths);
+ widths.getRawArray());
final int[] breaks = lineBreaks.breaks;
final float[] lineWidths = lineBreaks.widths;
@@ -832,7 +778,7 @@
width += lineWidths[i];
} else {
for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
- width += widths[j];
+ width += widths.get(j);
}
}
flag |= flags[i] & TAB_MASK;
@@ -896,10 +842,10 @@
v = out(source, here, endPos,
ascent, descent, fmTop, fmBottom,
v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
- flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd,
- includepad, trackpad, addLastLineSpacing, chs, widths, paraStart,
- ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint,
- moreChars);
+ flags[breakIndex], needMultiply, measured, bufEnd,
+ includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
+ paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
+ paint, moreChars);
if (endPos < spanEnd) {
// preserve metrics for current span
@@ -927,7 +873,8 @@
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
&& mLineCount < mMaximumVisibleLineCount) {
- measured.setPara(source, bufEnd, bufEnd, textDir);
+ measured = MeasuredText.buildForStaticLayout(
+ paint, source, bufEnd, bufEnd, textDir, nativePtr, measured);
paint.getFontMetricsInt(fm);
@@ -937,12 +884,15 @@
v,
spacingmult, spacingadd, null,
null, fm, 0,
- needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
+ needMultiply, measured, bufEnd,
includepad, trackpad, addLastLineSpacing, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
}
} finally {
+ if (measured != null) {
+ measured.recycle();
+ }
nFinish(nativePtr);
}
}
@@ -952,8 +902,8 @@
private int out(final CharSequence text, final int start, final int end, int above, int below,
int top, int bottom, int v, final float spacingmult, final float spacingadd,
final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
- final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
- final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
+ final int flags, final boolean needMultiply, final MeasuredText measured,
+ final int bufEnd, final boolean includePad, final boolean trackPad,
final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
final float textWidth, final TextPaint paint, final boolean moreChars) {
@@ -961,6 +911,7 @@
final int off = j * mColumns;
final int want = off + mColumns + TOP;
int[] lines = mLines;
+ final int dir = measured.getParagraphDir();
if (want >= lines.length) {
final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
@@ -986,17 +937,8 @@
// one bit for start field
lines[off + TAB] |= flags & TAB_MASK;
lines[off + HYPHEN] = flags;
-
lines[off + DIR] |= dir << DIR_SHIFT;
- // easy means all chars < the first RTL, so no emoji, no nothing
- // XXX a run with no text or all spaces is easy but might be an empty
- // RTL paragraph. Make sure easy is false if this is the case.
- if (easy) {
- mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT;
- } else {
- mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
- start - widthStart, end - start);
- }
+ mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
final boolean firstLine = (j == 0);
final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);