diff options
author | 2017-08-24 17:39:14 +0000 | |
---|---|---|
committer | 2017-08-24 17:39:14 +0000 | |
commit | 612e67323feadfe2baea8181c0ad4c7cdf98ef1a (patch) | |
tree | 89656c8d77c5c63e72e581e56a282ea14796e290 | |
parent | d6fc25b757758e2b23423af95a8693bf661c2b44 (diff) | |
parent | 22a167cac8f585ffd3ca73e40b82a26c1e09df11 (diff) |
Merge "Add a builder for DynamicLayout and switch TextView to it"
-rw-r--r-- | api/current.txt | 17 | ||||
-rw-r--r-- | api/system-current.txt | 17 | ||||
-rw-r--r-- | api/test-current.txt | 17 | ||||
-rw-r--r-- | core/java/android/text/DynamicLayout.java | 438 | ||||
-rw-r--r-- | core/java/android/text/Layout.java | 10 | ||||
-rw-r--r-- | core/java/android/text/StaticLayout.java | 153 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 17 |
7 files changed, 523 insertions, 146 deletions
diff --git a/api/current.txt b/api/current.txt index 27474d69aa97..83b446d6d233 100644 --- a/api/current.txt +++ b/api/current.txt @@ -41401,6 +41401,21 @@ package android.text { method public int getTopPadding(); } + public static final class DynamicLayout.Builder { + method public android.text.DynamicLayout build(); + method public static android.text.DynamicLayout.Builder obtain(java.lang.CharSequence, android.text.TextPaint, int); + method public android.text.DynamicLayout.Builder setAlignment(android.text.Layout.Alignment); + method public android.text.DynamicLayout.Builder setBreakStrategy(int); + method public android.text.DynamicLayout.Builder setDisplayText(java.lang.CharSequence); + method public android.text.DynamicLayout.Builder setEllipsize(android.text.TextUtils.TruncateAt); + method public android.text.DynamicLayout.Builder setEllipsizedWidth(int); + method public android.text.DynamicLayout.Builder setHyphenationFrequency(int); + method public android.text.DynamicLayout.Builder setIncludePad(boolean); + method public android.text.DynamicLayout.Builder setJustificationMode(int); + method public android.text.DynamicLayout.Builder setLineSpacing(float, float); + method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic); + } + public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable { method public abstract android.text.Editable append(java.lang.CharSequence); method public abstract android.text.Editable append(java.lang.CharSequence, int, int); @@ -41565,6 +41580,8 @@ package android.text { field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2 field public static final int BREAK_STRATEGY_HIGH_QUALITY = 1; // 0x1 field public static final int BREAK_STRATEGY_SIMPLE = 0; // 0x0 + field public static final float DEFAULT_LINESPACING_ADDITION = 0.0f; + field public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f; field public static final int DIR_LEFT_TO_RIGHT = 1; // 0x1 field public static final int DIR_RIGHT_TO_LEFT = -1; // 0xffffffff field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2 diff --git a/api/system-current.txt b/api/system-current.txt index 06c3ce0a4c8d..124f1d23db6d 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -45005,6 +45005,21 @@ package android.text { method public int getTopPadding(); } + public static final class DynamicLayout.Builder { + method public android.text.DynamicLayout build(); + method public static android.text.DynamicLayout.Builder obtain(java.lang.CharSequence, android.text.TextPaint, int); + method public android.text.DynamicLayout.Builder setAlignment(android.text.Layout.Alignment); + method public android.text.DynamicLayout.Builder setBreakStrategy(int); + method public android.text.DynamicLayout.Builder setDisplayText(java.lang.CharSequence); + method public android.text.DynamicLayout.Builder setEllipsize(android.text.TextUtils.TruncateAt); + method public android.text.DynamicLayout.Builder setEllipsizedWidth(int); + method public android.text.DynamicLayout.Builder setHyphenationFrequency(int); + method public android.text.DynamicLayout.Builder setIncludePad(boolean); + method public android.text.DynamicLayout.Builder setJustificationMode(int); + method public android.text.DynamicLayout.Builder setLineSpacing(float, float); + method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic); + } + public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable { method public abstract android.text.Editable append(java.lang.CharSequence); method public abstract android.text.Editable append(java.lang.CharSequence, int, int); @@ -45169,6 +45184,8 @@ package android.text { field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2 field public static final int BREAK_STRATEGY_HIGH_QUALITY = 1; // 0x1 field public static final int BREAK_STRATEGY_SIMPLE = 0; // 0x0 + field public static final float DEFAULT_LINESPACING_ADDITION = 0.0f; + field public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f; field public static final int DIR_LEFT_TO_RIGHT = 1; // 0x1 field public static final int DIR_RIGHT_TO_LEFT = -1; // 0xffffffff field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2 diff --git a/api/test-current.txt b/api/test-current.txt index 6847d8eed2f9..b8a606c5e976 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -41665,6 +41665,21 @@ package android.text { method public int getTopPadding(); } + public static final class DynamicLayout.Builder { + method public android.text.DynamicLayout build(); + method public static android.text.DynamicLayout.Builder obtain(java.lang.CharSequence, android.text.TextPaint, int); + method public android.text.DynamicLayout.Builder setAlignment(android.text.Layout.Alignment); + method public android.text.DynamicLayout.Builder setBreakStrategy(int); + method public android.text.DynamicLayout.Builder setDisplayText(java.lang.CharSequence); + method public android.text.DynamicLayout.Builder setEllipsize(android.text.TextUtils.TruncateAt); + method public android.text.DynamicLayout.Builder setEllipsizedWidth(int); + method public android.text.DynamicLayout.Builder setHyphenationFrequency(int); + method public android.text.DynamicLayout.Builder setIncludePad(boolean); + method public android.text.DynamicLayout.Builder setJustificationMode(int); + method public android.text.DynamicLayout.Builder setLineSpacing(float, float); + method public android.text.DynamicLayout.Builder setTextDirection(android.text.TextDirectionHeuristic); + } + public abstract interface Editable implements java.lang.Appendable java.lang.CharSequence android.text.GetChars android.text.Spannable { method public abstract android.text.Editable append(java.lang.CharSequence); method public abstract android.text.Editable append(java.lang.CharSequence, int, int); @@ -41829,6 +41844,8 @@ package android.text { field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2 field public static final int BREAK_STRATEGY_HIGH_QUALITY = 1; // 0x1 field public static final int BREAK_STRATEGY_SIMPLE = 0; // 0x0 + field public static final float DEFAULT_LINESPACING_ADDITION = 0.0f; + field public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f; field public static final int DIR_LEFT_TO_RIGHT = 1; // 0x1 field public static final int DIR_RIGHT_TO_LEFT = -1; // 0xffffffff field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2 diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index d6a68fb54b7c..661b6085e4f0 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -16,12 +16,17 @@ package android.text; +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Paint; import android.graphics.Rect; import android.text.style.ReplacementSpan; import android.text.style.UpdateLayout; import android.text.style.WrapTogetherSpan; import android.util.ArraySet; +import android.util.Pools.SynchronizedPool; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; @@ -43,45 +48,276 @@ public class DynamicLayout extends Layout private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400; /** - * Make a layout for the specified text that will be updated as - * the text is changed. + * Builder for dynamic layouts. The builder is the preferred pattern for constructing + * DynamicLayout objects and should be preferred over the constructors, particularly to access + * newer features. To build a dynamic layout, first call {@link #obtain} with the required + * arguments (base, paint, and width), then call setters for optional parameters, and finally + * {@link #build} to build the DynamicLayout object. Parameters not explicitly set will get + * default values. */ - public DynamicLayout(CharSequence base, - TextPaint paint, - int width, Alignment align, - float spacingmult, float spacingadd, + public static final class Builder { + private Builder() { + } + + /** + * Obtain a builder for constructing DynamicLayout objects. + */ + @NonNull + public static Builder obtain(@NonNull CharSequence base, @NonNull TextPaint paint, + @IntRange(from = 0) int width) { + Builder b = sPool.acquire(); + if (b == null) { + b = new Builder(); + } + + // set default initial values + b.mBase = base; + b.mDisplay = base; + b.mPaint = paint; + b.mWidth = width; + b.mAlignment = Alignment.ALIGN_NORMAL; + b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; + b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER; + b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION; + b.mIncludePad = true; + b.mEllipsizedWidth = width; + b.mEllipsize = null; + b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE; + b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE; + b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE; + return b; + } + + /** + * This method should be called after the layout is finished getting constructed and the + * builder needs to be cleaned up and returned to the pool. + */ + private static void recycle(@NonNull Builder b) { + b.mBase = null; + b.mDisplay = null; + b.mPaint = null; + sPool.release(b); + } + + /** + * Set the transformed text (password transformation being the primary example of a + * transformation) that will be updated as the base text is changed. The default is the + * 'base' text passed to the builder's constructor. + * + * @param display the transformed text + * @return this builder, useful for chaining + */ + @NonNull + public Builder setDisplayText(@NonNull CharSequence display) { + mDisplay = display; + return this; + } + + /** + * Set the alignment. The default is {@link Layout.Alignment#ALIGN_NORMAL}. + * + * @param alignment Alignment for the resulting {@link DynamicLayout} + * @return this builder, useful for chaining + */ + @NonNull + public Builder setAlignment(@NonNull Alignment alignment) { + mAlignment = alignment; + return this; + } + + /** + * Set the text direction heuristic. The text direction heuristic is used to resolve text + * direction per-paragraph based on the input text. The default is + * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. + * + * @param textDir text direction heuristic for resolving bidi behavior. + * @return this builder, useful for chaining + */ + @NonNull + public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { + mTextDir = textDir; + return this; + } + + /** + * Set line spacing parameters. Each line will have its line spacing multiplied by + * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for + * {@code spacingAdd} and 1.0 for {@code spacingMult}. + * + * @param spacingAdd the amount of line spacing addition + * @param spacingMult the line spacing multiplier + * @return this builder, useful for chaining + * @see android.widget.TextView#setLineSpacing + */ + @NonNull + public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) { + mSpacingAdd = spacingAdd; + mSpacingMult = spacingMult; + return this; + } + + /** + * Set whether to include extra space beyond font ascent and descent (which is needed to + * avoid clipping in some languages, such as Arabic and Kannada). The default is + * {@code true}. + * + * @param includePad whether to include padding + * @return this builder, useful for chaining + * @see android.widget.TextView#setIncludeFontPadding + */ + @NonNull + public Builder setIncludePad(boolean includePad) { + mIncludePad = includePad; + return this; + } + + /** + * Set the width as used for ellipsizing purposes, if it differs from the normal layout + * width. The default is the {@code width} passed to {@link #obtain}. + * + * @param ellipsizedWidth width used for ellipsizing, in pixels + * @return this builder, useful for chaining + * @see android.widget.TextView#setEllipsize + */ + @NonNull + public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) { + mEllipsizedWidth = ellipsizedWidth; + return this; + } + + /** + * Set ellipsizing on the layout. Causes words that are longer than the view is wide, or + * exceeding the number of lines (see #setMaxLines) in the case of + * {@link android.text.TextUtils.TruncateAt#END} or + * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead of broken. + * The default is {@code null}, indicating no ellipsis is to be applied. + * + * @param ellipsize type of ellipsis behavior + * @return this builder, useful for chaining + * @see android.widget.TextView#setEllipsize + */ + public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { + mEllipsize = ellipsize; + return this; + } + + /** + * Set break strategy, useful for selecting high quality or balanced paragraph layout + * options. The default is {@link Layout#BREAK_STRATEGY_SIMPLE}. + * + * @param breakStrategy break strategy for paragraph layout + * @return this builder, useful for chaining + * @see android.widget.TextView#setBreakStrategy + */ + @NonNull + public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { + mBreakStrategy = breakStrategy; + return this; + } + + /** + * Set hyphenation frequency, to control the amount of automatic hyphenation used. The + * possible values are defined in {@link Layout}, by constants named with the pattern + * {@code HYPHENATION_FREQUENCY_*}. The default is + * {@link Layout#HYPHENATION_FREQUENCY_NONE}. + * + * @param hyphenationFrequency hyphenation frequency for the paragraph + * @return this builder, useful for chaining + * @see android.widget.TextView#setHyphenationFrequency + */ + @NonNull + public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { + mHyphenationFrequency = hyphenationFrequency; + return this; + } + + /** + * Set paragraph justification mode. The default value is + * {@link Layout#JUSTIFICATION_MODE_NONE}. If the last line is too short for justification, + * the last line will be displayed with the alignment set by {@link #setAlignment}. + * + * @param justificationMode justification mode for the paragraph. + * @return this builder, useful for chaining. + */ + @NonNull + public Builder setJustificationMode(@JustificationMode int justificationMode) { + mJustificationMode = justificationMode; + return this; + } + + /** + * Build the {@link DynamicLayout} after options have been set. + * + * <p>Note: the builder object must not be reused in any way after calling this method. + * Setting parameters after calling this method, or calling it a second time on the same + * builder object, will likely lead to unexpected results. + * + * @return the newly constructed {@link DynamicLayout} object + */ + @NonNull + public DynamicLayout build() { + final DynamicLayout result = new DynamicLayout(this); + Builder.recycle(this); + return result; + } + + private CharSequence mBase; + private CharSequence mDisplay; + private TextPaint mPaint; + private int mWidth; + private Alignment mAlignment; + private TextDirectionHeuristic mTextDir; + private float mSpacingMult; + private float mSpacingAdd; + private boolean mIncludePad; + private int mBreakStrategy; + private int mHyphenationFrequency; + private int mJustificationMode; + private TextUtils.TruncateAt mEllipsize; + private int mEllipsizedWidth; + + private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); + + private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); + } + + /** + * Make a layout for the specified text that will be updated as the text is changed. + */ + public DynamicLayout(@NonNull CharSequence base, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, @NonNull Alignment align, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad) { this(base, base, paint, width, align, spacingmult, spacingadd, includepad); } /** - * Make a layout for the transformed text (password transformation - * being the primary example of a transformation) - * that will be updated as the base text is changed. + * Make a layout for the transformed text (password transformation being the primary example of + * a transformation) that will be updated as the base text is changed. */ - public DynamicLayout(CharSequence base, CharSequence display, - TextPaint paint, - int width, Alignment align, - float spacingmult, float spacingadd, + public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, @NonNull Alignment align, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad) { this(base, display, paint, width, align, spacingmult, spacingadd, includepad, null, 0); } /** - * Make a layout for the transformed text (password transformation - * being the primary example of a transformation) - * that will be updated as the base text is changed. - * If ellipsize is non-null, the Layout will ellipsize the text - * down to ellipsizedWidth. + * Make a layout for the transformed text (password transformation being the primary example of + * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, + * the Layout will ellipsize the text down to ellipsizedWidth. */ - public DynamicLayout(CharSequence base, CharSequence display, - TextPaint paint, - int width, Alignment align, - float spacingmult, float spacingadd, + public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, @NonNull Alignment align, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, boolean includepad, - TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { + @Nullable TextUtils.TruncateAt ellipsize, + @IntRange(from = 0) int ellipsizedWidth) { this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingmult, spacingadd, includepad, StaticLayout.BREAK_STRATEGY_SIMPLE, StaticLayout.HYPHENATION_FREQUENCY_NONE, @@ -89,83 +325,119 @@ public class DynamicLayout extends Layout } /** - * Make a layout for the transformed text (password transformation - * being the primary example of a transformation) - * that will be updated as the base text is changed. - * If ellipsize is non-null, the Layout will ellipsize the text - * down to ellipsizedWidth. - * * - * *@hide + * Make a layout for the transformed text (password transformation being the primary example of + * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, + * the Layout will ellipsize the text down to ellipsizedWidth. + * + * @hide */ - public DynamicLayout(CharSequence base, CharSequence display, - TextPaint paint, - int width, Alignment align, TextDirectionHeuristic textDir, - float spacingmult, float spacingadd, - boolean includepad, int breakStrategy, int hyphenationFrequency, - int justificationMode, TextUtils.TruncateAt ellipsize, - int ellipsizedWidth) { - super((ellipsize == null) - ? display - : (display instanceof Spanned) - ? new SpannedEllipsizer(display) - : new Ellipsizer(display), + public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, + @NonNull TextPaint paint, + @IntRange(from = 0) int width, + @NonNull Alignment align, @NonNull TextDirectionHeuristic textDir, + @FloatRange(from = 0.0) float spacingmult, float spacingadd, + boolean includepad, @BreakStrategy int breakStrategy, + @HyphenationFrequency int hyphenationFrequency, + @JustificationMode int justificationMode, + @Nullable TextUtils.TruncateAt ellipsize, + @IntRange(from = 0) int ellipsizedWidth) { + super(createEllipsizer(ellipsize, display), paint, width, align, textDir, spacingmult, spacingadd); - mBase = base; + final Builder b = Builder.obtain(base, paint, width) + .setAlignment(align) + .setTextDirection(textDir) + .setLineSpacing(spacingadd, spacingmult) + .setEllipsizedWidth(ellipsizedWidth) + .setEllipsize(ellipsize); mDisplay = display; - - if (ellipsize != null) { - mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); - mEllipsizedWidth = ellipsizedWidth; - mEllipsizeAt = ellipsize; - } else { - mInts = new PackedIntVector(COLUMNS_NORMAL); - mEllipsizedWidth = width; - mEllipsizeAt = null; - } - - mObjects = new PackedObjectVector<Directions>(1); - mIncludePad = includepad; mBreakStrategy = breakStrategy; mJustificationMode = justificationMode; mHyphenationFrequency = hyphenationFrequency; - /* - * This is annoying, but we can't refer to the layout until - * superclass construction is finished, and the superclass - * constructor wants the reference to the display text. - * - * This will break if the superclass constructor ever actually - * cares about the content instead of just holding the reference. - */ - if (ellipsize != null) { - Ellipsizer e = (Ellipsizer) getText(); + generate(b); + + Builder.recycle(b); + } + + private DynamicLayout(@NonNull Builder b) { + super(createEllipsizer(b.mEllipsize, b.mDisplay), + b.mPaint, b.mWidth, b.mAlignment, b.mSpacingMult, b.mSpacingAdd); + + mDisplay = b.mDisplay; + mIncludePad = b.mIncludePad; + mBreakStrategy = b.mBreakStrategy; + mJustificationMode = b.mJustificationMode; + mHyphenationFrequency = b.mHyphenationFrequency; + + generate(b); + } + @NonNull + private static CharSequence createEllipsizer(@Nullable TextUtils.TruncateAt ellipsize, + @NonNull CharSequence display) { + if (ellipsize == null) { + return display; + } else if (display instanceof Spanned) { + return new SpannedEllipsizer(display); + } else { + return new Ellipsizer(display); + } + } + + private void generate(@NonNull Builder b) { + mBase = b.mBase; + if (b.mEllipsize != null) { + mInts = new PackedIntVector(COLUMNS_ELLIPSIZE); + mEllipsizedWidth = b.mEllipsizedWidth; + mEllipsizeAt = b.mEllipsize; + + /* + * This is annoying, but we can't refer to the layout until superclass construction is + * finished, and the superclass constructor wants the reference to the display text. + * + * In other words, the two Ellipsizer classes in Layout.java need a + * (Dynamic|Static)Layout as a parameter to do their calculations, but the Ellipsizers + * also need to be the input to the superclass's constructor (Layout). In order to go + * around the circular dependency, we construct the Ellipsizer with only one of the + * parameters, the text (in createEllipsizer). And we fill in the rest of the needed + * information (layout, width, and method) later, here. + * + * This will break if the superclass constructor ever actually cares about the content + * instead of just holding the reference. + */ + final Ellipsizer e = (Ellipsizer) getText(); e.mLayout = this; - e.mWidth = ellipsizedWidth; - e.mMethod = ellipsize; + e.mWidth = b.mEllipsizedWidth; + e.mMethod = b.mEllipsize; mEllipsize = true; + } else { + mInts = new PackedIntVector(COLUMNS_NORMAL); + mEllipsizedWidth = b.mWidth; + mEllipsizeAt = null; } - // Initial state is a single line with 0 characters (0 to 0), - // with top at 0 and bottom at whatever is natural, and - // undefined ellipsis. + mObjects = new PackedObjectVector<Directions>(1); + + // Initial state is a single line with 0 characters (0 to 0), with top at 0 and bottom at + // whatever is natural, and undefined ellipsis. int[] start; - if (ellipsize != null) { + if (b.mEllipsize != null) { start = new int[COLUMNS_ELLIPSIZE]; start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED; } else { start = new int[COLUMNS_NORMAL]; } - Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; + final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT }; - Paint.FontMetricsInt fm = paint.getFontMetricsInt(); - int asc = fm.ascent; - int desc = fm.descent; + final Paint.FontMetricsInt fm = b.mFontMetricsInt; + b.mPaint.getFontMetricsInt(fm); + final int asc = fm.ascent; + final int desc = fm.descent; start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT; start[TOP] = 0; @@ -177,20 +449,22 @@ public class DynamicLayout extends Layout mObjects.insertAt(0, dirs); + final int baseLength = mBase.length(); // Update from 0 characters to whatever the real text is - reflow(base, 0, 0, base.length()); + reflow(mBase, 0, 0, baseLength); - if (base instanceof Spannable) { + if (mBase instanceof Spannable) { if (mWatcher == null) mWatcher = new ChangeWatcher(this); // Strip out any watchers for other DynamicLayouts. - Spannable sp = (Spannable) base; - ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class); - for (int i = 0; i < spans.length; i++) + final Spannable sp = (Spannable) mBase; + final ChangeWatcher[] spans = sp.getSpans(0, baseLength, ChangeWatcher.class); + for (int i = 0; i < spans.length; i++) { sp.removeSpan(spans[i]); + } - sp.setSpan(mWatcher, 0, base.length(), + sp.setSpan(mWatcher, 0, baseLength, Spannable.SPAN_INCLUSIVE_INCLUSIVE | (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT)); } diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 5d9c8d88fd1b..04dadc8eb0f4 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -119,6 +119,16 @@ public abstract class Layout { */ public static final int JUSTIFICATION_MODE_INTER_WORD = 1; + /* + * Line spacing multiplier for default line spacing. + */ + public static final float DEFAULT_LINESPACING_MULTIPLIER = 1.0f; + + /* + * Line spacing addition for default line spacing. + */ + public static final float DEFAULT_LINESPACING_ADDITION = 0.0f; + /** * Return how wide a layout must be in order to display the specified text with one line per * paragraph. diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 69c04b33dc71..1725d40eec71 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -16,6 +16,9 @@ package android.text; +import android.annotation.FloatRange; +import android.annotation.IntRange; +import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Paint; import android.os.LocaleList; @@ -49,12 +52,11 @@ public class StaticLayout extends Layout { static final String TAG = "StaticLayout"; /** - * Builder for static layouts. The builder is a newer pattern for constructing - * StaticLayout objects and should be preferred over the constructors, - * particularly to access newer features. To build a static layout, first - * call {@link #obtain} with the required arguments (text, paint, and width), - * then call setters for optional parameters, and finally {@link #build} - * to build the StaticLayout object. Parameters not explicitly set will get + * Builder for static layouts. The builder is the preferred pattern for constructing + * StaticLayout objects and should be preferred over the constructors, particularly to access + * newer features. To build a static layout, first call {@link #obtain} with the required + * arguments (text, paint, and width), then call setters for optional parameters, and finally + * {@link #build} to build the StaticLayout object. Parameters not explicitly set will get * default values. */ public final static class Builder { @@ -63,7 +65,7 @@ public class StaticLayout extends Layout { } /** - * Obtain a builder for constructing StaticLayout objects + * Obtain a builder for constructing StaticLayout objects. * * @param source The text to be laid out, optionally with spans * @param start The index of the start of the text @@ -72,8 +74,10 @@ public class StaticLayout extends Layout { * @param width The width in pixels * @return a builder object used for constructing the StaticLayout */ - public static Builder obtain(CharSequence source, int start, int end, TextPaint paint, - int width) { + @NonNull + public static Builder obtain(@NonNull CharSequence source, @IntRange(from = 0) int start, + @IntRange(from = 0) int end, @NonNull TextPaint paint, + @IntRange(from = 0) int width) { Builder b = sPool.acquire(); if (b == null) { b = new Builder(); @@ -87,8 +91,8 @@ public class StaticLayout extends Layout { b.mWidth = width; b.mAlignment = Alignment.ALIGN_NORMAL; b.mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR; - b.mSpacingMult = 1.0f; - b.mSpacingAdd = 0.0f; + b.mSpacingMult = DEFAULT_LINESPACING_MULTIPLIER; + b.mSpacingAdd = DEFAULT_LINESPACING_ADDITION; b.mIncludePad = true; b.mFallbackLineSpacing = false; b.mEllipsizedWidth = width; @@ -102,7 +106,11 @@ public class StaticLayout extends Layout { return b; } - private static void recycle(Builder b) { + /** + * This method should be called after the layout is finished getting constructed and the + * builder needs to be cleaned up and returned to the pool. + */ + private static void recycle(@NonNull Builder b) { b.mPaint = null; b.mText = null; MeasuredText.recycle(b.mMeasuredText); @@ -139,7 +147,8 @@ public class StaticLayout extends Layout { * * @hide */ - public Builder setText(CharSequence source, int start, int end) { + @NonNull + public Builder setText(@NonNull CharSequence source, int start, int end) { mText = source; mStart = start; mEnd = end; @@ -154,7 +163,8 @@ public class StaticLayout extends Layout { * * @hide */ - public Builder setPaint(TextPaint paint) { + @NonNull + public Builder setPaint(@NonNull TextPaint paint) { mPaint = paint; return this; } @@ -167,7 +177,8 @@ public class StaticLayout extends Layout { * * @hide */ - public Builder setWidth(int width) { + @NonNull + public Builder setWidth(@IntRange(from = 0) int width) { mWidth = width; if (mEllipsize == null) { mEllipsizedWidth = width; @@ -181,34 +192,38 @@ public class StaticLayout extends Layout { * @param alignment Alignment for the resulting {@link StaticLayout} * @return this builder, useful for chaining */ - public Builder setAlignment(Alignment alignment) { + @NonNull + public Builder setAlignment(@NonNull Alignment alignment) { mAlignment = alignment; return this; } /** * Set the text direction heuristic. The text direction heuristic is used to - * resolve text direction based per-paragraph based on the input text. The default is + * resolve text direction per-paragraph based on the input text. The default is * {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}. * - * @param textDir text direction heuristic for resolving BiDi behavior. + * @param textDir text direction heuristic for resolving bidi behavior. * @return this builder, useful for chaining */ - public Builder setTextDirection(TextDirectionHeuristic textDir) { + @NonNull + public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) { mTextDir = textDir; return this; } /** - * Set line spacing parameters. The default is 0.0 for {@code spacingAdd} - * and 1.0 for {@code spacingMult}. + * Set line spacing parameters. Each line will have its line spacing multiplied by + * {@code spacingMult} and then increased by {@code spacingAdd}. The default is 0.0 for + * {@code spacingAdd} and 1.0 for {@code spacingMult}. * - * @param spacingAdd line spacing add - * @param spacingMult line spacing multiplier + * @param spacingAdd the amount of line spacing addition + * @param spacingMult the line spacing multiplier * @return this builder, useful for chaining * @see android.widget.TextView#setLineSpacing */ - public Builder setLineSpacing(float spacingAdd, float spacingMult) { + @NonNull + public Builder setLineSpacing(float spacingAdd, @FloatRange(from = 0.0) float spacingMult) { mSpacingAdd = spacingAdd; mSpacingMult = spacingMult; return this; @@ -223,6 +238,7 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setIncludeFontPadding */ + @NonNull public Builder setIncludePad(boolean includePad) { mIncludePad = includePad; return this; @@ -241,6 +257,7 @@ public class StaticLayout extends Layout { * @param useLineSpacingFromFallbacks whether to expand linespacing based on fallback fonts * @return this builder, useful for chaining */ + @NonNull public Builder setUseLineSpacingFromFallbacks(boolean useLineSpacingFromFallbacks) { mFallbackLineSpacing = useLineSpacingFromFallbacks; return this; @@ -255,7 +272,8 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setEllipsize */ - public Builder setEllipsizedWidth(int ellipsizedWidth) { + @NonNull + public Builder setEllipsizedWidth(@IntRange(from = 0) int ellipsizedWidth) { mEllipsizedWidth = ellipsizedWidth; return this; } @@ -265,13 +283,13 @@ public class StaticLayout extends Layout { * is wide, or exceeding the number of lines (see #setMaxLines) in the case * of {@link android.text.TextUtils.TruncateAt#END} or * {@link android.text.TextUtils.TruncateAt#MARQUEE}, to be ellipsized instead - * of broken. The default is - * {@code null}, indicating no ellipsis is to be applied. + * of broken. The default is {@code null}, indicating no ellipsis is to be applied. * * @param ellipsize type of ellipsis behavior * @return this builder, useful for chaining * @see android.widget.TextView#setEllipsize */ + @NonNull public Builder setEllipsize(@Nullable TextUtils.TruncateAt ellipsize) { mEllipsize = ellipsize; return this; @@ -286,7 +304,8 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setMaxLines */ - public Builder setMaxLines(int maxLines) { + @NonNull + public Builder setMaxLines(@IntRange(from = 0) int maxLines) { mMaxLines = maxLines; return this; } @@ -299,6 +318,7 @@ public class StaticLayout extends Layout { * @return this builder, useful for chaining * @see android.widget.TextView#setBreakStrategy */ + @NonNull public Builder setBreakStrategy(@BreakStrategy int breakStrategy) { mBreakStrategy = breakStrategy; return this; @@ -306,12 +326,15 @@ public class StaticLayout extends Layout { /** * Set hyphenation frequency, to control the amount of automatic hyphenation used. The - * default is {@link Layout#HYPHENATION_FREQUENCY_NONE}. + * possible values are defined in {@link Layout}, by constants named with the pattern + * {@code HYPHENATION_FREQUENCY_*}. The default is + * {@link Layout#HYPHENATION_FREQUENCY_NONE}. * * @param hyphenationFrequency hyphenation frequency for the paragraph * @return this builder, useful for chaining * @see android.widget.TextView#setHyphenationFrequency */ + @NonNull public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) { mHyphenationFrequency = hyphenationFrequency; return this; @@ -325,7 +348,8 @@ public class StaticLayout extends Layout { * @param rightIndents array of indent values for right margin, in pixels * @return this builder, useful for chaining */ - public Builder setIndents(int[] leftIndents, int[] rightIndents) { + @NonNull + public Builder setIndents(@Nullable int[] leftIndents, @Nullable int[] rightIndents) { mLeftIndents = leftIndents; mRightIndents = rightIndents; int leftLen = leftIndents == null ? 0 : leftIndents.length; @@ -348,6 +372,7 @@ public class StaticLayout extends Layout { * @param justificationMode justification mode for the paragraph. * @return this builder, useful for chaining. */ + @NonNull public Builder setJustificationMode(@JustificationMode int justificationMode) { mJustificationMode = justificationMode; return this; @@ -359,12 +384,14 @@ public class StaticLayout extends Layout { * * @hide */ + @NonNull /* package */ Builder setAddLastLineLineSpacing(boolean value) { mAddLastLineLineSpacing = value; return this; } - private long[] getHyphenators(LocaleList locales) { + @NonNull + private long[] getHyphenators(@NonNull LocaleList locales) { final int length = locales.size(); final long[] result = new long[length]; for (int i = 0; i < length; i++) { @@ -424,6 +451,7 @@ public class StaticLayout extends Layout { * * @return the newly constructed {@link StaticLayout} object */ + @NonNull public StaticLayout build() { StaticLayout result = new StaticLayout(this); Builder.recycle(this); @@ -441,33 +469,33 @@ public class StaticLayout extends Layout { /* package */ long mNativePtr; - CharSequence mText; - int mStart; - int mEnd; - TextPaint mPaint; - int mWidth; - Alignment mAlignment; - TextDirectionHeuristic mTextDir; - float mSpacingMult; - float mSpacingAdd; - boolean mIncludePad; - boolean mFallbackLineSpacing; - int mEllipsizedWidth; - TextUtils.TruncateAt mEllipsize; - int mMaxLines; - int mBreakStrategy; - int mHyphenationFrequency; - int[] mLeftIndents; - int[] mRightIndents; - int mJustificationMode; - boolean mAddLastLineLineSpacing; - - Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); + private CharSequence mText; + private int mStart; + private int mEnd; + private TextPaint mPaint; + private int mWidth; + private Alignment mAlignment; + private TextDirectionHeuristic mTextDir; + private float mSpacingMult; + private float mSpacingAdd; + private boolean mIncludePad; + private boolean mFallbackLineSpacing; + private int mEllipsizedWidth; + private TextUtils.TruncateAt mEllipsize; + private int mMaxLines; + private int mBreakStrategy; + private int mHyphenationFrequency; + private int[] mLeftIndents; + private int[] mRightIndents; + private int mJustificationMode; + private boolean mAddLastLineLineSpacing; + + private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt(); // This will go away and be subsumed by native builder code - MeasuredText mMeasuredText; + private MeasuredText mMeasuredText; - LocaleList mLocales; + private LocaleList mLocales; private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<Builder>(3); } @@ -548,12 +576,17 @@ public class StaticLayout extends Layout { .setEllipsize(ellipsize) .setMaxLines(maxLines); /* - * This is annoying, but we can't refer to the layout until - * superclass construction is finished, and the superclass - * constructor wants the reference to the display text. + * This is annoying, but we can't refer to the layout until superclass construction is + * finished, and the superclass constructor wants the reference to the display text. + * + * In other words, the two Ellipsizer classes in Layout.java need a (Dynamic|Static)Layout + * as a parameter to do their calculations, but the Ellipsizers also need to be the input + * to the superclass's constructor (Layout). In order to go around the circular + * dependency, we construct the Ellipsizer with only one of the parameters, the text. And + * we fill in the rest of the needed information (layout, width, and method) later, here. * - * This will break if the superclass constructor ever actually - * cares about the content instead of just holding the reference. + * This will break if the superclass constructor ever actually cares about the content + * instead of just holding the reference. */ if (ellipsize != null) { Ellipsizer e = (Ellipsizer) getText(); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 7cf84615d08d..2417b0fd81fd 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -7956,10 +7956,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean useSaved) { Layout result = null; if (mText instanceof Spannable) { - result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth, - alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad, - mBreakStrategy, mHyphenationFrequency, mJustificationMode, - getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth); + final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint, + wantWidth) + .setDisplayText(mTransformed) + .setAlignment(alignment) + .setTextDirection(mTextDir) + .setLineSpacing(mSpacingAdd, mSpacingMult) + .setIncludePad(mIncludePad) + .setBreakStrategy(mBreakStrategy) + .setHyphenationFrequency(mHyphenationFrequency) + .setJustificationMode(mJustificationMode) + .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null) + .setEllipsizedWidth(ellipsisWidth); + result = builder.build(); } else { if (boring == UNKNOWN_BORING) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); |