Add a builder for DynamicLayout and switch TextView to it
The builder mostly copies the structure and the logic of
StaticLayout.
We also improve various parts of code and documentation in
StaticLayout's builder.
Bug: 28963299
Test: bit FrameworksCoreTests:android.text.
Test: bit FrameworksCoreTests:android.widget.TextViewTest
Test: bit CtsTextTestCases:*
Test: bit CtsWidgetTestCases:android.widget.cts.TextViewTest
Test: bit CtsWidgetTestCases:android.widget.cts.EditTextTest
Change-Id: I5c4a6e031bd0f41f765a3d85e0b9b7e9be42ad4b
diff --git a/api/current.txt b/api/current.txt
index b052c41..74adf5c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -41404,6 +41404,21 @@
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);
@@ -41568,6 +41583,8 @@
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 2c610f8..1841ce0 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -45006,6 +45006,21 @@
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);
@@ -45170,6 +45185,8 @@
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 8ab6f79..2bb96fe 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -41668,6 +41668,21 @@
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);
@@ -41832,6 +41847,8 @@
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 d6a68fb..661b608 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 @@
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 @@
}
/**
- * 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 @@
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 5d9c8d8..04dadc8e 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -119,6 +119,16 @@
*/
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 dd82e1e..8dddd63 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 @@
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 @@
}
/**
- * 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 @@
* @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 @@
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 @@
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 @@
*
* @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 @@
*
* @hide
*/
- public Builder setPaint(TextPaint paint) {
+ @NonNull
+ public Builder setPaint(@NonNull TextPaint paint) {
mPaint = paint;
return this;
}
@@ -167,7 +177,8 @@
*
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 @@
* 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 @@
* @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 @@
* @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 @@
/**
* 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 @@
* @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 @@
* @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 @@
*
* @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 @@
*
* @return the newly constructed {@link StaticLayout} object
*/
+ @NonNull
public StaticLayout build() {
StaticLayout result = new StaticLayout(this);
Builder.recycle(this);
@@ -441,33 +469,33 @@
/* 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;
+ 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;
- Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
+ 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 @@
.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.
*
- * This will break if the superclass constructor ever actually
- * cares about the content instead of just holding the reference.
+ * 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.
*/
if (ellipsize != null) {
Ellipsizer e = (Ellipsizer) getText();
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 7cf84615..2417b0f 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -7956,10 +7956,19 @@
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);