diff --git a/core/api/current.txt b/core/api/current.txt index 1c574f22890a2..735de5421f165 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -949,6 +949,7 @@ package android { field public static final int left = 16843181; // 0x10101ad field public static final int letterSpacing = 16843958; // 0x10104b6 field public static final int level = 16844032; // 0x1010500 + field public static final int lineBreakStyle = 16844365; // 0x101064d field public static final int lineHeight = 16844159; // 0x101057f field public static final int lineSpacingExtra = 16843287; // 0x1010217 field public static final int lineSpacingMultiplier = 16843288; // 0x1010218 @@ -17576,6 +17577,17 @@ package android.graphics.pdf { package android.graphics.text { + public final class LineBreakConfig { + ctor public LineBreakConfig(); + method public int getLineBreakStyle(); + method public void set(@Nullable android.graphics.text.LineBreakConfig); + method public void setLineBreakStyle(int); + field public static final int LINE_BREAK_STYLE_LOOSE = 1; // 0x1 + field public static final int LINE_BREAK_STYLE_NONE = 0; // 0x0 + field public static final int LINE_BREAK_STYLE_NORMAL = 2; // 0x2 + field public static final int LINE_BREAK_STYLE_STRICT = 3; // 0x3 + } + public class LineBreaker { method @NonNull public android.graphics.text.LineBreaker.Result computeLineBreaks(@NonNull android.graphics.text.MeasuredText, @NonNull android.graphics.text.LineBreaker.ParagraphConstraints, @IntRange(from=0) int); field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2 @@ -17631,6 +17643,7 @@ package android.graphics.text { ctor public MeasuredText.Builder(@NonNull android.graphics.text.MeasuredText); method @NonNull public android.graphics.text.MeasuredText.Builder appendReplacementRun(@NonNull android.graphics.Paint, @IntRange(from=0) int, @FloatRange(from=0) @Px float); method @NonNull public android.graphics.text.MeasuredText.Builder appendStyleRun(@NonNull android.graphics.Paint, @IntRange(from=0) int, boolean); + method @NonNull public android.graphics.text.MeasuredText.Builder appendStyleRun(@NonNull android.graphics.Paint, @Nullable android.graphics.text.LineBreakConfig, @IntRange(from=0) int, boolean); method @NonNull public android.graphics.text.MeasuredText build(); method @Deprecated @NonNull public android.graphics.text.MeasuredText.Builder setComputeHyphenation(boolean); method @NonNull public android.graphics.text.MeasuredText.Builder setComputeHyphenation(int); @@ -45434,6 +45447,7 @@ package android.text { public static final class PrecomputedText.Params { method public int getBreakStrategy(); method public int getHyphenationFrequency(); + method @Nullable public android.graphics.text.LineBreakConfig getLineBreakConfig(); method @NonNull public android.text.TextDirectionHeuristic getTextDirection(); method @NonNull public android.text.TextPaint getTextPaint(); } @@ -45444,6 +45458,7 @@ package android.text { method @NonNull public android.text.PrecomputedText.Params build(); method public android.text.PrecomputedText.Params.Builder setBreakStrategy(int); method public android.text.PrecomputedText.Params.Builder setHyphenationFrequency(int); + method @NonNull public android.text.PrecomputedText.Params.Builder setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig); method public android.text.PrecomputedText.Params.Builder setTextDirection(@NonNull android.text.TextDirectionHeuristic); } @@ -45604,6 +45619,7 @@ package android.text { method @NonNull public android.text.StaticLayout.Builder setIncludePad(boolean); method @NonNull public android.text.StaticLayout.Builder setIndents(@Nullable int[], @Nullable int[]); method @NonNull public android.text.StaticLayout.Builder setJustificationMode(int); + method @NonNull public android.text.StaticLayout.Builder setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig); method @NonNull public android.text.StaticLayout.Builder setLineSpacing(float, @FloatRange(from=0.0) float); method @NonNull public android.text.StaticLayout.Builder setMaxLines(@IntRange(from=0) int); method public android.text.StaticLayout.Builder setText(CharSequence); @@ -57682,6 +57698,7 @@ package android.widget { method public final android.text.Layout getLayout(); method public float getLetterSpacing(); method public int getLineBounds(int, android.graphics.Rect); + method @NonNull public android.graphics.text.LineBreakConfig getLineBreakConfig(); method public int getLineCount(); method public int getLineHeight(); method public float getLineSpacingExtra(); @@ -57809,6 +57826,7 @@ package android.widget { method public void setKeyListener(android.text.method.KeyListener); method public void setLastBaselineToBottomHeight(@IntRange(from=0) @Px int); method public void setLetterSpacing(float); + method public void setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig); method public void setLineHeight(@IntRange(from=0) @Px int); method public void setLineSpacing(float, float); method public void setLines(int); diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java index 6a3c6182b96df..748d55123b9a4 100644 --- a/core/java/android/text/MeasuredParagraph.java +++ b/core/java/android/text/MeasuredParagraph.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.text.LineBreakConfig; import android.graphics.text.MeasuredText; import android.text.AutoGrowArray.ByteArray; import android.text.AutoGrowArray.FloatArray; @@ -124,7 +125,7 @@ public class MeasuredParagraph { // The native MeasuredParagraph. private @Nullable MeasuredText mMeasuredText; - // Following two objects are for avoiding object allocation. + // Following three objects are for avoiding object allocation. private @NonNull TextPaint mCachedPaint = new TextPaint(); private @Nullable Paint.FontMetricsInt mCachedFm; @@ -350,7 +351,8 @@ public class MeasuredParagraph { if (mt.mSpanned == null) { // No style change by MetricsAffectingSpan. Just measure all text. mt.applyMetricsAffectingSpan( - paint, null /* spans */, start, end, null /* native builder ptr */); + paint, null /* lineBreakConfig */, null /* spans */, start, end, + null /* native builder ptr */); } else { // There may be a MetricsAffectingSpan. Split into span transitions and apply styles. int spanEnd; @@ -360,7 +362,8 @@ public class MeasuredParagraph { MetricAffectingSpan.class); spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class); mt.applyMetricsAffectingSpan( - paint, spans, spanStart, spanEnd, null /* native builder ptr */); + paint, null /* line break config */, spans, spanStart, spanEnd, + null /* native builder ptr */); } } return mt; @@ -373,6 +376,7 @@ public class MeasuredParagraph { * result to recycle and returns recycle. * * @param paint the paint to be used for rendering the text. + * @param lineBreakConfig the line break configuration for text wrapping. * @param text the character sequence to be measured * @param start the inclusive start offset of the target region in the text * @param end the exclusive end offset of the target region in the text @@ -386,6 +390,7 @@ public class MeasuredParagraph { */ public static @NonNull MeasuredParagraph buildForStaticLayout( @NonNull TextPaint paint, + @Nullable LineBreakConfig lineBreakConfig, @NonNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @@ -411,7 +416,8 @@ public class MeasuredParagraph { } else { if (mt.mSpanned == null) { // No style change by MetricsAffectingSpan. Just measure all text. - mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, builder); + mt.applyMetricsAffectingSpan(paint, lineBreakConfig, null /* spans */, start, end, + builder); mt.mSpanEndCache.append(end); } else { // There may be a MetricsAffectingSpan. Split into span transitions and apply @@ -424,7 +430,9 @@ public class MeasuredParagraph { MetricAffectingSpan.class); spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class); - mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, builder); + // TODO: Update line break config with spans. + mt.applyMetricsAffectingSpan(paint, lineBreakConfig, spans, spanStart, spanEnd, + builder); mt.mSpanEndCache.append(spanEnd); } } @@ -500,12 +508,13 @@ public class MeasuredParagraph { private void applyReplacementRun(@NonNull ReplacementSpan replacement, @IntRange(from = 0) int start, // inclusive, in copied buffer @IntRange(from = 0) int end, // exclusive, in copied buffer + @NonNull TextPaint paint, @Nullable MeasuredText.Builder builder) { // Use original text. Shouldn't matter. // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for // backward compatibility? or Should we initialize them for getFontMetricsInt? final float width = replacement.getSize( - mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm); + paint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm); if (builder == null) { // Assigns all width to the first character. This is the same behavior as minikin. mWidths.set(start, width); @@ -514,22 +523,24 @@ public class MeasuredParagraph { } mWholeWidth += width; } else { - builder.appendReplacementRun(mCachedPaint, end - start, width); + builder.appendReplacementRun(paint, end - start, width); } } private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer @IntRange(from = 0) int end, // exclusive, in copied buffer + @NonNull TextPaint paint, + @Nullable LineBreakConfig config, @Nullable MeasuredText.Builder builder) { if (mLtrWithoutBidi) { // If the whole text is LTR direction, just apply whole region. if (builder == null) { - mWholeWidth += mCachedPaint.getTextRunAdvances( + mWholeWidth += paint.getTextRunAdvances( mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */, mWidths.getRawArray(), start); } else { - builder.appendStyleRun(mCachedPaint, end - start, false /* isRtl */); + builder.appendStyleRun(paint, config, end - start, false /* isRtl */); } } else { // If there is multiple bidi levels, split into individual bidi level and apply style. @@ -541,11 +552,11 @@ public class MeasuredParagraph { final boolean isRtl = (level & 0x1) != 0; if (builder == null) { final int levelLength = levelEnd - levelStart; - mWholeWidth += mCachedPaint.getTextRunAdvances( + mWholeWidth += paint.getTextRunAdvances( mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, isRtl, mWidths.getRawArray(), levelStart); } else { - builder.appendStyleRun(mCachedPaint, levelEnd - levelStart, isRtl); + builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl); } if (levelEnd == end) { break; @@ -559,6 +570,7 @@ public class MeasuredParagraph { private void applyMetricsAffectingSpan( @NonNull TextPaint paint, + @Nullable LineBreakConfig lineBreakConfig, @Nullable MetricAffectingSpan[] spans, @IntRange(from = 0) int start, // inclusive, in original text buffer @IntRange(from = 0) int end, // exclusive, in original text buffer @@ -595,9 +607,11 @@ public class MeasuredParagraph { } if (replacement != null) { - applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, builder); + applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, mCachedPaint, + builder); } else { - applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, builder); + applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, mCachedPaint, + lineBreakConfig, builder); } if (needFontMetrics) { diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java index 152570ffd1c7a..ce63376a6d631 100644 --- a/core/java/android/text/PrecomputedText.java +++ b/core/java/android/text/PrecomputedText.java @@ -22,6 +22,7 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Rect; +import android.graphics.text.LineBreakConfig; import android.graphics.text.MeasuredText; import android.text.style.MetricAffectingSpan; @@ -96,6 +97,9 @@ public class PrecomputedText implements Spannable { // The hyphenation frequency for this measured text. private final @Layout.HyphenationFrequency int mHyphenationFrequency; + // The line break configuration for calculating text wrapping. + private final @Nullable LineBreakConfig mLineBreakConfig; + /** * A builder for creating {@link Params}. */ @@ -113,6 +117,9 @@ public class PrecomputedText implements Spannable { private @Layout.HyphenationFrequency int mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL; + // The line break configuration for calculating text wrapping. + private @Nullable LineBreakConfig mLineBreakConfig; + /** * Builder constructor. * @@ -130,6 +137,7 @@ public class PrecomputedText implements Spannable { mTextDir = params.mTextDir; mBreakStrategy = params.mBreakStrategy; mHyphenationFrequency = params.mHyphenationFrequency; + mLineBreakConfig = params.mLineBreakConfig; } /** @@ -176,25 +184,42 @@ public class PrecomputedText implements Spannable { return this; } + /** + * Set the line break config for the text wrapping. + * + * @param lineBreakConfig the newly line break configuration. + * @return this builder, useful for chaining. + * @see StaticLayout.Builder#setLineBreakConfig + */ + public @NonNull Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) { + mLineBreakConfig = lineBreakConfig; + return this; + } + /** * Build the {@link Params}. * * @return the layout parameter */ public @NonNull Params build() { - return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency); + return new Params(mPaint, mLineBreakConfig, mTextDir, mBreakStrategy, + mHyphenationFrequency); } } // This is public hidden for internal use. // For the external developers, use Builder instead. /** @hide */ - public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, - @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) { + public Params(@NonNull TextPaint paint, + @Nullable LineBreakConfig lineBreakConfig, + @NonNull TextDirectionHeuristic textDir, + @Layout.BreakStrategy int strategy, + @Layout.HyphenationFrequency int frequency) { mPaint = paint; mTextDir = textDir; mBreakStrategy = strategy; mHyphenationFrequency = frequency; + mLineBreakConfig = lineBreakConfig; } /** @@ -233,6 +258,15 @@ public class PrecomputedText implements Spannable { return mHyphenationFrequency; } + /** + * Return the line break configuration for this text. + * + * @return the current line break configuration, null if no line break configuration is set. + */ + public @Nullable LineBreakConfig getLineBreakConfig() { + return mLineBreakConfig; + } + /** @hide */ @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE }) @Retention(RetentionPolicy.SOURCE) @@ -262,8 +296,9 @@ public class PrecomputedText implements Spannable { /** @hide */ public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, - @Layout.HyphenationFrequency int frequency) { + @Layout.HyphenationFrequency int frequency, @Nullable LineBreakConfig lbConfig) { if (mBreakStrategy == strategy && mHyphenationFrequency == frequency + && isLineBreakEquals(mLineBreakConfig, lbConfig) && mPaint.equalsForTextMeasurement(paint)) { return mTextDir == textDir ? USABLE : NEED_RECOMPUTE; } else { @@ -271,6 +306,29 @@ public class PrecomputedText implements Spannable { } } + /** + * Check the two LineBreakConfig instances are equal. + * This method assumes they are equal if one parameter is null and the other parameter has + * a LineBreakStyle value of LineBreakConfig.LINE_BREAK_STYLE_NONE. + * + * @param o1 the first LineBreakConfig instance. + * @param o2 the second LineBreakConfig instance. + * @return true if the two LineBreakConfig instances are equal. + */ + private boolean isLineBreakEquals(LineBreakConfig o1, LineBreakConfig o2) { + if (Objects.equals(o1, o2)) { + return true; + } + if (o1 == null && (o2 != null + && o2.getLineBreakStyle() == LineBreakConfig.LINE_BREAK_STYLE_NONE)) { + return true; + } else if (o2 == null && (o1 != null + && o1.getLineBreakStyle() == LineBreakConfig.LINE_BREAK_STYLE_NONE)) { + return true; + } + return false; + } + /** * Check if the same text layout. * @@ -286,21 +344,25 @@ public class PrecomputedText implements Spannable { } Params param = (Params) o; return checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy, - param.mHyphenationFrequency) == Params.USABLE; + param.mHyphenationFrequency, param.mLineBreakConfig) == Params.USABLE; } @Override public int hashCode() { // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals. + int lineBreakStyle = (mLineBreakConfig != null) + ? mLineBreakConfig.getLineBreakStyle() : LineBreakConfig.LINE_BREAK_STYLE_NONE; return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(), mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(), mPaint.getTextLocales(), mPaint.getTypeface(), mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir, - mBreakStrategy, mHyphenationFrequency); + mBreakStrategy, mHyphenationFrequency, lineBreakStyle); } @Override public String toString() { + int lineBreakStyle = (mLineBreakConfig != null) + ? mLineBreakConfig.getLineBreakStyle() : LineBreakConfig.LINE_BREAK_STYLE_NONE; return "{" + "textSize=" + mPaint.getTextSize() + ", textScaleX=" + mPaint.getTextScaleX() @@ -313,6 +375,7 @@ public class PrecomputedText implements Spannable { + ", textDir=" + mTextDir + ", breakStrategy=" + mBreakStrategy + ", hyphenationFrequency=" + mHyphenationFrequency + + ", lineBreakStyle=" + lineBreakStyle + "}"; } }; @@ -369,7 +432,8 @@ public class PrecomputedText implements Spannable { final PrecomputedText.Params hintParams = hintPct.getParams(); final @Params.CheckResultUsableResult int checkResult = hintParams.checkResultUsable(params.mPaint, params.mTextDir, - params.mBreakStrategy, params.mHyphenationFrequency); + params.mBreakStrategy, params.mHyphenationFrequency, + params.mLineBreakConfig); switch (checkResult) { case Params.USABLE: return hintPct; @@ -418,9 +482,9 @@ public class PrecomputedText implements Spannable { final int paraStart = pct.getParagraphStart(i); final int paraEnd = pct.getParagraphEnd(i); result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( - params.getTextPaint(), pct, paraStart, paraEnd, params.getTextDirection(), - hyphenationMode, computeLayout, pct.getMeasuredParagraph(i), - null /* no recycle */))); + params.getTextPaint(), params.getLineBreakConfig(), pct, paraStart, paraEnd, + params.getTextDirection(), hyphenationMode, computeLayout, + pct.getMeasuredParagraph(i), null /* no recycle */))); } return result.toArray(new ParagraphInfo[result.size()]); } @@ -456,8 +520,9 @@ public class PrecomputedText implements Spannable { } result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout( - params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(), - hyphenationMode, computeLayout, null /* no hint */, null /* no recycle */))); + params.getTextPaint(), params.getLineBreakConfig(), text, paraStart, paraEnd, + params.getTextDirection(), hyphenationMode, computeLayout, null /* no hint */, + null /* no recycle */))); } return result.toArray(new ParagraphInfo[result.size()]); } @@ -544,11 +609,11 @@ public class PrecomputedText implements Spannable { public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint, @Layout.BreakStrategy int strategy, - @Layout.HyphenationFrequency int frequency) { + @Layout.HyphenationFrequency int frequency, @NonNull LineBreakConfig lbConfig) { if (mStart != start || mEnd != end) { return Params.UNUSABLE; } else { - return mParams.checkResultUsable(paint, textDir, strategy, frequency); + return mParams.checkResultUsable(paint, textDir, strategy, frequency, lbConfig); } } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index b1bc7667da165..b10fc37bff2fd 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.graphics.Paint; +import android.graphics.text.LineBreakConfig; import android.graphics.text.LineBreaker; import android.os.Build; import android.text.style.LeadingMarginSpan; @@ -402,6 +403,21 @@ public class StaticLayout extends Layout { return this; } + /** + * Set the line break configuration. The line break will be passed to native used for + * calculating the text wrapping. The default value of the line break style is + * {@link LineBreakConfig#LINE_BREAK_STYLE_NONE} + * + * @param lineBreakConfig the line break configuration for text wrapping. + * @return this builder, useful for chaining. + * @see android.widget.TextView#setLineBreakConfig + */ + @NonNull + public Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) { + mLineBreakConfig = lineBreakConfig; + return this; + } + /** * Build the {@link StaticLayout} after options have been set. * @@ -438,6 +454,7 @@ public class StaticLayout extends Layout { @Nullable private int[] mRightIndents; private int mJustificationMode; private boolean mAddLastLineLineSpacing; + private LineBreakConfig mLineBreakConfig; private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); @@ -670,7 +687,7 @@ public class StaticLayout extends Layout { PrecomputedText precomputed = (PrecomputedText) source; final @PrecomputedText.Params.CheckResultUsableResult int checkResult = precomputed.checkResultUsable(bufStart, bufEnd, textDir, paint, - b.mBreakStrategy, b.mHyphenationFrequency); + b.mBreakStrategy, b.mHyphenationFrequency, b.mLineBreakConfig); switch (checkResult) { case PrecomputedText.Params.UNUSABLE: break; @@ -680,6 +697,7 @@ public class StaticLayout extends Layout { .setBreakStrategy(b.mBreakStrategy) .setHyphenationFrequency(b.mHyphenationFrequency) .setTextDirection(textDir) + .setLineBreakConfig(b.mLineBreakConfig) .build(); precomputed = PrecomputedText.create(precomputed, newParams); paragraphInfo = precomputed.getParagraphInfo(); @@ -692,8 +710,8 @@ public class StaticLayout extends Layout { } if (paragraphInfo == null) { - final PrecomputedText.Params param = new PrecomputedText.Params(paint, textDir, - b.mBreakStrategy, b.mHyphenationFrequency); + final PrecomputedText.Params param = new PrecomputedText.Params(paint, + b.mLineBreakConfig, textDir, b.mBreakStrategy, b.mHyphenationFrequency); paragraphInfo = PrecomputedText.createMeasuredParagraphs(source, param, bufStart, bufEnd, false /* computeLayout */); } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 1a808b2e7c248..0fe06befa7893 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -79,6 +79,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.fonts.FontStyle; import android.graphics.fonts.FontVariationAxis; +import android.graphics.text.LineBreakConfig; import android.icu.text.DecimalFormatSymbols; import android.os.AsyncTask; import android.os.Build; @@ -348,6 +349,7 @@ import java.util.function.Supplier; * @attr ref android.R.styleable#TextView_fontVariationSettings * @attr ref android.R.styleable#TextView_breakStrategy * @attr ref android.R.styleable#TextView_hyphenationFrequency + * @attr ref android.R.styleable#TextView_lineBreakStyle * @attr ref android.R.styleable#TextView_autoSizeTextType * @attr ref android.R.styleable#TextView_autoSizeMinTextSize * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize @@ -775,6 +777,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private Layout mLayout; private boolean mLocalesChanged = false; private int mTextSizeUnit = -1; + private LineBreakConfig mLineBreakConfig = new LineBreakConfig(); // This is used to reflect the current user preference for changing font weight and making text // more bold. @@ -1442,6 +1445,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mHyphenationFrequency = a.getInt(attr, Layout.HYPHENATION_FREQUENCY_NONE); break; + case com.android.internal.R.styleable.TextView_lineBreakStyle: + mLineBreakConfig.setLineBreakStyle( + a.getInt(attr, LineBreakConfig.LINE_BREAK_STYLE_NONE)); + break; + case com.android.internal.R.styleable.TextView_autoSizeTextType: mAutoSizeTextType = a.getInt(attr, AUTO_SIZE_TEXT_TYPE_NONE); break; @@ -4787,6 +4795,42 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mHyphenationFrequency; } + /** + * Sets line break configuration indicates which strategy needs to be used when calculating the + * text wrapping. There are thee strategies for the line break style(lb): + * {@link LineBreakConfig#LINE_BREAK_STYLE_LOOSE}, + * {@link LineBreakConfig#LINE_BREAK_STYLE_NORMAL} and + * {@link LineBreakConfig#LINE_BREAK_STYLE_STRICT}. + * The default value of the line break style is {@link LineBreakConfig#LINE_BREAK_STYLE_NONE}, + * which means no line break style is specified. + * See + * the line-break property + * + * @param lineBreakConfig the line break config for text wrapping. + */ + public void setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) { + if (mLineBreakConfig.equals(lineBreakConfig)) { + return; + } + mLineBreakConfig.set(lineBreakConfig); + if (mLayout != null) { + nullLayouts(); + requestLayout(); + invalidate(); + } + } + + /** + * Get the current line break configuration for text wrapping. + * + * @return the current line break configuration to be used for text wrapping. + */ + public @NonNull LineBreakConfig getLineBreakConfig() { + LineBreakConfig lbConfig = new LineBreakConfig(); + lbConfig.set(mLineBreakConfig); + return lbConfig; + } + /** * Gets the parameters for text layout precomputation, for use with {@link PrecomputedText}. * @@ -4794,7 +4838,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @see PrecomputedText */ public @NonNull PrecomputedText.Params getTextMetricsParams() { - return new PrecomputedText.Params(new TextPaint(mTextPaint), getTextDirectionHeuristic(), + return new PrecomputedText.Params(new TextPaint(mTextPaint), mLineBreakConfig, + getTextDirectionHeuristic(), mBreakStrategy, mHyphenationFrequency); } @@ -4810,6 +4855,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTextDir = params.getTextDirection(); mBreakStrategy = params.getBreakStrategy(); mHyphenationFrequency = params.getHyphenationFrequency(); + mLineBreakConfig.set(params.getLineBreakConfig()); if (mLayout != null) { nullLayouts(); requestLayout(); @@ -6348,7 +6394,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } final @PrecomputedText.Params.CheckResultUsableResult int checkResult = precomputed.getParams().checkResultUsable(getPaint(), mTextDir, mBreakStrategy, - mHyphenationFrequency); + mHyphenationFrequency, mLineBreakConfig); switch (checkResult) { case PrecomputedText.Params.UNUSABLE: throw new IllegalArgumentException( @@ -9244,7 +9290,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setBreakStrategy(mBreakStrategy) .setHyphenationFrequency(mHyphenationFrequency) .setJustificationMode(mJustificationMode) - .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE); + .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE) + .setLineBreakConfig(mLineBreakConfig); if (shouldEllipsize) { builder.setEllipsize(mEllipsize) .setEllipsizedWidth(ellipsisWidth); @@ -9358,7 +9405,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setBreakStrategy(mBreakStrategy) .setHyphenationFrequency(mHyphenationFrequency) .setJustificationMode(mJustificationMode) - .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE); + .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE) + .setLineBreakConfig(mLineBreakConfig); if (shouldEllipsize) { builder.setEllipsize(effectiveEllipsize) .setEllipsizedWidth(ellipsisWidth); @@ -9725,7 +9773,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setHyphenationFrequency(getHyphenationFrequency()) .setJustificationMode(getJustificationMode()) .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE) - .setTextDirection(getTextDirectionHeuristic()); + .setTextDirection(getTextDirectionHeuristic()) + .setLineBreakConfig(mLineBreakConfig); final StaticLayout layout = layoutBuilder.build(); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index a08bca3b3fe94..e232d858bb434 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -5417,6 +5417,17 @@ ignores some hyphen character related typographic features, e.g. kerning. --> + + + + + + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index e801ac0ba71a6..e83f4a66883c3 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -3211,6 +3211,7 @@ + diff --git a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java index d6a7682475f22..045e746281a6e 100644 --- a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java +++ b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java @@ -136,8 +136,8 @@ public class MeasuredParagraphTest { MeasuredParagraph mt = null; mt = MeasuredParagraph.buildForStaticLayout( - PAINT, "XXX", 0, 3, LTR, MeasuredText.Builder.HYPHENATION_MODE_NONE, false, - null /* no hint */, null); + PAINT, null /* line break config */, "XXX", 0, 3, LTR, + MeasuredText.Builder.HYPHENATION_MODE_NONE, false, null /* no hint */, null); assertNotNull(mt); assertNotNull(mt.getChars()); assertEquals("XXX", charsToString(mt.getChars())); @@ -152,8 +152,8 @@ public class MeasuredParagraphTest { // Recycle it MeasuredParagraph mt2 = MeasuredParagraph.buildForStaticLayout( - PAINT, "_VVV_", 1, 4, RTL, MeasuredText.Builder.HYPHENATION_MODE_NONE, false, - null /* no hint */, mt); + PAINT, null /* line break config */, "_VVV_", 1, 4, RTL, + MeasuredText.Builder.HYPHENATION_MODE_NONE, false, null /* no hint */, mt); assertEquals(mt2, mt); assertNotNull(mt2.getChars()); assertEquals("VVV", charsToString(mt.getChars())); diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java new file mode 100644 index 0000000000000..4d818583fd236 --- /dev/null +++ b/graphics/java/android/graphics/text/LineBreakConfig.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics.text; + +import android.annotation.IntDef; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Indicates the strategies can be used when calculating the text wrapping. + * + * See the line-break property + */ +public final class LineBreakConfig { + + /** + * No line break style specified. + */ + public static final int LINE_BREAK_STYLE_NONE = 0; + + /** + * Use the least restrictive rule for line-breaking. This is usually used for short lines. + */ + public static final int LINE_BREAK_STYLE_LOOSE = 1; + + /** + * Indicate breaking text with the most comment set of line-breaking rules. + */ + public static final int LINE_BREAK_STYLE_NORMAL = 2; + + /** + * Indicates breaking text with the most strictest line-breaking rules. + */ + public static final int LINE_BREAK_STYLE_STRICT = 3; + + /** @hide */ + @IntDef(prefix = { "LINE_BREAK_STYLE_" }, value = { + LINE_BREAK_STYLE_NONE, LINE_BREAK_STYLE_LOOSE, LINE_BREAK_STYLE_NORMAL, + LINE_BREAK_STYLE_STRICT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface LineBreakStyle {} + + private @LineBreakStyle int mLineBreakStyle = LINE_BREAK_STYLE_NONE; + + public LineBreakConfig() { + } + + /** + * Set the line break configuration. + * + * @param config the new line break configuration. + */ + public void set(@Nullable LineBreakConfig config) { + if (config != null) { + mLineBreakStyle = config.getLineBreakStyle(); + } else { + mLineBreakStyle = LineBreakConfig.LINE_BREAK_STYLE_NONE; + } + } + + /** + * Get the line break style. + * + * @return The current line break style to be used for the text wrapping. + */ + public @LineBreakStyle int getLineBreakStyle() { + return mLineBreakStyle; + } + + /** + * Set the line break style. + * + * @param lineBreakStyle the new line break style. + */ + public void setLineBreakStyle(@LineBreakStyle int lineBreakStyle) { + mLineBreakStyle = lineBreakStyle; + } + + @Override + public boolean equals(Object o) { + if (o == null) return false; + if (this == o) return true; + if (!(o instanceof LineBreakConfig)) return false; + LineBreakConfig that = (LineBreakConfig) o; + return mLineBreakStyle == that.mLineBreakStyle; + } + + @Override + public int hashCode() { + return Objects.hash(mLineBreakStyle); + } +} diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java index a34d0abcf753c..5f4afb7b98880 100644 --- a/graphics/java/android/graphics/text/MeasuredText.java +++ b/graphics/java/android/graphics/text/MeasuredText.java @@ -239,11 +239,33 @@ public class MeasuredText { */ public @NonNull Builder appendStyleRun(@NonNull Paint paint, @IntRange(from = 0) int length, boolean isRtl) { + return appendStyleRun(paint, null, length, isRtl); + } + + /** + * Apply styles to the given length. + * + * Keeps an internal offset which increases at every append. The initial value for this + * offset is zero. After the style is applied the internal offset is moved to {@code offset + * + length}, and next call will start from this new position. + * + * @param paint a paint + * @param lineBreakConfig a line break configuration. + * @param length a length to be applied with a given paint, can not exceed the length of the + * text + * @param isRtl true if the text is in RTL context, otherwise false. + */ + public @NonNull Builder appendStyleRun(@NonNull Paint paint, + @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length, + boolean isRtl) { Preconditions.checkNotNull(paint); Preconditions.checkArgument(length > 0, "length can not be negative"); final int end = mCurrentOffset + length; Preconditions.checkArgument(end <= mText.length, "Style exceeds the text length"); - nAddStyleRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, isRtl); + int lbStyle = (lineBreakConfig != null) ? lineBreakConfig.getLineBreakStyle() : + LineBreakConfig.LINE_BREAK_STYLE_NONE; + nAddStyleRun(mNativePtr, paint.getNativeInstance(), lbStyle, mCurrentOffset, end, + isRtl); mCurrentOffset = end; return this; } @@ -423,12 +445,14 @@ public class MeasuredText { * * @param nativeBuilderPtr The native MeasuredParagraph builder pointer. * @param paintPtr The native paint pointer to be applied. + * @param lineBreakStyle The line break style of the text. * @param start The start offset in the copied buffer. * @param end The end offset in the copied buffer. * @param isRtl True if the text is RTL. */ private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr, /* Non Zero */ long paintPtr, + int lineBreakStyle, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl); diff --git a/libs/hwui/jni/text/MeasuredText.cpp b/libs/hwui/jni/text/MeasuredText.cpp index bd9bd7121f8a0..09539ecc34b0e 100644 --- a/libs/hwui/jni/text/MeasuredText.cpp +++ b/libs/hwui/jni/text/MeasuredText.cpp @@ -65,11 +65,11 @@ static jlong nInitBuilder(CRITICAL_JNI_PARAMS) { // Regular JNI static void nAddStyleRun(JNIEnv* /* unused */, jclass /* unused */, jlong builderPtr, - jlong paintPtr, jint start, jint end, jboolean isRtl) { + jlong paintPtr, jint lbStyle, jint start, jint end, jboolean isRtl) { Paint* paint = toPaint(paintPtr); const Typeface* typeface = Typeface::resolveDefault(paint->getAndroidTypeface()); minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface); - toBuilder(builderPtr)->addStyleRun(start, end, std::move(minikinPaint), isRtl); + toBuilder(builderPtr)->addStyleRun(start, end, std::move(minikinPaint), lbStyle, isRtl); } // Regular JNI @@ -144,7 +144,7 @@ static jint nGetMemoryUsage(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { static const JNINativeMethod gMTBuilderMethods[] = { // MeasuredParagraphBuilder native functions. {"nInitBuilder", "()J", (void*)nInitBuilder}, - {"nAddStyleRun", "(JJIIZ)V", (void*)nAddStyleRun}, + {"nAddStyleRun", "(JJIIIZ)V", (void*)nAddStyleRun}, {"nAddReplacementRun", "(JJIIF)V", (void*)nAddReplacementRun}, {"nBuildMeasuredText", "(JJ[CZZZ)J", (void*)nBuildMeasuredText}, {"nFreeBuilder", "(J)V", (void*)nFreeBuilder},