Add setLineBreakConfig API

Add new API to allow applications to specify the line break word style. The line break style is one of the locale extension. When the line break style is set, it will be brought to ICU for calculation.

Bug: 183780874
Test: atest minikin_tests; atest TextViewTest; atest MeasuredTextTest; atest PrecomputedTextTest
Change-Id: Ia9cdb5b83e346f96ae22abbb1bcce05c43207bba
This commit is contained in:
Seigo Nonaka
2022-01-08 12:12:12 -08:00
committed by James.cf Lin
parent 0d042eb0ec
commit 94904647a4
11 changed files with 353 additions and 43 deletions

View File

@@ -948,6 +948,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
@@ -17520,6 +17521,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
@@ -17575,6 +17587,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);
@@ -45283,6 +45296,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();
}
@@ -45293,6 +45307,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);
}
@@ -45453,6 +45468,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);
@@ -57467,6 +57483,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();
@@ -57594,6 +57611,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);

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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 */);
}

View File

@@ -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 <a href="https://drafts.csswg.org/css-text/#line-break-property">
* the line-break property</a>
*
* @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();

View File

@@ -5417,6 +5417,17 @@
ignores some hyphen character related typographic features, e.g. kerning. -->
<enum name="fullFast" value="4" />
</attr>
<!-- Indicates the line break strategies can be used when calculating the text wrapping. -->
<attr name="lineBreakStyle">
<!-- No line break style specific. -->
<enum name="none" value="0" />
<!-- Use the least restrictive rule for line-breaking. -->
<enum name="loose" value="1" />
<!-- Indicate breaking text with the most comment set of line-breaking rules. -->
<enum name="normal" value="2" />
<!-- ndicates breaking text with the most strictest line-breaking rules. -->
<enum name="strict" value="3" />
</attr>
<!-- Specify the type of auto-size. Note that this feature is not supported by EditText,
works only for TextView. -->
<attr name="autoSizeTextType" format="enum">

View File

@@ -3211,6 +3211,7 @@
</staging-public-group-final>
<public type="attr" name="shouldUseDefaultUnfoldTransition" id="0x0101064c" />
<public type="attr" name="lineBreakStyle" id="0x0101064d" />
<staging-public-group-final type="id" first-id="0x01fe0000">
<public name="accessibilityActionDragStart" />

View File

@@ -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()));

View File

@@ -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 <a href="https://drafts.csswg.org/css-text/#line-break-property">the line-break property</a>
*/
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);
}
}

View File

@@ -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);

View File

@@ -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},