1920 lines
83 KiB
Java
1920 lines
83 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 android.annotation.IntRange;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Paint.FontMetricsInt;
|
|
import android.graphics.Rect;
|
|
import android.graphics.RectF;
|
|
import android.graphics.text.PositionedGlyphs;
|
|
import android.graphics.text.TextRunShaper;
|
|
import android.os.Build;
|
|
import android.text.Layout.Directions;
|
|
import android.text.Layout.TabStops;
|
|
import android.text.style.CharacterStyle;
|
|
import android.text.style.MetricAffectingSpan;
|
|
import android.text.style.ReplacementSpan;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.util.ArrayUtils;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
/**
|
|
* Represents a line of styled text, for measuring in visual order and
|
|
* for rendering.
|
|
*
|
|
* <p>Get a new instance using obtain(), and when finished with it, return it
|
|
* to the pool using recycle().
|
|
*
|
|
* <p>Call set to prepare the instance for use, then either draw, measure,
|
|
* metrics, or caretToLeftRightOf.
|
|
*
|
|
* @hide
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public class TextLine {
|
|
private static final boolean DEBUG = false;
|
|
|
|
private static final char TAB_CHAR = '\t';
|
|
|
|
private TextPaint mPaint;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private CharSequence mText;
|
|
private int mStart;
|
|
private int mLen;
|
|
private int mDir;
|
|
private Directions mDirections;
|
|
private boolean mHasTabs;
|
|
private TabStops mTabs;
|
|
private char[] mChars;
|
|
private boolean mCharsValid;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
private Spanned mSpanned;
|
|
private PrecomputedText mComputed;
|
|
private RectF mTmpRectForMeasure;
|
|
private RectF mTmpRectForPaintAPI;
|
|
private Rect mTmpRectForPrecompute;
|
|
|
|
// Recycling object for Paint APIs. Do not use outside getRunAdvances method.
|
|
private Paint.RunInfo mRunInfo;
|
|
|
|
public static final class LineInfo {
|
|
private int mClusterCount;
|
|
|
|
public int getClusterCount() {
|
|
return mClusterCount;
|
|
}
|
|
|
|
public void setClusterCount(int clusterCount) {
|
|
mClusterCount = clusterCount;
|
|
}
|
|
};
|
|
|
|
private boolean mUseFallbackExtent = false;
|
|
|
|
// The start and end of a potentially existing ellipsis on this text line.
|
|
// We use them to filter out replacement and metric affecting spans on ellipsized away chars.
|
|
private int mEllipsisStart;
|
|
private int mEllipsisEnd;
|
|
|
|
// Additional width of whitespace for justification. This value is per whitespace, thus
|
|
// the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces).
|
|
private float mAddedWordSpacingInPx;
|
|
private float mAddedLetterSpacingInPx;
|
|
private boolean mIsJustifying;
|
|
|
|
@VisibleForTesting
|
|
public float getAddedWordSpacingInPx() {
|
|
return mAddedWordSpacingInPx;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public float getAddedLetterSpacingInPx() {
|
|
return mAddedLetterSpacingInPx;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public boolean isJustifying() {
|
|
return mIsJustifying;
|
|
}
|
|
|
|
private final TextPaint mWorkPaint = new TextPaint();
|
|
private final TextPaint mActivePaint = new TextPaint();
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =
|
|
new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =
|
|
new SpanSet<CharacterStyle>(CharacterStyle.class);
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =
|
|
new SpanSet<ReplacementSpan>(ReplacementSpan.class);
|
|
|
|
private final DecorationInfo mDecorationInfo = new DecorationInfo();
|
|
private final ArrayList<DecorationInfo> mDecorations = new ArrayList<>();
|
|
|
|
/** Not allowed to access. If it's for memory leak workaround, it was already fixed M. */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
|
|
private static final TextLine[] sCached = new TextLine[3];
|
|
|
|
/**
|
|
* Returns a new TextLine from the shared pool.
|
|
*
|
|
* @return an uninitialized TextLine
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
@UnsupportedAppUsage
|
|
public static TextLine obtain() {
|
|
TextLine tl;
|
|
synchronized (sCached) {
|
|
for (int i = sCached.length; --i >= 0;) {
|
|
if (sCached[i] != null) {
|
|
tl = sCached[i];
|
|
sCached[i] = null;
|
|
return tl;
|
|
}
|
|
}
|
|
}
|
|
tl = new TextLine();
|
|
if (DEBUG) {
|
|
Log.v("TLINE", "new: " + tl);
|
|
}
|
|
return tl;
|
|
}
|
|
|
|
/**
|
|
* Puts a TextLine back into the shared pool. Do not use this TextLine once
|
|
* it has been returned.
|
|
* @param tl the textLine
|
|
* @return null, as a convenience from clearing references to the provided
|
|
* TextLine
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public static TextLine recycle(TextLine tl) {
|
|
tl.mText = null;
|
|
tl.mPaint = null;
|
|
tl.mDirections = null;
|
|
tl.mSpanned = null;
|
|
tl.mTabs = null;
|
|
tl.mChars = null;
|
|
tl.mComputed = null;
|
|
tl.mUseFallbackExtent = false;
|
|
|
|
tl.mMetricAffectingSpanSpanSet.recycle();
|
|
tl.mCharacterStyleSpanSet.recycle();
|
|
tl.mReplacementSpanSpanSet.recycle();
|
|
|
|
synchronized(sCached) {
|
|
for (int i = 0; i < sCached.length; ++i) {
|
|
if (sCached[i] == null) {
|
|
sCached[i] = tl;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Initializes a TextLine and prepares it for use.
|
|
*
|
|
* @param paint the base paint for the line
|
|
* @param text the text, can be Styled
|
|
* @param start the start of the line relative to the text
|
|
* @param limit the limit of the line relative to the text
|
|
* @param dir the paragraph direction of this line
|
|
* @param directions the directions information of this line
|
|
* @param hasTabs true if the line might contain tabs
|
|
* @param tabStops the tabStops. Can be null
|
|
* @param ellipsisStart the start of the ellipsis relative to the line
|
|
* @param ellipsisEnd the end of the ellipsis relative to the line. When there
|
|
* is no ellipsis, this should be equal to ellipsisStart.
|
|
* @param useFallbackLineSpacing true for enabling fallback line spacing. false for disabling
|
|
* fallback line spacing.
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,
|
|
Directions directions, boolean hasTabs, TabStops tabStops,
|
|
int ellipsisStart, int ellipsisEnd, boolean useFallbackLineSpacing) {
|
|
mPaint = paint;
|
|
mText = text;
|
|
mStart = start;
|
|
mLen = limit - start;
|
|
mDir = dir;
|
|
mDirections = directions;
|
|
mUseFallbackExtent = useFallbackLineSpacing;
|
|
if (mDirections == null) {
|
|
throw new IllegalArgumentException("Directions cannot be null");
|
|
}
|
|
mHasTabs = hasTabs;
|
|
mSpanned = null;
|
|
|
|
boolean hasReplacement = false;
|
|
if (text instanceof Spanned) {
|
|
mSpanned = (Spanned) text;
|
|
mReplacementSpanSpanSet.init(mSpanned, start, limit);
|
|
hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
|
|
}
|
|
|
|
mComputed = null;
|
|
if (text instanceof PrecomputedText) {
|
|
// Here, no need to check line break strategy or hyphenation frequency since there is no
|
|
// line break concept here.
|
|
mComputed = (PrecomputedText) text;
|
|
if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {
|
|
mComputed = null;
|
|
}
|
|
}
|
|
|
|
mCharsValid = hasReplacement;
|
|
|
|
if (mCharsValid) {
|
|
if (mChars == null || mChars.length < mLen) {
|
|
mChars = ArrayUtils.newUnpaddedCharArray(mLen);
|
|
}
|
|
TextUtils.getChars(text, start, limit, mChars, 0);
|
|
if (hasReplacement) {
|
|
// Handle these all at once so we don't have to do it as we go.
|
|
// Replace the first character of each replacement run with the
|
|
// object-replacement character and the remainder with zero width
|
|
// non-break space aka BOM. Cursor movement code skips these
|
|
// zero-width characters.
|
|
char[] chars = mChars;
|
|
for (int i = start, inext; i < limit; i = inext) {
|
|
inext = mReplacementSpanSpanSet.getNextTransition(i, limit);
|
|
if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)
|
|
&& (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {
|
|
// transition into a span
|
|
chars[i - start] = '\ufffc';
|
|
for (int j = i - start + 1, e = inext - start; j < e; ++j) {
|
|
chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mTabs = tabStops;
|
|
mAddedWordSpacingInPx = 0;
|
|
mIsJustifying = false;
|
|
|
|
mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;
|
|
mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0;
|
|
}
|
|
|
|
private char charAt(int i) {
|
|
return mCharsValid ? mChars[i] : mText.charAt(i + mStart);
|
|
}
|
|
|
|
/**
|
|
* Justify the line to the given width.
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) {
|
|
int end = mLen;
|
|
while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) {
|
|
end--;
|
|
}
|
|
if (justificationMode == Layout.JUSTIFICATION_MODE_INTER_WORD) {
|
|
float width = Math.abs(measure(end, false, null, null, null));
|
|
final int spaces = countStretchableSpaces(0, end);
|
|
if (spaces == 0) {
|
|
// There are no stretchable spaces, so we can't help the justification by adding any
|
|
// width.
|
|
return;
|
|
}
|
|
mAddedWordSpacingInPx = (justifyWidth - width) / spaces;
|
|
mAddedLetterSpacingInPx = 0;
|
|
} else { // justificationMode == Layout.JUSTIFICATION_MODE_LETTER_SPACING
|
|
LineInfo lineInfo = new LineInfo();
|
|
float width = Math.abs(measure(end, false, null, null, lineInfo));
|
|
|
|
int lettersCount = lineInfo.getClusterCount();
|
|
if (lettersCount < 2) {
|
|
return;
|
|
}
|
|
mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
|
|
if (mAddedLetterSpacingInPx > 0.03) {
|
|
// If the letter spacing is more than 0.03em, the ligatures are automatically
|
|
// disabled, so re-calculate everything without ligatures.
|
|
final String oldFontFeatures = mPaint.getFontFeatureSettings();
|
|
mPaint.setFontFeatureSettings(oldFontFeatures + ", \"liga\" off, \"cliga\" off");
|
|
width = Math.abs(measure(end, false, null, null, lineInfo));
|
|
lettersCount = lineInfo.getClusterCount();
|
|
mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1);
|
|
mPaint.setFontFeatureSettings(oldFontFeatures);
|
|
}
|
|
mAddedWordSpacingInPx = 0;
|
|
}
|
|
mIsJustifying = true;
|
|
}
|
|
|
|
/**
|
|
* Returns the run flag of at the given BiDi run.
|
|
*
|
|
* @param bidiRunIndex a BiDi run index.
|
|
* @return a run flag of the given BiDi run.
|
|
*/
|
|
@VisibleForTesting
|
|
public static int calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection) {
|
|
if (bidiRunCount == 1) {
|
|
// Easy case. If there is only single run, it is most left and most right run.
|
|
return Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
|
|
}
|
|
if (bidiRunIndex != 0 && bidiRunIndex != (bidiRunCount - 1)) {
|
|
// Easy case. If the given run is the middle of the line, it is not the most left or
|
|
// the most right run.
|
|
return 0;
|
|
}
|
|
|
|
int runFlag = 0;
|
|
// For the historical reasons, the BiDi implementation of Android works differently
|
|
// from the Java BiDi APIs. The mDirections holds the BiDi runs in visual order, but
|
|
// it is reversed order if the paragraph direction is RTL. So, the first BiDi run of
|
|
// mDirections is located the most left of the line if the paragraph direction is LTR.
|
|
// If the paragraph direction is RTL, the first BiDi run is located the most right of
|
|
// the line.
|
|
if (bidiRunIndex == 0) {
|
|
if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) {
|
|
runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE;
|
|
} else {
|
|
runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
|
|
}
|
|
}
|
|
if (bidiRunIndex == (bidiRunCount - 1)) {
|
|
if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) {
|
|
runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
|
|
} else {
|
|
runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE;
|
|
}
|
|
}
|
|
return runFlag;
|
|
}
|
|
|
|
/**
|
|
* Resolve the runFlag for the inline span range.
|
|
*
|
|
* @param runFlag the runFlag of the current BiDi run.
|
|
* @param isRtlRun true for RTL run, false for LTR run.
|
|
* @param runStart the inclusive BiDi run start offset.
|
|
* @param runEnd the exclusive BiDi run end offset.
|
|
* @param spanStart the inclusive span start offset.
|
|
* @param spanEnd the exclusive span end offset.
|
|
* @return the resolved runFlag.
|
|
*/
|
|
@VisibleForTesting
|
|
public static int resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart,
|
|
int runEnd, int spanStart, int spanEnd) {
|
|
if (runFlag == 0) {
|
|
// Easy case. If the run is in the middle of the line, any inline span is also in the
|
|
// middle of the line.
|
|
return 0;
|
|
}
|
|
int localRunFlag = runFlag;
|
|
if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) != 0) {
|
|
if (isRtlRun) {
|
|
if (spanEnd != runEnd) {
|
|
// In the RTL context, the last run is the most left run.
|
|
localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE;
|
|
}
|
|
} else { // LTR
|
|
if (spanStart != runStart) {
|
|
// In the LTR context, the first run is the most left run.
|
|
localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE;
|
|
}
|
|
}
|
|
}
|
|
if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) != 0) {
|
|
if (isRtlRun) {
|
|
if (spanStart != runStart) {
|
|
// In the RTL context, the start of the run is the most right run.
|
|
localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
|
|
}
|
|
} else { // LTR
|
|
if (spanEnd != runEnd) {
|
|
// In the LTR context, the last run is the most right position.
|
|
localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE;
|
|
}
|
|
}
|
|
}
|
|
return localRunFlag;
|
|
}
|
|
|
|
/**
|
|
* Renders the TextLine.
|
|
*
|
|
* @param c the canvas to render on
|
|
* @param x the leading margin position
|
|
* @param top the top of the line
|
|
* @param y the baseline
|
|
* @param bottom the bottom of the line
|
|
*/
|
|
void draw(Canvas c, float x, int top, int y, int bottom) {
|
|
float h = 0;
|
|
final int runCount = mDirections.getRunCount();
|
|
for (int runIndex = 0; runIndex < runCount; runIndex++) {
|
|
final int runStart = mDirections.getRunStart(runIndex);
|
|
if (runStart > mLen) break;
|
|
final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
|
|
final boolean runIsRtl = mDirections.isRunRtl(runIndex);
|
|
|
|
final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
|
|
|
|
int segStart = runStart;
|
|
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
|
|
if (j == runLimit || charAt(j) == TAB_CHAR) {
|
|
h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
|
|
runIndex != (runCount - 1) || j != mLen, runFlag);
|
|
|
|
if (j != runLimit) { // charAt(j) == TAB_CHAR
|
|
h = mDir * nextTab(h * mDir);
|
|
}
|
|
segStart = j + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns metrics information for the entire line.
|
|
*
|
|
* @param fmi receives font metrics information, can be null
|
|
* @param drawBounds output parameter for drawing bounding box. optional.
|
|
* @param returnDrawWidth true for returning width of the bounding box, false for returning
|
|
* total advances.
|
|
* @param lineInfo an optional output parameter for filling line information.
|
|
* @return the signed width of the line
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
|
|
@Nullable LineInfo lineInfo) {
|
|
if (returnDrawWidth) {
|
|
if (drawBounds == null) {
|
|
if (mTmpRectForMeasure == null) {
|
|
mTmpRectForMeasure = new RectF();
|
|
}
|
|
drawBounds = mTmpRectForMeasure;
|
|
}
|
|
drawBounds.setEmpty();
|
|
float w = measure(mLen, false, fmi, drawBounds, lineInfo);
|
|
float boundsWidth;
|
|
if (w >= 0) {
|
|
boundsWidth = Math.max(drawBounds.right, w) - Math.min(0, drawBounds.left);
|
|
} else {
|
|
boundsWidth = Math.max(drawBounds.right, 0) - Math.min(w, drawBounds.left);
|
|
}
|
|
if (Math.abs(w) > boundsWidth) {
|
|
return w;
|
|
} else {
|
|
// bounds width is always positive but output of measure is signed width.
|
|
// To be able to use bounds width as signed width, use the sign of the width.
|
|
return Math.signum(w) * boundsWidth;
|
|
}
|
|
} else {
|
|
return measure(mLen, false, fmi, drawBounds, lineInfo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shape the TextLine.
|
|
*/
|
|
void shape(TextShaper.GlyphsConsumer consumer) {
|
|
float horizontal = 0;
|
|
float x = 0;
|
|
final int runCount = mDirections.getRunCount();
|
|
for (int runIndex = 0; runIndex < runCount; runIndex++) {
|
|
final int runStart = mDirections.getRunStart(runIndex);
|
|
if (runStart > mLen) break;
|
|
final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
|
|
final boolean runIsRtl = mDirections.isRunRtl(runIndex);
|
|
|
|
final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
|
|
int segStart = runStart;
|
|
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
|
|
if (j == runLimit || charAt(j) == TAB_CHAR) {
|
|
horizontal += shapeRun(consumer, segStart, j, runIsRtl, x + horizontal,
|
|
runIndex != (runCount - 1) || j != mLen, runFlag);
|
|
|
|
if (j != runLimit) { // charAt(j) == TAB_CHAR
|
|
horizontal = mDir * nextTab(horizontal * mDir);
|
|
}
|
|
segStart = j + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the signed graphical offset from the leading margin.
|
|
*
|
|
* Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a
|
|
* character which has LTR BiDi property. On the other hand, RX(e.g. R0, R1, ...) denotes a
|
|
* character which has RTL BiDi property. Assuming all character has 1em width.
|
|
*
|
|
* Example 1: All LTR chars within LTR context
|
|
* Input Text (logical) : L0 L1 L2 L3 L4 L5 L6 L7 L8
|
|
* Input Text (visual) : L0 L1 L2 L3 L4 L5 L6 L7 L8
|
|
* Output(trailing=true) : |--------| (Returns 3em)
|
|
* Output(trailing=false): |--------| (Returns 3em)
|
|
*
|
|
* Example 2: All RTL chars within RTL context.
|
|
* Input Text (logical) : R0 R1 R2 R3 R4 R5 R6 R7 R8
|
|
* Input Text (visual) : R8 R7 R6 R5 R4 R3 R2 R1 R0
|
|
* Output(trailing=true) : |--------| (Returns -3em)
|
|
* Output(trailing=false): |--------| (Returns -3em)
|
|
*
|
|
* Example 3: BiDi chars within LTR context.
|
|
* Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
|
|
* Input Text (visual) : L0 L1 L2 R5 R4 R3 L6 L7 L8
|
|
* Output(trailing=true) : |-----------------| (Returns 6em)
|
|
* Output(trailing=false): |--------| (Returns 3em)
|
|
*
|
|
* Example 4: BiDi chars within RTL context.
|
|
* Input Text (logical) : L0 L1 L2 R3 R4 R5 L6 L7 L8
|
|
* Input Text (visual) : L6 L7 L8 R5 R4 R3 L0 L1 L2
|
|
* Output(trailing=true) : |-----------------| (Returns -6em)
|
|
* Output(trailing=false): |--------| (Returns -3em)
|
|
*
|
|
* @param offset the line-relative character offset, between 0 and the line length, inclusive
|
|
* @param trailing no effect if the offset is not on the BiDi transition offset. If the offset
|
|
* is on the BiDi transition offset and true is passed, the offset is regarded
|
|
* as the edge of the trailing run's edge. If false, the offset is regarded as
|
|
* the edge of the preceding run's edge. See example above.
|
|
* @param fmi receives metrics information about the requested character, can be null
|
|
* @param drawBounds output parameter for drawing bounding box. optional.
|
|
* @param lineInfo an optional output parameter for filling line information.
|
|
* @return the signed graphical offset from the leading margin to the requested character edge.
|
|
* The positive value means the offset is right from the leading edge. The negative
|
|
* value means the offset is left from the leading edge.
|
|
*/
|
|
public float measure(@IntRange(from = 0) int offset, boolean trailing,
|
|
@NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) {
|
|
if (offset > mLen) {
|
|
throw new IndexOutOfBoundsException(
|
|
"offset(" + offset + ") should be less than line limit(" + mLen + ")");
|
|
}
|
|
if (lineInfo != null) {
|
|
lineInfo.setClusterCount(0);
|
|
}
|
|
final int target = trailing ? offset - 1 : offset;
|
|
if (target < 0) {
|
|
return 0;
|
|
}
|
|
|
|
float h = 0;
|
|
final int runCount = mDirections.getRunCount();
|
|
for (int runIndex = 0; runIndex < runCount; runIndex++) {
|
|
final int runStart = mDirections.getRunStart(runIndex);
|
|
if (runStart > mLen) break;
|
|
final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
|
|
final boolean runIsRtl = mDirections.isRunRtl(runIndex);
|
|
final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
|
|
|
|
int segStart = runStart;
|
|
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
|
|
if (j == runLimit || charAt(j) == TAB_CHAR) {
|
|
final boolean targetIsInThisSegment = target >= segStart && target < j;
|
|
final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
|
|
|
|
if (targetIsInThisSegment && sameDirection) {
|
|
return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null,
|
|
0, h, lineInfo, runFlag);
|
|
}
|
|
|
|
final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds,
|
|
null, 0, h, lineInfo, runFlag);
|
|
h += sameDirection ? segmentWidth : -segmentWidth;
|
|
|
|
if (targetIsInThisSegment) {
|
|
return h + measureRun(segStart, offset, j, runIsRtl, null, null, null, 0,
|
|
h, lineInfo, runFlag);
|
|
}
|
|
|
|
if (j != runLimit) { // charAt(j) == TAB_CHAR
|
|
if (offset == j) {
|
|
return h;
|
|
}
|
|
h = mDir * nextTab(h * mDir);
|
|
if (target == j) {
|
|
return h;
|
|
}
|
|
}
|
|
|
|
segStart = j + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return h;
|
|
}
|
|
|
|
/**
|
|
* Return the signed horizontal bounds of the characters in the line.
|
|
*
|
|
* The length of the returned array equals to 2 * mLen. The left bound of the i th character
|
|
* is stored at index 2 * i. And the right bound of the i th character is stored at index
|
|
* (2 * i + 1).
|
|
*
|
|
* Check the following examples. LX(e.g. L0, L1, ...) denotes a character which has LTR BiDi
|
|
* property. On the other hand, RX(e.g. R0, R1, ...) denotes a character which has RTL BiDi
|
|
* property. Assuming all character has 1em width.
|
|
*
|
|
* Example 1: All LTR chars within LTR context
|
|
* Input Text (logical) : L0 L1 L2 L3
|
|
* Input Text (visual) : L0 L1 L2 L3
|
|
* Output : [0em, 1em, 1em, 2em, 2em, 3em, 3em, 4em]
|
|
*
|
|
* Example 2: All RTL chars within RTL context.
|
|
* Input Text (logical) : R0 R1 R2 R3
|
|
* Input Text (visual) : R3 R2 R1 R0
|
|
* Output : [-1em, 0em, -2em, -1em, -3em, -2em, -4em, -3em]
|
|
|
|
*
|
|
* Example 3: BiDi chars within LTR context.
|
|
* Input Text (logical) : L0 L1 R2 R3 L4 L5
|
|
* Input Text (visual) : L0 L1 R3 R2 L4 L5
|
|
* Output : [0em, 1em, 1em, 2em, 3em, 4em, 2em, 3em, 4em, 5em, 5em, 6em]
|
|
|
|
*
|
|
* Example 4: BiDi chars within RTL context.
|
|
* Input Text (logical) : L0 L1 R2 R3 L4 L5
|
|
* Input Text (visual) : L4 L5 R3 R2 L0 L1
|
|
* Output : [-2em, -1em, -1em, 0em, -3em, -2em, -4em, -3em, -6em, -5em, -5em, -4em]
|
|
*
|
|
* @param bounds the array to receive the character bounds data. Its length should be at least
|
|
* 2 times of the line length.
|
|
* @param advances the array to receive the character advance data, nullable. If provided, its
|
|
* length should be equal or larger than the line length.
|
|
*
|
|
* @throws IllegalArgumentException if the given {@code bounds} is null.
|
|
* @throws IndexOutOfBoundsException if the given {@code bounds} or {@code advances} doesn't
|
|
* have enough space to hold the result.
|
|
*/
|
|
public void measureAllBounds(@NonNull float[] bounds, @Nullable float[] advances) {
|
|
if (bounds == null) {
|
|
throw new IllegalArgumentException("bounds can't be null");
|
|
}
|
|
if (bounds.length < 2 * mLen) {
|
|
throw new IndexOutOfBoundsException("bounds doesn't have enough space to receive the "
|
|
+ "result, needed: " + (2 * mLen) + " had: " + bounds.length);
|
|
}
|
|
if (advances == null) {
|
|
advances = new float[mLen];
|
|
}
|
|
if (advances.length < mLen) {
|
|
throw new IndexOutOfBoundsException("advance doesn't have enough space to receive the "
|
|
+ "result, needed: " + mLen + " had: " + advances.length);
|
|
}
|
|
float h = 0;
|
|
final int runCount = mDirections.getRunCount();
|
|
for (int runIndex = 0; runIndex < runCount; runIndex++) {
|
|
final int runStart = mDirections.getRunStart(runIndex);
|
|
if (runStart > mLen) break;
|
|
final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
|
|
final boolean runIsRtl = mDirections.isRunRtl(runIndex);
|
|
final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
|
|
|
|
int segStart = runStart;
|
|
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
|
|
if (j == runLimit || charAt(j) == TAB_CHAR) {
|
|
final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
|
|
final float segmentWidth =
|
|
measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0,
|
|
null, runFlag);
|
|
|
|
final float oldh = h;
|
|
h += sameDirection ? segmentWidth : -segmentWidth;
|
|
float currh = sameDirection ? oldh : h;
|
|
for (int offset = segStart; offset < j && offset < mLen; ++offset) {
|
|
if (runIsRtl) {
|
|
bounds[2 * offset + 1] = currh;
|
|
currh -= advances[offset];
|
|
bounds[2 * offset] = currh;
|
|
} else {
|
|
bounds[2 * offset] = currh;
|
|
currh += advances[offset];
|
|
bounds[2 * offset + 1] = currh;
|
|
}
|
|
}
|
|
|
|
if (j != runLimit) { // charAt(j) == TAB_CHAR
|
|
final float leftX;
|
|
final float rightX;
|
|
if (runIsRtl) {
|
|
rightX = h;
|
|
h = mDir * nextTab(h * mDir);
|
|
leftX = h;
|
|
} else {
|
|
leftX = h;
|
|
h = mDir * nextTab(h * mDir);
|
|
rightX = h;
|
|
}
|
|
bounds[2 * j] = leftX;
|
|
bounds[2 * j + 1] = rightX;
|
|
advances[j] = rightX - leftX;
|
|
}
|
|
|
|
segStart = j + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @see #measure(int, boolean, FontMetricsInt, RectF, LineInfo)
|
|
* @return The measure results for all possible offsets
|
|
*/
|
|
@VisibleForTesting
|
|
public float[] measureAllOffsets(boolean[] trailing, FontMetricsInt fmi) {
|
|
float[] measurement = new float[mLen + 1];
|
|
if (trailing[0]) {
|
|
measurement[0] = 0;
|
|
}
|
|
|
|
float horizontal = 0;
|
|
final int runCount = mDirections.getRunCount();
|
|
for (int runIndex = 0; runIndex < runCount; runIndex++) {
|
|
final int runStart = mDirections.getRunStart(runIndex);
|
|
if (runStart > mLen) break;
|
|
final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
|
|
final boolean runIsRtl = mDirections.isRunRtl(runIndex);
|
|
final int runFlag = calculateRunFlag(runIndex, runCount, mDir);
|
|
|
|
int segStart = runStart;
|
|
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) {
|
|
if (j == runLimit || charAt(j) == TAB_CHAR) {
|
|
final float oldHorizontal = horizontal;
|
|
final boolean sameDirection =
|
|
(mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
|
|
|
|
// We are using measurement to receive character advance here. So that it
|
|
// doesn't need to allocate a new array.
|
|
// But be aware that when trailing[segStart] is true, measurement[segStart]
|
|
// will be computed in the previous run. And we need to store it first in case
|
|
// measureRun overwrites the result.
|
|
final float previousSegEndHorizontal = measurement[segStart];
|
|
final float width =
|
|
measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart,
|
|
0, null, runFlag);
|
|
horizontal += sameDirection ? width : -width;
|
|
|
|
float currHorizontal = sameDirection ? oldHorizontal : horizontal;
|
|
final int segLimit = Math.min(j, mLen);
|
|
|
|
for (int offset = segStart; offset <= segLimit; ++offset) {
|
|
float advance = 0f;
|
|
// When offset == segLimit, advance is meaningless.
|
|
if (offset < segLimit) {
|
|
advance = runIsRtl ? -measurement[offset] : measurement[offset];
|
|
}
|
|
|
|
if (offset == segStart && trailing[offset]) {
|
|
// If offset == segStart and trailing[segStart] is true, restore the
|
|
// value of measurement[segStart] from the previous run.
|
|
measurement[offset] = previousSegEndHorizontal;
|
|
} else if (offset != segLimit || trailing[offset]) {
|
|
measurement[offset] = currHorizontal;
|
|
}
|
|
|
|
currHorizontal += advance;
|
|
}
|
|
|
|
if (j != runLimit) { // charAt(j) == TAB_CHAR
|
|
if (!trailing[j]) {
|
|
measurement[j] = horizontal;
|
|
}
|
|
horizontal = mDir * nextTab(horizontal * mDir);
|
|
if (trailing[j + 1]) {
|
|
measurement[j + 1] = horizontal;
|
|
}
|
|
}
|
|
|
|
segStart = j + 1;
|
|
}
|
|
}
|
|
}
|
|
if (!trailing[mLen]) {
|
|
measurement[mLen] = horizontal;
|
|
}
|
|
return measurement;
|
|
}
|
|
|
|
/**
|
|
* Draws a unidirectional (but possibly multi-styled) run of text.
|
|
*
|
|
*
|
|
* @param c the canvas to draw on
|
|
* @param start the line-relative start
|
|
* @param limit the line-relative limit
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param x the position of the run that is closest to the leading margin
|
|
* @param top the top of the line
|
|
* @param y the baseline
|
|
* @param bottom the bottom of the line
|
|
* @param needWidth true if the width value is required.
|
|
* @param runFlag the run flag to be applied for this run.
|
|
* @return the signed width of the run, based on the paragraph direction.
|
|
* Only valid if needWidth is true.
|
|
*/
|
|
private float drawRun(Canvas c, int start,
|
|
int limit, boolean runIsRtl, float x, int top, int y, int bottom,
|
|
boolean needWidth, int runFlag) {
|
|
|
|
if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
|
|
float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null,
|
|
runFlag);
|
|
handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
|
|
y, bottom, null, null, false, null, 0, null, runFlag);
|
|
return w;
|
|
}
|
|
|
|
return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
|
|
y, bottom, null, null, needWidth, null, 0, null, runFlag);
|
|
}
|
|
|
|
/**
|
|
* Measures a unidirectional (but possibly multi-styled) run of text.
|
|
*
|
|
*
|
|
* @param start the line-relative start of the run
|
|
* @param offset the offset to measure to, between start and limit inclusive
|
|
* @param limit the line-relative limit of the run
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param fmi receives metrics information about the requested
|
|
* run, can be null.
|
|
* @param advances receives the advance information about the requested run, can be null.
|
|
* @param advancesIndex the start index to fill in the advance information.
|
|
* @param x horizontal offset of the run.
|
|
* @param lineInfo an optional output parameter for filling line information.
|
|
* @param runFlag the run flag to be applied for this run.
|
|
* @return the signed width from the start of the run to the leading edge
|
|
* of the character at offset, based on the run (not paragraph) direction
|
|
*/
|
|
private float measureRun(int start, int offset, int limit, boolean runIsRtl,
|
|
@Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances,
|
|
int advancesIndex, float x, @Nullable LineInfo lineInfo, int runFlag) {
|
|
if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
|
|
float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null,
|
|
runFlag);
|
|
return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi,
|
|
drawBounds, true, advances, advancesIndex, lineInfo, runFlag);
|
|
}
|
|
return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds,
|
|
true, advances, advancesIndex, lineInfo, runFlag);
|
|
}
|
|
|
|
/**
|
|
* Shape a unidirectional (but possibly multi-styled) run of text.
|
|
*
|
|
* @param consumer the consumer of the shape result
|
|
* @param start the line-relative start
|
|
* @param limit the line-relative limit
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param x the position of the run that is closest to the leading margin
|
|
* @param needWidth true if the width value is required.
|
|
* @param runFlag the run flag to be applied for this run.
|
|
* @return the signed width of the run, based on the paragraph direction.
|
|
* Only valid if needWidth is true.
|
|
*/
|
|
private float shapeRun(TextShaper.GlyphsConsumer consumer, int start,
|
|
int limit, boolean runIsRtl, float x, boolean needWidth, int runFlag) {
|
|
|
|
if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
|
|
float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null,
|
|
runFlag);
|
|
handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null,
|
|
false, null, 0, null, runFlag);
|
|
return w;
|
|
}
|
|
|
|
return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null,
|
|
needWidth, null, 0, null, runFlag);
|
|
}
|
|
|
|
|
|
/**
|
|
* Walk the cursor through this line, skipping conjuncts and
|
|
* zero-width characters.
|
|
*
|
|
* <p>This function cannot properly walk the cursor off the ends of the line
|
|
* since it does not know about any shaping on the previous/following line
|
|
* that might affect the cursor position. Callers must either avoid these
|
|
* situations or handle the result specially.
|
|
*
|
|
* @param cursor the starting position of the cursor, between 0 and the
|
|
* length of the line, inclusive
|
|
* @param toLeft true if the caret is moving to the left.
|
|
* @return the new offset. If it is less than 0 or greater than the length
|
|
* of the line, the previous/following line should be examined to get the
|
|
* actual offset.
|
|
*/
|
|
int getOffsetToLeftRightOf(int cursor, boolean toLeft) {
|
|
// 1) The caret marks the leading edge of a character. The character
|
|
// logically before it might be on a different level, and the active caret
|
|
// position is on the character at the lower level. If that character
|
|
// was the previous character, the caret is on its trailing edge.
|
|
// 2) Take this character/edge and move it in the indicated direction.
|
|
// This gives you a new character and a new edge.
|
|
// 3) This position is between two visually adjacent characters. One of
|
|
// these might be at a lower level. The active position is on the
|
|
// character at the lower level.
|
|
// 4) If the active position is on the trailing edge of the character,
|
|
// the new caret position is the following logical character, else it
|
|
// is the character.
|
|
|
|
int lineStart = 0;
|
|
int lineEnd = mLen;
|
|
boolean paraIsRtl = mDir == -1;
|
|
int[] runs = mDirections.mDirections;
|
|
|
|
int runIndex, runLevel = 0, runStart = lineStart, runLimit = lineEnd, newCaret = -1;
|
|
boolean trailing = false;
|
|
|
|
if (cursor == lineStart) {
|
|
runIndex = -2;
|
|
} else if (cursor == lineEnd) {
|
|
runIndex = runs.length;
|
|
} else {
|
|
// First, get information about the run containing the character with
|
|
// the active caret.
|
|
for (runIndex = 0; runIndex < runs.length; runIndex += 2) {
|
|
runStart = lineStart + runs[runIndex];
|
|
if (cursor >= runStart) {
|
|
runLimit = runStart + (runs[runIndex+1] & Layout.RUN_LENGTH_MASK);
|
|
if (runLimit > lineEnd) {
|
|
runLimit = lineEnd;
|
|
}
|
|
if (cursor < runLimit) {
|
|
runLevel = (runs[runIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
|
|
Layout.RUN_LEVEL_MASK;
|
|
if (cursor == runStart) {
|
|
// The caret is on a run boundary, see if we should
|
|
// use the position on the trailing edge of the previous
|
|
// logical character instead.
|
|
int prevRunIndex, prevRunLevel, prevRunStart, prevRunLimit;
|
|
int pos = cursor - 1;
|
|
for (prevRunIndex = 0; prevRunIndex < runs.length; prevRunIndex += 2) {
|
|
prevRunStart = lineStart + runs[prevRunIndex];
|
|
if (pos >= prevRunStart) {
|
|
prevRunLimit = prevRunStart +
|
|
(runs[prevRunIndex+1] & Layout.RUN_LENGTH_MASK);
|
|
if (prevRunLimit > lineEnd) {
|
|
prevRunLimit = lineEnd;
|
|
}
|
|
if (pos < prevRunLimit) {
|
|
prevRunLevel = (runs[prevRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT)
|
|
& Layout.RUN_LEVEL_MASK;
|
|
if (prevRunLevel < runLevel) {
|
|
// Start from logically previous character.
|
|
runIndex = prevRunIndex;
|
|
runLevel = prevRunLevel;
|
|
runStart = prevRunStart;
|
|
runLimit = prevRunLimit;
|
|
trailing = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// caret might be == lineEnd. This is generally a space or paragraph
|
|
// separator and has an associated run, but might be the end of
|
|
// text, in which case it doesn't. If that happens, we ran off the
|
|
// end of the run list, and runIndex == runs.length. In this case,
|
|
// we are at a run boundary so we skip the below test.
|
|
if (runIndex != runs.length) {
|
|
boolean runIsRtl = (runLevel & 0x1) != 0;
|
|
boolean advance = toLeft == runIsRtl;
|
|
if (cursor != (advance ? runLimit : runStart) || advance != trailing) {
|
|
// Moving within or into the run, so we can move logically.
|
|
newCaret = getOffsetBeforeAfter(runIndex, runStart, runLimit,
|
|
runIsRtl, cursor, advance);
|
|
// If the new position is internal to the run, we're at the strong
|
|
// position already so we're finished.
|
|
if (newCaret != (advance ? runLimit : runStart)) {
|
|
return newCaret;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If newCaret is -1, we're starting at a run boundary and crossing
|
|
// into another run. Otherwise we've arrived at a run boundary, and
|
|
// need to figure out which character to attach to. Note we might
|
|
// need to run this twice, if we cross a run boundary and end up at
|
|
// another run boundary.
|
|
while (true) {
|
|
boolean advance = toLeft == paraIsRtl;
|
|
int otherRunIndex = runIndex + (advance ? 2 : -2);
|
|
if (otherRunIndex >= 0 && otherRunIndex < runs.length) {
|
|
int otherRunStart = lineStart + runs[otherRunIndex];
|
|
int otherRunLimit = otherRunStart +
|
|
(runs[otherRunIndex+1] & Layout.RUN_LENGTH_MASK);
|
|
if (otherRunLimit > lineEnd) {
|
|
otherRunLimit = lineEnd;
|
|
}
|
|
int otherRunLevel = (runs[otherRunIndex+1] >>> Layout.RUN_LEVEL_SHIFT) &
|
|
Layout.RUN_LEVEL_MASK;
|
|
boolean otherRunIsRtl = (otherRunLevel & 1) != 0;
|
|
|
|
advance = toLeft == otherRunIsRtl;
|
|
if (newCaret == -1) {
|
|
newCaret = getOffsetBeforeAfter(otherRunIndex, otherRunStart,
|
|
otherRunLimit, otherRunIsRtl,
|
|
advance ? otherRunStart : otherRunLimit, advance);
|
|
if (newCaret == (advance ? otherRunLimit : otherRunStart)) {
|
|
// Crossed and ended up at a new boundary,
|
|
// repeat a second and final time.
|
|
runIndex = otherRunIndex;
|
|
runLevel = otherRunLevel;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// The new caret is at a boundary.
|
|
if (otherRunLevel < runLevel) {
|
|
// The strong character is in the other run.
|
|
newCaret = advance ? otherRunStart : otherRunLimit;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (newCaret == -1) {
|
|
// We're walking off the end of the line. The paragraph
|
|
// level is always equal to or lower than any internal level, so
|
|
// the boundaries get the strong caret.
|
|
newCaret = advance ? mLen + 1 : -1;
|
|
break;
|
|
}
|
|
|
|
// Else we've arrived at the end of the line. That's a strong position.
|
|
// We might have arrived here by crossing over a run with no internal
|
|
// breaks and dropping out of the above loop before advancing one final
|
|
// time, so reset the caret.
|
|
// Note, we use '<=' below to handle a situation where the only run
|
|
// on the line is a counter-directional run. If we're not advancing,
|
|
// we can end up at the 'lineEnd' position but the caret we want is at
|
|
// the lineStart.
|
|
if (newCaret <= lineEnd) {
|
|
newCaret = advance ? lineEnd : lineStart;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return newCaret;
|
|
}
|
|
|
|
/**
|
|
* Returns the next valid offset within this directional run, skipping
|
|
* conjuncts and zero-width characters. This should not be called to walk
|
|
* off the end of the line, since the returned values might not be valid
|
|
* on neighboring lines. If the returned offset is less than zero or
|
|
* greater than the line length, the offset should be recomputed on the
|
|
* preceding or following line, respectively.
|
|
*
|
|
* @param runIndex the run index
|
|
* @param runStart the start of the run
|
|
* @param runLimit the limit of the run
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param offset the offset
|
|
* @param after true if the new offset should logically follow the provided
|
|
* offset
|
|
* @return the new offset
|
|
*/
|
|
private int getOffsetBeforeAfter(int runIndex, int runStart, int runLimit,
|
|
boolean runIsRtl, int offset, boolean after) {
|
|
|
|
if (runIndex < 0 || offset == (after ? mLen : 0)) {
|
|
// Walking off end of line. Since we don't know
|
|
// what cursor positions are available on other lines, we can't
|
|
// return accurate values. These are a guess.
|
|
if (after) {
|
|
return TextUtils.getOffsetAfter(mText, offset + mStart) - mStart;
|
|
}
|
|
return TextUtils.getOffsetBefore(mText, offset + mStart) - mStart;
|
|
}
|
|
|
|
TextPaint wp = mWorkPaint;
|
|
wp.set(mPaint);
|
|
if (mIsJustifying) {
|
|
wp.setWordSpacing(mAddedWordSpacingInPx);
|
|
wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize()); // Convert to Em
|
|
}
|
|
|
|
int spanStart = runStart;
|
|
int spanLimit;
|
|
if (mSpanned == null || runStart == runLimit) {
|
|
spanLimit = runLimit;
|
|
} else {
|
|
int target = after ? offset + 1 : offset;
|
|
int limit = mStart + runLimit;
|
|
while (true) {
|
|
spanLimit = mSpanned.nextSpanTransition(mStart + spanStart, limit,
|
|
MetricAffectingSpan.class) - mStart;
|
|
if (spanLimit >= target) {
|
|
break;
|
|
}
|
|
spanStart = spanLimit;
|
|
}
|
|
|
|
MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart,
|
|
mStart + spanLimit, MetricAffectingSpan.class);
|
|
spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
|
|
|
|
if (spans.length > 0) {
|
|
ReplacementSpan replacement = null;
|
|
for (int j = 0; j < spans.length; j++) {
|
|
MetricAffectingSpan span = spans[j];
|
|
if (span instanceof ReplacementSpan) {
|
|
replacement = (ReplacementSpan)span;
|
|
} else {
|
|
span.updateMeasureState(wp);
|
|
}
|
|
}
|
|
|
|
if (replacement != null) {
|
|
// If we have a replacement span, we're moving either to
|
|
// the start or end of this span.
|
|
return after ? spanLimit : spanStart;
|
|
}
|
|
}
|
|
}
|
|
|
|
int cursorOpt = after ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE;
|
|
if (mCharsValid) {
|
|
return wp.getTextRunCursor(mChars, spanStart, spanLimit - spanStart,
|
|
runIsRtl, offset, cursorOpt);
|
|
} else {
|
|
return wp.getTextRunCursor(mText, mStart + spanStart,
|
|
mStart + spanLimit, runIsRtl, mStart + offset, cursorOpt) - mStart;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param wp
|
|
*/
|
|
private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
|
|
final int previousTop = fmi.top;
|
|
final int previousAscent = fmi.ascent;
|
|
final int previousDescent = fmi.descent;
|
|
final int previousBottom = fmi.bottom;
|
|
final int previousLeading = fmi.leading;
|
|
|
|
wp.getFontMetricsInt(fmi);
|
|
|
|
updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
|
|
previousLeading);
|
|
}
|
|
|
|
private void expandMetricsFromPaint(TextPaint wp, int start, int end,
|
|
int contextStart, int contextEnd, boolean runIsRtl, FontMetricsInt fmi) {
|
|
|
|
final int previousTop = fmi.top;
|
|
final int previousAscent = fmi.ascent;
|
|
final int previousDescent = fmi.descent;
|
|
final int previousBottom = fmi.bottom;
|
|
final int previousLeading = fmi.leading;
|
|
|
|
int count = end - start;
|
|
int contextCount = contextEnd - contextStart;
|
|
if (mCharsValid) {
|
|
wp.getFontMetricsInt(mChars, start, count, contextStart, contextCount, runIsRtl,
|
|
fmi);
|
|
} else {
|
|
if (mComputed == null) {
|
|
wp.getFontMetricsInt(mText, mStart + start, count, mStart + contextStart,
|
|
contextCount, runIsRtl, fmi);
|
|
} else {
|
|
mComputed.getFontMetricsInt(mStart + start, mStart + end, fmi);
|
|
}
|
|
}
|
|
|
|
updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
|
|
previousLeading);
|
|
}
|
|
|
|
|
|
static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
|
|
int previousDescent, int previousBottom, int previousLeading) {
|
|
fmi.top = Math.min(fmi.top, previousTop);
|
|
fmi.ascent = Math.min(fmi.ascent, previousAscent);
|
|
fmi.descent = Math.max(fmi.descent, previousDescent);
|
|
fmi.bottom = Math.max(fmi.bottom, previousBottom);
|
|
fmi.leading = Math.max(fmi.leading, previousLeading);
|
|
}
|
|
|
|
private static void drawStroke(TextPaint wp, Canvas c, int color, float position,
|
|
float thickness, float xleft, float xright, float baseline) {
|
|
final float strokeTop = baseline + wp.baselineShift + position;
|
|
|
|
final int previousColor = wp.getColor();
|
|
final Paint.Style previousStyle = wp.getStyle();
|
|
final boolean previousAntiAlias = wp.isAntiAlias();
|
|
|
|
wp.setStyle(Paint.Style.FILL);
|
|
wp.setAntiAlias(true);
|
|
|
|
wp.setColor(color);
|
|
c.drawRect(xleft, strokeTop, xright, strokeTop + thickness, wp);
|
|
|
|
wp.setStyle(previousStyle);
|
|
wp.setColor(previousColor);
|
|
wp.setAntiAlias(previousAntiAlias);
|
|
}
|
|
|
|
private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
|
|
boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex,
|
|
RectF drawingBounds, @Nullable LineInfo lineInfo) {
|
|
if (lineInfo != null) {
|
|
if (mRunInfo == null) {
|
|
mRunInfo = new Paint.RunInfo();
|
|
}
|
|
mRunInfo.setClusterCount(0);
|
|
} else {
|
|
mRunInfo = null;
|
|
}
|
|
if (mCharsValid) {
|
|
float r = wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
|
|
runIsRtl, offset, advances, advancesIndex, drawingBounds, mRunInfo);
|
|
if (lineInfo != null) {
|
|
lineInfo.setClusterCount(lineInfo.getClusterCount() + mRunInfo.getClusterCount());
|
|
}
|
|
return r;
|
|
} else {
|
|
final int delta = mStart;
|
|
// TODO: Add cluster information to the PrecomputedText for better performance of
|
|
// justification.
|
|
if (mComputed == null || advances != null || lineInfo != null) {
|
|
float r = wp.getRunCharacterAdvance(mText, delta + start, delta + end,
|
|
delta + contextStart, delta + contextEnd, runIsRtl,
|
|
delta + offset, advances, advancesIndex, drawingBounds, mRunInfo);
|
|
if (lineInfo != null) {
|
|
lineInfo.setClusterCount(
|
|
lineInfo.getClusterCount() + mRunInfo.getClusterCount());
|
|
}
|
|
return r;
|
|
} else {
|
|
if (drawingBounds != null) {
|
|
if (mTmpRectForPrecompute == null) {
|
|
mTmpRectForPrecompute = new Rect();
|
|
}
|
|
mComputed.getBounds(start + delta, end + delta, mTmpRectForPrecompute);
|
|
drawingBounds.set(mTmpRectForPrecompute);
|
|
}
|
|
return mComputed.getWidth(start + delta, end + delta);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility function for measuring and rendering text. The text must
|
|
* not include a tab.
|
|
*
|
|
* @param wp the working paint
|
|
* @param start the start of the text
|
|
* @param end the end of the text
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param c the canvas, can be null if rendering is not needed
|
|
* @param consumer the output positioned glyph list, can be null if not necessary
|
|
* @param x the edge of the run closest to the leading margin
|
|
* @param top the top of the line
|
|
* @param y the baseline
|
|
* @param bottom the bottom of the line
|
|
* @param fmi receives metrics information, can be null
|
|
* @param needWidth true if the width of the run is needed
|
|
* @param offset the offset for the purpose of measuring
|
|
* @param decorations the list of locations and paremeters for drawing decorations
|
|
* @param advances receives the advance information about the requested run, can be null.
|
|
* @param advancesIndex the start index to fill in the advance information.
|
|
* @param lineInfo an optional output parameter for filling line information.
|
|
* @param runFlag the run flag to be applied for this run.
|
|
* @return the signed width of the run based on the run direction; only
|
|
* valid if needWidth is true
|
|
*/
|
|
private float handleText(TextPaint wp, int start, int end,
|
|
int contextStart, int contextEnd, boolean runIsRtl,
|
|
Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom,
|
|
FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset,
|
|
@Nullable ArrayList<DecorationInfo> decorations,
|
|
@Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo,
|
|
int runFlag) {
|
|
if (mIsJustifying) {
|
|
wp.setWordSpacing(mAddedWordSpacingInPx);
|
|
wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize()); // Convert to Em
|
|
}
|
|
// Get metrics first (even for empty strings or "0" width runs)
|
|
if (drawBounds != null && fmi == null) {
|
|
fmi = new FontMetricsInt();
|
|
}
|
|
if (fmi != null) {
|
|
expandMetricsFromPaint(fmi, wp);
|
|
}
|
|
|
|
// No need to do anything if the run width is "0"
|
|
if (end == start) {
|
|
return 0f;
|
|
}
|
|
|
|
float totalWidth = 0;
|
|
if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) == Paint.TEXT_RUN_FLAG_LEFT_EDGE) {
|
|
wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_LEFT_EDGE);
|
|
} else {
|
|
wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_LEFT_EDGE);
|
|
}
|
|
if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) == Paint.TEXT_RUN_FLAG_RIGHT_EDGE) {
|
|
wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_RIGHT_EDGE);
|
|
} else {
|
|
wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE);
|
|
}
|
|
final int numDecorations = decorations == null ? 0 : decorations.size();
|
|
if (needWidth || ((c != null || consumer != null) && (wp.bgColor != 0
|
|
|| numDecorations != 0 || runIsRtl))) {
|
|
if (drawBounds != null && mTmpRectForPaintAPI == null) {
|
|
mTmpRectForPaintAPI = new RectF();
|
|
}
|
|
totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset,
|
|
advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI,
|
|
lineInfo);
|
|
if (drawBounds != null) {
|
|
if (runIsRtl) {
|
|
mTmpRectForPaintAPI.offset(x - totalWidth, 0);
|
|
} else {
|
|
mTmpRectForPaintAPI.offset(x, 0);
|
|
}
|
|
drawBounds.union(mTmpRectForPaintAPI);
|
|
}
|
|
}
|
|
|
|
final float leftX, rightX;
|
|
if (runIsRtl) {
|
|
leftX = x - totalWidth;
|
|
rightX = x;
|
|
} else {
|
|
leftX = x;
|
|
rightX = x + totalWidth;
|
|
}
|
|
|
|
if (consumer != null) {
|
|
shapeTextRun(consumer, wp, start, end, contextStart, contextEnd, runIsRtl, leftX);
|
|
}
|
|
|
|
if (mUseFallbackExtent && fmi != null) {
|
|
expandMetricsFromPaint(wp, start, end, contextStart, contextEnd, runIsRtl, fmi);
|
|
}
|
|
|
|
if (c != null) {
|
|
if (wp.bgColor != 0) {
|
|
int previousColor = wp.getColor();
|
|
Paint.Style previousStyle = wp.getStyle();
|
|
|
|
wp.setColor(wp.bgColor);
|
|
wp.setStyle(Paint.Style.FILL);
|
|
c.drawRect(leftX, top, rightX, bottom, wp);
|
|
|
|
wp.setStyle(previousStyle);
|
|
wp.setColor(previousColor);
|
|
}
|
|
|
|
drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
|
|
leftX, y + wp.baselineShift);
|
|
|
|
if (numDecorations != 0) {
|
|
for (int i = 0; i < numDecorations; i++) {
|
|
final DecorationInfo info = decorations.get(i);
|
|
|
|
final int decorationStart = Math.max(info.start, start);
|
|
final int decorationEnd = Math.min(info.end, offset);
|
|
float decorationStartAdvance = getRunAdvance(wp, start, end, contextStart,
|
|
contextEnd, runIsRtl, decorationStart, null, 0, null, null);
|
|
float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart,
|
|
contextEnd, runIsRtl, decorationEnd, null, 0, null, null);
|
|
final float decorationXLeft, decorationXRight;
|
|
if (runIsRtl) {
|
|
decorationXLeft = rightX - decorationEndAdvance;
|
|
decorationXRight = rightX - decorationStartAdvance;
|
|
} else {
|
|
decorationXLeft = leftX + decorationStartAdvance;
|
|
decorationXRight = leftX + decorationEndAdvance;
|
|
}
|
|
|
|
// Theoretically, there could be cases where both Paint's and TextPaint's
|
|
// setUnderLineText() are called. For backward compatibility, we need to draw
|
|
// both underlines, the one with custom color first.
|
|
if (info.underlineColor != 0) {
|
|
drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
|
|
info.underlineThickness, decorationXLeft, decorationXRight, y);
|
|
}
|
|
if (info.isUnderlineText) {
|
|
final float thickness =
|
|
Math.max(wp.getUnderlineThickness(), 1.0f);
|
|
drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
|
|
decorationXLeft, decorationXRight, y);
|
|
}
|
|
|
|
if (info.isStrikeThruText) {
|
|
final float thickness =
|
|
Math.max(wp.getStrikeThruThickness(), 1.0f);
|
|
drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
|
|
decorationXLeft, decorationXRight, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return runIsRtl ? -totalWidth : totalWidth;
|
|
}
|
|
|
|
/**
|
|
* Utility function for measuring and rendering a replacement.
|
|
*
|
|
*
|
|
* @param replacement the replacement
|
|
* @param wp the work paint
|
|
* @param start the start of the run
|
|
* @param limit the limit of the run
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param c the canvas, can be null if not rendering
|
|
* @param x the edge of the replacement closest to the leading margin
|
|
* @param top the top of the line
|
|
* @param y the baseline
|
|
* @param bottom the bottom of the line
|
|
* @param fmi receives metrics information, can be null
|
|
* @param needWidth true if the width of the replacement is needed
|
|
* @return the signed width of the run based on the run direction; only
|
|
* valid if needWidth is true
|
|
*/
|
|
private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
|
|
int start, int limit, boolean runIsRtl, Canvas c,
|
|
float x, int top, int y, int bottom, FontMetricsInt fmi,
|
|
boolean needWidth) {
|
|
|
|
float ret = 0;
|
|
|
|
int textStart = mStart + start;
|
|
int textLimit = mStart + limit;
|
|
|
|
if (needWidth || (c != null && runIsRtl)) {
|
|
int previousTop = 0;
|
|
int previousAscent = 0;
|
|
int previousDescent = 0;
|
|
int previousBottom = 0;
|
|
int previousLeading = 0;
|
|
|
|
boolean needUpdateMetrics = (fmi != null);
|
|
|
|
if (needUpdateMetrics) {
|
|
previousTop = fmi.top;
|
|
previousAscent = fmi.ascent;
|
|
previousDescent = fmi.descent;
|
|
previousBottom = fmi.bottom;
|
|
previousLeading = fmi.leading;
|
|
}
|
|
|
|
ret = replacement.getSize(wp, mText, textStart, textLimit, fmi);
|
|
|
|
if (needUpdateMetrics) {
|
|
updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
|
|
previousLeading);
|
|
}
|
|
}
|
|
|
|
if (c != null) {
|
|
if (runIsRtl) {
|
|
x -= ret;
|
|
}
|
|
replacement.draw(c, mText, textStart, textLimit,
|
|
x, top, y, bottom, wp);
|
|
}
|
|
|
|
return runIsRtl ? -ret : ret;
|
|
}
|
|
|
|
private int adjustStartHyphenEdit(int start, @Paint.StartHyphenEdit int startHyphenEdit) {
|
|
// Only draw hyphens on first in line. Disable them otherwise.
|
|
return start > 0 ? Paint.START_HYPHEN_EDIT_NO_EDIT : startHyphenEdit;
|
|
}
|
|
|
|
private int adjustEndHyphenEdit(int limit, @Paint.EndHyphenEdit int endHyphenEdit) {
|
|
// Only draw hyphens on last run in line. Disable them otherwise.
|
|
return limit < mLen ? Paint.END_HYPHEN_EDIT_NO_EDIT : endHyphenEdit;
|
|
}
|
|
|
|
private static final class DecorationInfo {
|
|
public boolean isStrikeThruText;
|
|
public boolean isUnderlineText;
|
|
public int underlineColor;
|
|
public float underlineThickness;
|
|
public int start = -1;
|
|
public int end = -1;
|
|
|
|
public boolean hasDecoration() {
|
|
return isStrikeThruText || isUnderlineText || underlineColor != 0;
|
|
}
|
|
|
|
// Copies the info, but not the start and end range.
|
|
public DecorationInfo copyInfo() {
|
|
final DecorationInfo copy = new DecorationInfo();
|
|
copy.isStrikeThruText = isStrikeThruText;
|
|
copy.isUnderlineText = isUnderlineText;
|
|
copy.underlineColor = underlineColor;
|
|
copy.underlineThickness = underlineThickness;
|
|
return copy;
|
|
}
|
|
}
|
|
|
|
private void extractDecorationInfo(@NonNull TextPaint paint, @NonNull DecorationInfo info) {
|
|
info.isStrikeThruText = paint.isStrikeThruText();
|
|
if (info.isStrikeThruText) {
|
|
paint.setStrikeThruText(false);
|
|
}
|
|
info.isUnderlineText = paint.isUnderlineText();
|
|
if (info.isUnderlineText) {
|
|
paint.setUnderlineText(false);
|
|
}
|
|
info.underlineColor = paint.underlineColor;
|
|
info.underlineThickness = paint.underlineThickness;
|
|
paint.setUnderlineText(0, 0.0f);
|
|
}
|
|
|
|
/**
|
|
* Utility function for handling a unidirectional run. The run must not
|
|
* contain tabs but can contain styles.
|
|
*
|
|
*
|
|
* @param start the line-relative start of the run
|
|
* @param measureLimit the offset to measure to, between start and limit inclusive
|
|
* @param limit the limit of the run
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param c the canvas, can be null
|
|
* @param consumer the output positioned glyphs, can be null
|
|
* @param x the end of the run closest to the leading margin
|
|
* @param top the top of the line
|
|
* @param y the baseline
|
|
* @param bottom the bottom of the line
|
|
* @param fmi receives metrics information, can be null
|
|
* @param needWidth true if the width is required
|
|
* @param advances receives the advance information about the requested run, can be null.
|
|
* @param advancesIndex the start index to fill in the advance information.
|
|
* @param lineInfo an optional output parameter for filling line information.
|
|
* @param runFlag the run flag to be applied for this run.
|
|
* @return the signed width of the run based on the run direction; only
|
|
* valid if needWidth is true
|
|
*/
|
|
private float handleRun(int start, int measureLimit,
|
|
int limit, boolean runIsRtl, Canvas c,
|
|
TextShaper.GlyphsConsumer consumer, float x, int top, int y,
|
|
int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth,
|
|
@Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo,
|
|
int runFlag) {
|
|
|
|
if (measureLimit < start || measureLimit > limit) {
|
|
throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
|
|
+ "start (" + start + ") and limit (" + limit + ") bounds");
|
|
}
|
|
|
|
if (advances != null && advances.length - advancesIndex < measureLimit - start) {
|
|
throw new IndexOutOfBoundsException("advances doesn't have enough space to receive the "
|
|
+ "result");
|
|
}
|
|
|
|
// Case of an empty line, make sure we update fmi according to mPaint
|
|
if (start == measureLimit) {
|
|
final TextPaint wp = mWorkPaint;
|
|
wp.set(mPaint);
|
|
if (fmi != null) {
|
|
expandMetricsFromPaint(fmi, wp);
|
|
}
|
|
if (drawBounds != null) {
|
|
if (fmi == null) {
|
|
FontMetricsInt tmpFmi = new FontMetricsInt();
|
|
expandMetricsFromPaint(tmpFmi, wp);
|
|
fmi = tmpFmi;
|
|
}
|
|
drawBounds.union(0f, fmi.top, 0f, fmi.bottom);
|
|
}
|
|
return 0f;
|
|
}
|
|
|
|
final boolean needsSpanMeasurement;
|
|
if (mSpanned == null) {
|
|
needsSpanMeasurement = false;
|
|
} else {
|
|
mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
|
|
mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
|
|
needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
|
|
|| mCharacterStyleSpanSet.numberOfSpans != 0;
|
|
}
|
|
|
|
if (!needsSpanMeasurement) {
|
|
final TextPaint wp = mWorkPaint;
|
|
wp.set(mPaint);
|
|
wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
|
|
wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
|
|
return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top,
|
|
y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances,
|
|
advancesIndex, lineInfo, runFlag);
|
|
}
|
|
|
|
// Shaping needs to take into account context up to metric boundaries,
|
|
// but rendering needs to take into account character style boundaries.
|
|
// So we iterate through metric runs to get metric bounds,
|
|
// then within each metric run iterate through character style runs
|
|
// for the run bounds.
|
|
final float originalX = x;
|
|
for (int i = start, inext; i < measureLimit; i = inext) {
|
|
final TextPaint wp = mWorkPaint;
|
|
wp.set(mPaint);
|
|
|
|
inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
|
|
mStart;
|
|
int mlimit = Math.min(inext, measureLimit);
|
|
|
|
ReplacementSpan replacement = null;
|
|
|
|
for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
|
|
// Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
|
|
// empty by construction. This special case in getSpans() explains the >= & <= tests
|
|
if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
|
|
|| (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
|
|
|
|
boolean insideEllipsis =
|
|
mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
|
|
&& mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
|
|
final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
|
|
if (span instanceof ReplacementSpan) {
|
|
replacement = !insideEllipsis ? (ReplacementSpan) span : null;
|
|
} else {
|
|
// We might have a replacement that uses the draw
|
|
// state, otherwise measure state would suffice.
|
|
span.updateDrawState(wp);
|
|
}
|
|
}
|
|
|
|
if (replacement != null) {
|
|
final float width = handleReplacement(replacement, wp, i, mlimit, runIsRtl, c,
|
|
x, top, y, bottom, fmi, needWidth || mlimit < measureLimit);
|
|
x += width;
|
|
if (advances != null) {
|
|
// For replacement, the entire width is assigned to the first character.
|
|
advances[advancesIndex + i - start] = runIsRtl ? -width : width;
|
|
for (int j = i + 1; j < mlimit; ++j) {
|
|
advances[advancesIndex + j - start] = 0.0f;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
final TextPaint activePaint = mActivePaint;
|
|
activePaint.set(mPaint);
|
|
int activeStart = i;
|
|
int activeEnd = mlimit;
|
|
final DecorationInfo decorationInfo = mDecorationInfo;
|
|
mDecorations.clear();
|
|
for (int j = i, jnext; j < mlimit; j = jnext) {
|
|
jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
|
|
mStart;
|
|
|
|
final int offset = Math.min(jnext, mlimit);
|
|
wp.set(mPaint);
|
|
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
|
|
// Intentionally using >= and <= as explained above
|
|
if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
|
|
(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
|
|
|
|
final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
|
|
span.updateDrawState(wp);
|
|
}
|
|
|
|
extractDecorationInfo(wp, decorationInfo);
|
|
|
|
if (j == i) {
|
|
// First chunk of text. We can't handle it yet, since we may need to merge it
|
|
// with the next chunk. So we just save the TextPaint for future comparisons
|
|
// and use.
|
|
activePaint.set(wp);
|
|
} else if (!equalAttributes(wp, activePaint)) {
|
|
final int spanRunFlag = resolveRunFlagForSubSequence(
|
|
runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd);
|
|
|
|
// The style of the present chunk of text is substantially different from the
|
|
// style of the previous chunk. We need to handle the active piece of text
|
|
// and restart with the present chunk.
|
|
activePaint.setStartHyphenEdit(
|
|
adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
|
|
activePaint.setEndHyphenEdit(
|
|
adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
|
|
x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c,
|
|
consumer, x, top, y, bottom, fmi, drawBounds,
|
|
needWidth || activeEnd < measureLimit,
|
|
Math.min(activeEnd, mlimit), mDecorations,
|
|
advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag);
|
|
|
|
activeStart = j;
|
|
activePaint.set(wp);
|
|
mDecorations.clear();
|
|
} else {
|
|
// The present TextPaint is substantially equal to the last TextPaint except
|
|
// perhaps for decorations. We just need to expand the active piece of text to
|
|
// include the present chunk, which we always do anyway. We don't need to save
|
|
// wp to activePaint, since they are already equal.
|
|
}
|
|
|
|
activeEnd = jnext;
|
|
if (decorationInfo.hasDecoration()) {
|
|
final DecorationInfo copy = decorationInfo.copyInfo();
|
|
copy.start = j;
|
|
copy.end = jnext;
|
|
mDecorations.add(copy);
|
|
}
|
|
}
|
|
|
|
final int spanRunFlag = resolveRunFlagForSubSequence(
|
|
runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd);
|
|
// Handle the final piece of text.
|
|
activePaint.setStartHyphenEdit(
|
|
adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
|
|
activePaint.setEndHyphenEdit(
|
|
adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
|
|
x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x,
|
|
top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit,
|
|
Math.min(activeEnd, mlimit), mDecorations,
|
|
advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag);
|
|
}
|
|
|
|
return x - originalX;
|
|
}
|
|
|
|
/**
|
|
* Render a text run with the set-up paint.
|
|
*
|
|
* @param c the canvas
|
|
* @param wp the paint used to render the text
|
|
* @param start the start of the run
|
|
* @param end the end of the run
|
|
* @param contextStart the start of context for the run
|
|
* @param contextEnd the end of the context for the run
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param x the x position of the left edge of the run
|
|
* @param y the baseline of the run
|
|
*/
|
|
private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
|
|
int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
|
|
if (mCharsValid) {
|
|
int count = end - start;
|
|
int contextCount = contextEnd - contextStart;
|
|
c.drawTextRun(mChars, start, count, contextStart, contextCount,
|
|
x, y, runIsRtl, wp);
|
|
} else {
|
|
int delta = mStart;
|
|
c.drawTextRun(mText, delta + start, delta + end,
|
|
delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shape a text run with the set-up paint.
|
|
*
|
|
* @param consumer the output positioned glyphs list
|
|
* @param paint the paint used to render the text
|
|
* @param start the start of the run
|
|
* @param end the end of the run
|
|
* @param contextStart the start of context for the run
|
|
* @param contextEnd the end of the context for the run
|
|
* @param runIsRtl true if the run is right-to-left
|
|
* @param x the x position of the left edge of the run
|
|
*/
|
|
private void shapeTextRun(TextShaper.GlyphsConsumer consumer, TextPaint paint,
|
|
int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x) {
|
|
|
|
int count = end - start;
|
|
int contextCount = contextEnd - contextStart;
|
|
PositionedGlyphs glyphs;
|
|
if (mCharsValid) {
|
|
glyphs = TextRunShaper.shapeTextRun(
|
|
mChars,
|
|
start, count,
|
|
contextStart, contextCount,
|
|
x, 0f,
|
|
runIsRtl,
|
|
paint
|
|
);
|
|
} else {
|
|
glyphs = TextRunShaper.shapeTextRun(
|
|
mText,
|
|
mStart + start, count,
|
|
mStart + contextStart, contextCount,
|
|
x, 0f,
|
|
runIsRtl,
|
|
paint
|
|
);
|
|
}
|
|
consumer.accept(start, count, glyphs, paint);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the next tab position.
|
|
*
|
|
* @param h the (unsigned) offset from the leading margin
|
|
* @return the (unsigned) tab position after this offset
|
|
*/
|
|
float nextTab(float h) {
|
|
if (mTabs != null) {
|
|
return mTabs.nextTab(h);
|
|
}
|
|
return TabStops.nextDefaultStop(h, TAB_INCREMENT);
|
|
}
|
|
|
|
private boolean isStretchableWhitespace(int ch) {
|
|
// TODO: Support NBSP and other stretchable whitespace (b/34013491 and b/68204709).
|
|
return ch == 0x0020;
|
|
}
|
|
|
|
/* Return the number of spaces in the text line, for the purpose of justification */
|
|
private int countStretchableSpaces(int start, int end) {
|
|
int count = 0;
|
|
for (int i = start; i < end; i++) {
|
|
final char c = mCharsValid ? mChars[i] : mText.charAt(i + mStart);
|
|
if (isStretchableWhitespace(c)) {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// Note: keep this in sync with Minikin LineBreaker::isLineEndSpace()
|
|
public static boolean isLineEndSpace(char ch) {
|
|
return ch == ' ' || ch == '\t' || ch == 0x1680
|
|
|| (0x2000 <= ch && ch <= 0x200A && ch != 0x2007)
|
|
|| ch == 0x205F || ch == 0x3000;
|
|
}
|
|
|
|
private static final int TAB_INCREMENT = 20;
|
|
|
|
private static boolean equalAttributes(@NonNull TextPaint lp, @NonNull TextPaint rp) {
|
|
return lp.getColorFilter() == rp.getColorFilter()
|
|
&& lp.getMaskFilter() == rp.getMaskFilter()
|
|
&& lp.getShader() == rp.getShader()
|
|
&& lp.getTypeface() == rp.getTypeface()
|
|
&& lp.getXfermode() == rp.getXfermode()
|
|
&& lp.getTextLocales().equals(rp.getTextLocales())
|
|
&& TextUtils.equals(lp.getFontFeatureSettings(), rp.getFontFeatureSettings())
|
|
&& TextUtils.equals(lp.getFontVariationSettings(), rp.getFontVariationSettings())
|
|
&& lp.getShadowLayerRadius() == rp.getShadowLayerRadius()
|
|
&& lp.getShadowLayerDx() == rp.getShadowLayerDx()
|
|
&& lp.getShadowLayerDy() == rp.getShadowLayerDy()
|
|
&& lp.getShadowLayerColor() == rp.getShadowLayerColor()
|
|
&& lp.getFlags() == rp.getFlags()
|
|
&& lp.getHinting() == rp.getHinting()
|
|
&& lp.getStyle() == rp.getStyle()
|
|
&& lp.getColor() == rp.getColor()
|
|
&& lp.getStrokeWidth() == rp.getStrokeWidth()
|
|
&& lp.getStrokeMiter() == rp.getStrokeMiter()
|
|
&& lp.getStrokeCap() == rp.getStrokeCap()
|
|
&& lp.getStrokeJoin() == rp.getStrokeJoin()
|
|
&& lp.getTextAlign() == rp.getTextAlign()
|
|
&& lp.isElegantTextHeight() == rp.isElegantTextHeight()
|
|
&& lp.getTextSize() == rp.getTextSize()
|
|
&& lp.getTextScaleX() == rp.getTextScaleX()
|
|
&& lp.getTextSkewX() == rp.getTextSkewX()
|
|
&& lp.getLetterSpacing() == rp.getLetterSpacing()
|
|
&& lp.getWordSpacing() == rp.getWordSpacing()
|
|
&& lp.getStartHyphenEdit() == rp.getStartHyphenEdit()
|
|
&& lp.getEndHyphenEdit() == rp.getEndHyphenEdit()
|
|
&& lp.bgColor == rp.bgColor
|
|
&& lp.baselineShift == rp.baselineShift
|
|
&& lp.linkColor == rp.linkColor
|
|
&& lp.drawableState == rp.drawableState
|
|
&& lp.density == rp.density
|
|
&& lp.underlineColor == rp.underlineColor
|
|
&& lp.underlineThickness == rp.underlineThickness;
|
|
}
|
|
}
|