Add cluster count API
This is a preparation of the inter character justification
Bug: 283193133
Test: CtsTextTestCases
Test: minikin_tests
Change-Id: Ia3a69a83cc1a3cde56d8a66a7cab1c85c7109050
diff --git a/core/api/current.txt b/core/api/current.txt
index f245b5c..cc376d8 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -47216,6 +47216,7 @@
method public int getLineForOffset(int);
method public int getLineForVertical(int);
method public float getLineLeft(int);
+ method @FlaggedApi("com.android.text.flags.inter_character_justification") @IntRange(from=0) public int getLineLetterSpacingUnitCount(@IntRange(from=0) int, boolean);
method public float getLineMax(int);
method public float getLineRight(int);
method @FlaggedApi("com.android.text.flags.use_bounds_for_width") public final float getLineSpacingAmount();
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index 4c81888..a6d3bb4 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -454,7 +454,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, null, false));
+ mMax = (int) Math.ceil(line.metrics(null, null, false, null));
TextLine.recycle(line);
}
@@ -603,7 +603,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.mDrawingBounds, false));
+ fm.width = (int) Math.ceil(line.metrics(fm, fm.mDrawingBounds, false, null));
TextLine.recycle(line);
return fm;
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 89aceb9..42d00d8 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -18,6 +18,7 @@
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
+import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION;
import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
@@ -50,8 +51,10 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
/**
* A base class that manages text layout in visual elements on
@@ -669,7 +672,8 @@
int start = previousLineEnd;
previousLineEnd = getLineStart(lineNum + 1);
final boolean justify = isJustificationRequired(lineNum);
- int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
+ int end = getLineVisibleEnd(lineNum, start, previousLineEnd,
+ true /* trailingSpaceAtLastLineIsVisible */);
paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
@@ -1056,7 +1060,7 @@
if (isJustificationRequired(line)) {
tl.justify(getJustifyWidth(line));
}
- tl.metrics(null, rectF, false);
+ tl.metrics(null, rectF, false, null);
float lineLeft = rectF.left;
float lineRight = rectF.right;
@@ -1456,7 +1460,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, null);
+ float wid = tl.measure(offset - start, trailing, null, null, null);
TextLine.recycle(tl);
if (clamped && wid > mWidth) {
@@ -1792,12 +1796,69 @@
if (isJustificationRequired(line)) {
tl.justify(getJustifyWidth(line));
}
- final float width = tl.metrics(null, null, mUseBoundsForWidth);
+ final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
TextLine.recycle(tl);
return width;
}
/**
+ * Returns the number of letter spacing unit in the line.
+ *
+ * <p>
+ * This API returns a number of letters that is a target of letter spacing. The letter spacing
+ * won't be added to the middle of the characters that are needed to be treated as a single,
+ * e.g. ligatured or conjunct form. Note that this value is different from the number of]
+ * grapheme clusters that is calculated by {@link BreakIterator#getCharacterInstance(Locale)}.
+ * For example, if the "fi" is ligatured, the ligatured form is treated as single uni and letter
+ * spacing is not added, but it has two separate grapheme cluster.
+ *
+ * <p>
+ * This value is used for calculating the letter spacing amount for the justification because
+ * the letter spacing is applied between clusters. For example, if extra {@code W} pixels needed
+ * to be filled by letter spacing, the amount of letter spacing to be applied is
+ * {@code W}/(letter spacing unit count - 1) px.
+ *
+ * @param line the index of the line
+ * @param includeTrailingWhitespace whether to include trailing whitespace
+ * @return the number of cluster count in the line.
+ */
+ @IntRange(from = 0)
+ @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION)
+ public int getLineLetterSpacingUnitCount(@IntRange(from = 0) int line,
+ boolean includeTrailingWhitespace) {
+ final int start = getLineStart(line);
+ final int end = includeTrailingWhitespace ? getLineEnd(line)
+ : getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+ false // trailingSpaceAtLastLineIsVisible: Treating trailing whitespaces at
+ // the last line as a invisible chars for single line justification.
+ );
+
+ final Directions directions = getLineDirections(line);
+ // Returned directions can actually be null
+ if (directions == null) {
+ return 0;
+ }
+ final int dir = getParagraphDirection(line);
+
+ final TextLine tl = TextLine.obtain();
+ final TextPaint paint = mWorkPaint;
+ paint.set(mPaint);
+ paint.setStartHyphenEdit(getStartHyphenEdit(line));
+ paint.setEndHyphenEdit(getEndHyphenEdit(line));
+ tl.set(paint, mText, start, end, dir, directions,
+ false, null, // tab width is not used for cluster counting.
+ getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line),
+ isFallbackLineSpacingEnabled());
+ if (mLineInfo == null) {
+ mLineInfo = new TextLine.LineInfo();
+ }
+ mLineInfo.setClusterCount(0);
+ tl.metrics(null, null, mUseBoundsForWidth, mLineInfo);
+ TextLine.recycle(tl);
+ return mLineInfo.getClusterCount();
+ }
+
+ /**
* Returns the signed horizontal extent of the specified line, excluding
* leading margin. If full is false, excludes trailing whitespace.
* @param line the index of the line
@@ -1823,7 +1884,7 @@
if (isJustificationRequired(line)) {
tl.justify(getJustifyWidth(line));
}
- final float width = tl.metrics(null, null, mUseBoundsForWidth);
+ final float width = tl.metrics(null, null, mUseBoundsForWidth, null);
TextLine.recycle(tl);
return width;
}
@@ -2432,14 +2493,21 @@
* is not counted) on the specified line.
*/
public int getLineVisibleEnd(int line) {
- return getLineVisibleEnd(line, getLineStart(line), getLineStart(line+1));
+ return getLineVisibleEnd(line, getLineStart(line), getLineStart(line + 1),
+ true /* trailingSpaceAtLastLineIsVisible */);
}
- private int getLineVisibleEnd(int line, int start, int end) {
+ private int getLineVisibleEnd(int line, int start, int end,
+ boolean trailingSpaceAtLastLineIsVisible) {
CharSequence text = mText;
char ch;
- if (line == getLineCount() - 1) {
- return end;
+
+ // Historically, trailing spaces at the last line is counted as visible. However, this
+ // doesn't work well for justification.
+ if (trailingSpaceAtLastLineIsVisible) {
+ if (line == getLineCount() - 1) {
+ return end;
+ }
}
for (; end > start; end--) {
@@ -2939,7 +3007,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, null, useBoundsForWidth));
+ return margin + Math.abs(tl.metrics(null, null, useBoundsForWidth, null));
} finally {
TextLine.recycle(tl);
if (mt != null) {
@@ -3337,6 +3405,8 @@
private boolean mUseBoundsForWidth;
private @Nullable Paint.FontMetrics mMinimumFontMetrics;
+ private TextLine.LineInfo mLineInfo = null;
+
/** @hide */
@IntDef(prefix = { "DIR_" }, value = {
DIR_LEFT_TO_RIGHT,
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index f9abec0..135935c 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -76,6 +76,21 @@
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.
@@ -270,7 +285,7 @@
// width.
return;
}
- final float width = Math.abs(measure(end, false, null, null));
+ final float width = Math.abs(measure(end, false, null, null, null));
mAddedWidthForJustify = (justifyWidth - width) / spaces;
mIsJustifying = true;
}
@@ -315,10 +330,12 @@
* @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) {
+ public float metrics(FontMetricsInt fmi, @Nullable RectF drawBounds, boolean returnDrawWidth,
+ @Nullable LineInfo lineInfo) {
if (returnDrawWidth) {
if (drawBounds == null) {
if (mTmpRectForMeasure == null) {
@@ -327,7 +344,7 @@
drawBounds = mTmpRectForMeasure;
}
drawBounds.setEmpty();
- float w = measure(mLen, false, fmi, drawBounds);
+ float w = measure(mLen, false, fmi, drawBounds, lineInfo);
float boundsWidth = drawBounds.width();
if (Math.abs(w) > boundsWidth) {
return w;
@@ -337,7 +354,7 @@
return Math.signum(w) * boundsWidth;
}
} else {
- return measure(mLen, false, fmi, drawBounds);
+ return measure(mLen, false, fmi, drawBounds, lineInfo);
}
}
@@ -407,12 +424,13 @@
* 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) {
+ @NonNull FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable LineInfo lineInfo) {
if (offset > mLen) {
throw new IndexOutOfBoundsException(
"offset(" + offset + ") should be less than line limit(" + mLen + ")");
@@ -437,16 +455,16 @@
if (targetIsInThisSegment && sameDirection) {
return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null,
- 0, h);
+ 0, h, lineInfo);
}
final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds,
- null, 0, h);
+ null, 0, h, lineInfo);
h += sameDirection ? segmentWidth : -segmentWidth;
if (targetIsInThisSegment) {
return h + measureRun(segStart, offset, j, runIsRtl, null, null, null, 0,
- h);
+ h, lineInfo);
}
if (j != runLimit) { // charAt(j) == TAB_CHAR
@@ -537,7 +555,8 @@
final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl;
final float segmentWidth =
- measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0);
+ measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0,
+ null);
final float oldh = h;
h += sameDirection ? segmentWidth : -segmentWidth;
@@ -578,7 +597,7 @@
}
/**
- * @see #measure(int, boolean, FontMetricsInt, RectF)
+ * @see #measure(int, boolean, FontMetricsInt, RectF, LineInfo)
* @return The measure results for all possible offsets
*/
@VisibleForTesting
@@ -610,7 +629,7 @@
final float previousSegEndHorizontal = measurement[segStart];
final float width =
measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart,
- 0);
+ 0, null);
horizontal += sameDirection ? width : -width;
float currHorizontal = sameDirection ? oldHorizontal : horizontal;
@@ -675,14 +694,14 @@
boolean needWidth) {
if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
- float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0);
+ float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null);
handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
- y, bottom, null, null, false, null, 0);
+ y, bottom, null, null, false, null, 0, null);
return w;
}
return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
- y, bottom, null, null, needWidth, null, 0);
+ y, bottom, null, null, needWidth, null, 0, null);
}
/**
@@ -698,19 +717,20 @@
* @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.
* @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) {
+ int advancesIndex, float x, @Nullable LineInfo lineInfo) {
if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
- float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0);
+ float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null);
return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi,
- drawBounds, true, advances, advancesIndex);
+ drawBounds, true, advances, advancesIndex, lineInfo);
}
return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds,
- true, advances, advancesIndex);
+ true, advances, advancesIndex, lineInfo);
}
/**
@@ -729,14 +749,14 @@
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, null, 0, 0);
+ float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null);
handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null,
- false, null, 0);
+ false, null, 0, null);
return w;
}
return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null,
- needWidth, null, 0);
+ needWidth, null, 0, null);
}
@@ -1077,16 +1097,35 @@
private float getRunAdvance(TextPaint wp, int start, int end, int contextStart, int contextEnd,
boolean runIsRtl, int offset, @Nullable float[] advances, int advancesIndex,
- RectF drawingBounds) {
+ RectF drawingBounds, @Nullable LineInfo lineInfo) {
+ if (lineInfo != null) {
+ if (mRunInfo == null) {
+ mRunInfo = new Paint.RunInfo();
+ }
+ mRunInfo.setClusterCount(0);
+ } else {
+ mRunInfo = null;
+ }
if (mCharsValid) {
- return wp.getRunCharacterAdvance(mChars, start, end, contextStart, contextEnd,
- runIsRtl, offset, advances, advancesIndex, drawingBounds);
+ 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;
- if (mComputed == null || advances != null) {
- return wp.getRunCharacterAdvance(mText, delta + start, delta + end,
+ // 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);
+ delta + offset, advances, advancesIndex, drawingBounds, mRunInfo);
+ if (lineInfo != null) {
+ lineInfo.setClusterCount(
+ lineInfo.getClusterCount() + mRunInfo.getClusterCount());
+ }
+ return r;
} else {
if (drawingBounds != null) {
if (mTmpRectForPrecompute == null) {
@@ -1120,6 +1159,7 @@
* @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.
* @return the signed width of the run based on the run direction; only
* valid if needWidth is true
*/
@@ -1128,7 +1168,7 @@
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 float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) {
if (mIsJustifying) {
wp.setWordSpacing(mAddedWidthForJustify);
@@ -1155,7 +1195,8 @@
mTmpRectForPaintAPI = new RectF();
}
totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset,
- advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI);
+ advances, advancesIndex, drawBounds == null ? null : mTmpRectForPaintAPI,
+ lineInfo);
if (drawBounds != null) {
if (runIsRtl) {
mTmpRectForPaintAPI.offset(x - totalWidth, 0);
@@ -1206,9 +1247,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, null);
+ contextEnd, runIsRtl, decorationStart, null, 0, null, null);
float decorationEndAdvance = getRunAdvance(wp, start, end, contextStart,
- contextEnd, runIsRtl, decorationEnd, null, 0, null);
+ contextEnd, runIsRtl, decorationEnd, null, 0, null, null);
final float decorationXLeft, decorationXRight;
if (runIsRtl) {
decorationXLeft = rightX - decorationEndAdvance;
@@ -1377,6 +1418,7 @@
* @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.
* @return the signed width of the run based on the run direction; only
* valid if needWidth is true
*/
@@ -1384,7 +1426,7 @@
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 float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) {
if (measureLimit < start || measureLimit > limit) {
throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
@@ -1431,7 +1473,7 @@
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);
+ advancesIndex, lineInfo);
}
// Shaping needs to take into account context up to metric boundaries,
@@ -1523,7 +1565,7 @@
consumer, x, top, y, bottom, fmi, drawBounds,
needWidth || activeEnd < measureLimit,
Math.min(activeEnd, mlimit), mDecorations,
- advances, advancesIndex + activeStart - start);
+ advances, advancesIndex + activeStart - start, lineInfo);
activeStart = j;
activePaint.set(wp);
@@ -1551,7 +1593,7 @@
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);
+ advances, advancesIndex + activeStart - start, lineInfo);
}
return x - originalX;
diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java
index bf56df1..0dec756 100644
--- a/core/tests/coretests/src/android/graphics/PaintTest.java
+++ b/core/tests/coretests/src/android/graphics/PaintTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertNotEquals;
import android.test.InstrumentationTestCase;
+import android.text.TextUtils;
import androidx.test.filters.SmallTest;
@@ -362,4 +363,44 @@
// = 30
assertEquals(30.0f, p.getUnderlineThickness(), 0.5f);
}
+
+ private int getClusterCount(Paint p, String text) {
+ Paint.RunInfo runInfo = new Paint.RunInfo();
+ p.getRunCharacterAdvance(text, 0, text.length(), 0, text.length(), false, 0, null, 0, null,
+ runInfo);
+ int ccByString = runInfo.getClusterCount();
+ runInfo.setClusterCount(0);
+ char[] buf = new char[text.length()];
+ TextUtils.getChars(text, 0, text.length(), buf, 0);
+ p.getRunCharacterAdvance(buf, 0, buf.length, 0, buf.length, false, 0, null, 0, null,
+ runInfo);
+ int ccByChars = runInfo.getClusterCount();
+ assertEquals(ccByChars, ccByString);
+ return ccByChars;
+ }
+
+ public void testCluster() {
+ final Paint p = new Paint();
+ p.setTextSize(100);
+
+ // Regular String
+ assertEquals(1, getClusterCount(p, "A"));
+ assertEquals(2, getClusterCount(p, "AB"));
+
+ // Ligature is in the same cluster
+ assertEquals(1, getClusterCount(p, "fi")); // Ligature
+ p.setFontFeatureSettings("'liga' off");
+ assertEquals(2, getClusterCount(p, "fi")); // Ligature is disabled
+ p.setFontFeatureSettings("");
+
+ // Combining character
+ assertEquals(1, getClusterCount(p, "\u0061\u0300")); // A + COMBINING GRAVE ACCENT
+
+ // BiDi
+ final String rtlStr = "\u05D0\u05D1\u05D2";
+ final String ltrStr = "abc";
+ assertEquals(3, getClusterCount(p, rtlStr));
+ assertEquals(6, getClusterCount(p, rtlStr + ltrStr));
+ assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr));
+ }
}
diff --git a/core/tests/coretests/src/android/text/TextLineTest.java b/core/tests/coretests/src/android/text/TextLineTest.java
index 34842a0..a31992c 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, null, false);
+ final float originalWidth = tl.metrics(null, null, false, null);
final float expandedWidth = 2 * originalWidth;
tl.justify(expandedWidth);
- final float newWidth = tl.metrics(null, null, false);
+ final float newWidth = tl.metrics(null, null, false, null);
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, null), 0.0f);
+ assertEquals(expected[offset], tl.measure(offset, trailing, null, 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 */, null);
+ tl.measure(text.length(), false /* trailing */, null /* fmi */, null, 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 */, null);
+ tl.measure(text.length(), false /* trailing */, null /* fmi */, null, 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 */, null);
+ tl.measure(text.length(), false /* trailing */, null /* fmi */, null, null);
assertTrue(span.mIsUsed);
}
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index f10cdb8..3e8f442 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -2474,6 +2474,19 @@
nGetFontMetricsInt(mNativePaint, metrics, true);
}
+ /** @hide */
+ public static final class RunInfo {
+ private int mClusterCount = 0;
+
+ public int getClusterCount() {
+ return mClusterCount;
+ }
+
+ public void setClusterCount(int clusterCount) {
+ mClusterCount = clusterCount;
+ }
+ }
+
/**
* Return the recommend line spacing based on the current typeface and
* text size.
@@ -3320,7 +3333,7 @@
int contextEnd, boolean isRtl, int offset,
@Nullable float[] advances, int advancesIndex) {
return getRunCharacterAdvance(text, start, end, contextStart, contextEnd, isRtl, offset,
- advances, advancesIndex, null);
+ advances, advancesIndex, null, null);
}
/**
@@ -3339,12 +3352,14 @@
* @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
+ * @param runInfo the output parameter for storing run information.
* @return width measurement between start and offset
- * @hide
+ * @hide TODO: Reorganize APIs
*/
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) {
+ @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+ @Nullable RunInfo runInfo) {
if (text == null) {
throw new IllegalArgumentException("text cannot be null");
}
@@ -3370,11 +3385,14 @@
}
if (end == start) {
+ if (runInfo != null) {
+ runInfo.setClusterCount(0);
+ }
return 0.0f;
}
return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd,
- isRtl, offset, advances, advancesIndex, drawBounds);
+ isRtl, offset, advances, advancesIndex, drawBounds, runInfo);
}
/**
@@ -3402,7 +3420,7 @@
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);
+ advances, advancesIndex, null, null);
}
/**
@@ -3418,12 +3436,14 @@
* @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
+ * @param runInfo an optional output parameter for filling run information.
* @return width measurement between start and offset
- * @hide
+ * @hide TODO: Reorganize APIs
*/
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) {
+ @Nullable float[] advances, int advancesIndex, @Nullable RectF drawBounds,
+ @Nullable RunInfo runInfo) {
if (text == null) {
throw new IllegalArgumentException("text cannot be null");
}
@@ -3456,7 +3476,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, drawBounds);
+ advances, advancesIndex, drawBounds, runInfo);
TemporaryBuffer.recycle(buf);
return result;
}
@@ -3574,7 +3594,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, RectF drawingBounds);
+ int advancesIndex, RectF drawingBounds, RunInfo runInfo);
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/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp
index 7552b56d..833069f 100644
--- a/libs/hwui/hwui/MinikinUtils.cpp
+++ b/libs/hwui/hwui/MinikinUtils.cpp
@@ -96,7 +96,7 @@
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,
- minikin::MinikinRect* bounds) {
+ minikin::MinikinRect* bounds, uint32_t* clusterCount) {
minikin::MinikinPaint minikinPaint = prepareMinikinPaint(paint, typeface);
const minikin::U16StringPiece textBuf(buf, bufSize);
const minikin::Range range(start, start + count);
@@ -104,7 +104,7 @@
const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit();
return minikin::Layout::measureText(textBuf, range, bidiFlags, minikinPaint, startHyphen,
- endHyphen, advances, bounds);
+ endHyphen, advances, bounds, clusterCount);
}
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 61bc881..f8574ee 100644
--- a/libs/hwui/hwui/MinikinUtils.h
+++ b/libs/hwui/hwui/MinikinUtils.h
@@ -53,7 +53,7 @@
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);
+ float* advances, minikin::MinikinRect* bounds, uint32_t* clusterCount);
static minikin::MinikinExtent getFontExtent(const Paint* paint, minikin::Bidi bidiFlags,
const Typeface* typeface, const uint16_t* buf,
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 7cc4866..8315c4c 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -247,6 +247,9 @@
static jfieldID gFontMetricsInt_bottom;
static jfieldID gFontMetricsInt_leading;
+static jclass gRunInfo_class;
+static jfieldID gRunInfo_clusterCount;
+
///////////////////////////////////////////////////////////////////////////////
void GraphicsJNI::get_jrect(JNIEnv* env, jobject obj, int* L, int* T, int* R, int* B)
@@ -511,6 +514,10 @@
return descent - ascent + leading;
}
+void GraphicsJNI::set_cluster_count_to_run_info(JNIEnv* env, jobject runInfo, jint clusterCount) {
+ env->SetIntField(runInfo, gRunInfo_clusterCount, clusterCount);
+}
+
///////////////////////////////////////////////////////////////////////////////////////////
jobject GraphicsJNI::createBitmapRegionDecoder(JNIEnv* env, BitmapRegionDecoderWrapper* bitmap) {
@@ -834,5 +841,10 @@
gFontMetricsInt_bottom = GetFieldIDOrDie(env, gFontMetricsInt_class, "bottom", "I");
gFontMetricsInt_leading = GetFieldIDOrDie(env, gFontMetricsInt_class, "leading", "I");
+ gRunInfo_class = FindClassOrDie(env, "android/graphics/Paint$RunInfo");
+ gRunInfo_class = MakeGlobalRefOrDie(env, gRunInfo_class);
+
+ gRunInfo_clusterCount = GetFieldIDOrDie(env, gRunInfo_class, "mClusterCount", "I");
+
return 0;
}
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index b9fff36..b0a1074 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -77,6 +77,8 @@
static SkRect* jrect_to_rect(JNIEnv*, jobject jrect, SkRect*);
static void rect_to_jrectf(const SkRect&, JNIEnv*, jobject jrectf);
+ static void set_cluster_count_to_run_info(JNIEnv* env, jobject runInfo, jint clusterCount);
+
static void set_jpoint(JNIEnv*, jobject jrect, int x, int y);
static SkIPoint* jpoint_to_ipoint(JNIEnv*, jobject jpoint, SkIPoint* point);
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index d84b73d..286f06a 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -114,7 +114,7 @@
std::unique_ptr<float[]> advancesArray(new float[count]);
MinikinUtils::measureText(&paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, 0,
- count, count, advancesArray.get(), nullptr);
+ count, count, advancesArray.get(), nullptr, nullptr);
for (int i = 0; i < count; i++) {
// traverse in the given direction
@@ -206,7 +206,7 @@
}
const float advance = MinikinUtils::measureText(
paint, static_cast<minikin::Bidi>(bidiFlags), typeface, text, start, count,
- contextCount, advancesArray.get(), nullptr);
+ contextCount, advancesArray.get(), nullptr, nullptr);
if (advances) {
env->SetFloatArrayRegion(advances, advancesIndex, count, advancesArray.get());
}
@@ -244,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(), nullptr);
+ advancesArray.get(), nullptr, nullptr);
size_t result = minikin::GraphemeBreak::getTextRunCursor(advancesArray.get(), text,
start, count, offset, moveOpt);
return static_cast<jint>(result);
@@ -508,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, SkRect* drawBounds) {
+ jint advancesIndex, SkRect* drawBounds, uint32_t* clusterCount) {
if (advances) {
size_t advancesLength = env->GetArrayLength(advances);
if ((size_t)(count + advancesIndex) > advancesLength) {
@@ -519,9 +519,9 @@
minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR;
minikin::MinikinRect bounds;
if (offset == start + count && advances == nullptr) {
- float result =
- MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
- bufSize, nullptr, drawBounds ? &bounds : nullptr);
+ float result = MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count,
+ bufSize, nullptr,
+ drawBounds ? &bounds : nullptr, clusterCount);
if (drawBounds) {
copyMinikinRectToSkRect(bounds, drawBounds);
}
@@ -529,7 +529,8 @@
}
std::unique_ptr<float[]> advancesArray(new float[count]);
MinikinUtils::measureText(paint, bidiFlags, typeface, buf, start, count, bufSize,
- advancesArray.get(), drawBounds ? &bounds : nullptr);
+ advancesArray.get(), drawBounds ? &bounds : nullptr,
+ clusterCount);
if (drawBounds) {
copyMinikinRectToSkRect(bounds, drawBounds);
@@ -549,7 +550,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, nullptr);
+ isRtl, offset - contextStart, nullptr, 0, nullptr, nullptr);
return result;
}
@@ -558,18 +559,22 @@
jint contextStart, jint contextEnd,
jboolean isRtl, jint offset,
jfloatArray advances, jint advancesIndex,
- jobject drawBounds) {
+ jobject drawBounds, jobject runInfo) {
const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
const Typeface* typeface = paint->getAndroidTypeface();
ScopedCharArrayRO textArray(env, text);
SkRect skDrawBounds;
+ uint32_t clusterCount = 0;
jfloat result = doRunAdvance(env, paint, typeface, textArray.get() + contextStart,
start - contextStart, end - start, contextEnd - contextStart,
isRtl, offset - contextStart, advances, advancesIndex,
- drawBounds ? &skDrawBounds : nullptr);
+ drawBounds ? &skDrawBounds : nullptr, &clusterCount);
if (drawBounds != nullptr) {
GraphicsJNI::rect_to_jrectf(skDrawBounds, env, drawBounds);
}
+ if (runInfo) {
+ GraphicsJNI::set_cluster_count_to_run_info(env, runInfo, clusterCount);
+ }
return result;
}
@@ -578,7 +583,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(), nullptr);
+ advancesArray.get(), nullptr, nullptr);
return minikin::getOffsetForAdvance(advancesArray.get(), buf, start, count, advance);
}
@@ -1145,7 +1150,8 @@
(void*)PaintGlue::getCharArrayBounds},
{"nHasGlyph", "(JILjava/lang/String;)Z", (void*)PaintGlue::hasGlyph},
{"nGetRunAdvance", "(J[CIIIIZI)F", (void*)PaintGlue::getRunAdvance___CIIIIZI_F},
- {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F",
+ {"nGetRunCharacterAdvance",
+ "(J[CIIIIZI[FILandroid/graphics/RectF;Landroid/graphics/Paint$RunInfo;)F",
(void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F},
{"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I},
{"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V",