diff --git a/core/api/current.txt b/core/api/current.txt index 639a99134d3dbf3bdcbb6c29cf465ddf98701c5d..9c408bb794493605e6fbe03d4ad0dbf9b1557470 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -16259,6 +16259,8 @@ package android.graphics { public static class Paint.FontMetricsInt { ctor public Paint.FontMetricsInt(); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void set(@NonNull android.graphics.Paint.FontMetricsInt); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void set(@NonNull android.graphics.Paint.FontMetrics); field public int ascent; field public int bottom; field public int descent; @@ -46718,6 +46720,7 @@ package android.text { method @NonNull public android.text.DynamicLayout.Builder setJustificationMode(int); method @FlaggedApi("com.android.text.flags.no_break_no_hyphenation_span") @NonNull public android.text.DynamicLayout.Builder setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig); method @NonNull public android.text.DynamicLayout.Builder setLineSpacing(float, @FloatRange(from=0.0) float); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") @NonNull public android.text.DynamicLayout.Builder setMinimumFontMetrics(@Nullable android.graphics.Paint.FontMetrics); method @NonNull public android.text.DynamicLayout.Builder setTextDirection(@NonNull android.text.TextDirectionHeuristic); method @FlaggedApi("com.android.text.flags.use_bounds_for_width") @NonNull public android.text.DynamicLayout.Builder setUseBoundsForWidth(boolean); method @NonNull public android.text.DynamicLayout.Builder setUseLineSpacingFromFallbacks(boolean); @@ -46906,6 +46909,7 @@ package android.text { method public int getLineVisibleEnd(int); method public float getLineWidth(int); method @FlaggedApi("com.android.text.flags.use_bounds_for_width") @IntRange(from=1) public final int getMaxLines(); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") @Nullable public android.graphics.Paint.FontMetrics getMinimumFontMetrics(); method public int getOffsetForHorizontal(int, float); method public int getOffsetToLeftOf(int); method public int getOffsetToRightOf(int); @@ -46972,6 +46976,7 @@ package android.text { method @NonNull public android.text.Layout.Builder setLineSpacingAmount(float); method @NonNull public android.text.Layout.Builder setLineSpacingMultiplier(@FloatRange(from=0) float); method @NonNull public android.text.Layout.Builder setMaxLines(@IntRange(from=1) int); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") @NonNull public android.text.Layout.Builder setMinimumFontMetrics(@Nullable android.graphics.Paint.FontMetrics); method @NonNull public android.text.Layout.Builder setRightIndents(@Nullable int[]); method @NonNull public android.text.Layout.Builder setTextDirectionHeuristic(@NonNull android.text.TextDirectionHeuristic); method @FlaggedApi("com.android.text.flags.use_bounds_for_width") @NonNull public android.text.Layout.Builder setUseBoundsForWidth(boolean); @@ -47243,6 +47248,7 @@ package android.text { 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 @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") @NonNull public android.text.StaticLayout.Builder setMinimumFontMetrics(@Nullable android.graphics.Paint.FontMetrics); method public android.text.StaticLayout.Builder setText(CharSequence); method @NonNull public android.text.StaticLayout.Builder setTextDirection(@NonNull android.text.TextDirectionHeuristic); method @FlaggedApi("com.android.text.flags.use_bounds_for_width") @NonNull public android.text.StaticLayout.Builder setUseBoundsForWidth(boolean); @@ -59928,6 +59934,7 @@ package android.widget { method public int getMinHeight(); method public int getMinLines(); method public int getMinWidth(); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") @Nullable public android.graphics.Paint.FontMetrics getMinimumFontMetrics(); method public final android.text.method.MovementMethod getMovementMethod(); method public int getOffsetForPosition(float, float); method public android.text.TextPaint getPaint(); @@ -60064,6 +60071,7 @@ package android.widget { method public void setMinHeight(int); method public void setMinLines(int); method public void setMinWidth(int); + method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void setMinimumFontMetrics(@Nullable android.graphics.Paint.FontMetrics); method public final void setMovementMethod(android.text.method.MovementMethod); method public void setOnEditorActionListener(android.widget.TextView.OnEditorActionListener); method public void setPaintFlags(int); diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 65a1da6b81b84a883c2c72e9f7b052896d7debb3..4c8188801eff6b49e8cb5386cb83f38a362b30f4 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -190,7 +190,8 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth, boolean useFallbackLineSpacing) { return replaceOrMake(source, paint, outerWidth, align, 1.0f, 0.0f, metrics, includePad, - ellipsize, ellipsizedWidth, useFallbackLineSpacing, false /* useBoundsForWidth */); + ellipsize, ellipsizedWidth, useFallbackLineSpacing, false /* useBoundsForWidth */, + null /* minimumFontMetrics */); } /** @hide */ @@ -199,7 +200,8 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback @NonNull Alignment align, float spacingMultiplier, float spacingAmount, @NonNull BoringLayout.Metrics metrics, boolean includePad, @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth, - boolean useFallbackLineSpacing, boolean useBoundsForWidth) { + boolean useFallbackLineSpacing, boolean useBoundsForWidth, + @Nullable Paint.FontMetrics minimumFontMetrics) { boolean trust; if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) { @@ -270,7 +272,8 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback spacingAdd, includePad, false /* fallbackLineSpacing */, outerwidth /* ellipsizedWidth */, null /* ellipsize */, 1 /* maxLines */, BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null /* leftIndents */, - null /* rightIndents */, JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false); + null /* rightIndents */, JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false, + null); mEllipsizedWidth = outerwidth; mEllipsizedStart = 0; @@ -343,7 +346,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback ellipsizedWidth, ellipsize, 1 /* maxLines */, BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null /* leftIndents */, null /* rightIndents */, JUSTIFICATION_MODE_NONE, - LineBreakConfig.NONE, metrics, false /* useBoundsForWidth */); + LineBreakConfig.NONE, metrics, false /* useBoundsForWidth */, null); } /** @hide */ @@ -359,12 +362,13 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback int ellipsizedWidth, TextUtils.TruncateAt ellipsize, Metrics metrics, - boolean useBoundsForWidth) { + boolean useBoundsForWidth, + @Nullable Paint.FontMetrics minimumFontMetrics) { this(text, paint, width, align, TextDirectionHeuristics.LTR, spacingMult, spacingAdd, includePad, fallbackLineSpacing, ellipsizedWidth, ellipsize, 1 /* maxLines */, Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE, null, null, Layout.JUSTIFICATION_MODE_NONE, - LineBreakConfig.NONE, metrics, useBoundsForWidth); + LineBreakConfig.NONE, metrics, useBoundsForWidth, minimumFontMetrics); } /* package */ BoringLayout( @@ -387,12 +391,13 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback int justificationMode, LineBreakConfig lineBreakConfig, Metrics metrics, - boolean useBoundsForWidth) { + boolean useBoundsForWidth, + @Nullable Paint.FontMetrics minimumFontMetrics) { super(text, paint, width, align, textDir, spacingMult, spacingAdd, includePad, fallbackLineSpacing, ellipsizedWidth, ellipsize, maxLines, breakStrategy, hyphenationFrequency, leftIndents, rightIndents, justificationMode, - lineBreakConfig, useBoundsForWidth); + lineBreakConfig, useBoundsForWidth, minimumFontMetrics); boolean trust; @@ -548,6 +553,15 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback public static @Nullable Metrics isBoring(@NonNull CharSequence text, @NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, boolean useFallbackLineSpacing, @Nullable Metrics metrics) { + return isBoring(text, paint, textDir, useFallbackLineSpacing, null, metrics); + } + + /** + * @hide + */ + public static @Nullable Metrics isBoring(@NonNull CharSequence text, @NonNull TextPaint paint, + @NonNull TextDirectionHeuristic textDir, boolean useFallbackLineSpacing, + @Nullable Paint.FontMetrics minimumFontMetrics, @Nullable Metrics metrics) { final int textLength = text.length(); if (hasAnyInterestingChars(text, textLength)) { return null; // There are some interesting characters. Not boring. @@ -570,6 +584,19 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback fm.reset(); } + if (ClientFlags.fixLineHeightForLocale()) { + if (minimumFontMetrics == null) { + paint.getFontMetricsIntForLocale(fm); + } else { + fm.set(minimumFontMetrics); + // Because the font metrics is provided by public APIs, adjust the top/bottom with + // ascent/descent: top must be smaller than ascent, bottom must be larger than + // descent. + fm.top = Math.min(fm.top, fm.ascent); + fm.bottom = Math.max(fm.bottom, fm.descent); + } + } + TextLine line = TextLine.obtain(); line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT, Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null, diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java index e17a955121b0df02cd47c3e407f1d1831ac18cd3..0421d5aaa69b11bb13c9134e1bfa5c27cc85664c 100644 --- a/core/java/android/text/ClientFlags.java +++ b/core/java/android/text/ClientFlags.java @@ -47,4 +47,11 @@ public class ClientFlags { public static boolean useBoundsForWidth() { return TextFlags.isFeatureEnabled(Flags.FLAG_USE_BOUNDS_FOR_WIDTH); } + + /** + * @see Flags#fixLineHeightForLocale() + */ + public static boolean fixLineHeightForLocale() { + return TextFlags.isFeatureEnabled(Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE); + } } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index a0cd0748f5094c977411dc53518c967c450ffd20..7b9cb6afd6a0d0a4d457d5f589468e26575984a2 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -16,6 +16,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_NO_BREAK_NO_HYPHENATION_SPAN; import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; @@ -314,6 +315,43 @@ public class DynamicLayout extends Layout { return this; } + /** + * Set the minimum font metrics used for line spacing. + * + * <p> + * {@code null} is the default value. If {@code null} is set or left as default, the + * font metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is + * used. + * + * <p> + * The minimum meaning here is the minimum value of line spacing: maximum value of + * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}. + * + * <p> + * By setting this value, each line will have minimum line spacing regardless of the text + * rendered. For example, usually Japanese script has larger vertical metrics than Latin + * script. By setting the metrics obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it + * {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved + * if the text is an English text. If the vertical metrics of the text is larger than + * Japanese, for example Burmese, the bigger font metrics is used. + * + * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the + * value obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} + * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics) + * @see android.widget.TextView#getMinimumFontMetrics() + * @see Layout#getMinimumFontMetrics() + * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + */ + @NonNull + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) { + mMinimumFontMetrics = minimumFontMetrics; + return this; + } + /** * Build the {@link DynamicLayout} after options have been set. * @@ -347,6 +385,7 @@ public class DynamicLayout extends Layout { private int mEllipsizedWidth; private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE; private boolean mUseBoundsForWidth; + private @Nullable Paint.FontMetrics mMinimumFontMetrics; private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); @@ -422,7 +461,7 @@ public class DynamicLayout extends Layout { false /* fallbackLineSpacing */, ellipsizedWidth, ellipsize, Integer.MAX_VALUE /* maxLines */, breakStrategy, hyphenationFrequency, null /* leftIndents */, null /* rightIndents */, justificationMode, - lineBreakConfig, false /* useBoundsForWidth */); + lineBreakConfig, false /* useBoundsForWidth */, null /* minimumFontMetrics */); final Builder b = Builder.obtain(base, paint, width) .setAlignment(align) @@ -448,7 +487,7 @@ public class DynamicLayout extends Layout { b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize, Integer.MAX_VALUE /* maxLines */, b.mBreakStrategy, b.mHyphenationFrequency, null /* leftIndents */, null /* rightIndents */, b.mJustificationMode, - b.mLineBreakConfig, b.mUseBoundsForWidth); + b.mLineBreakConfig, b.mUseBoundsForWidth, b.mMinimumFontMetrics); mDisplay = b.mDisplay; mIncludePad = b.mIncludePad; @@ -476,6 +515,7 @@ public class DynamicLayout extends Layout { mBase = b.mBase; mFallbackLineSpacing = b.mFallbackLineSpacing; mUseBoundsForWidth = b.mUseBoundsForWidth; + mMinimumFontMetrics = b.mMinimumFontMetrics; if (b.mEllipsize != null) { mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); mEllipsizedWidth = b.mEllipsizedWidth; @@ -672,6 +712,7 @@ public class DynamicLayout extends Layout { .setAddLastLineLineSpacing(!islast) .setIncludePad(false) .setUseBoundsForWidth(mUseBoundsForWidth) + .setMinimumFontMetrics(mMinimumFontMetrics) .setCalculateBounds(true); reflowed = b.buildPartialStaticLayoutForDynamicLayout(true /* trackpadding */, reflowed); @@ -1324,6 +1365,7 @@ public class DynamicLayout extends Layout { private Rect mTempRect = new Rect(); private boolean mUseBoundsForWidth; + @Nullable Paint.FontMetrics mMinimumFontMetrics; @UnsupportedAppUsage private static StaticLayout sStaticLayout = null; diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 4f4dea7801713ef652b992c19b97a0aa50b73a16..47c29d96855868c527ea4c482bff571d8de6fc65 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -16,6 +16,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 android.annotation.FlaggedApi; @@ -287,7 +288,7 @@ public abstract class Layout { this(text, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingMult, spacingAdd, false, false, 0, null, Integer.MAX_VALUE, BREAK_STRATEGY_SIMPLE, HYPHENATION_FREQUENCY_NONE, null, null, - JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false); + JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, false, null); } /** @@ -336,7 +337,8 @@ public abstract class Layout { int[] rightIndents, int justificationMode, LineBreakConfig lineBreakConfig, - boolean useBoundsForWidth + boolean useBoundsForWidth, + Paint.FontMetrics minimumFontMetrics ) { if (width < 0) @@ -371,6 +373,7 @@ public abstract class Layout { mJustificationMode = justificationMode; mLineBreakConfig = lineBreakConfig; mUseBoundsForWidth = useBoundsForWidth; + mMinimumFontMetrics = minimumFontMetrics; } /** @@ -3332,6 +3335,7 @@ public abstract class Layout { private int mJustificationMode; private LineBreakConfig mLineBreakConfig; private boolean mUseBoundsForWidth; + private @Nullable Paint.FontMetrics mMinimumFontMetrics; /** @hide */ @IntDef(prefix = { "DIR_" }, value = { @@ -3787,12 +3791,48 @@ public abstract class Layout { return this; } + /** + * Set the minimum font metrics used for line spacing. + * + * <p> + * {@code null} is the default value. If {@code null} is set or left it as default, the font + * metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is used. + * + * <p> + * The minimum meaning here is the minimum value of line spacing: maximum value of + * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}. + * + * <p> + * By setting this value, each line will have minimum line spacing regardless of the text + * rendered. For example, usually Japanese script has larger vertical metrics than Latin + * script. By setting the metrics obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it + * {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved + * if the text is an English text. If the vertical metrics of the text is larger than + * Japanese, for example Burmese, the bigger font metrics is used. + * + * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the + * value obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} + * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics) + * @see android.widget.TextView#getMinimumFontMetrics() + * @see Layout#getMinimumFontMetrics() + * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + */ + @NonNull + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) { + mMinimumFontMetrics = minimumFontMetrics; + return this; + } + private BoringLayout.Metrics isBoring() { if (mStart != 0 || mEnd != mText.length()) { // BoringLayout only support entire text. return null; } BoringLayout.Metrics metrics = BoringLayout.isBoring(mText, mPaint, mTextDir, - mFallbackLineSpacing, null); + mFallbackLineSpacing, mMinimumFontMetrics, null); if (metrics == null) { return null; } @@ -3833,7 +3873,8 @@ public abstract class Layout { mText, mPaint, mWidth, mAlignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, mFallbackLineSpacing, mEllipsizedWidth, mEllipsize, mMaxLines, mBreakStrategy, mHyphenationFrequency, mLeftIndents, mRightIndents, - mJustificationMode, mLineBreakConfig, metrics, mUseBoundsForWidth); + mJustificationMode, mLineBreakConfig, metrics, mUseBoundsForWidth, + mMinimumFontMetrics); } } @@ -3858,6 +3899,7 @@ public abstract class Layout { private int mJustificationMode = JUSTIFICATION_MODE_NONE; private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE; private boolean mUseBoundsForWidth; + private Paint.FontMetrics mMinimumFontMetrics; } /////////////////////////////////////////////////////////////////////////////////////////////// @@ -4164,4 +4206,22 @@ public abstract class Layout { public boolean getUseBoundsForWidth() { return mUseBoundsForWidth; } + + /** + * Get the minimum font metrics used for line spacing. + * + * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics) + * @see android.widget.TextView#getMinimumFontMetrics() + * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * + * @return a minimum font metrics. {@code null} for using the value obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} + */ + @Nullable + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public Paint.FontMetrics getMinimumFontMetrics() { + return mMinimumFontMetrics; + } } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 01279cea073f2c3034a760a04b80315ccf35ee03..77e616b358cba716fb805cf57784ef50b4db0ecb 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,6 +16,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 android.annotation.FlaggedApi; @@ -459,6 +460,43 @@ public class StaticLayout extends Layout { return this; } + /** + * Set the minimum font metrics used for line spacing. + * + * <p> + * {@code null} is the default value. If {@code null} is set or left as default, the + * font metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is + * used. + * + * <p> + * The minimum meaning here is the minimum value of line spacing: maximum value of + * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}. + * + * <p> + * By setting this value, each line will have minimum line spacing regardless of the text + * rendered. For example, usually Japanese script has larger vertical metrics than Latin + * script. By setting the metrics obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it + * {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved + * if the text is an English text. If the vertical metrics of the text is larger than + * Japanese, for example Burmese, the bigger font metrics is used. + * + * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the + * value obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} + * @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics) + * @see android.widget.TextView#getMinimumFontMetrics() + * @see Layout#getMinimumFontMetrics() + * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + */ + @NonNull + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) { + mMinimumFontMetrics = minimumFontMetrics; + return this; + } + /** * Build the {@link StaticLayout} after options have been set. * @@ -520,6 +558,7 @@ public class StaticLayout extends Layout { private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE; private boolean mUseBoundsForWidth; private boolean mCalculateBounds; + @Nullable private Paint.FontMetrics mMinimumFontMetrics; private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); @@ -550,7 +589,8 @@ public class StaticLayout extends Layout { null, // rightIndents JUSTIFICATION_MODE_NONE, null, // lineBreakConfig, - false // useBoundsForWidth + false, // useBoundsForWidth + null // minimumFontMetrics ); mColumns = COLUMNS_ELLIPSIZE; @@ -627,7 +667,8 @@ public class StaticLayout extends Layout { b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd, b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize, b.mMaxLines, b.mBreakStrategy, b.mHyphenationFrequency, b.mLeftIndents, - b.mRightIndents, b.mJustificationMode, b.mLineBreakConfig, b.mUseBoundsForWidth); + b.mRightIndents, b.mJustificationMode, b.mLineBreakConfig, b.mUseBoundsForWidth, + b.mMinimumFontMetrics); mColumns = columnSize; if (b.mEllipsize != null) { @@ -711,6 +752,35 @@ public class StaticLayout extends Layout { indents = null; } + int defaultTop; + int defaultAscent; + int defaultDescent; + int defaultBottom; + if (ClientFlags.fixLineHeightForLocale()) { + if (b.mMinimumFontMetrics != null) { + defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top); + defaultAscent = Math.round(b.mMinimumFontMetrics.ascent); + defaultDescent = Math.round(b.mMinimumFontMetrics.descent); + defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom); + } else { + paint.getFontMetricsIntForLocale(fm); + defaultTop = fm.top; + defaultAscent = fm.ascent; + defaultDescent = fm.descent; + defaultBottom = fm.bottom; + } + + // Because the font metrics is provided by public APIs, adjust the top/bottom with + // ascent/descent: top must be smaller than ascent, bottom must be larger than descent. + defaultTop = Math.min(defaultTop, defaultAscent); + defaultBottom = Math.max(defaultBottom, defaultDescent); + } else { + defaultTop = 0; + defaultAscent = 0; + defaultDescent = 0; + defaultBottom = 0; + } + final LineBreaker lineBreaker = new LineBreaker.Builder() .setBreakStrategy(b.mBreakStrategy) .setHyphenationFrequency(getBaseHyphenationFrequency(b.mHyphenationFrequency)) @@ -889,7 +959,10 @@ public class StaticLayout extends Layout { // measuring int here = paraStart; - int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0; + int fmTop = defaultTop; + int fmBottom = defaultBottom; + int fmAscent = defaultAscent; + int fmDescent = defaultDescent; int fmCacheIndex = 0; int spanEndCacheIndex = 0; int breakIndex = 0; @@ -982,7 +1055,15 @@ public class StaticLayout extends Layout { && mLineCount < mMaximumVisibleLineCount) { final MeasuredParagraph measuredPara = MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null); - paint.getFontMetricsInt(fm); + if (ClientFlags.fixLineHeightForLocale()) { + fm.top = defaultTop; + fm.ascent = defaultAscent; + fm.descent = defaultDescent; + fm.bottom = defaultBottom; + } else { + paint.getFontMetricsInt(fm); + } + v = out(source, bufEnd, bufEnd, fm.ascent, fm.descent, fm.top, fm.bottom, diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java index b8b30c230e5e24c315a3993107d4995fe33cd1f1..24663862400dc8ba5925f3e3d9f470341442c693 100644 --- a/core/java/android/text/TextFlags.java +++ b/core/java/android/text/TextFlags.java @@ -58,6 +58,7 @@ public final class TextFlags { Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN, Flags.FLAG_PHRASE_STRICT_FALLBACK, Flags.FLAG_USE_BOUNDS_FOR_WIDTH, + Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE, }; /** @@ -69,6 +70,7 @@ public final class TextFlags { Flags.noBreakNoHyphenationSpan(), Flags.phraseStrictFallback(), Flags.useBoundsForWidth(), + Flags.fixLineHeightForLocale(), }; /** diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index a0628c4b95804d2ee043d2e0c217e45a4b1339d5..6da6a64dc0423bc93bfa53fdfdad4cad0703c571 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -28,6 +28,7 @@ import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_C import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY; import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; +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 android.R; @@ -865,6 +866,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private final boolean mUseTextPaddingForUiTranslation; private boolean mUseBoundsForWidth; + @Nullable private Paint.FontMetrics mMinimumFontMetrics; @ViewDebug.ExportedProperty(category = "text") @UnsupportedAppUsage @@ -4900,6 +4902,58 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mUseBoundsForWidth; } + /** + * Set the minimum font metrics used for line spacing. + * + * <p> + * {@code null} is the default value. If {@code null} is set or left as default, the font + * metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is used. + * + * <p> + * The minimum meaning here is the minimum value of line spacing: maximum value of + * {@link Paint#ascent()}, minimum value of {@link Paint#descent()}. + * + * <p> + * By setting this value, each line will have minimum line spacing regardless of the text + * rendered. For example, usually Japanese script has larger vertical metrics than Latin script. + * By setting the metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} + * for Japanese or leave it {@code null} if the TextView's locale or system locale is Japanese, + * the line spacing for Japanese is reserved if the TextView contains English text. If the + * vertical metrics of the text is larger than Japanese, for example Burmese, the bigger font + * metrics is used. + * + * @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the value + * obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} + * @see #getMinimumFontMetrics() + * @see Layout#getMinimumFontMetrics() + * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + */ + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public void setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) { + mMinimumFontMetrics = minimumFontMetrics; + } + + /** + * Get the minimum font metrics used for line spacing. + * + * @see #setMinimumFontMetrics(Paint.FontMetrics) + * @see Layout#getMinimumFontMetrics() + * @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * @see DynamicLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics) + * + * @return a minimum font metrics. {@code null} for using the value obtained by + * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} + */ + @Nullable + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public Paint.FontMetrics getMinimumFontMetrics() { + return mMinimumFontMetrics; + } + /** * @return whether fallback line spacing is enabled, {@code true} by default * @@ -10683,7 +10737,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (hintBoring == UNKNOWN_BORING) { hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), mHintBoring); + isFallbackLineSpacingForBoringLayout(), + mMinimumFontMetrics, mHintBoring); if (hintBoring != null) { mHintBoring = hintBoring; } @@ -10732,7 +10787,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE) .setLineBreakConfig(LineBreakConfig.getLineBreakConfig( mLineBreakStyle, mLineBreakWordStyle)) - .setUseBoundsForWidth(mUseBoundsForWidth); + .setUseBoundsForWidth(mUseBoundsForWidth) + .setMinimumFontMetrics(mMinimumFontMetrics); if (shouldEllipsize) { builder.setEllipsize(mEllipsize) .setEllipsizedWidth(ellipsisWidth); @@ -10796,12 +10852,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mLineBreakStyle, mLineBreakWordStyle)) .setUseBoundsForWidth(mUseBoundsForWidth) .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null) - .setEllipsizedWidth(ellipsisWidth); + .setEllipsizedWidth(ellipsisWidth) + .setMinimumFontMetrics(mMinimumFontMetrics); result = builder.build(); } else { if (boring == UNKNOWN_BORING) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), mBoring); + isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring); if (boring != null) { mBoring = boring; } @@ -10815,7 +10872,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, null, wantWidth, isFallbackLineSpacingForBoringLayout(), - mUseBoundsForWidth); + mUseBoundsForWidth, mMinimumFontMetrics); } else { result = new BoringLayout( mTransformed, @@ -10829,7 +10886,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener wantWidth, null, boring, - mUseBoundsForWidth); + mUseBoundsForWidth, + mMinimumFontMetrics); } if (useSaved) { @@ -10841,7 +10899,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener wantWidth, alignment, mSpacingMult, mSpacingAdd, boring, mIncludePad, effectiveEllipsize, ellipsisWidth, isFallbackLineSpacingForBoringLayout(), - mUseBoundsForWidth); + mUseBoundsForWidth, mMinimumFontMetrics); } else { result = new BoringLayout( mTransformed, @@ -10855,7 +10913,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener ellipsisWidth, effectiveEllipsize, boring, - mUseBoundsForWidth); + mUseBoundsForWidth, + mMinimumFontMetrics); } } } @@ -10874,7 +10933,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE) .setLineBreakConfig(LineBreakConfig.getLineBreakConfig( mLineBreakStyle, mLineBreakWordStyle)) - .setUseBoundsForWidth(mUseBoundsForWidth); + .setUseBoundsForWidth(mUseBoundsForWidth) + .setMinimumFontMetrics(mMinimumFontMetrics); if (shouldEllipsize) { builder.setEllipsize(effectiveEllipsize) .setEllipsizedWidth(ellipsisWidth); @@ -11002,7 +11062,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (des < 0) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), mBoring); + isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring); if (boring != null) { mBoring = boring; } @@ -11042,7 +11102,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (hintDes < 0) { hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, - isFallbackLineSpacingForBoringLayout(), mHintBoring); + isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, + mHintBoring); if (hintBoring != null) { mHintBoring = hintBoring; } @@ -11254,7 +11315,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener .setTextDirection(getTextDirectionHeuristic()) .setLineBreakConfig(LineBreakConfig.getLineBreakConfig( mLineBreakStyle, mLineBreakWordStyle)) - .setUseBoundsForWidth(mUseBoundsForWidth); + .setUseBoundsForWidth(mUseBoundsForWidth) + .setMinimumFontMetrics(mMinimumFontMetrics); final StaticLayout layout = layoutBuilder.build(); diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 9fde0fd6e6ab69f72aa4de2e20a154c68dc368e0..4eaa01309ab129a59fc14ec5f7a82f206661fce3 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -2113,6 +2113,31 @@ public class Paint { * The recommended additional space to add between lines of text. */ public float leading; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof FontMetrics)) return false; + FontMetrics that = (FontMetrics) o; + return that.top == top && that.ascent == ascent && that.descent == descent + && that.bottom == bottom && that.leading == leading; + } + + @Override + public int hashCode() { + return Objects.hash(top, ascent, descent, bottom, leading); + } + + @Override + public String toString() { + return "FontMetrics{" + + "top=" + top + + ", ascent=" + ascent + + ", descent=" + descent + + ", bottom=" + bottom + + ", leading=" + leading + + '}'; + } } /** @@ -2309,6 +2334,33 @@ public class Paint { */ public int leading; + /** + * Set values from {@link FontMetricsInt}. + * @param fontMetricsInt a font metrics. + */ + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public void set(@NonNull FontMetricsInt fontMetricsInt) { + top = fontMetricsInt.top; + ascent = fontMetricsInt.ascent; + descent = fontMetricsInt.descent; + bottom = fontMetricsInt.bottom; + leading = fontMetricsInt.leading; + } + + /** + * Set values from {@link FontMetrics} with rounding accordingly. + * @param fontMetrics a font metrics. + */ + @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE) + public void set(@NonNull FontMetrics fontMetrics) { + // See GraphicsJNI::set_metrics_int method for consistency. + top = (int) Math.floor(fontMetrics.top); + ascent = Math.round(fontMetrics.ascent); + descent = Math.round(fontMetrics.descent); + bottom = (int) Math.ceil(fontMetrics.bottom); + leading = Math.round(fontMetrics.leading); + } + @Override public String toString() { return "FontMetricsInt: top=" + top + " ascent=" + ascent + " descent=" + descent + " bottom=" + bottom +