Add bounding box based text layout APIs.
This CL adds bounding box based text layout features.
By setting setUseBoundsForWidth, the line break width and drawing
offset is adjusted based on bounding box instead of advances.
Bug: 63938206
Test: atest CtsTextTestCases
Change-Id: I993c455eee1b4100656db9aef38675e3cda3309d
diff --git a/core/api/current.txt b/core/api/current.txt
index 06bd72d..d092976 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -17679,6 +17679,7 @@
method @NonNull public android.graphics.text.LineBreaker.Builder setHyphenationFrequency(int);
method @NonNull public android.graphics.text.LineBreaker.Builder setIndents(@Nullable int[]);
method @NonNull public android.graphics.text.LineBreaker.Builder setJustificationMode(int);
+ method @NonNull public android.graphics.text.LineBreaker.Builder setUseBoundsForWidth(boolean);
}
public static class LineBreaker.ParagraphConstraints {
@@ -47302,6 +47303,7 @@
public static class BoringLayout.Metrics extends android.graphics.Paint.FontMetricsInt {
ctor public BoringLayout.Metrics();
+ method @NonNull public android.graphics.RectF getDrawingBoundingBox();
field public int width;
}
@@ -47343,6 +47345,7 @@
method @NonNull public android.text.DynamicLayout.Builder setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig);
method @NonNull public android.text.DynamicLayout.Builder setLineSpacing(float, @FloatRange(from=0.0) float);
method @NonNull public android.text.DynamicLayout.Builder setTextDirection(@NonNull android.text.TextDirectionHeuristic);
+ method @NonNull public android.text.DynamicLayout.Builder setUseBoundsForWidth(boolean);
method @NonNull public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean);
}
@@ -47485,6 +47488,7 @@
public abstract class Layout {
ctor protected Layout(CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float);
+ method @NonNull public android.graphics.RectF computeDrawingBoundingBox();
method public void draw(android.graphics.Canvas);
method public void draw(android.graphics.Canvas, android.graphics.Path, android.graphics.Paint, int);
method public void draw(@NonNull android.graphics.Canvas, @Nullable java.util.List<android.graphics.Path>, @Nullable java.util.List<android.graphics.Paint>, @Nullable android.graphics.Path, @Nullable android.graphics.Paint, int);
@@ -47546,6 +47550,7 @@
method @NonNull public final CharSequence getText();
method @NonNull public final android.text.TextDirectionHeuristic getTextDirectionHeuristic();
method public abstract int getTopPadding();
+ method public boolean getUseBoundsForWidth();
method @IntRange(from=0) public final int getWidth();
method public final void increaseWidthTo(int);
method public boolean isFallbackLineSpacingEnabled();
@@ -47595,6 +47600,7 @@
method @NonNull public android.text.Layout.Builder setMaxLines(@IntRange(from=1) int);
method @NonNull public android.text.Layout.Builder setRightIndents(@Nullable int[]);
method @NonNull public android.text.Layout.Builder setTextDirectionHeuristic(@NonNull android.text.TextDirectionHeuristic);
+ method @NonNull public android.text.Layout.Builder setUseBoundsForWidth(boolean);
}
public static class Layout.Directions {
@@ -47863,6 +47869,7 @@
method @NonNull public android.text.StaticLayout.Builder setMaxLines(@IntRange(from=0) int);
method public android.text.StaticLayout.Builder setText(CharSequence);
method @NonNull public android.text.StaticLayout.Builder setTextDirection(@NonNull android.text.TextDirectionHeuristic);
+ method @NonNull public android.text.StaticLayout.Builder setUseBoundsForWidth(boolean);
method @NonNull public android.text.StaticLayout.Builder setUseLineSpacingFromFallbacks(boolean);
}
@@ -60548,6 +60555,7 @@
method public final android.text.method.TransformationMethod getTransformationMethod();
method public android.graphics.Typeface getTypeface();
method public android.text.style.URLSpan[] getUrls();
+ method public boolean getUseBoundsForWidth();
method public boolean hasSelection();
method public boolean isAllCaps();
method public boolean isCursorVisible();
@@ -60690,6 +60698,7 @@
method public final void setTransformationMethod(android.text.method.TransformationMethod);
method public void setTypeface(@Nullable android.graphics.Typeface, int);
method public void setTypeface(@Nullable android.graphics.Typeface);
+ method public void setUseBoundsForWidth(boolean);
method public void setWidth(int);
field public static final int AUTO_SIZE_TEXT_TYPE_NONE = 0; // 0x0
field public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1; // 0x1
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index 7748d5b..14fc585 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -23,6 +23,7 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
+import android.graphics.RectF;
import android.graphics.text.LineBreakConfig;
import android.text.style.ParagraphStyle;
@@ -191,6 +192,17 @@
@NonNull Alignment align, @NonNull BoringLayout.Metrics metrics, boolean includePad,
@Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth,
boolean useFallbackLineSpacing) {
+ return replaceOrMake(source, paint, outerWidth, align, 1.0f, 0.0f, metrics, includePad,
+ ellipsize, ellipsizedWidth, useFallbackLineSpacing, false /* useBoundsForWidth */);
+ }
+
+ /** @hide */
+ public @NonNull BoringLayout replaceOrMake(@NonNull CharSequence source,
+ @NonNull TextPaint paint, @IntRange(from = 0) int outerWidth,
+ @NonNull Alignment align, float spacingMultiplier, float spacingAmount,
+ @NonNull BoringLayout.Metrics metrics, boolean includePad,
+ @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth,
+ boolean useFallbackLineSpacing, boolean useBoundsForWidth) {
boolean trust;
if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
@@ -202,7 +214,7 @@
trust = true;
} else {
replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),
- paint, outerWidth, align, 1f, 0f);
+ paint, outerWidth, align, spacingMultiplier, spacingAmount);
mEllipsizedWidth = ellipsizedWidth;
trust = false;
@@ -263,8 +275,7 @@
spacingAdd, includePad, false /* fallbackLineSpacing */,
outerwidth /* ellipsizedWidth */, null /* ellipsize */, 1 /* maxLines */,
BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null /* leftIndents */,
- null /* rightIndents */, JUSTIFICATION_MODE_NONE,
- LineBreakConfig.NONE);
+ null /* rightIndents */, JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false);
mEllipsizedWidth = outerwidth;
mEllipsizedStart = 0;
@@ -341,7 +352,28 @@
ellipsizedWidth, ellipsize, 1 /* maxLines */,
BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null /* leftIndents */,
null /* rightIndents */, JUSTIFICATION_MODE_NONE,
- LineBreakConfig.NONE, metrics);
+ LineBreakConfig.NONE, metrics, false /* useBoundsForWidth */);
+ }
+
+ /** @hide */
+ public BoringLayout(
+ CharSequence text,
+ TextPaint paint,
+ int width,
+ Alignment align,
+ float spacingMult,
+ float spacingAdd,
+ boolean includePad,
+ boolean fallbackLineSpacing,
+ int ellipsizedWidth,
+ TextUtils.TruncateAt ellipsize,
+ Metrics metrics,
+ boolean useBoundsForWidth) {
+ this(text, paint, width, align, TextDirectionHeuristics.LTR,
+ spacingMult, spacingAdd, includePad, fallbackLineSpacing, ellipsizedWidth,
+ ellipsize, 1 /* maxLines */, Layout.BREAK_STRATEGY_SIMPLE,
+ Layout.HYPHENATION_FREQUENCY_NONE, null, null, Layout.JUSTIFICATION_MODE_NONE,
+ LineBreakConfig.NONE, metrics, useBoundsForWidth);
}
/* package */ BoringLayout(
@@ -363,12 +395,13 @@
int[] rightIndents,
int justificationMode,
LineBreakConfig lineBreakConfig,
- Metrics metrics) {
+ Metrics metrics,
+ boolean useBoundsForWidth) {
super(text, paint, width, align, textDir, spacingMult, spacingAdd, includePad,
fallbackLineSpacing, ellipsizedWidth, ellipsize, maxLines, breakStrategy,
hyphenationFrequency, leftIndents, rightIndents, justificationMode,
- lineBreakConfig);
+ lineBreakConfig, useBoundsForWidth);
boolean trust;
@@ -425,7 +458,7 @@
line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
mEllipsizedStart, mEllipsizedStart + mEllipsizedCount, useFallbackLineSpacing);
- mMax = (int) Math.ceil(line.metrics(null));
+ mMax = (int) Math.ceil(line.metrics(null, null, false));
TextLine.recycle(line);
}
@@ -433,6 +466,9 @@
mTopPadding = metrics.top - metrics.ascent;
mBottomPadding = metrics.bottom - metrics.descent;
}
+
+ mDrawingBounds.set(metrics.mDrawingBounds);
+ mDrawingBounds.offset(0, mBottom - mDesc);
}
/**
@@ -555,7 +591,7 @@
0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,
0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */,
useFallbackLineSpacing);
- fm.width = (int) Math.ceil(line.metrics(fm));
+ fm.width = (int) Math.ceil(line.metrics(fm, fm.mDrawingBounds, false));
TextLine.recycle(line);
return fm;
@@ -604,12 +640,20 @@
@Override
public float getLineMax(int line) {
- return mMax;
+ if (getUseBoundsForWidth()) {
+ return super.getLineMax(line);
+ } else {
+ return mMax;
+ }
}
@Override
public float getLineWidth(int line) {
- return (line == 0 ? mMax : 0);
+ if (getUseBoundsForWidth()) {
+ return super.getLineWidth(line);
+ } else {
+ return (line == 0 ? mMax : 0);
+ }
}
@Override
@@ -647,12 +691,29 @@
return mUseFallbackLineSpacing;
}
+ @Override
+ public @NonNull RectF computeDrawingBoundingBox() {
+ return mDrawingBounds;
+ }
+
// Override draw so it will be faster.
@Override
public void draw(Canvas c, Path highlight, Paint highlightpaint,
int cursorOffset) {
if (mDirect != null && highlight == null) {
+ if (getUseBoundsForWidth()) {
+ c.save();
+ RectF drawingRect = computeDrawingBoundingBox();
+ if (drawingRect.left < 0) {
+ c.translate(-drawingRect.left, 0);
+ }
+ }
+
c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
+
+ if (getUseBoundsForWidth()) {
+ c.restore();
+ }
} else {
super.draw(c, highlight, highlightpaint, cursorOffset);
}
@@ -674,12 +735,23 @@
private int mTopPadding, mBottomPadding;
private float mMax;
private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;
+ private final RectF mDrawingBounds = new RectF();
public static class Metrics extends Paint.FontMetricsInt {
public int width;
+ private final RectF mDrawingBounds = new RectF();
+
+ /**
+ * Returns drawing bounding box.
+ *
+ * @return a drawing bounding box.
+ */
+ @NonNull public RectF getDrawingBoundingBox() {
+ return mDrawingBounds;
+ }
@Override public String toString() {
- return super.toString() + " width=" + width;
+ return super.toString() + " width=" + width + ", drawingBounds = " + mDrawingBounds;
}
private void reset() {
@@ -689,6 +761,7 @@
descent = 0;
width = 0;
leading = 0;
+ mDrawingBounds.setEmpty();
}
}
}
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index e287bd9..2b3a081 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -286,6 +286,29 @@
}
/**
+ * Set true for using width of bounding box as a source of automatic line breaking and
+ * drawing.
+ *
+ * If this value is false, the Layout determines the drawing offset and automatic line
+ * breaking based on total advances. By setting true, use all joined glyph's bounding boxes
+ * as a source of text width.
+ *
+ * If the font has glyphs that have negative bearing X or its xMax is greater than advance,
+ * the glyph clipping can happen because the drawing area may be bigger. By setting this to
+ * true, the Layout will reserve more spaces for drawing.
+ *
+ * @param useBoundsForWidth True for using bounding box, false for advances.
+ * @return this builder instance
+ * @see Layout#getUseBoundsForWidth()
+ * @see Layout.Builder#setUseBoundsForWidth(boolean)
+ */
+ @NonNull
+ public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
+ mUseBoundsForWidth = useBoundsForWidth;
+ return this;
+ }
+
+ /**
* Build the {@link DynamicLayout} after options have been set.
*
* <p>Note: the builder object must not be reused in any way after calling this method.
@@ -317,6 +340,7 @@
private TextUtils.TruncateAt mEllipsize;
private int mEllipsizedWidth;
private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE;
+ private boolean mUseBoundsForWidth;
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
@@ -392,7 +416,7 @@
false /* fallbackLineSpacing */, ellipsizedWidth, ellipsize,
Integer.MAX_VALUE /* maxLines */, breakStrategy, hyphenationFrequency,
null /* leftIndents */, null /* rightIndents */, justificationMode,
- lineBreakConfig);
+ lineBreakConfig, false /* useBoundsForWidth */);
final Builder b = Builder.obtain(base, paint, width)
.setAlignment(align)
@@ -418,7 +442,7 @@
b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize,
Integer.MAX_VALUE /* maxLines */, b.mBreakStrategy, b.mHyphenationFrequency,
null /* leftIndents */, null /* rightIndents */, b.mJustificationMode,
- b.mLineBreakConfig);
+ b.mLineBreakConfig, b.mUseBoundsForWidth);
mDisplay = b.mDisplay;
mIncludePad = b.mIncludePad;
@@ -445,6 +469,7 @@
private void generate(@NonNull Builder b) {
mBase = b.mBase;
mFallbackLineSpacing = b.mFallbackLineSpacing;
+ mUseBoundsForWidth = b.mUseBoundsForWidth;
if (b.mEllipsize != null) {
mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
mEllipsizedWidth = b.mEllipsizedWidth;
@@ -639,7 +664,9 @@
.setJustificationMode(mJustificationMode)
.setLineBreakConfig(mLineBreakConfig)
.setAddLastLineLineSpacing(!islast)
- .setIncludePad(false);
+ .setIncludePad(false)
+ .setUseBoundsForWidth(mUseBoundsForWidth)
+ .setCalculateBounds(true);
reflowed = b.buildPartialStaticLayoutForDynamicLayout(true /* trackpadding */, reflowed);
int n = reflowed.getLineCount();
@@ -1290,6 +1317,8 @@
private Rect mTempRect = new Rect();
+ private boolean mUseBoundsForWidth;
+
@UnsupportedAppUsage
private static StaticLayout sStaticLayout = null;
private static StaticLayout.Builder sBuilder = null;
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index c450dc8..469e166 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -21,6 +21,7 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -228,7 +229,7 @@
*/
public static float getDesiredWidth(CharSequence source, int start, int end, TextPaint paint,
TextDirectionHeuristic textDir) {
- return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE);
+ return getDesiredWidthWithLimit(source, start, end, paint, textDir, Float.MAX_VALUE, false);
}
/**
* Return how wide a layout must be in order to display the
@@ -238,7 +239,8 @@
* @hide
*/
public static float getDesiredWidthWithLimit(CharSequence source, int start, int end,
- TextPaint paint, TextDirectionHeuristic textDir, float upperLimit) {
+ TextPaint paint, TextDirectionHeuristic textDir, float upperLimit,
+ boolean useBoundsForWidth) {
float need = 0;
int next;
@@ -249,7 +251,7 @@
next = end;
// note, omits trailing paragraph char
- float w = measurePara(paint, source, i, next, textDir);
+ float w = measurePara(paint, source, i, next, textDir, useBoundsForWidth);
if (w > upperLimit) {
return upperLimit;
}
@@ -282,7 +284,7 @@
this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
spacingMult, spacingAdd, false, false, 0, null, Integer.MAX_VALUE,
BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null, null,
- JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE);
+ JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false);
}
/**
@@ -330,7 +332,8 @@
int[] leftIndents,
int[] rightIndents,
int justificationMode,
- LineBreakConfig lineBreakConfig
+ LineBreakConfig lineBreakConfig,
+ boolean useBoundsForWidth
) {
if (width < 0)
@@ -364,6 +367,7 @@
mRightIndents = rightIndents;
mJustificationMode = justificationMode;
mLineBreakConfig = lineBreakConfig;
+ mUseBoundsForWidth = useBoundsForWidth;
}
/**
@@ -443,6 +447,13 @@
@Nullable Path selectionPath,
@Nullable Paint selectionPaint,
int cursorOffsetVertical) {
+ if (mUseBoundsForWidth) {
+ canvas.save();
+ RectF drawingRect = computeDrawingBoundingBox();
+ if (drawingRect.left < 0) {
+ canvas.translate(-drawingRect.left, 0);
+ }
+ }
final long lineRange = getLineRangeForDraw(canvas);
int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
@@ -451,6 +462,9 @@
drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
cursorOffsetVertical, firstLine, lastLine);
drawText(canvas, firstLine, lastLine);
+ if (mUseBoundsForWidth) {
+ canvas.restore();
+ }
}
/**
@@ -985,6 +999,84 @@
public abstract int getLineCount();
/**
+ * Get an actual bounding box that draws text content.
+ *
+ * Note that the {@link RectF#top} and {@link RectF#bottom} may be different from the
+ * {@link Layout#getLineTop(int)} of the first line and {@link Layout#getLineBottom(int)} of
+ * the last line. The line top and line bottom are calculated based on yMin/yMax or
+ * ascent/descent value of font file. On the other hand, the drawing bounding boxes are
+ * calculated based on actual glyphs used there.
+ *
+ * @return bounding rectangle
+ */
+ @NonNull
+ public RectF computeDrawingBoundingBox() {
+ float left = 0;
+ float right = 0;
+ float top = 0;
+ float bottom = 0;
+ TextLine tl = TextLine.obtain();
+ RectF rectF = new RectF();
+ for (int line = 0; line < getLineCount(); ++line) {
+ final int start = getLineStart(line);
+ final int end = getLineVisibleEnd(line);
+
+ final boolean hasTabs = getLineContainsTab(line);
+ TabStops tabStops = null;
+ if (hasTabs && mText instanceof Spanned) {
+ // Just checking this line should be good enough, tabs should be
+ // consistent across all lines in a paragraph.
+ TabStopSpan[] tabs = getParagraphSpans((Spanned) mText, start, end,
+ TabStopSpan.class);
+ if (tabs.length > 0) {
+ tabStops = new TabStops(TAB_INCREMENT, tabs); // XXX should reuse
+ }
+ }
+ final Directions directions = getLineDirections(line);
+ // Returned directions can actually be null
+ if (directions == null) {
+ continue;
+ }
+ final int dir = getParagraphDirection(line);
+
+ final TextPaint paint = mWorkPaint;
+ paint.set(mPaint);
+ paint.setStartHyphenEdit(getStartHyphenEdit(line));
+ paint.setEndHyphenEdit(getEndHyphenEdit(line));
+ tl.set(paint, mText, start, end, dir, directions, hasTabs, tabStops,
+ getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
+ isFallbackLineSpacingEnabled());
+ if (isJustificationRequired(line)) {
+ tl.justify(getJustifyWidth(line));
+ }
+ tl.metrics(null, rectF, false);
+
+ float lineLeft = rectF.left;
+ float lineRight = rectF.right;
+ float lineTop = rectF.top + getLineBaseline(line);
+ float lineBottom = rectF.bottom + getLineBaseline(line);
+ if (getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT) {
+ lineLeft += getWidth();
+ lineRight += getWidth();
+ }
+
+ if (line == 0) {
+ left = lineLeft;
+ right = lineRight;
+ top = lineTop;
+ bottom = lineBottom;
+ } else {
+ left = Math.min(left, lineLeft);
+ right = Math.max(right, lineRight);
+ top = Math.min(top, lineTop);
+ bottom = Math.max(bottom, lineBottom);
+ }
+ }
+ TextLine.recycle(tl);
+ return new RectF(left, top, right, bottom);
+ }
+
+ /**
* Return the baseline for the specified line (0…getLineCount() - 1)
* If bounds is not null, return the top, left, right, bottom extents
* of the specified line in it.
@@ -1357,7 +1449,7 @@
tl.set(mPaint, mText, start, end, dir, directions, hasTab, tabStops,
getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
isFallbackLineSpacingEnabled());
- float wid = tl.measure(offset - start, trailing, null);
+ float wid = tl.measure(offset - start, trailing, null, null);
TextLine.recycle(tl);
if (clamped && wid > mWidth) {
@@ -1693,7 +1785,7 @@
if (isJustificationRequired(line)) {
tl.justify(getJustifyWidth(line));
}
- final float width = tl.metrics(null);
+ final float width = tl.metrics(null, null, mUseBoundsForWidth);
TextLine.recycle(tl);
return width;
}
@@ -1724,7 +1816,7 @@
if (isJustificationRequired(line)) {
tl.justify(getJustifyWidth(line));
}
- final float width = tl.metrics(null);
+ final float width = tl.metrics(null, null, mUseBoundsForWidth);
TextLine.recycle(tl);
return width;
}
@@ -2800,7 +2892,7 @@
}
private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
- TextDirectionHeuristic textDir) {
+ TextDirectionHeuristic textDir, boolean useBoundsForWidth) {
MeasuredParagraph mt = null;
TextLine tl = TextLine.obtain();
try {
@@ -2840,7 +2932,7 @@
tl.set(paint, text, start, end, dir, directions, hasTabs, tabStops,
0 /* ellipsisStart */, 0 /* ellipsisEnd */,
false /* use fallback line spacing. unused */);
- return margin + Math.abs(tl.metrics(null));
+ return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth));
} finally {
TextLine.recycle(tl);
if (mt != null) {
@@ -3235,6 +3327,7 @@
private int[] mRightIndents;
private int mJustificationMode;
private LineBreakConfig mLineBreakConfig;
+ private boolean mUseBoundsForWidth;
/** @hide */
@IntDef(prefix = { "DIR_" }, value = {
@@ -3663,6 +3756,31 @@
return this;
}
+ /**
+ * Set true for using width of bounding box as a source of automatic line breaking and
+ * drawing.
+ *
+ * If this value is false, the Layout determines the drawing offset and automatic line
+ * breaking based on total advances. By setting true, use all joined glyph's bounding boxes
+ * as a source of text width.
+ *
+ * If the font has glyphs that have negative bearing X or its xMax is greater than advance,
+ * the glyph clipping can happen because the drawing area may be bigger. By setting this to
+ * true, the Layout will reserve more spaces for drawing.
+ *
+ * @param useBoundsForWidth True for using bounding box, false for advances.
+ * @return this builder instance
+ * @see Layout#getUseBoundsForWidth()
+ * @see StaticLayout.Builder#setUseBoundsForWidth(boolean)
+ */
+ // The corresponding getter is getUseBoundsForWidth
+ @NonNull
+ @SuppressLint("MissingGetterMatchingBuilder")
+ public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
+ mUseBoundsForWidth = useBoundsForWidth;
+ return this;
+ }
+
private BoringLayout.Metrics isBoring() {
if (mStart != 0 || mEnd != mText.length()) { // BoringLayout only support entire text.
return null;
@@ -3702,13 +3820,14 @@
.setIndents(mLeftIndents, mRightIndents)
.setJustificationMode(mJustificationMode)
.setLineBreakConfig(mLineBreakConfig)
+ .setUseBoundsForWidth(mUseBoundsForWidth)
.build();
} else {
return new BoringLayout(
mText, mPaint, mWidth, mAlignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad, mFallbackLineSpacing, mEllipsizedWidth, mEllipsize, mMaxLines,
mBreakStrategy, mHyphenationFrequency, mLeftIndents, mRightIndents,
- mJustificationMode, mLineBreakConfig, metrics);
+ mJustificationMode, mLineBreakConfig, metrics, mUseBoundsForWidth);
}
}
@@ -3732,6 +3851,7 @@
private int[] mRightIndents = null;
private int mJustificationMode = JUSTIFICATION_MODE_NONE;
private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE;
+ private boolean mUseBoundsForWidth;
}
///////////////////////////////////////////////////////////////////////////////////////////////
@@ -4011,4 +4131,17 @@
public LineBreakConfig getLineBreakConfig() {
return mLineBreakConfig;
}
+
+ /**
+ * Returns true if using bounding box as a width, false for using advance as a width.
+ *
+ * @return True if using bounding box for width, false if using advance for width.
+ * @see android.widget.TextView#setUseBoundsForWidth(boolean)
+ * @see android.widget.TextView#getUseBoundsForWidth()
+ * @see StaticLayout.Builder#setUseBoundsForWidth(boolean)
+ * @see DynamicLayout.Builder#setUseBoundsForWidth(boolean)
+ */
+ public boolean getUseBoundsForWidth() {
+ return mUseBoundsForWidth;
+ }
}
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index 5b4f195..c1d0e9b9 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -460,10 +460,11 @@
@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, hint, recycle, null);
+ hyphenationMode, computeLayout, computeBounds, hint, recycle, null);
}
/**
@@ -498,7 +499,7 @@
boolean computeLayout,
@Nullable StyleRunCallback testCallback) {
return buildForStaticLayoutInternal(paint, lineBreakConfig, text, start, end, textDir,
- hyphenationMode, computeLayout, null, null, testCallback);
+ hyphenationMode, computeLayout, false, null, null, testCallback);
}
private static @NonNull MeasuredParagraph buildForStaticLayoutInternal(
@@ -510,6 +511,7 @@
@NonNull TextDirectionHeuristic textDir,
int hyphenationMode,
boolean computeLayout,
+ boolean computeBounds,
@Nullable MeasuredParagraph hint,
@Nullable MeasuredParagraph recycle,
@Nullable StyleRunCallback testCallback) {
@@ -519,7 +521,8 @@
if (hint == null) {
builder = new MeasuredText.Builder(mt.mCopiedBuffer)
.setComputeHyphenation(hyphenationMode)
- .setComputeLayout(computeLayout);
+ .setComputeLayout(computeLayout)
+ .setComputeBounds(computeBounds);
} else {
builder = new MeasuredText.Builder(hint.mMeasuredText);
}
diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java
index fd97801..517ae4f 100644
--- a/core/java/android/text/PrecomputedText.java
+++ b/core/java/android/text/PrecomputedText.java
@@ -434,7 +434,8 @@
}
if (paraInfo == null) {
paraInfo = createMeasuredParagraphs(
- text, params, 0, text.length(), true /* computeLayout */);
+ text, params, 0, text.length(), true /* computeLayout */,
+ true /* computeBounds */);
}
return new PrecomputedText(text, 0, text.length(), params, paraInfo);
}
@@ -462,7 +463,7 @@
final int paraEnd = pct.getParagraphEnd(i);
result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
params.getTextPaint(), params.getLineBreakConfig(), pct, paraStart, paraEnd,
- params.getTextDirection(), hyphenationMode, computeLayout,
+ params.getTextDirection(), hyphenationMode, computeLayout, true,
pct.getMeasuredParagraph(i), null /* no recycle */)));
}
return result.toArray(new ParagraphInfo[result.size()]);
@@ -471,7 +472,8 @@
/** @hide */
public static ParagraphInfo[] createMeasuredParagraphs(
@NonNull CharSequence text, @NonNull Params params,
- @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) {
+ @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout,
+ boolean computeBounds) {
ArrayList<ParagraphInfo> result = new ArrayList<>();
Preconditions.checkNotNull(text);
@@ -500,7 +502,8 @@
result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
params.getTextPaint(), params.getLineBreakConfig(), text, paraStart, paraEnd,
- params.getTextDirection(), hyphenationMode, computeLayout, null /* no hint */,
+ params.getTextDirection(), hyphenationMode, computeLayout, computeBounds,
+ null /* no hint */,
null /* no recycle */)));
}
return result.toArray(new ParagraphInfo[result.size()]);
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 3d1895c..e3c72c9 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Paint;
+import android.graphics.RectF;
import android.graphics.text.LineBreakConfig;
import android.graphics.text.LineBreaker;
import android.os.Build;
@@ -421,6 +422,40 @@
}
/**
+ * Set true for using width of bounding box as a source of automatic line breaking and
+ * drawing.
+ *
+ * If this value is false, the Layout determines the drawing offset and automatic line
+ * breaking based on total advances. By setting true, use all joined glyph's bounding boxes
+ * as a source of text width.
+ *
+ * If the font has glyphs that have negative bearing X or its xMax is greater than advance,
+ * the glyph clipping can happen because the drawing area may be bigger. By setting this to
+ * true, the Layout will reserve more spaces for drawing.
+ *
+ * @param useBoundsForWidth True for using bounding box, false for advances.
+ * @return this builder instance
+ * @see Layout#getUseBoundsForWidth()
+ * @see Layout.Builder#setUseBoundsForWidth(boolean)
+ */
+ @NonNull
+ public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
+ mUseBoundsForWidth = useBoundsForWidth;
+ return this;
+ }
+
+ /**
+ * Internal API that tells underlying line breaker that calculating bounding boxes even if
+ * the line break is performed with advances. This is useful for DynamicLayout internal
+ * implementation because it uses bounding box as well as advances.
+ * @hide
+ */
+ public Builder setCalculateBounds(boolean value) {
+ mCalculateBounds = value;
+ return this;
+ }
+
+ /**
* Build the {@link StaticLayout} after options have been set.
*
* <p>Note: the builder object must not be reused in any way after calling this
@@ -479,6 +514,8 @@
private int mJustificationMode;
private boolean mAddLastLineLineSpacing;
private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE;
+ private boolean mUseBoundsForWidth;
+ private boolean mCalculateBounds;
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
@@ -508,7 +545,8 @@
null, // leftIndents
null, // rightIndents
JUSTIFICATION_MODE_NONE,
- null // lineBreakConfig
+ null, // lineBreakConfig,
+ false // useBoundsForWidth
);
mColumns = COLUMNS_ELLIPSIZE;
@@ -585,7 +623,7 @@
b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd,
b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize,
b.mMaxLines, b.mBreakStrategy, b.mHyphenationFrequency, b.mLeftIndents,
- b.mRightIndents, b.mJustificationMode, b.mLineBreakConfig);
+ b.mRightIndents, b.mJustificationMode, b.mLineBreakConfig, b.mUseBoundsForWidth);
mColumns = columnSize;
if (b.mEllipsize != null) {
@@ -644,6 +682,7 @@
mLineCount = 0;
mEllipsized = false;
mMaxLineHeight = mMaximumVisibleLineCount < 1 ? 0 : DEFAULT_MAX_LINE_HEIGHT;
+ mDrawingBounds = null;
boolean isFallbackLineSpacing = b.mFallbackLineSpacing;
int v = 0;
@@ -674,6 +713,7 @@
// TODO: Support more justification mode, e.g. letter spacing, stretching.
.setJustificationMode(b.mJustificationMode)
.setIndents(indents)
+ .setUseBoundsForWidth(b.mUseBoundsForWidth)
.build();
LineBreaker.ParagraphConstraints constraints =
@@ -711,7 +751,7 @@
final PrecomputedText.Params param = new PrecomputedText.Params(paint,
b.mLineBreakConfig, textDir, b.mBreakStrategy, b.mHyphenationFrequency);
paragraphInfo = PrecomputedText.createMeasuredParagraphs(source, param, bufStart,
- bufEnd, false /* computeLayout */);
+ bufEnd, false /* computeLayout */, b.mCalculateBounds);
}
for (int paraIndex = 0; paraIndex < paragraphInfo.length; paraIndex++) {
@@ -1406,6 +1446,16 @@
return mLines[mColumns * line + ELLIPSIS_START];
}
+ @Override
+ @NonNull
+ public RectF computeDrawingBoundingBox() {
+ // Cache the drawing bounds result because it does not change after created.
+ if (mDrawingBounds == null) {
+ mDrawingBounds = super.computeDrawingBoundingBox();
+ }
+ return mDrawingBounds;
+ }
+
/**
* Return the total height of this layout.
*
@@ -1431,6 +1481,7 @@
private int mTopPadding, mBottomPadding;
@UnsupportedAppUsage
private int mColumns;
+ private RectF mDrawingBounds = null; // lazy calculation.
/**
* Keeps track if ellipsize is applied to the text.
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index e38073a..f9abec0 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -23,6 +23,8 @@
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;
@@ -70,6 +72,9 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private Spanned mSpanned;
private PrecomputedText mComputed;
+ private RectF mTmpRectForMeasure;
+ private RectF mTmpRectForPaintAPI;
+ private Rect mTmpRectForPrecompute;
private boolean mUseFallbackExtent = false;
@@ -265,7 +270,7 @@
// width.
return;
}
- final float width = Math.abs(measure(end, false, null));
+ final float width = Math.abs(measure(end, false, null, null));
mAddedWidthForJustify = (justifyWidth - width) / spaces;
mIsJustifying = true;
}
@@ -307,11 +312,33 @@
* 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.
* @return the signed width of the line
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- public float metrics(FontMetricsInt fmi) {
- return measure(mLen, false, fmi);
+ public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth) {
+ if (returnDrawWidth) {
+ if (drawBounds == null) {
+ if (mTmpRectForMeasure == null) {
+ mTmpRectForMeasure = new RectF();
+ }
+ drawBounds = mTmpRectForMeasure;
+ }
+ drawBounds.setEmpty();
+ float w = measure(mLen, false, fmi, drawBounds);
+ float boundsWidth = drawBounds.width();
+ 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);
+ }
}
/**
@@ -379,12 +406,13 @@
* 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.
* @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) {
+ @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds) {
if (offset > mLen) {
throw new IndexOutOfBoundsException(
"offset(" + offset + ") should be less than line limit(" + mLen + ")");
@@ -408,14 +436,17 @@
final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
if (targetIsInThisSegment && sameDirection) {
- return h + measureRun(segStart, offset, j, runIsRtl, fmi, null, 0);
+ return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null,
+ 0, h);
}
- final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, null, 0);
+ final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds,
+ null, 0, h);
h += sameDirection ? segmentWidth : -segmentWidth;
if (targetIsInThisSegment) {
- return h + measureRun(segStart, offset, j, runIsRtl, null, null, 0);
+ return h + measureRun(segStart, offset, j, runIsRtl, null, null, null, 0,
+ h);
}
if (j != runLimit) { // charAt(j) == TAB_CHAR
@@ -506,7 +537,7 @@
final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
final float segmentWidth =
- measureRun(segStart, j, j, runIsRtl, null, advances, segStart);
+ measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0);
final float oldh = h;
h += sameDirection ? segmentWidth : -segmentWidth;
@@ -547,7 +578,7 @@
}
/**
- * @see #measure(int, boolean, FontMetricsInt)
+ * @see #measure(int, boolean, FontMetricsInt, RectF)
* @return The measure results for all possible offsets
*/
@VisibleForTesting
@@ -578,7 +609,8 @@
// measureRun overwrites the result.
final float previousSegEndHorizontal = measurement[segStart];
final float width =
- measureRun(segStart, j, j, runIsRtl, fmi, measurement, segStart);
+ measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart,
+ 0);
horizontal += sameDirection ? width : -width;
float currHorizontal = sameDirection ? oldHorizontal : horizontal;
@@ -643,14 +675,14 @@
boolean needWidth) {
if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
- float w = -measureRun(start, limit, limit, runIsRtl, null, null, 0);
+ float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0);
handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
- y, bottom, null, false, null, 0);
+ y, bottom, null, null, false, null, 0);
return w;
}
return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
- y, bottom, null, needWidth, null, 0);
+ y, bottom, null, null, needWidth, null, 0);
}
/**
@@ -665,13 +697,20 @@
* 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.
* @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 float[] advances, int advancesIndex) {
- return handleRun(start, offset, limit, runIsRtl, null, null, 0, 0, 0, 0, fmi, true,
- advances, advancesIndex);
+ @Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances,
+ int advancesIndex, float x) {
+ if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
+ float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0);
+ return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi,
+ drawBounds, true, advances, advancesIndex);
+ }
+ return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds,
+ true, advances, advancesIndex);
}
/**
@@ -690,13 +729,13 @@
int limit, boolean runIsRtl, float x, boolean needWidth) {
if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
- float w = -measureRun(start, limit, limit, runIsRtl, null, null, 0);
- handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null,
+ float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0);
+ handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null,
false, null, 0);
return w;
}
- return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null,
+ return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null,
needWidth, null, 0);
}
@@ -1037,17 +1076,25 @@
}
private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
- boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex) {
+ boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex,
+ RectF drawingBounds) {
if (mCharsValid) {
return wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
- runIsRtl, offset, advances, advancesIndex);
+ runIsRtl, offset, advances, advancesIndex, drawingBounds);
} else {
final int delta = mStart;
if (mComputed == null || advances != null) {
return wp.getRunCharacterAdvance(mText, delta + start, delta + end,
delta + contextStart, delta + contextEnd, runIsRtl,
- delta + offset, advances, advancesIndex);
+ delta + offset, advances, advancesIndex, drawingBounds);
} 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);
}
}
@@ -1079,7 +1126,7 @@
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, boolean needWidth, int offset,
+ FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset,
@Nullable ArrayList<DecorationInfo> decorations,
@Nullable float[] advances, int advancesIndex) {
@@ -1087,6 +1134,9 @@
wp.setWordSpacing(mAddedWidthForJustify);
}
// 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);
}
@@ -1101,8 +1151,19 @@
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);
+ advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI);
+ if (drawBounds != null) {
+ if (runIsRtl) {
+ mTmpRectForPaintAPI.offset(x - totalWidth, 0);
+ } else {
+ mTmpRectForPaintAPI.offset(x, 0);
+ }
+ drawBounds.union(mTmpRectForPaintAPI);
+ }
}
final float leftX, rightX;
@@ -1145,9 +1206,9 @@
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);
+ contextEnd, runIsRtl, decorationStart, null, 0, null);
float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart,
- contextEnd, runIsRtl, decorationEnd, null, 0);
+ contextEnd, runIsRtl, decorationEnd, null, 0, null);
final float decorationXLeft, decorationXRight;
if (runIsRtl) {
decorationXLeft = rightX - decorationEndAdvance;
@@ -1322,7 +1383,7 @@
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, boolean needWidth,
+ int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth,
@Nullable float[] advances, int advancesIndex) {
if (measureLimit < start || measureLimit > limit) {
@@ -1342,6 +1403,14 @@
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;
}
@@ -1361,7 +1430,8 @@
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, needWidth, measureLimit, null, advances, advancesIndex);
+ y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances,
+ advancesIndex);
}
// Shaping needs to take into account context up to metric boundaries,
@@ -1450,7 +1520,8 @@
activePaint.setEndHyphenEdit(
adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c,
- consumer, x, top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
+ consumer, x, top, y, bottom, fmi, drawBounds,
+ needWidth || activeEnd < measureLimit,
Math.min(activeEnd, mlimit), mDecorations,
advances, advancesIndex + activeStart - start);
@@ -1478,7 +1549,7 @@
activePaint.setEndHyphenEdit(
adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x,
- top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
+ top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit,
Math.min(activeEnd, mlimit), mDecorations,
advances, advancesIndex + activeStart - start);
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 0001fe6..a220c57 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -849,6 +849,8 @@
// True if the view text can be padded for compat reasons, when the view is translated.
private final boolean mUseTextPaddingForUiTranslation;
+ private boolean mUseBoundsForWidth;
+
@ViewDebug.ExportedProperty(category = "text")
@UnsupportedAppUsage
private int mGravity = Gravity.TOP | Gravity.START;
@@ -1618,6 +1620,9 @@
} else {
mUseFallbackLineSpacing = FALLBACK_LINE_SPACING_NONE;
}
+
+ mUseBoundsForWidth = false; // TODO: Make enable this by default.
+
// TODO(b/179693024): Use a ChangeId instead.
mUseTextPaddingForUiTranslation = targetSdkVersion <= Build.VERSION_CODES.R;
@@ -4826,6 +4831,45 @@
}
/**
+ * Set true for using width of bounding box as a source of automatic line breaking and drawing.
+ *
+ * If this value is false, the TextView determines the View width, drawing offset and automatic
+ * line breaking based on total advances as text widths. By setting true, use glyph bound's as a
+ * source of text width.
+ *
+ * If the font used for this TextView has glyphs that has negative bearing X or glyph xMax is
+ * greater than advance, the glyph clipping can be happened because the drawing area may be
+ * bigger than advance. By setting this to true, the TextView will reserve more spaces for
+ * drawing are, so clipping can be prevented.
+ *
+ * This value is true by default if the target API version is 35 or later.
+ *
+ * @param useBoundsForWidth true for using bounding box for width. false for using advances for
+ * width.
+ * @see #getUseBoundsForWidth()
+ */
+ public void setUseBoundsForWidth(boolean useBoundsForWidth) {
+ if (mUseBoundsForWidth != useBoundsForWidth) {
+ mUseBoundsForWidth = useBoundsForWidth;
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Returns true if using bounding box as a width, false for using advance as a width.
+ *
+ * @see #setUseBoundsForWidth(boolean)
+ * @return True if using bounding box for width, false if using advance for width.
+ */
+ public boolean getUseBoundsForWidth() {
+ return mUseBoundsForWidth;
+ }
+
+ /**
* @return whether fallback line spacing is enabled, {@code true} by default
*
* @see #setFallbackLineSpacing(boolean)
@@ -10653,7 +10697,8 @@
.setJustificationMode(mJustificationMode)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
.setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
- mLineBreakStyle, mLineBreakWordStyle));
+ mLineBreakStyle, mLineBreakWordStyle))
+ .setUseBoundsForWidth(mUseBoundsForWidth);
if (shouldEllipsize) {
builder.setEllipsize(mEllipsize)
.setEllipsizedWidth(ellipsisWidth);
@@ -10715,6 +10760,7 @@
.setJustificationMode(mJustificationMode)
.setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
mLineBreakStyle, mLineBreakWordStyle))
+ .setUseBoundsForWidth(mUseBoundsForWidth)
.setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
.setEllipsizedWidth(ellipsisWidth);
result = builder.build();
@@ -10733,11 +10779,23 @@
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
- boring, mIncludePad);
+ boring, mIncludePad, null, wantWidth,
+ isFallbackLineSpacingForBoringLayout(),
+ mUseBoundsForWidth);
} else {
- result = BoringLayout.make(mTransformed, mTextPaint,
- wantWidth, alignment, mSpacingMult, mSpacingAdd,
- boring, mIncludePad);
+ result = new BoringLayout(
+ mTransformed,
+ mTextPaint,
+ wantWidth,
+ alignment,
+ mSpacingMult,
+ mSpacingAdd,
+ mIncludePad,
+ isFallbackLineSpacingForBoringLayout(),
+ wantWidth,
+ null,
+ boring,
+ mUseBoundsForWidth);
}
if (useSaved) {
@@ -10748,12 +10806,22 @@
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
- ellipsisWidth);
+ ellipsisWidth, isFallbackLineSpacingForBoringLayout(),
+ mUseBoundsForWidth);
} else {
- result = BoringLayout.make(mTransformed, mTextPaint,
- wantWidth, alignment, mSpacingMult, mSpacingAdd,
- boring, mIncludePad, effectiveEllipsize,
- ellipsisWidth);
+ result = new BoringLayout(
+ mTransformed,
+ mTextPaint,
+ wantWidth,
+ alignment,
+ mSpacingMult,
+ mSpacingAdd,
+ mIncludePad,
+ isFallbackLineSpacingForBoringLayout(),
+ ellipsisWidth,
+ effectiveEllipsize,
+ boring,
+ mUseBoundsForWidth);
}
}
}
@@ -10771,7 +10839,8 @@
.setJustificationMode(mJustificationMode)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
.setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
- mLineBreakStyle, mLineBreakWordStyle));
+ mLineBreakStyle, mLineBreakWordStyle))
+ .setUseBoundsForWidth(mUseBoundsForWidth);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth);
@@ -10804,7 +10873,7 @@
return false;
}
- private static int desired(Layout layout) {
+ private static int desired(Layout layout, boolean useBoundsForWidth) {
int n = layout.getLineCount();
CharSequence text = layout.getText();
float max = 0;
@@ -10822,6 +10891,10 @@
max = Math.max(max, layout.getLineMax(i));
}
+ if (useBoundsForWidth) {
+ max = Math.max(max, layout.computeDrawingBoundingBox().width());
+ }
+
return (int) Math.ceil(max);
}
@@ -10890,7 +10963,7 @@
width = widthSize;
} else {
if (mLayout != null && mEllipsize == null) {
- des = desired(mLayout);
+ des = desired(mLayout, mUseBoundsForWidth);
}
if (des < 0) {
@@ -10906,11 +10979,17 @@
if (boring == null || boring == UNKNOWN_BORING) {
if (des < 0) {
des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
- mTransformed.length(), mTextPaint, mTextDir, widthLimit));
+ mTransformed.length(), mTextPaint, mTextDir, widthLimit,
+ mUseBoundsForWidth));
}
width = des;
} else {
- width = boring.width;
+ if (mUseBoundsForWidth) {
+ width = Math.max(boring.width,
+ (int) Math.ceil(boring.getDrawingBoundingBox().width()));
+ } else {
+ width = boring.width;
+ }
}
final Drawables dr = mDrawables;
@@ -10924,7 +11003,7 @@
int hintWidth;
if (mHintLayout != null && mEllipsize == null) {
- hintDes = desired(mHintLayout);
+ hintDes = desired(mHintLayout, mUseBoundsForWidth);
}
if (hintDes < 0) {
@@ -10938,7 +11017,8 @@
if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
if (hintDes < 0) {
hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
- mHint.length(), mTextPaint, mTextDir, widthLimit));
+ mHint.length(), mTextPaint, mTextDir, widthLimit,
+ mUseBoundsForWidth));
}
hintWidth = hintDes;
} else {
@@ -11139,7 +11219,8 @@
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
.setTextDirection(getTextDirectionHeuristic())
.setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
- mLineBreakStyle, mLineBreakWordStyle));
+ mLineBreakStyle, mLineBreakWordStyle))
+ .setUseBoundsForWidth(mUseBoundsForWidth);
final StaticLayout layout = layoutBuilder.build();
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 93a6b15..5649e71 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -584,6 +584,11 @@
public int getTopPadding() {
return 0;
}
+
+ @Override
+ public RectF computeDrawingBoundingBox() {
+ return new RectF();
+ }
}
@Test
diff --git a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
index 045e746..02b67e2 100644
--- a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
+++ b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
@@ -137,7 +137,7 @@
mt = MeasuredParagraph.buildForStaticLayout(
PAINT, null /* line break config */, "XXX", 0, 3, LTR,
- MeasuredText.Builder.HYPHENATION_MODE_NONE, false, null /* no hint */, null);
+ MeasuredText.Builder.HYPHENATION_MODE_NONE, false, false, null /* no hint */, null);
assertNotNull(mt);
assertNotNull(mt.getChars());
assertEquals("XXX", charsToString(mt.getChars()));
@@ -153,7 +153,7 @@
// Recycle it
MeasuredParagraph mt2 = MeasuredParagraph.buildForStaticLayout(
PAINT, null /* line break config */, "_VVV_", 1, 4, RTL,
- MeasuredText.Builder.HYPHENATION_MODE_NONE, false, null /* no hint */, mt);
+ MeasuredText.Builder.HYPHENATION_MODE_NONE, false, false, null /* no hint */, mt);
assertEquals(mt2, mt);
assertNotNull(mt2.getChars());
assertEquals("VVV", charsToString(mt.getChars()));
diff --git a/core/tests/coretests/src/android/text/TextLineTest.java b/core/tests/coretests/src/android/text/TextLineTest.java
index 213e2a9..34842a0 100644
--- a/core/tests/coretests/src/android/text/TextLineTest.java
+++ b/core/tests/coretests/src/android/text/TextLineTest.java
@@ -50,11 +50,11 @@
tl.set(paint, line, 0, line.length(), Layout.DIR_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT, false /* hasTabs */, null /* tabStops */,
0, 0 /* no ellipsis */, false /* useFallbackLinespace */);
- final float originalWidth = tl.metrics(null);
+ final float originalWidth = tl.metrics(null, null, false);
final float expandedWidth = 2 * originalWidth;
tl.justify(expandedWidth);
- final float newWidth = tl.metrics(null);
+ final float newWidth = tl.metrics(null, null, false);
TextLine.recycle(tl);
return Math.abs(newWidth - expandedWidth) < 0.5;
}
@@ -128,7 +128,7 @@
private void assertMeasurements(final TextLine tl, final int length, boolean trailing,
final float[] expected) {
for (int offset = 0; offset <= length; ++offset) {
- assertEquals(expected[offset], tl.measure(offset, trailing, null), 0.0f);
+ assertEquals(expected[offset], tl.measure(offset, trailing, null, null), 0.0f);
}
final boolean[] trailings = new boolean[length + 1];
@@ -318,7 +318,7 @@
tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
false /* hasTabs */, null /* tabStops */, 9, 12,
false /* useFallbackLineSpacing */);
- tl.measure(text.length(), false /* trailing */, null /* fmi */);
+ tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
assertFalse(span.mIsUsed);
}
@@ -335,7 +335,7 @@
tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
false /* hasTabs */, null /* tabStops */, 9, 12,
false /* useFallbackLineSpacing */);
- tl.measure(text.length(), false /* trailing */, null /* fmi */);
+ tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
assertTrue(span.mIsUsed);
}
@@ -352,7 +352,7 @@
tl.set(new TextPaint(), text, 0, text.length(), 1, Layout.DIRS_ALL_LEFT_TO_RIGHT,
false /* hasTabs */, null /* tabStops */, 9, 12,
false /* useFallbackLineSpacing */);
- tl.measure(text.length(), false /* trailing */, null /* fmi */);
+ tl.measure(text.length(), false /* trailing */, null /* fmi */, null);
assertTrue(span.mIsUsed);
}
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index d35dcab..9d32272 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -3172,6 +3172,32 @@
public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart,
int contextEnd, boolean isRtl, int offset,
@Nullable float[] advances, int advancesIndex) {
+ return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
+ advances, advancesIndex, null);
+ }
+
+ /**
+ * Measure the advance of each character within a run of text and also return the cursor
+ * position within the run.
+ *
+ * @see #getRunAdvance(char[], int, int, int, int, boolean, int) for more details.
+ *
+ * @param text the text to measure. Cannot be null.
+ * @param start the start index of the range to measure, inclusive
+ * @param end the end index of the range to measure, exclusive
+ * @param contextStart the start index of the shaping context, inclusive
+ * @param contextEnd the end index of the shaping context, exclusive
+ * @param isRtl whether the run is in RTL direction
+ * @param offset index of caret position
+ * @param advances the array that receives the computed character advances
+ * @param advancesIndex the start index from which the advances array is filled
+ * @param drawBounds the output parameter for the bounding box of drawing text, optional
+ * @return width measurement between start and offset
+ * @hide
+ */
+ public float getRunCharacterAdvance(@NonNull char[] text, int start, int end, int contextStart,
+ int contextEnd, boolean isRtl, int offset,
+ @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds) {
if (text == null) {
throw new IllegalArgumentException("text cannot be null");
}
@@ -3201,7 +3227,7 @@
}
return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
- isRtl, offset, advances, advancesIndex);
+ isRtl, offset, advances, advancesIndex, drawBounds);
}
/**
@@ -3228,6 +3254,29 @@
public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end,
int contextStart, int contextEnd, boolean isRtl, int offset,
@Nullable float[] advances, int advancesIndex) {
+ return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
+ advances, advancesIndex, null);
+ }
+
+ /**
+ * @see #getRunCharacterAdvance(char[], int, int, int, int, boolean, int, float[], int)
+ *
+ * @param text the text to measure. Cannot be null.
+ * @param start the index of the start of the range to measure
+ * @param end the index + 1 of the end of the range to measure
+ * @param contextStart the index of the start of the shaping context
+ * @param contextEnd the index + 1 of the end of the shaping context
+ * @param isRtl whether the run is in RTL direction
+ * @param offset index of caret position
+ * @param advances the array that receives the computed character advances
+ * @param advancesIndex the start index from which the advances array is filled
+ * @param drawBounds the output parameter for the bounding box of drawing text, optional
+ * @return width measurement between start and offset
+ * @hide
+ */
+ public float getRunCharacterAdvance(@NonNull CharSequence text, int start, int end,
+ int contextStart, int contextEnd, boolean isRtl, int offset,
+ @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds) {
if (text == null) {
throw new IllegalArgumentException("text cannot be null");
}
@@ -3260,7 +3309,7 @@
TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
final float result = getRunCharacterAdvance(buf, start - contextStart, end - contextStart,
0, contextEnd - contextStart, isRtl, offset - contextStart,
- advances, advancesIndex);
+ advances, advancesIndex, drawBounds);
TemporaryBuffer.recycle(buf);
return result;
}
@@ -3378,7 +3427,7 @@
int contextStart, int contextEnd, boolean isRtl, int offset);
private static native float nGetRunCharacterAdvance(long paintPtr, char[] text, int start,
int end, int contextStart, int contextEnd, boolean isRtl, int offset, float[] advances,
- int advancesIndex);
+ int advancesIndex, RectF drawingBounds);
private static native int nGetOffsetForAdvance(long paintPtr, char[] text, int start, int end,
int contextStart, int contextEnd, boolean isRtl, float advance);
private static native void nGetFontMetricsIntForText(long paintPtr, char[] text,
diff --git a/graphics/java/android/graphics/text/LineBreaker.java b/graphics/java/android/graphics/text/LineBreaker.java
index babcfc3..34ab833 100644
--- a/graphics/java/android/graphics/text/LineBreaker.java
+++ b/graphics/java/android/graphics/text/LineBreaker.java
@@ -22,6 +22,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Px;
+import android.text.Layout;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -182,6 +183,7 @@
private @HyphenationFrequency int mHyphenationFrequency = HYPHENATION_FREQUENCY_NONE;
private @JustificationMode int mJustificationMode = JUSTIFICATION_MODE_NONE;
private @Nullable int[] mIndents = null;
+ private boolean mUseBoundsForWidth = false;
/**
* Set break strategy.
@@ -231,13 +233,34 @@
}
/**
+ * Set true for using width of bounding box as a source of automatic line breaking.
+ *
+ * If this value is false, the automatic line breaking uses total amount of advances as text
+ * widths. By setting true, it uses joined all glyph bound's width as a width of the text.
+ *
+ * If the font has glyphs that have negative bearing X or its xMax is greater than advance,
+ * the glyph clipping can happen because the drawing area may be bigger. By setting this to
+ * true, the line breaker will break line based on bounding box, so clipping can be
+ * prevented.
+ *
+ * @param useBoundsForWidth True for using bounding box, false for advances.
+ * @return this builder instance
+ * @see Layout#getUseBoundsForWidth()
+ * @see StaticLayout.Builder#setUseBoundsForWidth(boolean)
+ */
+ public @NonNull Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
+ mUseBoundsForWidth = useBoundsForWidth;
+ return this;
+ }
+
+ /**
* Build a new LineBreaker with given parameters.
*
* You can reuse the Builder instance even after calling this method.
*/
public @NonNull LineBreaker build() {
return new LineBreaker(mBreakStrategy, mHyphenationFrequency, mJustificationMode,
- mIndents);
+ mIndents, mUseBoundsForWidth);
}
}
@@ -456,9 +479,9 @@
*/
private LineBreaker(@BreakStrategy int breakStrategy,
@HyphenationFrequency int hyphenationFrequency, @JustificationMode int justify,
- @Nullable int[] indents) {
+ @Nullable int[] indents, boolean useBoundsForWidth) {
mNativePtr = nInit(breakStrategy, hyphenationFrequency,
- justify == JUSTIFICATION_MODE_INTER_WORD, indents);
+ justify == JUSTIFICATION_MODE_INTER_WORD, indents, useBoundsForWidth);
sRegistry.registerNativeAllocation(this, mNativePtr);
}
@@ -493,7 +516,7 @@
@FastNative
private static native long nInit(@BreakStrategy int breakStrategy,
@HyphenationFrequency int hyphenationFrequency, boolean isJustified,
- @Nullable int[] indents);
+ @Nullable int[] indents, boolean useBoundsForWidth);
@CriticalNative
private static native long nGetReleaseFunc();
diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java
index fd08c8b..8317985 100644
--- a/graphics/java/android/graphics/text/MeasuredText.java
+++ b/graphics/java/android/graphics/text/MeasuredText.java
@@ -60,17 +60,19 @@
private final long mNativePtr;
private final boolean mComputeHyphenation;
private final boolean mComputeLayout;
+ private final boolean mComputeBounds;
@NonNull private final char[] mChars;
private final int mTop;
private final int mBottom;
// Use builder instead.
private MeasuredText(long ptr, @NonNull char[] chars, boolean computeHyphenation,
- boolean computeLayout, int top, int bottom) {
+ boolean computeLayout, boolean computeBounds, int top, int bottom) {
mNativePtr = ptr;
mChars = chars;
mComputeHyphenation = computeHyphenation;
mComputeLayout = computeLayout;
+ mComputeBounds = computeBounds;
mTop = top;
mBottom = bottom;
}
@@ -217,6 +219,7 @@
private final @NonNull char[] mText;
private boolean mComputeHyphenation = false;
private boolean mComputeLayout = true;
+ private boolean mComputeBounds = true;
private boolean mFastHyphenation = false;
private int mCurrentOffset = 0;
private @Nullable MeasuredText mHintMt = null;
@@ -434,6 +437,20 @@
}
/**
+ * Hidden API that tells native to calculate bounding box as well.
+ * Different from {@link #setComputeLayout(boolean)}, the result bounding box is not stored
+ * into MeasuredText instance. Just warm up the global word cache entry.
+ *
+ * @hide
+ * @param computeBounds
+ * @return
+ */
+ public @NonNull Builder setComputeBounds(boolean computeBounds) {
+ mComputeBounds = computeBounds;
+ return this;
+ }
+
+ /**
* Creates a MeasuredText.
*
* Once you called build() method, you can't reuse the Builder class again.
@@ -453,9 +470,9 @@
try {
long hintPtr = (mHintMt == null) ? 0 : mHintMt.getNativePtr();
long ptr = nBuildMeasuredText(mNativePtr, hintPtr, mText, mComputeHyphenation,
- mComputeLayout, mFastHyphenation);
+ mComputeLayout, mComputeBounds, mFastHyphenation);
final MeasuredText res = new MeasuredText(ptr, mText, mComputeHyphenation,
- mComputeLayout, mTop, mBottom);
+ mComputeLayout, mComputeBounds, mTop, mBottom);
sRegistry.registerNativeAllocation(res, ptr);
return res;
} finally {
@@ -517,6 +534,7 @@
@NonNull char[] text,
boolean computeHyphenation,
boolean computeLayout,
+ boolean computeBounds,
boolean fastHyphenationMode);
private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index e359145..bcfb4c8 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -84,7 +84,8 @@
float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags,
const Typeface* typeface, const uint16_t* buf, size_t start,
- size_t count, size_t bufSize, float* advances) {
+ size_t count, size_t bufSize, float* advances,
+ minikin::MinikinRect* bounds) {
minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface);
const minikin::U16StringPiece textBuf(buf, bufSize);
const minikin::Range range(start, start + count);
@@ -92,7 +93,7 @@
const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
return minikin::Layout::measureText(textBuf, range, bidiFlags, minikinPaint, startHyphen,
- endHyphen, advances);
+ endHyphen, advances, bounds);
}
minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin::Bidi bidiFlags,
diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h
index 51960b0..61bc881 100644
--- a/libs/hwui/hwui/MinikinUtils.h
+++ b/libs/hwui/hwui/MinikinUtils.h
@@ -51,10 +51,9 @@
static void getBounds(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface,
const uint16_t* buf, size_t bufSize, minikin::MinikinRect* out);
- static float measureText(const Paint* paint, minikin::Bidi bidiFlags,
- const Typeface* typeface, const uint16_t* buf,
- size_t start, size_t count, size_t bufSize,
- float* advances);
+ static float measureText(const Paint* paint, minikin::Bidi bidiFlags, const Typeface* typeface,
+ const uint16_t* buf, size_t start, size_t count, size_t bufSize,
+ float* advances, minikin::MinikinRect* bounds);
static minikin::MinikinExtent getFontExtent(const Paint* paint, minikin::Bidi bidiFlags,
const Typeface* typeface, const uint16_t* buf,
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index 1ba7f70..a17f2f7 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -53,6 +53,17 @@
namespace android {
+namespace {
+
+void copyMinikinRectToSkRect(const minikin::MinikinRect& minikinRect, SkRect* skRect) {
+ skRect->fLeft = minikinRect.mLeft;
+ skRect->fTop = minikinRect.mTop;
+ skRect->fRight = minikinRect.mRight;
+ skRect->fBottom = minikinRect.mBottom;
+}
+
+} // namespace
+
static void getPosTextPath(const SkFont& font, const uint16_t glyphs[], int count,
const SkPoint pos[], SkPath* dst) {
dst->reset();
@@ -101,8 +112,8 @@
float measured = 0;
std::unique_ptr<float[]> advancesArray(new float[count]);
- MinikinUtils::measureText(&paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text,
- 0, count, count, advancesArray.get());
+ MinikinUtils::measureText(&paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, 0,
+ count, count, advancesArray.get(), nullptr);
for (int i = 0; i < count; i++) {
// traverse in the given direction
@@ -192,9 +203,10 @@
if (advances) {
advancesArray.reset(new jfloat[count]);
}
- const float advance = MinikinUtils::measureText(paint,
- static_cast<minikin::Bidi>(bidiFlags), typeface, text, start, count, contextCount,
- advancesArray.get());
+ minikin::MinikinRect bounds;
+ const float advance = MinikinUtils::measureText(
+ paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, start, count,
+ contextCount, advancesArray.get(), &bounds);
if (advances) {
env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get());
}
@@ -232,7 +244,7 @@
minikin::Bidi bidiFlags = dir == 1 ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
std::unique_ptr<float[]> advancesArray(new float[count]);
MinikinUtils::measureText(paint, bidiFlags, typeface, text, start, count, start + count,
- advancesArray.get());
+ advancesArray.get(), nullptr);
size_t result = minikin::GraphemeBreak::getTextRunCursor(advancesArray.get(), text,
start, count, offset, moveOpt);
return static_cast<jint>(result);
@@ -496,7 +508,7 @@
static jfloat doRunAdvance(JNIEnv* env, const Paint* paint, const Typeface* typeface,
const jchar buf[], jint start, jint count, jint bufSize,
jboolean isRtl, jint offset, jfloatArray advances,
- jint advancesIndex) {
+ jint advancesIndex, SkRect* drawBounds) {
if (advances) {
size_t advancesLength = env->GetArrayLength(advances);
if ((size_t)(count + advancesIndex) > advancesLength) {
@@ -505,14 +517,23 @@
}
}
minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
+ minikin::MinikinRect bounds;
if (offset == start + count && advances == nullptr) {
- return MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
- bufSize, nullptr);
+ float result =
+ MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
+ bufSize, nullptr, drawBounds ? &bounds : nullptr);
+ if (drawBounds) {
+ copyMinikinRectToSkRect(bounds, drawBounds);
+ }
+ return result;
}
std::unique_ptr<float[]> advancesArray(new float[count]);
MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
- advancesArray.get());
+ advancesArray.get(), drawBounds ? &bounds : nullptr);
+ if (drawBounds) {
+ copyMinikinRectToSkRect(bounds, drawBounds);
+ }
float result = minikin::getRunAdvance(advancesArray.get(), buf, start, count, offset);
if (advances) {
minikin::distributeAdvances(advancesArray.get(), buf, start, count);
@@ -528,7 +549,7 @@
ScopedCharArrayRO textArray(env, text);
jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
start - contextStart, end - start, contextEnd - contextStart,
- isRtl, offset - contextStart, nullptr, 0);
+ isRtl, offset - contextStart, nullptr, 0, nullptr);
return result;
}
@@ -536,13 +557,19 @@
jcharArray text, jint start, jint end,
jint contextStart, jint contextEnd,
jboolean isRtl, jint offset,
- jfloatArray advances, jint advancesIndex) {
+ jfloatArray advances, jint advancesIndex,
+ jobject drawBounds) {
const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
const Typeface* typeface = paint->getAndroidTypeface();
ScopedCharArrayRO textArray(env, text);
+ SkRect skDrawBounds;
jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
start - contextStart, end - start, contextEnd - contextStart,
- isRtl, offset - contextStart, advances, advancesIndex);
+ isRtl, offset - contextStart, advances, advancesIndex,
+ drawBounds ? &skDrawBounds : nullptr);
+ if (drawBounds != nullptr) {
+ GraphicsJNI::rect_to_jrectf(skDrawBounds, env, drawBounds);
+ }
return result;
}
@@ -551,7 +578,7 @@
minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
std::unique_ptr<float[]> advancesArray(new float[count]);
MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
- advancesArray.get());
+ advancesArray.get(), nullptr);
return minikin::getOffsetForAdvance(advancesArray.get(), buf, start, count, advance);
}
@@ -1081,7 +1108,7 @@
(void*)PaintGlue::getCharArrayBounds},
{"nHasGlyph", "(JILjava/lang/String;)Z", (void*)PaintGlue::hasGlyph},
{"nGetRunAdvance", "(J[CIIIIZI)F", (void*)PaintGlue::getRunAdvance___CIIIIZI_F},
- {"nGetRunCharacterAdvance", "(J[CIIIIZI[FI)F",
+ {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
(void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
{"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
{"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",
diff --git a/libs/hwui/jni/text/LineBreaker.cpp b/libs/hwui/jni/text/LineBreaker.cpp
index 9ebf23c..c512256 100644
--- a/libs/hwui/jni/text/LineBreaker.cpp
+++ b/libs/hwui/jni/text/LineBreaker.cpp
@@ -51,13 +51,12 @@
// set text and set a number of parameters for creating a layout (width, tabstops, strategy,
// hyphenFrequency)
-static jlong nInit(JNIEnv* env, jclass /* unused */,
- jint breakStrategy, jint hyphenationFrequency, jboolean isJustified, jintArray indents) {
+static jlong nInit(JNIEnv* env, jclass /* unused */, jint breakStrategy, jint hyphenationFrequency,
+ jboolean isJustified, jintArray indents, jboolean useBoundsForWidth) {
return reinterpret_cast<jlong>(new minikin::android::StaticLayoutNative(
static_cast<minikin::BreakStrategy>(breakStrategy),
- static_cast<minikin::HyphenationFrequency>(hyphenationFrequency),
- isJustified,
- jintArrayToFloatVector(env, indents)));
+ static_cast<minikin::HyphenationFrequency>(hyphenationFrequency), isJustified,
+ jintArrayToFloatVector(env, indents), useBoundsForWidth));
}
static void nFinish(jlong nativePtr) {
@@ -128,39 +127,44 @@
}
static const JNINativeMethod gMethods[] = {
- // Fast Natives
- {"nInit", "("
- "I" // breakStrategy
- "I" // hyphenationFrequency
- "Z" // isJustified
- "[I" // indents
- ")J", (void*) nInit},
+ // Fast Natives
+ {"nInit",
+ "("
+ "I" // breakStrategy
+ "I" // hyphenationFrequency
+ "Z" // isJustified
+ "[I" // indents
+ "Z" // useBoundsForWidth
+ ")J",
+ (void*)nInit},
- // Critical Natives
- {"nGetReleaseFunc", "()J", (void*) nGetReleaseFunc},
+ // Critical Natives
+ {"nGetReleaseFunc", "()J", (void*)nGetReleaseFunc},
- // Regular JNI
- {"nComputeLineBreaks", "("
- "J" // nativePtr
- "[C" // text
- "J" // MeasuredParagraph ptr.
- "I" // length
- "F" // firstWidth
- "I" // firstWidthLineCount
- "F" // restWidth
- "[F" // variableTabStops
- "F" // defaultTabStop
- "I" // indentsOffset
- ")J", (void*) nComputeLineBreaks},
+ // Regular JNI
+ {"nComputeLineBreaks",
+ "("
+ "J" // nativePtr
+ "[C" // text
+ "J" // MeasuredParagraph ptr.
+ "I" // length
+ "F" // firstWidth
+ "I" // firstWidthLineCount
+ "F" // restWidth
+ "[F" // variableTabStops
+ "F" // defaultTabStop
+ "I" // indentsOffset
+ ")J",
+ (void*)nComputeLineBreaks},
- // Result accessors, CriticalNatives
- {"nGetLineCount", "(J)I", (void*)nGetLineCount},
- {"nGetLineBreakOffset", "(JI)I", (void*)nGetLineBreakOffset},
- {"nGetLineWidth", "(JI)F", (void*)nGetLineWidth},
- {"nGetLineAscent", "(JI)F", (void*)nGetLineAscent},
- {"nGetLineDescent", "(JI)F", (void*)nGetLineDescent},
- {"nGetLineFlag", "(JI)I", (void*)nGetLineFlag},
- {"nGetReleaseResultFunc", "()J", (void*)nGetReleaseResultFunc},
+ // Result accessors, CriticalNatives
+ {"nGetLineCount", "(J)I", (void*)nGetLineCount},
+ {"nGetLineBreakOffset", "(JI)I", (void*)nGetLineBreakOffset},
+ {"nGetLineWidth", "(JI)F", (void*)nGetLineWidth},
+ {"nGetLineAscent", "(JI)F", (void*)nGetLineAscent},
+ {"nGetLineDescent", "(JI)F", (void*)nGetLineDescent},
+ {"nGetLineFlag", "(JI)I", (void*)nGetLineFlag},
+ {"nGetReleaseResultFunc", "()J", (void*)nGetReleaseResultFunc},
};
int register_android_graphics_text_LineBreaker(JNIEnv* env) {
diff --git a/libs/hwui/jni/text/MeasuredText.cpp b/libs/hwui/jni/text/MeasuredText.cpp
index 081713a..f6ae169 100644
--- a/libs/hwui/jni/text/MeasuredText.cpp
+++ b/libs/hwui/jni/text/MeasuredText.cpp
@@ -81,13 +81,14 @@
// Regular JNI
static jlong nBuildMeasuredText(JNIEnv* env, jclass /* unused */, jlong builderPtr, jlong hintPtr,
jcharArray javaText, jboolean computeHyphenation,
- jboolean computeLayout, jboolean fastHyphenationMode) {
+ jboolean computeLayout, jboolean computeBounds,
+ jboolean fastHyphenationMode) {
ScopedCharArrayRO text(env, javaText);
const minikin::U16StringPiece textBuffer(text.get(), text.size());
// Pass the ownership to Java.
return toJLong(toBuilder(builderPtr)
- ->build(textBuffer, computeHyphenation, computeLayout,
+ ->build(textBuffer, computeHyphenation, computeLayout, computeBounds,
fastHyphenationMode, toMeasuredParagraph(hintPtr))
.release());
}
@@ -160,7 +161,7 @@
{"nInitBuilder", "()J", (void*)nInitBuilder},
{"nAddStyleRun", "(JJIIIIZ)V", (void*)nAddStyleRun},
{"nAddReplacementRun", "(JJIIF)V", (void*)nAddReplacementRun},
- {"nBuildMeasuredText", "(JJ[CZZZ)J", (void*)nBuildMeasuredText},
+ {"nBuildMeasuredText", "(JJ[CZZZZ)J", (void*)nBuildMeasuredText},
{"nFreeBuilder", "(J)V", (void*)nFreeBuilder},
};