Merge "Revert "Refactor MeasuredText""

This commit is contained in:
TreeHugger Robot
2017-12-04 20:26:32 +00:00
committed by Android (Google) Code Review
5 changed files with 332 additions and 919 deletions

View File

@@ -1,374 +0,0 @@
/*
* 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.text;
import android.annotation.IntRange;
import android.annotation.NonNull;
import com.android.internal.util.ArrayUtils;
import libcore.util.EmptyArray;
/**
* Implements a growing array of int primitives.
*
* These arrays are NOT thread safe.
*
* @hide
*/
public final class AutoGrowArray {
private static final int MIN_CAPACITY_INCREMENT = 12;
private static final int MAX_CAPACITY_TO_BE_KEPT = 10000;
/**
* Returns next capacity size.
*
* The returned capacity is larger than requested capacity.
*/
private static int computeNewCapacity(int currentSize, int requested) {
final int targetCapacity = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2)
? MIN_CAPACITY_INCREMENT : currentSize >> 1);
return targetCapacity > requested ? targetCapacity : requested;
}
/**
* An auto growing byte array.
*/
public static class ByteArray {
private @NonNull byte[] mValues;
private @IntRange(from = 0) int mSize;
/**
* Creates an empty ByteArray with the default initial capacity.
*/
public ByteArray() {
this(10);
}
/**
* Creates an empty ByteArray with the specified initial capacity.
*/
public ByteArray(@IntRange(from = 0) int initialCapacity) {
if (initialCapacity == 0) {
mValues = EmptyArray.BYTE;
} else {
mValues = ArrayUtils.newUnpaddedByteArray(initialCapacity);
}
mSize = 0;
}
/**
* Changes the size of this ByteArray. If this ByteArray is shrinked, the backing array
* capacity is unchanged.
*/
public void resize(@IntRange(from = 0) int newSize) {
if (newSize > mValues.length) {
ensureCapacity(newSize - mSize);
}
mSize = newSize;
}
/**
* Appends the specified value to the end of this array.
*/
public void append(byte value) {
ensureCapacity(1);
mValues[mSize++] = value;
}
/**
* Ensures capacity to append at least <code>count</code> values.
*/
private void ensureCapacity(@IntRange int count) {
final int requestedSize = mSize + count;
if (requestedSize >= mValues.length) {
final int newCapacity = computeNewCapacity(mSize, requestedSize);
final byte[] newValues = ArrayUtils.newUnpaddedByteArray(newCapacity);
System.arraycopy(mValues, 0, newValues, 0, mSize);
mValues = newValues;
}
}
/**
* Removes all values from this array.
*/
public void clear() {
mSize = 0;
}
/**
* Removes all values from this array and release the internal array object if it is too
* large.
*/
public void clearWithReleasingLargeArray() {
clear();
if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
mValues = EmptyArray.BYTE;
}
}
/**
* Returns the value at the specified position in this array.
*/
public byte get(@IntRange(from = 0) int index) {
return mValues[index];
}
/**
* Sets the value at the specified position in this array.
*/
public void set(@IntRange(from = 0) int index, byte value) {
mValues[index] = value;
}
/**
* Returns the number of values in this array.
*/
public @IntRange(from = 0) int size() {
return mSize;
}
/**
* Returns internal raw array.
*
* Note that this array may have larger size than you requested.
* Use size() instead for getting the actual array size.
*/
public @NonNull byte[] getRawArray() {
return mValues;
}
}
/**
* An auto growing int array.
*/
public static class IntArray {
private @NonNull int[] mValues;
private @IntRange(from = 0) int mSize;
/**
* Creates an empty IntArray with the default initial capacity.
*/
public IntArray() {
this(10);
}
/**
* Creates an empty IntArray with the specified initial capacity.
*/
public IntArray(@IntRange(from = 0) int initialCapacity) {
if (initialCapacity == 0) {
mValues = EmptyArray.INT;
} else {
mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity);
}
mSize = 0;
}
/**
* Changes the size of this IntArray. If this IntArray is shrinked, the backing array
* capacity is unchanged.
*/
public void resize(@IntRange(from = 0) int newSize) {
if (newSize > mValues.length) {
ensureCapacity(newSize - mSize);
}
mSize = newSize;
}
/**
* Appends the specified value to the end of this array.
*/
public void append(int value) {
ensureCapacity(1);
mValues[mSize++] = value;
}
/**
* Ensures capacity to append at least <code>count</code> values.
*/
private void ensureCapacity(@IntRange(from = 0) int count) {
final int requestedSize = mSize + count;
if (requestedSize >= mValues.length) {
final int newCapacity = computeNewCapacity(mSize, requestedSize);
final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity);
System.arraycopy(mValues, 0, newValues, 0, mSize);
mValues = newValues;
}
}
/**
* Removes all values from this array.
*/
public void clear() {
mSize = 0;
}
/**
* Removes all values from this array and release the internal array object if it is too
* large.
*/
public void clearWithReleasingLargeArray() {
clear();
if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
mValues = EmptyArray.INT;
}
}
/**
* Returns the value at the specified position in this array.
*/
public int get(@IntRange(from = 0) int index) {
return mValues[index];
}
/**
* Sets the value at the specified position in this array.
*/
public void set(@IntRange(from = 0) int index, int value) {
mValues[index] = value;
}
/**
* Returns the number of values in this array.
*/
public @IntRange(from = 0) int size() {
return mSize;
}
/**
* Returns internal raw array.
*
* Note that this array may have larger size than you requested.
* Use size() instead for getting the actual array size.
*/
public @NonNull int[] getRawArray() {
return mValues;
}
}
/**
* An auto growing float array.
*/
public static class FloatArray {
private @NonNull float[] mValues;
private @IntRange(from = 0) int mSize;
/**
* Creates an empty FloatArray with the default initial capacity.
*/
public FloatArray() {
this(10);
}
/**
* Creates an empty FloatArray with the specified initial capacity.
*/
public FloatArray(@IntRange(from = 0) int initialCapacity) {
if (initialCapacity == 0) {
mValues = EmptyArray.FLOAT;
} else {
mValues = ArrayUtils.newUnpaddedFloatArray(initialCapacity);
}
mSize = 0;
}
/**
* Changes the size of this FloatArray. If this FloatArray is shrinked, the backing array
* capacity is unchanged.
*/
public void resize(@IntRange(from = 0) int newSize) {
if (newSize > mValues.length) {
ensureCapacity(newSize - mSize);
}
mSize = newSize;
}
/**
* Appends the specified value to the end of this array.
*/
public void append(float value) {
ensureCapacity(1);
mValues[mSize++] = value;
}
/**
* Ensures capacity to append at least <code>count</code> values.
*/
private void ensureCapacity(int count) {
final int requestedSize = mSize + count;
if (requestedSize >= mValues.length) {
final int newCapacity = computeNewCapacity(mSize, requestedSize);
final float[] newValues = ArrayUtils.newUnpaddedFloatArray(newCapacity);
System.arraycopy(mValues, 0, newValues, 0, mSize);
mValues = newValues;
}
}
/**
* Removes all values from this array.
*/
public void clear() {
mSize = 0;
}
/**
* Removes all values from this array and release the internal array object if it is too
* large.
*/
public void clearWithReleasingLargeArray() {
clear();
if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
mValues = EmptyArray.FLOAT;
}
}
/**
* Returns the value at the specified position in this array.
*/
public float get(@IntRange(from = 0) int index) {
return mValues[index];
}
/**
* Sets the value at the specified position in this array.
*/
public void set(@IntRange(from = 0) int index, float value) {
mValues[index] = value;
}
/**
* Returns the number of values in this array.
*/
public @IntRange(from = 0) int size() {
return mSize;
}
/**
* Returns internal raw array.
*
* Note that this array may have larger size than you requested.
* Use size() instead for getting the actual array size.
*/
public @NonNull float[] getRawArray() {
return mValues;
}
}
}

