diff options
Diffstat (limited to 'core/java/android/text/DynamicLayout.java')
| -rw-r--r-- | core/java/android/text/DynamicLayout.java | 438 |
1 files changed, 356 insertions, 82 deletions
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)); } |
