980 lines
40 KiB
Java
980 lines
40 KiB
Java
/*
|
|
* Copyright (C) 2010 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 static com.android.text.flags.Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN;
|
|
|
|
import android.annotation.FlaggedApi;
|
|
import android.annotation.FloatRange;
|
|
import android.annotation.IntRange;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.Px;
|
|
import android.annotation.SuppressLint;
|
|
import android.annotation.TestApi;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Rect;
|
|
import android.graphics.text.LineBreakConfig;
|
|
import android.graphics.text.MeasuredText;
|
|
import android.icu.lang.UCharacter;
|
|
import android.icu.lang.UCharacterDirection;
|
|
import android.icu.text.Bidi;
|
|
import android.text.AutoGrowArray.ByteArray;
|
|
import android.text.AutoGrowArray.FloatArray;
|
|
import android.text.AutoGrowArray.IntArray;
|
|
import android.text.Layout.Directions;
|
|
import android.text.style.LineBreakConfigSpan;
|
|
import android.text.style.MetricAffectingSpan;
|
|
import android.text.style.ReplacementSpan;
|
|
import android.util.Pools.SynchronizedPool;
|
|
|
|
import java.util.Arrays;
|
|
|
|
/**
|
|
* MeasuredParagraph 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.
|
|
*
|
|
* MeasuredParagraph is NOT a thread safe object.
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
public class MeasuredParagraph {
|
|
private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
|
|
|
|
private MeasuredParagraph() {} // Use build static functions instead.
|
|
|
|
private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
|
|
|
|
private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
|
|
final MeasuredParagraph mt = sPool.acquire();
|
|
return mt != null ? mt : new MeasuredParagraph();
|
|
}
|
|
|
|
/**
|
|
* Recycle the MeasuredParagraph.
|
|
*
|
|
* Do not call any methods after you call this method.
|
|
* @hide
|
|
*/
|
|
public void recycle() {
|
|
release();
|
|
sPool.release(this);
|
|
}
|
|
|
|
// 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();
|
|
|
|
private Bidi mBidi;
|
|
|
|
// 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);
|
|
|
|
// The native MeasuredParagraph.
|
|
private @Nullable MeasuredText mMeasuredText;
|
|
|
|
// Following three objects are for avoiding object allocation.
|
|
private final @NonNull TextPaint mCachedPaint = new TextPaint();
|
|
private @Nullable Paint.FontMetricsInt mCachedFm;
|
|
private final @NonNull LineBreakConfig.Builder mLineBreakConfigBuilder =
|
|
new LineBreakConfig.Builder();
|
|
|
|
/**
|
|
* Releases internal buffers.
|
|
* @hide
|
|
*/
|
|
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();
|
|
mMeasuredText = null;
|
|
mBidi = null;
|
|
}
|
|
|
|
/**
|
|
* Returns the length of the paragraph.
|
|
*
|
|
* This is always available.
|
|
* @hide
|
|
*/
|
|
public int getTextLength() {
|
|
return mTextLength;
|
|
}
|
|
|
|
/**
|
|
* Returns the characters to be measured.
|
|
*
|
|
* This is always available.
|
|
* @hide
|
|
*/
|
|
public @NonNull char[] getChars() {
|
|
return mCopiedBuffer;
|
|
}
|
|
|
|
/**
|
|
* Returns the paragraph direction.
|
|
*
|
|
* This is always available.
|
|
* @hide
|
|
*/
|
|
public @Layout.Direction int getParagraphDir() {
|
|
if (ClientFlags.icuBidiMigration()) {
|
|
if (mBidi == null) {
|
|
return Layout.DIR_LEFT_TO_RIGHT;
|
|
}
|
|
return (mBidi.getParaLevel() & 0x01) == 0
|
|
? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT;
|
|
}
|
|
return mParaDir;
|
|
}
|
|
|
|
/**
|
|
* Returns the directions.
|
|
*
|
|
* This is always available.
|
|
* @hide
|
|
*/
|
|
public Directions getDirections(@IntRange(from = 0) int start, // inclusive
|
|
@IntRange(from = 0) int end) { // exclusive
|
|
if (ClientFlags.icuBidiMigration()) {
|
|
// Easy case: mBidi == null means the text is all LTR and no bidi suppot is needed.
|
|
if (mBidi == null) {
|
|
return Layout.DIRS_ALL_LEFT_TO_RIGHT;
|
|
}
|
|
|
|
// Easy case: If the original text only contains single directionality run, the
|
|
// substring is only single run.
|
|
if (start == end) {
|
|
if ((mBidi.getParaLevel() & 0x01) == 0) {
|
|
return Layout.DIRS_ALL_LEFT_TO_RIGHT;
|
|
} else {
|
|
return Layout.DIRS_ALL_RIGHT_TO_LEFT;
|
|
}
|
|
}
|
|
|
|
// Okay, now we need to generate the line instance.
|
|
Bidi bidi = mBidi.createLineBidi(start, end);
|
|
|
|
// Easy case: If the line instance only contains single directionality run, no need
|
|
// to reorder visually.
|
|
if (bidi.getRunCount() == 1) {
|
|
if (bidi.getRunLevel(0) == 1) {
|
|
return Layout.DIRS_ALL_RIGHT_TO_LEFT;
|
|
} else if (bidi.getRunLevel(0) == 0) {
|
|
return Layout.DIRS_ALL_LEFT_TO_RIGHT;
|
|
} else {
|
|
return new Directions(new int[] {
|
|
0, bidi.getRunLevel(0) << Layout.RUN_LEVEL_SHIFT | (end - start)});
|
|
}
|
|
}
|
|
|
|
// Reorder directionality run visually.
|
|
byte[] levels = new byte[bidi.getRunCount()];
|
|
for (int i = 0; i < bidi.getRunCount(); ++i) {
|
|
levels[i] = (byte) bidi.getRunLevel(i);
|
|
}
|
|
int[] visualOrders = Bidi.reorderVisual(levels);
|
|
|
|
int[] dirs = new int[bidi.getRunCount() * 2];
|
|
for (int i = 0; i < bidi.getRunCount(); ++i) {
|
|
int vIndex;
|
|
if ((mBidi.getBaseLevel() & 0x01) == 1) {
|
|
// For the historical reasons, if the base directionality is RTL, the Android
|
|
// draws from the right, i.e. the visually reordered run needs to be reversed.
|
|
vIndex = visualOrders[bidi.getRunCount() - i - 1];
|
|
} else {
|
|
vIndex = visualOrders[i];
|
|
}
|
|
|
|
// Special packing of dire
|
|
dirs[i * 2] = bidi.getRunStart(vIndex);
|
|
dirs[i * 2 + 1] = bidi.getRunLevel(vIndex) << Layout.RUN_LEVEL_SHIFT
|
|
| (bidi.getRunLimit(vIndex) - dirs[i * 2]);
|
|
}
|
|
|
|
return new Directions(dirs);
|
|
}
|
|
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 MeasuredParagraph is computed with buildForMeasurement.
|
|
* Returns 0 in other cases.
|
|
* @hide
|
|
*/
|
|
public @FloatRange(from = 0.0f) float getWholeWidth() {
|
|
return mWholeWidth;
|
|
}
|
|
|
|
/**
|
|
* Returns the individual character's width.
|
|
*
|
|
* This is available only if the MeasuredParagraph is computed with buildForMeasurement.
|
|
* Returns empty array in other cases.
|
|
* @hide
|
|
*/
|
|
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 MeasuredParagraph is computed with buildForStaticLayout.
|
|
* Returns empty array in other cases.
|
|
* @hide
|
|
*/
|
|
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 MeasuredParagraph is computed with buildForStaticLayout.
|
|
* Returns empty array in other cases.
|
|
* @hide
|
|
*/
|
|
public @NonNull IntArray getFontMetrics() {
|
|
return mFontMetrics;
|
|
}
|
|
|
|
/**
|
|
* Returns the native ptr of the MeasuredParagraph.
|
|
*
|
|
* This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
|
|
* Returns null in other cases.
|
|
* @hide
|
|
*/
|
|
public MeasuredText getMeasuredText() {
|
|
return mMeasuredText;
|
|
}
|
|
|
|
/**
|
|
* Returns the width of the given range.
|
|
*
|
|
* This is not available if the MeasuredParagraph is computed with buildForBidi.
|
|
* Returns 0 if the MeasuredParagraph is computed with buildForBidi.
|
|
*
|
|
* @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
|
|
* @hide
|
|
*/
|
|
public float getWidth(int start, int end) {
|
|
if (mMeasuredText == null) {
|
|
// We have result in Java.
|
|
final float[] widths = mWidths.getRawArray();
|
|
float r = 0.0f;
|
|
for (int i = start; i < end; ++i) {
|
|
r += widths[i];
|
|
}
|
|
return r;
|
|
} else {
|
|
// We have result in native.
|
|
return mMeasuredText.getWidth(start, end);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves the bounding rectangle that encloses all of the characters, with an implied origin
|
|
* at (0, 0).
|
|
*
|
|
* This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
|
|
* @hide
|
|
*/
|
|
public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
|
|
@NonNull Rect bounds) {
|
|
mMeasuredText.getBounds(start, end, bounds);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the font metrics for the given range.
|
|
*
|
|
* This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
|
|
* @hide
|
|
*/
|
|
public void getFontMetricsInt(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
|
|
@NonNull Paint.FontMetricsInt fmi) {
|
|
mMeasuredText.getFontMetricsInt(start, end, fmi);
|
|
}
|
|
|
|
/**
|
|
* Returns a width of the character at the offset.
|
|
*
|
|
* This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
|
|
* @hide
|
|
*/
|
|
public float getCharWidthAt(@IntRange(from = 0) int offset) {
|
|
return mMeasuredText.getCharWidthAt(offset);
|
|
}
|
|
|
|
/**
|
|
* Generates new MeasuredParagraph 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 MeasuredParagraph if you want to recycle it.
|
|
*
|
|
* @return measured text
|
|
* @hide
|
|
*/
|
|
public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
|
|
@IntRange(from = 0) int start,
|
|
@IntRange(from = 0) int end,
|
|
@NonNull TextDirectionHeuristic textDir,
|
|
@Nullable MeasuredParagraph recycle) {
|
|
final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
|
|
mt.resetAndAnalyzeBidi(text, start, end, textDir);
|
|
return mt;
|
|
}
|
|
|
|
/**
|
|
* Generates new MeasuredParagraph 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 MeasuredParagraph if you want to recycle it.
|
|
*
|
|
* @return measured text
|
|
* @hide
|
|
*/
|
|
public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
|
|
@NonNull CharSequence text,
|
|
@IntRange(from = 0) int start,
|
|
@IntRange(from = 0) int end,
|
|
@NonNull TextDirectionHeuristic textDir,
|
|
@Nullable MeasuredParagraph recycle) {
|
|
final MeasuredParagraph 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 /* lineBreakConfig */, null /* spans */, null /* lbcSpans */,
|
|
start, end, null /* native builder ptr */, null);
|
|
} else {
|
|
// There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
|
|
int spanEnd;
|
|
for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
|
|
int maSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
|
|
MetricAffectingSpan.class);
|
|
int lbcSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
|
|
LineBreakConfigSpan.class);
|
|
spanEnd = Math.min(maSpanEnd, lbcSpanEnd);
|
|
MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
|
|
MetricAffectingSpan.class);
|
|
LineBreakConfigSpan[] lbcSpans = mt.mSpanned.getSpans(spanStart, spanEnd,
|
|
LineBreakConfigSpan.class);
|
|
spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
|
|
lbcSpans = TextUtils.removeEmptySpans(lbcSpans, mt.mSpanned,
|
|
LineBreakConfigSpan.class);
|
|
mt.applyMetricsAffectingSpan(
|
|
paint, null /* line break config */, spans, lbcSpans, spanStart, spanEnd,
|
|
null /* native builder ptr */, null);
|
|
}
|
|
}
|
|
return mt;
|
|
}
|
|
|
|
/**
|
|
* A test interface for observing the style run calculation.
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
@FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
|
|
public interface StyleRunCallback {
|
|
/**
|
|
* Called when a single style run is identified.
|
|
*/
|
|
@FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
|
|
void onAppendStyleRun(@NonNull Paint paint,
|
|
@Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length,
|
|
boolean isRtl);
|
|
|
|
/**
|
|
* Called when a single replacement run is identified.
|
|
*/
|
|
@FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
|
|
void onAppendReplacementRun(@NonNull Paint paint,
|
|
@IntRange(from = 0) int length, @Px @FloatRange(from = 0) float width);
|
|
}
|
|
|
|
/**
|
|
* Generates new MeasuredParagraph 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 lineBreakConfig the line break configuration for text wrapping.
|
|
* @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 hyphenationMode a hyphenation mode
|
|
* @param computeLayout true if need to compute full layout, otherwise false.
|
|
* @param hint pass if you already have measured paragraph.
|
|
* @param recycle pass existing MeasuredParagraph if you want to recycle it.
|
|
*
|
|
* @return measured text
|
|
* @hide
|
|
*/
|
|
public static @NonNull MeasuredParagraph buildForStaticLayout(
|
|
@NonNull TextPaint paint,
|
|
@Nullable LineBreakConfig lineBreakConfig,
|
|
@NonNull CharSequence text,
|
|
@IntRange(from = 0) int start,
|
|
@IntRange(from = 0) int end,
|
|
@NonNull TextDirectionHeuristic textDir,
|
|
int hyphenationMode,
|
|
boolean computeLayout,
|
|
boolean computeBounds,
|
|
@Nullable MeasuredParagraph hint,
|
|
@Nullable MeasuredParagraph recycle) {
|
|
return buildForStaticLayoutInternal(paint, lineBreakConfig, text, start, end, textDir,
|
|
hyphenationMode, computeLayout, computeBounds, hint, recycle, null);
|
|
}
|
|
|
|
/**
|
|
* Generates new MeasuredParagraph 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 lineBreakConfig the line break configuration for text wrapping.
|
|
* @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 hyphenationMode a hyphenation mode
|
|
* @param computeLayout true if need to compute full layout, otherwise false.
|
|
*
|
|
* @return measured text
|
|
* @hide
|
|
*/
|
|
@SuppressLint("ExecutorRegistration")
|
|
@TestApi
|
|
@NonNull
|
|
@FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
|
|
public static MeasuredParagraph buildForStaticLayoutTest(
|
|
@NonNull TextPaint paint,
|
|
@Nullable LineBreakConfig lineBreakConfig,
|
|
@NonNull CharSequence text,
|
|
@IntRange(from = 0) int start,
|
|
@IntRange(from = 0) int end,
|
|
@NonNull TextDirectionHeuristic textDir,
|
|
int hyphenationMode,
|
|
boolean computeLayout,
|
|
@Nullable StyleRunCallback testCallback) {
|
|
return buildForStaticLayoutInternal(paint, lineBreakConfig, text, start, end, textDir,
|
|
hyphenationMode, computeLayout, false, null, null, testCallback);
|
|
}
|
|
|
|
private static @NonNull MeasuredParagraph buildForStaticLayoutInternal(
|
|
@NonNull TextPaint paint,
|
|
@Nullable LineBreakConfig lineBreakConfig,
|
|
@NonNull CharSequence text,
|
|
@IntRange(from = 0) int start,
|
|
@IntRange(from = 0) int end,
|
|
@NonNull TextDirectionHeuristic textDir,
|
|
int hyphenationMode,
|
|
boolean computeLayout,
|
|
boolean computeBounds,
|
|
@Nullable MeasuredParagraph hint,
|
|
@Nullable MeasuredParagraph recycle,
|
|
@Nullable StyleRunCallback testCallback) {
|
|
final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
|
|
mt.resetAndAnalyzeBidi(text, start, end, textDir);
|
|
final MeasuredText.Builder builder;
|
|
if (hint == null) {
|
|
builder = new MeasuredText.Builder(mt.mCopiedBuffer)
|
|
.setComputeHyphenation(hyphenationMode)
|
|
.setComputeLayout(computeLayout)
|
|
.setComputeBounds(computeBounds);
|
|
} else {
|
|
builder = new MeasuredText.Builder(hint.mMeasuredText);
|
|
}
|
|
if (mt.mTextLength == 0) {
|
|
// Need to build empty native measured text for StaticLayout.
|
|
// TODO: Stop creating empty measured text for empty lines.
|
|
mt.mMeasuredText = builder.build();
|
|
} else {
|
|
if (mt.mSpanned == null) {
|
|
// No style change by MetricsAffectingSpan. Just measure all text.
|
|
mt.applyMetricsAffectingSpan(paint, lineBreakConfig, null /* spans */, null,
|
|
start, end, builder, testCallback);
|
|
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) {
|
|
int maSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
|
|
MetricAffectingSpan.class);
|
|
int lbcSpanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
|
|
LineBreakConfigSpan.class);
|
|
spanEnd = Math.min(maSpanEnd, lbcSpanEnd);
|
|
MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
|
|
MetricAffectingSpan.class);
|
|
LineBreakConfigSpan[] lbcSpans = mt.mSpanned.getSpans(spanStart, spanEnd,
|
|
LineBreakConfigSpan.class);
|
|
spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
|
|
MetricAffectingSpan.class);
|
|
lbcSpans = TextUtils.removeEmptySpans(lbcSpans, mt.mSpanned,
|
|
LineBreakConfigSpan.class);
|
|
mt.applyMetricsAffectingSpan(paint, lineBreakConfig, spans, lbcSpans, spanStart,
|
|
spanEnd, builder, testCallback);
|
|
mt.mSpanEndCache.append(spanEnd);
|
|
}
|
|
}
|
|
mt.mMeasuredText = builder.build();
|
|
}
|
|
|
|
return mt;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
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;
|
|
mTextStart = start;
|
|
mTextLength = end - start;
|
|
|
|
if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
|
|
mCopiedBuffer = new char[mTextLength];
|
|
}
|
|
TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
|
|
|
|
// Replace characters associated with ReplacementSpan to U+FFFC.
|
|
if (mSpanned != null) {
|
|
ReplacementSpan[] spans = mSpanned.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)
|
|
if (startInPara < 0) startInPara = 0;
|
|
if (endInPara > mTextLength) endInPara = mTextLength;
|
|
Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
|
|
}
|
|
}
|
|
|
|
if (ClientFlags.icuBidiMigration()) {
|
|
if ((textDir == TextDirectionHeuristics.LTR
|
|
|| textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR
|
|
|| textDir == TextDirectionHeuristics.ANYRTL_LTR)
|
|
&& TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
|
|
mLevels.clear();
|
|
mLtrWithoutBidi = true;
|
|
return;
|
|
}
|
|
final int bidiRequest;
|
|
if (textDir == TextDirectionHeuristics.LTR) {
|
|
bidiRequest = Bidi.LTR;
|
|
} else if (textDir == TextDirectionHeuristics.RTL) {
|
|
bidiRequest = Bidi.RTL;
|
|
} else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
|
|
bidiRequest = Bidi.LEVEL_DEFAULT_LTR;
|
|
} else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
|
|
bidiRequest = Bidi.LEVEL_DEFAULT_RTL;
|
|
} else {
|
|
final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
|
|
bidiRequest = isRtl ? Bidi.RTL : Bidi.LTR;
|
|
}
|
|
mBidi = new Bidi(mCopiedBuffer, 0, null, 0, mCopiedBuffer.length, bidiRequest);
|
|
|
|
if (mCopiedBuffer.length > 0
|
|
&& mBidi.getParagraphIndex(mCopiedBuffer.length - 1) != 0) {
|
|
// Historically, the MeasuredParagraph does not treat the CR letters as paragraph
|
|
// breaker but ICU BiDi treats it as paragraph breaker. In the MeasureParagraph,
|
|
// the given range always represents a single paragraph, so if the BiDi object has
|
|
// multiple paragraph, it should contains a CR letters in the text. Using CR is not
|
|
// common in Android and also it should not penalize the easy case, e.g. all LTR,
|
|
// check the paragraph count here and replace the CR letters and re-calculate
|
|
// BiDi again.
|
|
for (int i = 0; i < mTextLength; ++i) {
|
|
if (Character.isSurrogate(mCopiedBuffer[i])) {
|
|
// All block separators are in BMP.
|
|
continue;
|
|
}
|
|
if (UCharacter.getDirection(mCopiedBuffer[i])
|
|
== UCharacterDirection.BLOCK_SEPARATOR) {
|
|
mCopiedBuffer[i] = OBJECT_REPLACEMENT_CHARACTER;
|
|
}
|
|
}
|
|
mBidi = new Bidi(mCopiedBuffer, 0, null, 0, mCopiedBuffer.length, bidiRequest);
|
|
}
|
|
mLevels.resize(mTextLength);
|
|
byte[] rawArray = mLevels.getRawArray();
|
|
for (int i = 0; i < mTextLength; ++i) {
|
|
rawArray[i] = mBidi.getLevelAt(i);
|
|
}
|
|
mLtrWithoutBidi = false;
|
|
return;
|
|
}
|
|
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;
|
|
} else {
|
|
final int bidiRequest;
|
|
if (textDir == TextDirectionHeuristics.LTR) {
|
|
bidiRequest = Layout.DIR_REQUEST_LTR;
|
|
} else if (textDir == TextDirectionHeuristics.RTL) {
|
|
bidiRequest = Layout.DIR_REQUEST_RTL;
|
|
} else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
|
|
bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
|
|
} else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
|
|
bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
|
|
} else {
|
|
final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
|
|
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
|
|
@NonNull TextPaint paint,
|
|
@Nullable MeasuredText.Builder builder,
|
|
@Nullable StyleRunCallback testCallback) {
|
|
// 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(
|
|
paint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
|
|
if (builder == null) {
|
|
// 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 {
|
|
builder.appendReplacementRun(paint, end - start, width);
|
|
}
|
|
if (testCallback != null) {
|
|
testCallback.onAppendReplacementRun(paint, end - start, width);
|
|
}
|
|
}
|
|
|
|
private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
|
|
@IntRange(from = 0) int end, // exclusive, in copied buffer
|
|
@NonNull TextPaint paint,
|
|
@Nullable LineBreakConfig config,
|
|
@Nullable MeasuredText.Builder builder,
|
|
@Nullable StyleRunCallback testCallback) {
|
|
|
|
if (mLtrWithoutBidi) {
|
|
// If the whole text is LTR direction, just apply whole region.
|
|
if (builder == null) {
|
|
// For the compatibility reasons, the letter spacing should not be dropped at the
|
|
// left and right edge.
|
|
int oldFlag = paint.getFlags();
|
|
paint.setFlags(paint.getFlags()
|
|
| (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE));
|
|
try {
|
|
mWholeWidth += paint.getTextRunAdvances(
|
|
mCopiedBuffer, start, end - start, start, end - start,
|
|
false /* isRtl */, mWidths.getRawArray(), start);
|
|
} finally {
|
|
paint.setFlags(oldFlag);
|
|
}
|
|
} else {
|
|
builder.appendStyleRun(paint, config, end - start, false /* isRtl */);
|
|
}
|
|
if (testCallback != null) {
|
|
testCallback.onAppendStyleRun(paint, config, end - start, false);
|
|
}
|
|
} 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 (builder == null) {
|
|
final int levelLength = levelEnd - levelStart;
|
|
int oldFlag = paint.getFlags();
|
|
paint.setFlags(paint.getFlags()
|
|
| (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE));
|
|
try {
|
|
mWholeWidth += paint.getTextRunAdvances(
|
|
mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
|
|
isRtl, mWidths.getRawArray(), levelStart);
|
|
} finally {
|
|
paint.setFlags(oldFlag);
|
|
}
|
|
} else {
|
|
builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl);
|
|
}
|
|
if (testCallback != null) {
|
|
testCallback.onAppendStyleRun(paint, config, levelEnd - levelStart, isRtl);
|
|
}
|
|
if (levelEnd == end) {
|
|
break;
|
|
}
|
|
levelStart = levelEnd;
|
|
level = mLevels.get(levelEnd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void applyMetricsAffectingSpan(
|
|
@NonNull TextPaint paint,
|
|
@Nullable LineBreakConfig lineBreakConfig,
|
|
@Nullable MetricAffectingSpan[] spans,
|
|
@Nullable LineBreakConfigSpan[] lbcSpans,
|
|
@IntRange(from = 0) int start, // inclusive, in original text buffer
|
|
@IntRange(from = 0) int end, // exclusive, in original text buffer
|
|
@Nullable MeasuredText.Builder builder,
|
|
@Nullable StyleRunCallback testCallback) {
|
|
mCachedPaint.set(paint);
|
|
// XXX paint should not have a baseline shift, but...
|
|
mCachedPaint.baselineShift = 0;
|
|
|
|
final boolean needFontMetrics = builder != null;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (lbcSpans != null) {
|
|
mLineBreakConfigBuilder.reset(lineBreakConfig);
|
|
for (LineBreakConfigSpan lbcSpan : lbcSpans) {
|
|
mLineBreakConfigBuilder.merge(lbcSpan.getLineBreakConfig());
|
|
}
|
|
lineBreakConfig = mLineBreakConfigBuilder.build();
|
|
}
|
|
|
|
final int startInCopiedBuffer = start - mTextStart;
|
|
final int endInCopiedBuffer = end - mTextStart;
|
|
|
|
if (builder != null) {
|
|
mCachedPaint.getFontMetricsInt(mCachedFm);
|
|
}
|
|
|
|
if (replacement != null) {
|
|
applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, mCachedPaint,
|
|
builder, testCallback);
|
|
} else {
|
|
applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, mCachedPaint,
|
|
lineBreakConfig, builder, testCallback);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum index that the accumulated width not exceeds the width.
|
|
*
|
|
* If forward=false is passed, returns the minimum index from the end instead.
|
|
*
|
|
* This only works if the MeasuredParagraph is computed with buildForMeasurement.
|
|
* Undefined behavior in other case.
|
|
*/
|
|
@IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
|
|
float[] w = mWidths.getRawArray();
|
|
if (forwards) {
|
|
int i = 0;
|
|
while (i < limit) {
|
|
width -= w[i];
|
|
if (width < 0.0f) break;
|
|
i++;
|
|
}
|
|
while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
|
|
return i;
|
|
} else {
|
|
int i = limit - 1;
|
|
while (i >= 0) {
|
|
width -= w[i];
|
|
if (width < 0.0f) break;
|
|
i--;
|
|
}
|
|
while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
|
|
i++;
|
|
}
|
|
return limit - i - 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the length of the substring.
|
|
*
|
|
* This only works if the MeasuredParagraph is computed with buildForMeasurement.
|
|
* Undefined behavior in other case.
|
|
*/
|
|
@FloatRange(from = 0.0f) float measure(int start, int limit) {
|
|
float width = 0;
|
|
float[] w = mWidths.getRawArray();
|
|
for (int i = start; i < limit; ++i) {
|
|
width += w[i];
|
|
}
|
|
return width;
|
|
}
|
|
|
|
/**
|
|
* This only works if the MeasuredParagraph is computed with buildForStaticLayout.
|
|
* @hide
|
|
*/
|
|
public @IntRange(from = 0) int getMemoryUsage() {
|
|
return mMeasuredText.getMemoryUsage();
|
|
}
|
|
}
|