Merge "Revert "Refactor MeasuredText""
This commit is contained in:
committed by
Android (Google) Code Review
commit
6fdd4f49a2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user