diff options
author | 2023-12-07 08:56:31 +0900 | |
---|---|---|
committer | 2023-12-12 16:25:53 +0900 | |
commit | a9d3b49463a361536dec9061f4930c356ab128b9 (patch) | |
tree | fe1a857adeda2a7057a187280acc332b7fd6ee0b | |
parent | 7c5b2bf2d52dd6cfb2f6f737ade961de4c4f4155 (diff) |
Add cluster count API
This is a preparation of the inter character justification
Bug: 283193133
Test: CtsTextTestCases
Test: minikin_tests
Change-Id: Ia3a69a83cc1a3cde56d8a66a7cab1c85c7109050
-rw-r--r-- | core/api/current.txt | 1 | ||||
-rw-r--r-- | core/java/android/text/BoringLayout.java | 4 | ||||
-rw-r--r-- | core/java/android/text/Layout.java | 90 | ||||
-rw-r--r-- | core/java/android/text/TextLine.java | 112 | ||||
-rw-r--r-- | core/tests/coretests/src/android/graphics/PaintTest.java | 41 | ||||
-rw-r--r-- | core/tests/coretests/src/android/text/TextLineTest.java | 12 | ||||
-rw-r--r-- | graphics/java/android/graphics/Paint.java | 38 | ||||
-rw-r--r-- | libs/hwui/hwui/MinikinUtils.cpp | 4 | ||||
-rw-r--r-- | libs/hwui/hwui/MinikinUtils.h | 2 | ||||
-rw-r--r-- | libs/hwui/jni/Graphics.cpp | 12 | ||||
-rw-r--r-- | libs/hwui/jni/GraphicsJNI.h | 2 | ||||
-rw-r--r-- | libs/hwui/jni/Paint.cpp | 32 |
12 files changed, 272 insertions, 78 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index f245b5c38e19..cc376d8c25f9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -47216,6 +47216,7 @@ package android.text { 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 4c8188801eff..a6d3bb47d9c8 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -454,7 +454,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback 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 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback 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 89aceb9d76e9..42d00d8dc69b 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -18,6 +18,7 @@ package android.text; 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 com.android.internal.util.GrowingArrayUtils; 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 @@ public abstract class Layout { 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 @@ public abstract class Layout { 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 @@ public abstract class Layout { 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 @@ public abstract class Layout { 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 @@ public abstract class Layout { 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 @@ public abstract class Layout { * 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 @@ public abstract class Layout { 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 @@ public abstract class Layout { 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 f9abec04e71d..135935cb0632 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -76,6 +76,21 @@ public class TextLine { 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 @@ public class TextLine { // 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 @@ public class TextLine { * @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 @@ public class TextLine { 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 @@ public class TextLine { return Math.signum(w) * boundsWidth; } } else { - return measure(mLen, false, fmi, drawBounds); + return measure(mLen, false, fmi, drawBounds, lineInfo); } } @@ -407,12 +424,13 @@ public class TextLine { * 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 @@ public class TextLine { 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 @@ public class TextLine { 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 @@ public class TextLine { } /** - * @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 @@ public class TextLine { 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 @@ public class TextLine { 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 @@ public class TextLine { * @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 @@ public class TextLine { 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 @@ public class TextLine { 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 @@ public class TextLine { * @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 @@ public class TextLine { 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 @@ public class TextLine { 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 @@ public class TextLine { 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 @@ public class TextLine { * @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 @@ public class TextLine { 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 @@ public class TextLine { 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 @@ public class TextLine { 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 @@ public class TextLine { 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 bf56df1c9441..0dec756d7611 100644 --- a/core/tests/coretests/src/android/graphics/PaintTest.java +++ b/core/tests/coretests/src/android/graphics/PaintTest.java @@ -19,6 +19,7 @@ package android.graphics; import static org.junit.Assert.assertNotEquals; import android.test.InstrumentationTestCase; +import android.text.TextUtils; import androidx.test.filters.SmallTest; @@ -362,4 +363,44 @@ public class PaintTest extends InstrumentationTestCase { // = 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 34842a0b7597..a31992c8cfa1 100644 --- a/core/tests/coretests/src/android/text/TextLineTest.java +++ b/core/tests/coretests/src/android/text/TextLineTest.java @@ -50,11 +50,11 @@ public class TextLineTest { 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 @@ public class TextLineTest { 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 @@ public class TextLineTest { 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 @@ public class TextLineTest { 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 @@ public class TextLineTest { 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 f10cdb82022e..3e8f4420666d 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -2474,6 +2474,19 @@ public class Paint { 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 @@ public class Paint { 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 @@ public class Paint { * @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 @@ public class Paint { } 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 @@ public class Paint { 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 @@ public class Paint { * @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 @@ public class Paint { 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 @@ public class Paint { 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 7552b56d2ad6..833069f363c8 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -96,7 +96,7 @@ void MinikinUtils::getBounds(const Paint* paint, minikin::Bidi bidiFlags, const 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 @@ float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags, 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 61bc881faa54..f8574ee50525 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -53,7 +53,7 @@ public: 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 7cc48661619a..8315c4c0dd4d 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -247,6 +247,9 @@ static jfieldID gFontMetricsInt_descent; 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 @@ int GraphicsJNI::set_metrics_int(JNIEnv* env, jobject metrics, const SkFontMetri 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 @@ int register_android_graphics_Graphics(JNIEnv* env) 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 b9fff36d372e..b0a1074d6693 100644 --- a/libs/hwui/jni/GraphicsJNI.h +++ b/libs/hwui/jni/GraphicsJNI.h @@ -77,6 +77,8 @@ public: 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 d84b73d1a1ca..286f06a6bad8 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -114,7 +114,7 @@ namespace PaintGlue { 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 @@ namespace PaintGlue { } 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 @@ namespace PaintGlue { 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 @@ namespace PaintGlue { 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 @@ namespace PaintGlue { 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 @@ namespace PaintGlue { } 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 @@ namespace PaintGlue { 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 @@ namespace PaintGlue { 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 @@ namespace PaintGlue { 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 @@ static const JNINativeMethod methods[] = { (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", |