View File

@@ -1907,14 +1907,22 @@ public abstract class Layout {
private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
TextDirectionHeuristic textDir) {
MeasuredText mt = null;
MeasuredText mt = MeasuredText.obtain();
TextLine tl = TextLine.obtain();
try {
mt = MeasuredText.buildForBidi(text, start, end, textDir, mt);
final char[] chars = mt.getChars();
final int len = chars.length;
final Directions directions = mt.getDirections(0, len);
final int dir = mt.getParagraphDir();
mt.setPara(text, start, end, textDir);
Directions directions;
int dir;
if (mt.mEasy) {
directions = DIRS_ALL_LEFT_TO_RIGHT;
dir = Layout.DIR_LEFT_TO_RIGHT;
} else {
directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
0, mt.mChars, 0, mt.mLen);
dir = mt.mDir;
}
char[] chars = mt.mChars;
int len = mt.mLen;
boolean hasTabs = false;
TabStops tabStops = null;
// leading margins should be taken into account when measuring a paragraph
@@ -1947,9 +1955,7 @@ public abstract class Layout {
return margin + Math.abs(tl.metrics(null));
} finally {
TextLine.recycle(tl);
if (mt != null) {
mt.recycle();
}
MeasuredText.recycle(mt);
}
}
@@ -2266,11 +2272,6 @@ public abstract class Layout {
private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
private int mJustificationMode;
/** @hide */
@IntDef({DIR_LEFT_TO_RIGHT, DIR_RIGHT_TO_LEFT})
@Retention(RetentionPolicy.SOURCE)
public @interface Direction {}
public static final int DIR_LEFT_TO_RIGHT = 1;
public static final int DIR_RIGHT_TO_LEFT = -1;

View File

@@ -16,384 +16,125 @@
package android.text;
import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Paint;
import android.text.AutoGrowArray.ByteArray;
import android.text.AutoGrowArray.FloatArray;
import android.text.AutoGrowArray.IntArray;
import android.text.Layout.Directions;
import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
import android.util.Pools.SynchronizedPool;
import android.util.Log;
import java.util.Arrays;
import com.android.internal.util.ArrayUtils;
/**
* MeasuredText provides text information for rendering purpose.
*
* The first motivation of this class is identify the text directions and retrieving individual
* character widths. However retrieving character widths is slower than identifying text directions.
* Thus, this class provides several builder methods for specific purposes.
*
* - buildForBidi:
* Compute only text directions.
* - buildForMeasurement:
* Compute text direction and all character widths.
* - buildForStaticLayout:
* This is bit special. StaticLayout also needs to know text direction and character widths for
* line breaking, but all things are done in native code. Similarly, text measurement is done
* in native code. So instead of storing result to Java array, this keeps the result in native
* code since there is no good reason to move the results to Java layer.
*
* In addition to the character widths, some additional information is computed for each purposes,
* e.g. whole text length for measurement or font metrics for static layout.
*
* MeasuredText is NOT a thread safe object.
* @hide
*/
class MeasuredText {
private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
private static final boolean localLOGV = false;
CharSequence mText;
int mTextStart;
float[] mWidths;
char[] mChars;
byte[] mLevels;
int mDir;
boolean mEasy;
int mLen;
private MeasuredText() {} // Use build static functions instead.
private int mPos;
private TextPaint mWorkPaint;
private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
private static @NonNull MeasuredText obtain() { // Use build static functions instead.
final MeasuredText mt = sPool.acquire();
return mt != null ? mt : new MeasuredText();
private MeasuredText() {
mWorkPaint = new TextPaint();
}
/**
* Recycle the MeasuredText.
*
* Do not call any methods after you call this method.
*/
public void recycle() {
release();
sPool.release(this);
}
private static final Object[] sLock = new Object[0];
private static final MeasuredText[] sCached = new MeasuredText[3];
// The casted original text.
//
// This may be null if the passed text is not a Spanned.
private @Nullable Spanned mSpanned;
// The start offset of the target range in the original text (mSpanned);
private @IntRange(from = 0) int mTextStart;
// The length of the target range in the original text.
private @IntRange(from = 0) int mTextLength;
// The copied character buffer for measuring text.
//
// The length of this array is mTextLength.
private @Nullable char[] mCopiedBuffer;
// The whole paragraph direction.
private @Layout.Direction int mParaDir;
// True if the text is LTR direction and doesn't contain any bidi characters.
private boolean mLtrWithoutBidi;
// The bidi level for individual characters.
//
// This is empty if mLtrWithoutBidi is true.
private @NonNull ByteArray mLevels = new ByteArray();
// The whole width of the text.
// See getWholeWidth comments.
private @FloatRange(from = 0.0f) float mWholeWidth;
// Individual characters' widths.
// See getWidths comments.
private @Nullable FloatArray mWidths = new FloatArray();
// The span end positions.
// See getSpanEndCache comments.
private @Nullable IntArray mSpanEndCache = new IntArray(4);
// The font metrics.
// See getFontMetrics comments.
private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
// Following two objects are for avoiding object allocation.
private @NonNull TextPaint mCachedPaint = new TextPaint();
private @Nullable Paint.FontMetricsInt mCachedFm;
/**
* Releases internal buffers.
*/
public void release() {
reset();
mLevels.clearWithReleasingLargeArray();
mWidths.clearWithReleasingLargeArray();
mFontMetrics.clearWithReleasingLargeArray();
mSpanEndCache.clearWithReleasingLargeArray();
}
/**
* Resets the internal state for starting new text.
*/
private void reset() {
mSpanned = null;
mCopiedBuffer = null;
mWholeWidth = 0;
mLevels.clear();
mWidths.clear();
mFontMetrics.clear();
mSpanEndCache.clear();
}
/**
* Returns the characters to be measured.
*
* This is always available.
*/
public @NonNull char[] getChars() {
return mCopiedBuffer;
}
/**
* Returns the paragraph direction.
*
* This is always available.
*/
public @Layout.Direction int getParagraphDir() {
return mParaDir;
}
/**
* Returns the directions.
*
* This is always available.
*/
public Directions getDirections(@IntRange(from = 0) int start, // inclusive
@IntRange(from = 0) int end) { // exclusive
if (mLtrWithoutBidi) {
return Layout.DIRS_ALL_LEFT_TO_RIGHT;
}
final int length = end - start;
return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
length);
}
/**
* Returns the whole text width.
*
* This is available only if the MeasureText is computed with computeForMeasurement.
* Returns 0 in other cases.
*/
public @FloatRange(from = 0.0f) float getWholeWidth() {
return mWholeWidth;
}
/**
* Returns the individual character's width.
*
* This is available only if the MeasureText is computed with computeForMeasurement.
* Returns empty array in other cases.
*/
public @NonNull FloatArray getWidths() {
return mWidths;
}
/**
* Returns the MetricsAffectingSpan end indices.
*
* If the input text is not a spanned string, this has one value that is the length of the text.
*
* This is available only if the MeasureText is computed with computeForStaticLayout.
* Returns empty array in other cases.
*/
public @NonNull IntArray getSpanEndCache() {
return mSpanEndCache;
}
/**
* Returns the int array which holds FontMetrics.
*
* This array holds the repeat of top, bottom, ascent, descent of font metrics value.
*
* This is available only if the MeasureText is computed with computeForStaticLayout.
* Returns empty array in other cases.
*/
public @NonNull IntArray getFontMetrics() {
return mFontMetrics;
}
/**
* Generates new MeasuredText for Bidi computation.
*
* If recycle is null, this returns new instance. If recycle is not null, this fills computed
* result to recycle and returns recycle.
*
* @param text the character sequence to be measured
* @param start the inclusive start offset of the target region in the text
* @param end the exclusive end offset of the target region in the text
* @param textDir the text direction
* @param recycle pass existing MeasuredText if you want to recycle it.
*
* @return measured text
*/
public static @NonNull MeasuredText buildForBidi(@NonNull CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@NonNull TextDirectionHeuristic textDir,
@Nullable MeasuredText recycle) {
final MeasuredText mt = recycle == null ? obtain() : recycle;
mt.resetAndAnalyzeBidi(text, start, end, textDir);
return mt;
}
/**
* Generates new MeasuredText for measuring texts.
*
* If recycle is null, this returns new instance. If recycle is not null, this fills computed
* result to recycle and returns recycle.
*
* @param paint the paint to be used for rendering the text.
* @param text the character sequence to be measured
* @param start the inclusive start offset of the target region in the text
* @param end the exclusive end offset of the target region in the text
* @param textDir the text direction
* @param recycle pass existing MeasuredText if you want to recycle it.
*
* @return measured text
*/
public static @NonNull MeasuredText buildForMeasurement(@NonNull TextPaint paint,
@NonNull CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@NonNull TextDirectionHeuristic textDir,
@Nullable MeasuredText recycle) {
final MeasuredText mt = recycle == null ? obtain() : recycle;
mt.resetAndAnalyzeBidi(text, start, end, textDir);
mt.mWidths.resize(mt.mTextLength);
if (mt.mTextLength == 0) {
return mt;
}
if (mt.mSpanned == null) {
// No style change by MetricsAffectingSpan. Just measure all text.
mt.applyMetricsAffectingSpan(
paint, null /* spans */, start, end, 0 /* native static layout ptr */);
} else {
// There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
int spanEnd;
for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
MetricAffectingSpan.class);
spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
mt.applyMetricsAffectingSpan(
paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
static MeasuredText obtain() {
MeasuredText mt;
synchronized (sLock) {
for (int i = sCached.length; --i >= 0;) {
if (sCached[i] != null) {
mt = sCached[i];
sCached[i] = null;
return mt;
}
}
}
mt = new MeasuredText();
if (localLOGV) {
Log.v("MEAS", "new: " + mt);
}
return mt;
}
/**
* Generates new MeasuredText for StaticLayout.
*
* If recycle is null, this returns new instance. If recycle is not null, this fills computed
* result to recycle and returns recycle.
*
* @param paint the paint to be used for rendering the text.
* @param text the character sequence to be measured
* @param start the inclusive start offset of the target region in the text
* @param end the exclusive end offset of the target region in the text
* @param textDir the text direction
* @param nativeStaticLayoutPtr the pointer to the native static layout object
* @param recycle pass existing MeasuredText if you want to recycle it.
*
* @return measured text
*/
public static @NonNull MeasuredText buildForStaticLayout(
@NonNull TextPaint paint,
@NonNull CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@NonNull TextDirectionHeuristic textDir,
/* Non-Zero */ long nativeStaticLayoutPtr,
@Nullable MeasuredText recycle) {
final MeasuredText mt = recycle == null ? obtain() : recycle;
mt.resetAndAnalyzeBidi(text, start, end, textDir);
if (mt.mTextLength == 0) {
return mt;
}
if (mt.mSpanned == null) {
// No style change by MetricsAffectingSpan. Just measure all text.
mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end,
nativeStaticLayoutPtr);
mt.mSpanEndCache.append(end);
} else {
// There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
int spanEnd;
for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
MetricAffectingSpan.class);
spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
nativeStaticLayoutPtr);
mt.mSpanEndCache.append(spanEnd);
static MeasuredText recycle(MeasuredText mt) {
mt.finish();
synchronized(sLock) {
for (int i = 0; i < sCached.length; ++i) {
if (sCached[i] == null) {
sCached[i] = mt;
mt.mText = null;
break;
}
}
}
return null;
}
return mt;
void finish() {
mText = null;
if (mLen > 1000) {
mWidths = null;
mChars = null;
mLevels = null;
}
}
/**
* Reset internal state and analyzes text for bidirectional runs.
*
* @param text the character sequence to be measured
* @param start the inclusive start offset of the target region in the text
* @param end the exclusive end offset of the target region in the text
* @param textDir the text direction
* Analyzes text for bidirectional runs. Allocates working buffers.
*/
private void resetAndAnalyzeBidi(@NonNull CharSequence text,
@IntRange(from = 0) int start, // inclusive
@IntRange(from = 0) int end, // exclusive
@NonNull TextDirectionHeuristic textDir) {
reset();
mSpanned = text instanceof Spanned ? (Spanned) text : null;
void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
mText = text;
mTextStart = start;
mTextLength = end - start;
if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
mCopiedBuffer = new char[mTextLength];
int len = end - start;
mLen = len;
mPos = 0;
if (mWidths == null || mWidths.length < len) {
mWidths = ArrayUtils.newUnpaddedFloatArray(len);
}
TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
if (mChars == null || mChars.length != len) {
mChars = new char[len];
}
TextUtils.getChars(text, start, end, mChars, 0);
// Replace characters associated with ReplacementSpan to U+FFFC.
if (mSpanned != null) {
ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
ReplacementSpan[] spans = spanned.getSpans(start, end,
ReplacementSpan.class);
for (int i = 0; i < spans.length; i++) {
int startInPara = mSpanned.getSpanStart(spans[i]) - start;
int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
// The span interval may be larger and must be restricted to [start, end)
int startInPara = spanned.getSpanStart(spans[i]) - start;
int endInPara = spanned.getSpanEnd(spans[i]) - start;
// The span interval may be larger and must be restricted to [start, end[
if (startInPara < 0) startInPara = 0;
if (endInPara > mTextLength) endInPara = mTextLength;
Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
if (endInPara > len) endInPara = len;
for (int j = startInPara; j < endInPara; j++) {
mChars[j] = '\uFFFC'; // object replacement character
}
}
}
if ((textDir == TextDirectionHeuristics.LTR ||
textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR ||
textDir == TextDirectionHeuristics.ANYRTL_LTR) &&
TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
mLevels.clear();
mParaDir = Layout.DIR_LEFT_TO_RIGHT;
mLtrWithoutBidi = true;
TextUtils.doesNotNeedBidi(mChars, 0, len)) {
mDir = Layout.DIR_LEFT_TO_RIGHT;
mEasy = true;
} else {
final int bidiRequest;
if (mLevels == null || mLevels.length < len) {
mLevels = ArrayUtils.newUnpaddedByteArray(len);
}
int bidiRequest;
if (textDir == TextDirectionHeuristics.LTR) {
bidiRequest = Layout.DIR_REQUEST_LTR;
} else if (textDir == TextDirectionHeuristics.RTL) {
@@ -403,146 +144,122 @@ class MeasuredText {
} else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
} else {
final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
boolean isRtl = textDir.isRtl(mChars, 0, len);
bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
}
mLevels.resize(mTextLength);
mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
mLtrWithoutBidi = false;
}
}
private void applyReplacementRun(@NonNull ReplacementSpan replacement,
@IntRange(from = 0) int start, // inclusive, in copied buffer
@IntRange(from = 0) int end, // exclusive, in copied buffer
/* Maybe Zero */ long nativeStaticLayoutPtr) {
// Use original text. Shouldn't matter.
// TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
// backward compatibility? or Should we initialize them for getFontMetricsInt?
final float width = replacement.getSize(
mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
if (nativeStaticLayoutPtr == 0) {
// Assigns all width to the first character. This is the same behavior as minikin.
mWidths.set(start, width);
if (end > start + 1) {
Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
}
mWholeWidth += width;
} else {
StaticLayout.addReplacementRun(nativeStaticLayoutPtr, mCachedPaint, start, end, width);
}
}
private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
@IntRange(from = 0) int end, // exclusive, in copied buffer
/* Maybe Zero */ long nativeStaticLayoutPtr) {
if (nativeStaticLayoutPtr != 0) {
mCachedPaint.getFontMetricsInt(mCachedFm);
}
if (mLtrWithoutBidi) {
// If the whole text is LTR direction, just apply whole region.
if (nativeStaticLayoutPtr == 0) {
mWholeWidth += mCachedPaint.getTextRunAdvances(
mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
mWidths.getRawArray(), start);
} else {
StaticLayout.addStyleRun(nativeStaticLayoutPtr, mCachedPaint, start, end,
false /* isRtl */);
}
} else {
// If there is multiple bidi levels, split into individual bidi level and apply style.
byte level = mLevels.get(start);
// Note that the empty text or empty range won't reach this method.
// Safe to search from start + 1.
for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
final boolean isRtl = (level & 0x1) != 0;
if (nativeStaticLayoutPtr == 0) {
final int levelLength = levelEnd - levelStart;
mWholeWidth += mCachedPaint.getTextRunAdvances(
mCopiedBuffer, levelStart, levelLength, levelStart, levelEnd, isRtl,
mWidths.getRawArray(), levelStart);
} else {
StaticLayout.addStyleRun(
nativeStaticLayoutPtr, mCachedPaint, levelStart, levelEnd, isRtl);
}
if (levelEnd == end) {
break;
}
levelStart = levelEnd;
level = mLevels.get(levelEnd);
}
}
}
}
private void applyMetricsAffectingSpan(
@NonNull TextPaint paint,
@Nullable MetricAffectingSpan[] spans,
@IntRange(from = 0) int start, // inclusive, in original text buffer
@IntRange(from = 0) int end, // exclusive, in original text buffer
/* Maybe Zero */ long nativeStaticLayoutPtr) {
mCachedPaint.set(paint);
// XXX paint should not have a baseline shift, but...
mCachedPaint.baselineShift = 0;
final boolean needFontMetrics = nativeStaticLayoutPtr != 0;
if (needFontMetrics && mCachedFm == null) {
mCachedFm = new Paint.FontMetricsInt();
}
ReplacementSpan replacement = null;
if (spans != null) {
for (int i = 0; i < spans.length; i++) {
MetricAffectingSpan span = spans[i];
if (span instanceof ReplacementSpan) {
// The last ReplacementSpan is effective for backward compatibility reasons.
replacement = (ReplacementSpan) span;
} else {
// TODO: No need to call updateMeasureState for ReplacementSpan as well?
span.updateMeasureState(mCachedPaint);
}
}
}
final int startInCopiedBuffer = start - mTextStart;
final int endInCopiedBuffer = end - mTextStart;
if (replacement != null) {
applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
nativeStaticLayoutPtr);
} else {
applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeStaticLayoutPtr);
}
if (needFontMetrics) {
if (mCachedPaint.baselineShift < 0) {
mCachedFm.ascent += mCachedPaint.baselineShift;
mCachedFm.top += mCachedPaint.baselineShift;
} else {
mCachedFm.descent += mCachedPaint.baselineShift;
mCachedFm.bottom += mCachedPaint.baselineShift;
}
mFontMetrics.append(mCachedFm.top);
mFontMetrics.append(mCachedFm.bottom);
mFontMetrics.append(mCachedFm.ascent);
mFontMetrics.append(mCachedFm.descent);
mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels);
mEasy = false;
}
}
/**
* Returns the maximum index that the accumulated width not exceeds the width.
* Apply the style.
*
* If forward=false is passed, returns the minimum index from the end instead.
*
* This only works if the MeasuredText is computed with computeForMeasurement.
* Undefined behavior in other case.
* If nativeStaticLayoutPtr is 0, this method measures the styled text width.
* If nativeStaticLayoutPtr is not 0, this method just passes the style information to native
* code by calling StaticLayout.addstyleRun() and returns 0.
*/
@IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
float[] w = mWidths.getRawArray();
float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm,
long nativeStaticLayoutPtr) {
if (fm != null) {
paint.getFontMetricsInt(fm);
}
final int p = mPos;
mPos = p + len;
if (mEasy) {
final boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT;
if (nativeStaticLayoutPtr == 0) {
return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p);
} else {
StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, p, p + len, isRtl);
return 0.0f; // Builder.addStyleRun doesn't return the width.
}
}
float totalAdvance = 0;
int level = mLevels[p];
for (int q = p, i = p + 1, e = p + len;; ++i) {
if (i == e || mLevels[i] != level) {
final boolean isRtl = (level & 0x1) != 0;
if (nativeStaticLayoutPtr == 0) {
totalAdvance +=
paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q);
} else {
// Builder.addStyleRun doesn't return the width.
StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, q, i, isRtl);
}
if (i == e) {
break;
}
q = i;
level = mLevels[i];
}
}
return totalAdvance; // If nativeStaticLayoutPtr is 0, the result is zero.
}
float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
return addStyleRun(paint, len, fm, 0 /* native ptr */);
}
float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
Paint.FontMetricsInt fm, long nativeStaticLayoutPtr) {
TextPaint workPaint = mWorkPaint;
workPaint.set(paint);
// XXX paint should not have a baseline shift, but...
workPaint.baselineShift = 0;
ReplacementSpan replacement = null;
for (int i = 0; i < spans.length; i++) {
MetricAffectingSpan span = spans[i];
if (span instanceof ReplacementSpan) {
replacement = (ReplacementSpan)span;
} else {
span.updateMeasureState(workPaint);
}
}
float wid;
if (replacement == null) {
wid = addStyleRun(workPaint, len, fm, nativeStaticLayoutPtr);
} else {
// Use original text. Shouldn't matter.
wid = replacement.getSize(workPaint, mText, mTextStart + mPos,
mTextStart + mPos + len, fm);
if (nativeStaticLayoutPtr == 0) {
float[] w = mWidths;
w[mPos] = wid;
for (int i = mPos + 1, e = mPos + len; i < e; i++)
w[i] = 0;
} else {
StaticLayout.addReplacementRun(nativeStaticLayoutPtr, paint, mPos, mPos + len, wid);
}
mPos += len;
}
if (fm != null) {
if (workPaint.baselineShift < 0) {
fm.ascent += workPaint.baselineShift;
fm.top += workPaint.baselineShift;
} else {
fm.descent += workPaint.baselineShift;
fm.bottom += workPaint.baselineShift;
}
}
return wid;
}
float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
Paint.FontMetricsInt fm) {
return addStyleRun(paint, spans, len, fm, 0 /* native ptr */);
}
int breakText(int limit, boolean forwards, float width) {
float[] w = mWidths;
if (forwards) {
int i = 0;
while (i < limit) {
@@ -550,7 +267,7 @@ class MeasuredText {
if (width < 0.0f) break;
i++;
}
while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
while (i > 0 && mChars[i - 1] == ' ') i--;
return i;
} else {
int i = limit - 1;
@@ -559,22 +276,16 @@ class MeasuredText {
if (width < 0.0f) break;
i--;
}
while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
while (i < limit - 1 && (mChars[i + 1] == ' ' || w[i + 1] == 0.0f)) {
i++;
}
return limit - i - 1;
}
}
/**
* Returns the length of the substring.
*
* This only works if the MeasuredText is computed with computeForMeasurement.
* Undefined behavior in other case.
*/
@FloatRange(from = 0.0f) float measure(int start, int limit) {
float measure(int start, int limit) {
float width = 0;
float[] w = mWidths.getRawArray();
float[] w = mWidths;
for (int i = start; i < limit; ++i) {
width += w[i];
}

View File

@@ -21,10 +21,10 @@ import android.annotation.IntRange;
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,6 +99,8 @@ public class StaticLayout extends Layout {
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
b.mMeasuredText = MeasuredText.obtain();
return b;
}
@@ -109,6 +111,8 @@ public class StaticLayout extends Layout {
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;
@@ -124,6 +128,7 @@ public class StaticLayout extends Layout {
mRightIndents = null;
mLeftPaddings = null;
mRightPaddings = null;
mMeasuredText.finish();
}
public Builder setText(CharSequence source) {
@@ -439,6 +444,9 @@ public class StaticLayout extends Layout {
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);
}
@@ -610,7 +618,11 @@ public class StaticLayout extends Layout {
TextUtils.TruncateAt ellipsize = b.mEllipsize;
final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs
FloatArray widths = new FloatArray();
// 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];
mLineCount = 0;
mEllipsized = false;
@@ -622,6 +634,8 @@ public class StaticLayout extends Layout {
Paint.FontMetricsInt fm = b.mFontMetricsInt;
int[] chooseHtv = null;
MeasuredText measured = b.mMeasuredText;
Spanned spanned = null;
if (source instanceof Spanned)
spanned = (Spanned) source;
@@ -648,7 +662,6 @@ public class StaticLayout extends Layout {
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
indents, mLeftPaddings, mRightPaddings);
MeasuredText measured = null;
try {
int paraEnd;
for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
@@ -708,6 +721,13 @@ public class StaticLayout extends Layout {
}
}
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) {
@@ -723,16 +743,50 @@ public class StaticLayout extends Layout {
}
}
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,
@@ -755,7 +809,7 @@ public class StaticLayout extends Layout {
lineBreaks.ascents,
lineBreaks.descents,
lineBreaks.flags,
widths.getRawArray());
widths);
final int[] breaks = lineBreaks.breaks;
final float[] lineWidths = lineBreaks.widths;
@@ -778,7 +832,7 @@ public class StaticLayout extends Layout {
width += lineWidths[i];
} else {
for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
width += widths.get(j);
width += widths[j];
}
}
flag |= flags[i] & TAB_MASK;
@@ -842,10 +896,10 @@ public class StaticLayout extends Layout {
v = out(source, here, endPos,
ascent, descent, fmTop, fmBottom,
v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
flags[breakIndex], needMultiply, measured, bufEnd,
includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
paint, moreChars);
flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd,
includepad, trackpad, addLastLineSpacing, chs, widths, paraStart,
ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint,
moreChars);
if (endPos < spanEnd) {
// preserve metrics for current span
@@ -873,8 +927,7 @@ public class StaticLayout extends Layout {
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
&& mLineCount < mMaximumVisibleLineCount) {
measured = MeasuredText.buildForStaticLayout(
paint, source, bufEnd, bufEnd, textDir, nativePtr, measured);
measured.setPara(source, bufEnd, bufEnd, textDir);
paint.getFontMetricsInt(fm);
@@ -884,15 +937,12 @@ public class StaticLayout extends Layout {
v,
spacingmult, spacingadd, null,
null, fm, 0,
needMultiply, measured, bufEnd,
needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
includepad, trackpad, addLastLineSpacing, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
}
} finally {
if (measured != null) {
measured.recycle();
}
nFinish(nativePtr);
}
}
@@ -902,8 +952,8 @@ public class StaticLayout extends Layout {
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 MeasuredText measured,
final int bufEnd, final boolean includePad, final boolean trackPad,
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 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) {
@@ -911,7 +961,6 @@ public class StaticLayout extends Layout {
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));
@@ -937,8 +986,17 @@ public class StaticLayout extends Layout {
// one bit for start field
lines[off + TAB] |= flags & TAB_MASK;
lines[off + HYPHEN] = flags;
lines[off + DIR] |= dir << DIR_SHIFT;
mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
// 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);
}
final boolean firstLine = (j == 0);
final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);

View File

@@ -42,6 +42,7 @@ import android.text.style.EasyEditSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.LocaleSpan;
import android.text.style.MetricAffectingSpan;
import android.text.style.ParagraphStyle;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
@@ -1250,11 +1251,10 @@ public class TextUtils {
@NonNull String ellipsis) {
final int len = text.length();
MeasuredText mt = null;
final MeasuredText mt = MeasuredText.obtain();
MeasuredText resultMt = null;
try {
mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
float width = mt.getWholeWidth();
float width = setPara(mt, paint, text, 0, text.length(), textDir);
if (width <= avail) {
if (callback != null) {
@@ -1263,6 +1263,7 @@ public class TextUtils {
return text;
}
resultMt = MeasuredText.obtain();
// First estimate of effective width of ellipsis.
float ellipsisWidth = paint.measureText(ellipsis);
int numberOfTries = 0;
@@ -1289,7 +1290,7 @@ public class TextUtils {
}
}
final char[] buf = mt.getChars();
final char[] buf = mt.mChars;
final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
final int removed = end - start;
@@ -1332,9 +1333,7 @@ public class TextUtils {
if (remaining == 0) { // All text is gone.
textFits = true;
} else {
resultMt = MeasuredText.buildForMeasurement(
paint, result, 0, result.length(), textDir, resultMt);
width = resultMt.getWholeWidth();
width = setPara(resultMt, paint, result, 0, result.length(), textDir);
if (width <= avail) {
textFits = true;
} else {
@@ -1358,11 +1357,9 @@ public class TextUtils {
}
return result;
} finally {
if (mt != null) {
mt.recycle();
}
MeasuredText.recycle(mt);
if (resultMt != null) {
resultMt.recycle();
MeasuredText.recycle(resultMt);
}
}
}
@@ -1479,17 +1476,15 @@ public class TextUtils {
public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
MeasuredText mt = null;
MeasuredText tempMt = null;
MeasuredText mt = MeasuredText.obtain();
try {
int len = text.length();
mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt);
float width = mt.getWholeWidth();
float width = setPara(mt, p, text, 0, len, textDir);
if (width <= avail) {
return text;
}
char[] buf = mt.getChars();
char[] buf = mt.mChars;
int commaCount = 0;
for (int i = 0; i < len; i++) {
@@ -1505,8 +1500,9 @@ public class TextUtils {
int w = 0;
int count = 0;
float[] widths = mt.getWidths().getRawArray();
float[] widths = mt.mWidths;
MeasuredText tempMt = MeasuredText.obtain();
for (int i = 0; i < len; i++) {
w += widths[i];
@@ -1523,9 +1519,8 @@ public class TextUtils {
}
// XXX this is probably ok, but need to look at it more
tempMt = MeasuredText.buildForMeasurement(
p, format, 0, format.length(), textDir, tempMt);
float moreWid = tempMt.getWholeWidth();
tempMt.setPara(format, 0, format.length(), textDir);
float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
if (w + moreWid <= avail) {
ok = i + 1;
@@ -1533,18 +1528,40 @@ public class TextUtils {
}
}
}
MeasuredText.recycle(tempMt);
SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
out.insert(0, text, 0, ok);
return out;
} finally {
if (mt != null) {
mt.recycle();
}
if (tempMt != null) {
tempMt.recycle();
MeasuredText.recycle(mt);
}
}
private static float setPara(MeasuredText mt, TextPaint paint,
CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
mt.setPara(text, start, end, textDir);
float width;
Spanned sp = text instanceof Spanned ? (Spanned) text : null;
int len = end - start;
if (sp == null) {
width = mt.addStyleRun(paint, len, null);
} else {
width = 0;
int spanEnd;
for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
spanEnd = sp.nextSpanTransition(spanStart, len,
MetricAffectingSpan.class);
MetricAffectingSpan[] spans = sp.getSpans(
spanStart, spanEnd, MetricAffectingSpan.class);
spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
}
}
return width;
}
// Returns true if the character's presence could affect RTL layout.