1463 lines
57 KiB
Java
1463 lines
57 KiB
Java
/*
|
|
* Copyright (C) 2006 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package android.text;
|
|
|
|
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
|
|
import static com.android.text.flags.Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN;
|
|
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
|
|
|
|
import android.annotation.FlaggedApi;
|
|
import android.annotation.FloatRange;
|
|
import android.annotation.IntRange;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.SuppressLint;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Rect;
|
|
import android.graphics.text.LineBreakConfig;
|
|
import android.os.Build;
|
|
import android.text.method.OffsetMapping;
|
|
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;
|
|
import com.android.internal.util.GrowingArrayUtils;
|
|
import com.android.text.flags.Flags;
|
|
|
|
import java.lang.ref.WeakReference;
|
|
|
|
/**
|
|
* DynamicLayout is a text layout that updates itself as the text is edited.
|
|
* <p>This is used by widgets to control text layout. You should not need
|
|
* to use this class directly unless you are implementing your own widget
|
|
* or custom display object, or need to call
|
|
* {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
|
|
* Canvas.drawText()} directly.</p>
|
|
*/
|
|
public class DynamicLayout extends Layout {
|
|
private static final int PRIORITY = 128;
|
|
private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
|
|
|
|
/**
|
|
* 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 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.mFallbackLineSpacing = false;
|
|
b.mEllipsizedWidth = width;
|
|
b.mEllipsize = null;
|
|
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
|
|
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
|
|
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
|
|
b.mLineBreakConfig = LineBreakConfig.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 whether to respect the ascent and descent of the fallback fonts that are used in
|
|
* displaying the text (which is needed to avoid text from consecutive lines running into
|
|
* each other). If set, fallback fonts that end up getting used can increase the ascent
|
|
* and descent of the lines that they are used on.
|
|
*
|
|
* <p>For backward compatibility reasons, the default is {@code false}, but setting this to
|
|
* true is strongly recommended. It is required to be true if text could be in languages
|
|
* like Burmese or Tibetan where text is typically much taller or deeper than Latin text.
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Set the line break configuration. The line break will be passed to native used for
|
|
* calculating the text wrapping. The default value of the line break style is
|
|
* {@link LineBreakConfig#LINE_BREAK_STYLE_NONE}
|
|
*
|
|
* @param lineBreakConfig the line break configuration for text wrapping.
|
|
* @return this builder, useful for chaining.
|
|
* @see android.widget.TextView#setLineBreakStyle
|
|
* @see android.widget.TextView#setLineBreakWordStyle
|
|
*/
|
|
@NonNull
|
|
@FlaggedApi(FLAG_NO_BREAK_NO_HYPHENATION_SPAN)
|
|
public Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) {
|
|
mLineBreakConfig = lineBreakConfig;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set true for using width of bounding box as a source of automatic line breaking and
|
|
* drawing.
|
|
*
|
|
* If this value is false, the Layout determines the drawing offset and automatic line
|
|
* breaking based on total advances. By setting true, use all joined glyph's bounding boxes
|
|
* as a source of text width.
|
|
*
|
|
* If the font has glyphs that have negative bearing X or its xMax is greater than advance,
|
|
* the glyph clipping can happen because the drawing area may be bigger. By setting this to
|
|
* true, the Layout will reserve more spaces for drawing.
|
|
*
|
|
* @param useBoundsForWidth True for using bounding box, false for advances.
|
|
* @return this builder instance
|
|
* @see Layout#getUseBoundsForWidth()
|
|
* @see Layout.Builder#setUseBoundsForWidth(boolean)
|
|
*/
|
|
@NonNull
|
|
@FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
|
|
public Builder setUseBoundsForWidth(boolean useBoundsForWidth) {
|
|
mUseBoundsForWidth = useBoundsForWidth;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set true for shifting the drawing x offset for showing overhang at the start position.
|
|
*
|
|
* This flag is ignored if the {@link #getUseBoundsForWidth()} is false.
|
|
*
|
|
* If this value is false, the Layout draws text from the zero even if there is a glyph
|
|
* stroke in a region where the x coordinate is negative.
|
|
*
|
|
* If this value is true, the Layout draws text with shifting the x coordinate of the
|
|
* drawing bounding box.
|
|
*
|
|
* This value is false by default.
|
|
*
|
|
* @param shiftDrawingOffsetForStartOverhang true for shifting the drawing offset for
|
|
* showing the stroke that is in the region where
|
|
* the x coordinate is negative.
|
|
* @see #setUseBoundsForWidth(boolean)
|
|
* @see #getUseBoundsForWidth()
|
|
*/
|
|
@NonNull
|
|
// The corresponding getter is getShiftDrawingOffsetForStartOverhang()
|
|
@SuppressLint("MissingGetterMatchingBuilder")
|
|
@FlaggedApi(FLAG_USE_BOUNDS_FOR_WIDTH)
|
|
public Builder setShiftDrawingOffsetForStartOverhang(
|
|
boolean shiftDrawingOffsetForStartOverhang) {
|
|
mShiftDrawingOffsetForStartOverhang = shiftDrawingOffsetForStartOverhang;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the minimum font metrics used for line spacing.
|
|
*
|
|
* <p>
|
|
* {@code null} is the default value. If {@code null} is set or left as default, the
|
|
* font metrics obtained by {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} is
|
|
* used.
|
|
*
|
|
* <p>
|
|
* The minimum meaning here is the minimum value of line spacing: maximum value of
|
|
* {@link Paint#ascent()}, minimum value of {@link Paint#descent()}.
|
|
*
|
|
* <p>
|
|
* By setting this value, each line will have minimum line spacing regardless of the text
|
|
* rendered. For example, usually Japanese script has larger vertical metrics than Latin
|
|
* script. By setting the metrics obtained by
|
|
* {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)} for Japanese or leave it
|
|
* {@code null} if the Paint's locale is Japanese, the line spacing for Japanese is reserved
|
|
* if the text is an English text. If the vertical metrics of the text is larger than
|
|
* Japanese, for example Burmese, the bigger font metrics is used.
|
|
*
|
|
* @param minimumFontMetrics A minimum font metrics. Passing {@code null} for using the
|
|
* value obtained by
|
|
* {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}
|
|
* @see android.widget.TextView#setMinimumFontMetrics(Paint.FontMetrics)
|
|
* @see android.widget.TextView#getMinimumFontMetrics()
|
|
* @see Layout#getMinimumFontMetrics()
|
|
* @see Layout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
|
|
* @see StaticLayout.Builder#setMinimumFontMetrics(Paint.FontMetrics)
|
|
*/
|
|
@NonNull
|
|
@FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
|
|
public Builder setMinimumFontMetrics(@Nullable Paint.FontMetrics minimumFontMetrics) {
|
|
mMinimumFontMetrics = minimumFontMetrics;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Build the {@link DynamicLayout} after options have been set.
|
|
*
|
|
* <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 boolean mFallbackLineSpacing;
|
|
private int mBreakStrategy;
|
|
private int mHyphenationFrequency;
|
|
private int mJustificationMode;
|
|
private TextUtils.TruncateAt mEllipsize;
|
|
private int mEllipsizedWidth;
|
|
private LineBreakConfig mLineBreakConfig = LineBreakConfig.NONE;
|
|
private boolean mUseBoundsForWidth;
|
|
private boolean mShiftDrawingOffsetForStartOverhang;
|
|
private @Nullable Paint.FontMetrics mMinimumFontMetrics;
|
|
|
|
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
|
|
|
|
private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use {@link Builder} instead.
|
|
*/
|
|
@Deprecated
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use {@link Builder} instead.
|
|
*/
|
|
@Deprecated
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use {@link Builder} instead.
|
|
*/
|
|
@Deprecated
|
|
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,
|
|
@Nullable TextUtils.TruncateAt ellipsize,
|
|
@IntRange(from = 0) int ellipsizedWidth) {
|
|
this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
|
|
spacingmult, spacingadd, includepad,
|
|
Layout.BREAK_STRATEGY_SIMPLE, Layout.HYPHENATION_FREQUENCY_NONE,
|
|
Layout.JUSTIFICATION_MODE_NONE, LineBreakConfig.NONE, ellipsize, 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.
|
|
*
|
|
* @hide
|
|
* @deprecated Use {@link Builder} instead.
|
|
*/
|
|
@Deprecated
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
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,
|
|
@NonNull LineBreakConfig lineBreakConfig,
|
|
@Nullable TextUtils.TruncateAt ellipsize,
|
|
@IntRange(from = 0) int ellipsizedWidth) {
|
|
super(createEllipsizer(ellipsize, display),
|
|
paint, width, align, textDir, spacingmult, spacingadd, includepad,
|
|
false /* fallbackLineSpacing */, ellipsizedWidth, ellipsize,
|
|
Integer.MAX_VALUE /* maxLines */, breakStrategy, hyphenationFrequency,
|
|
null /* leftIndents */, null /* rightIndents */, justificationMode,
|
|
lineBreakConfig, false /* useBoundsForWidth */, false,
|
|
null /* minimumFontMetrics */);
|
|
|
|
final Builder b = Builder.obtain(base, paint, width)
|
|
.setAlignment(align)
|
|
.setTextDirection(textDir)
|
|
.setLineSpacing(spacingadd, spacingmult)
|
|
.setEllipsizedWidth(ellipsizedWidth)
|
|
.setEllipsize(ellipsize);
|
|
mDisplay = display;
|
|
mIncludePad = includepad;
|
|
mBreakStrategy = breakStrategy;
|
|
mJustificationMode = justificationMode;
|
|
mHyphenationFrequency = hyphenationFrequency;
|
|
mLineBreakConfig = lineBreakConfig;
|
|
|
|
generate(b);
|
|
|
|
Builder.recycle(b);
|
|
}
|
|
|
|
private DynamicLayout(@NonNull Builder b) {
|
|
super(createEllipsizer(b.mEllipsize, b.mDisplay),
|
|
b.mPaint, b.mWidth, b.mAlignment, b.mTextDir, b.mSpacingMult, b.mSpacingAdd,
|
|
b.mIncludePad, b.mFallbackLineSpacing, b.mEllipsizedWidth, b.mEllipsize,
|
|
Integer.MAX_VALUE /* maxLines */, b.mBreakStrategy, b.mHyphenationFrequency,
|
|
null /* leftIndents */, null /* rightIndents */, b.mJustificationMode,
|
|
b.mLineBreakConfig, b.mUseBoundsForWidth, b.mShiftDrawingOffsetForStartOverhang,
|
|
b.mMinimumFontMetrics);
|
|
|
|
mDisplay = b.mDisplay;
|
|
mIncludePad = b.mIncludePad;
|
|
mBreakStrategy = b.mBreakStrategy;
|
|
mJustificationMode = b.mJustificationMode;
|
|
mHyphenationFrequency = b.mHyphenationFrequency;
|
|
mLineBreakConfig = b.mLineBreakConfig;
|
|
|
|
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;
|
|
mFallbackLineSpacing = b.mFallbackLineSpacing;
|
|
mUseBoundsForWidth = b.mUseBoundsForWidth;
|
|
mShiftDrawingOffsetForStartOverhang = b.mShiftDrawingOffsetForStartOverhang;
|
|
mMinimumFontMetrics = b.mMinimumFontMetrics;
|
|
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 = b.mEllipsizedWidth;
|
|
e.mMethod = b.mEllipsize;
|
|
mEllipsize = true;
|
|
} else {
|
|
mInts = new PackedIntVector(COLUMNS_NORMAL);
|
|
mEllipsizedWidth = b.mWidth;
|
|
mEllipsizeAt = null;
|
|
}
|
|
|
|
mObjects = new PackedObjectVector<>(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 (b.mEllipsize != null) {
|
|
start = new int[COLUMNS_ELLIPSIZE];
|
|
start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
|
|
} else {
|
|
start = new int[COLUMNS_NORMAL];
|
|
}
|
|
|
|
final Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
|
|
|
|
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;
|
|
start[DESCENT] = desc;
|
|
mInts.insertAt(0, start);
|
|
|
|
start[TOP] = desc - asc;
|
|
mInts.insertAt(1, start);
|
|
|
|
mObjects.insertAt(0, dirs);
|
|
|
|
// Update from 0 characters to whatever the displayed text is
|
|
reflow(mBase, 0, 0, mDisplay.length());
|
|
|
|
if (mBase instanceof Spannable) {
|
|
if (mWatcher == null)
|
|
mWatcher = new ChangeWatcher(this);
|
|
|
|
// Strip out any watchers for other DynamicLayouts.
|
|
final Spannable sp = (Spannable) mBase;
|
|
final int baseLength = mBase.length();
|
|
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, baseLength,
|
|
Spannable.SPAN_INCLUSIVE_INCLUSIVE |
|
|
(PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public void reflow(CharSequence s, int where, int before, int after) {
|
|
if (s != mBase)
|
|
return;
|
|
|
|
CharSequence text = mDisplay;
|
|
int len = text.length();
|
|
|
|
// seek back to the start of the paragraph
|
|
|
|
int find = TextUtils.lastIndexOf(text, '\n', where - 1);
|
|
if (find < 0)
|
|
find = 0;
|
|
else
|
|
find = find + 1;
|
|
|
|
{
|
|
int diff = where - find;
|
|
before += diff;
|
|
after += diff;
|
|
where -= diff;
|
|
}
|
|
|
|
// seek forward to the end of the paragraph
|
|
|
|
int look = TextUtils.indexOf(text, '\n', where + after);
|
|
if (look < 0)
|
|
look = len;
|
|
else
|
|
look++; // we want the index after the \n
|
|
|
|
int change = look - (where + after);
|
|
before += change;
|
|
after += change;
|
|
|
|
// seek further out to cover anything that is forced to wrap together
|
|
|
|
if (text instanceof Spanned) {
|
|
Spanned sp = (Spanned) text;
|
|
boolean again;
|
|
|
|
do {
|
|
again = false;
|
|
|
|
Object[] force = sp.getSpans(where, where + after,
|
|
WrapTogetherSpan.class);
|
|
|
|
for (int i = 0; i < force.length; i++) {
|
|
int st = sp.getSpanStart(force[i]);
|
|
int en = sp.getSpanEnd(force[i]);
|
|
|
|
if (st < where) {
|
|
again = true;
|
|
|
|
int diff = where - st;
|
|
before += diff;
|
|
after += diff;
|
|
where -= diff;
|
|
}
|
|
|
|
if (en > where + after) {
|
|
again = true;
|
|
|
|
int diff = en - (where + after);
|
|
before += diff;
|
|
after += diff;
|
|
}
|
|
}
|
|
} while (again);
|
|
}
|
|
|
|
// find affected region of old layout
|
|
|
|
int startline = getLineForOffset(where);
|
|
int startv = getLineTop(startline);
|
|
|
|
int endline = getLineForOffset(where + before);
|
|
if (where + after == len)
|
|
endline = getLineCount();
|
|
int endv = getLineTop(endline);
|
|
boolean islast = (endline == getLineCount());
|
|
|
|
// generate new layout for affected text
|
|
|
|
StaticLayout reflowed;
|
|
StaticLayout.Builder b;
|
|
|
|
synchronized (sLock) {
|
|
reflowed = sStaticLayout;
|
|
b = sBuilder;
|
|
sStaticLayout = null;
|
|
sBuilder = null;
|
|
}
|
|
|
|
if (b == null) {
|
|
b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
|
|
}
|
|
|
|
b.setText(text, where, where + after)
|
|
.setPaint(getPaint())
|
|
.setWidth(getWidth())
|
|
.setTextDirection(getTextDirectionHeuristic())
|
|
.setLineSpacing(getSpacingAdd(), getSpacingMultiplier())
|
|
.setUseLineSpacingFromFallbacks(mFallbackLineSpacing)
|
|
.setEllipsizedWidth(mEllipsizedWidth)
|
|
.setEllipsize(mEllipsizeAt)
|
|
.setBreakStrategy(mBreakStrategy)
|
|
.setHyphenationFrequency(mHyphenationFrequency)
|
|
.setJustificationMode(mJustificationMode)
|
|
.setLineBreakConfig(mLineBreakConfig)
|
|
.setAddLastLineLineSpacing(!islast)
|
|
.setIncludePad(false)
|
|
.setUseBoundsForWidth(mUseBoundsForWidth)
|
|
.setShiftDrawingOffsetForStartOverhang(mShiftDrawingOffsetForStartOverhang)
|
|
.setMinimumFontMetrics(mMinimumFontMetrics)
|
|
.setCalculateBounds(true);
|
|
|
|
reflowed = b.buildPartialStaticLayoutForDynamicLayout(true /* trackpadding */, reflowed);
|
|
int n = reflowed.getLineCount();
|
|
// If the new layout has a blank line at the end, but it is not
|
|
// the very end of the buffer, then we already have a line that
|
|
// starts there, so disregard the blank line.
|
|
|
|
if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
|
|
n--;
|
|
|
|
// remove affected lines from old layout
|
|
mInts.deleteAt(startline, endline - startline);
|
|
mObjects.deleteAt(startline, endline - startline);
|
|
|
|
// adjust offsets in layout for new height and offsets
|
|
|
|
int ht = reflowed.getLineTop(n);
|
|
int toppad = 0, botpad = 0;
|
|
|
|
if (mIncludePad && startline == 0) {
|
|
toppad = reflowed.getTopPadding();
|
|
mTopPadding = toppad;
|
|
ht -= toppad;
|
|
}
|
|
if (mIncludePad && islast) {
|
|
botpad = reflowed.getBottomPadding();
|
|
mBottomPadding = botpad;
|
|
ht += botpad;
|
|
}
|
|
|
|
mInts.adjustValuesBelow(startline, START, after - before);
|
|
mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
|
|
|
|
// insert new layout
|
|
|
|
int[] ints;
|
|
|
|
if (mEllipsize) {
|
|
ints = new int[COLUMNS_ELLIPSIZE];
|
|
ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
|
|
} else {
|
|
ints = new int[COLUMNS_NORMAL];
|
|
}
|
|
|
|
Directions[] objects = new Directions[1];
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
final int start = reflowed.getLineStart(i);
|
|
ints[START] = start;
|
|
ints[DIR] |= reflowed.getParagraphDirection(i) << DIR_SHIFT;
|
|
ints[TAB] |= reflowed.getLineContainsTab(i) ? TAB_MASK : 0;
|
|
|
|
int top = reflowed.getLineTop(i) + startv;
|
|
if (i > 0)
|
|
top -= toppad;
|
|
ints[TOP] = top;
|
|
|
|
int desc = reflowed.getLineDescent(i);
|
|
if (i == n - 1)
|
|
desc += botpad;
|
|
|
|
ints[DESCENT] = desc;
|
|
ints[EXTRA] = reflowed.getLineExtra(i);
|
|
objects[0] = reflowed.getLineDirections(i);
|
|
|
|
final int end = (i == n - 1) ? where + after : reflowed.getLineStart(i + 1);
|
|
ints[HYPHEN] = StaticLayout.packHyphenEdit(
|
|
reflowed.getStartHyphenEdit(i), reflowed.getEndHyphenEdit(i));
|
|
ints[MAY_PROTRUDE_FROM_TOP_OR_BOTTOM] |=
|
|
contentMayProtrudeFromLineTopOrBottom(text, start, end) ?
|
|
MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK : 0;
|
|
|
|
if (mEllipsize) {
|
|
ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
|
|
ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
|
|
}
|
|
|
|
mInts.insertAt(startline + i, ints);
|
|
mObjects.insertAt(startline + i, objects);
|
|
}
|
|
|
|
updateBlocks(startline, endline - 1, n);
|
|
|
|
b.finish();
|
|
synchronized (sLock) {
|
|
sStaticLayout = reflowed;
|
|
sBuilder = b;
|
|
}
|
|
}
|
|
|
|
private boolean contentMayProtrudeFromLineTopOrBottom(CharSequence text, int start, int end) {
|
|
if (text instanceof Spanned) {
|
|
final Spanned spanned = (Spanned) text;
|
|
if (spanned.getSpans(start, end, ReplacementSpan.class).length > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
// Spans other than ReplacementSpan can be ignored because line top and bottom are
|
|
// disjunction of all tops and bottoms, although it's not optimal.
|
|
final Paint paint = getPaint();
|
|
if (text instanceof PrecomputedText) {
|
|
PrecomputedText precomputed = (PrecomputedText) text;
|
|
precomputed.getBounds(start, end, mTempRect);
|
|
} else {
|
|
paint.getTextBounds(text, start, end, mTempRect);
|
|
}
|
|
final Paint.FontMetricsInt fm = paint.getFontMetricsInt();
|
|
return mTempRect.top < fm.top || mTempRect.bottom > fm.bottom;
|
|
}
|
|
|
|
/**
|
|
* Create the initial block structure, cutting the text into blocks of at least
|
|
* BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
|
|
*/
|
|
private void createBlocks() {
|
|
int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
|
|
mNumberOfBlocks = 0;
|
|
final CharSequence text = mDisplay;
|
|
|
|
while (true) {
|
|
offset = TextUtils.indexOf(text, '\n', offset);
|
|
if (offset < 0) {
|
|
addBlockAtOffset(text.length());
|
|
break;
|
|
} else {
|
|
addBlockAtOffset(offset);
|
|
offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
|
|
}
|
|
}
|
|
|
|
// mBlockIndices and mBlockEndLines should have the same length
|
|
mBlockIndices = new int[mBlockEndLines.length];
|
|
for (int i = 0; i < mBlockEndLines.length; i++) {
|
|
mBlockIndices[i] = INVALID_BLOCK_INDEX;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
public ArraySet<Integer> getBlocksAlwaysNeedToBeRedrawn() {
|
|
return mBlocksAlwaysNeedToBeRedrawn;
|
|
}
|
|
|
|
private void updateAlwaysNeedsToBeRedrawn(int blockIndex) {
|
|
int startLine = blockIndex == 0 ? 0 : (mBlockEndLines[blockIndex - 1] + 1);
|
|
int endLine = mBlockEndLines[blockIndex];
|
|
for (int i = startLine; i <= endLine; i++) {
|
|
if (getContentMayProtrudeFromTopOrBottom(i)) {
|
|
if (mBlocksAlwaysNeedToBeRedrawn == null) {
|
|
mBlocksAlwaysNeedToBeRedrawn = new ArraySet<>();
|
|
}
|
|
mBlocksAlwaysNeedToBeRedrawn.add(blockIndex);
|
|
return;
|
|
}
|
|
}
|
|
if (mBlocksAlwaysNeedToBeRedrawn != null) {
|
|
mBlocksAlwaysNeedToBeRedrawn.remove(blockIndex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new block, ending at the specified character offset.
|
|
* A block will actually be created only if has at least one line, i.e. this offset is
|
|
* not on the end line of the previous block.
|
|
*/
|
|
private void addBlockAtOffset(int offset) {
|
|
final int line = getLineForOffset(offset);
|
|
if (mBlockEndLines == null) {
|
|
// Initial creation of the array, no test on previous block ending line
|
|
mBlockEndLines = ArrayUtils.newUnpaddedIntArray(1);
|
|
mBlockEndLines[mNumberOfBlocks] = line;
|
|
updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
|
|
mNumberOfBlocks++;
|
|
return;
|
|
}
|
|
|
|
final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
|
|
if (line > previousBlockEndLine) {
|
|
mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
|
|
updateAlwaysNeedsToBeRedrawn(mNumberOfBlocks);
|
|
mNumberOfBlocks++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is called every time the layout is reflowed after an edition.
|
|
* It updates the internal block data structure. The text is split in blocks
|
|
* of contiguous lines, with at least one block for the entire text.
|
|
* When a range of lines is edited, new blocks (from 0 to 3 depending on the
|
|
* overlap structure) will replace the set of overlapping blocks.
|
|
* Blocks are listed in order and are represented by their ending line number.
|
|
* An index is associated to each block (which will be used by display lists),
|
|
* this class simply invalidates the index of blocks overlapping a modification.
|
|
*
|
|
* @param startLine the first line of the range of modified lines
|
|
* @param endLine the last line of the range, possibly equal to startLine, lower
|
|
* than getLineCount()
|
|
* @param newLineCount the number of lines that will replace the range, possibly 0
|
|
*
|
|
* @hide
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public void updateBlocks(int startLine, int endLine, int newLineCount) {
|
|
if (mBlockEndLines == null) {
|
|
createBlocks();
|
|
return;
|
|
}
|
|
|
|
/*final*/ int firstBlock = -1;
|
|
/*final*/ int lastBlock = -1;
|
|
for (int i = 0; i < mNumberOfBlocks; i++) {
|
|
if (mBlockEndLines[i] >= startLine) {
|
|
firstBlock = i;
|
|
break;
|
|
}
|
|
}
|
|
for (int i = firstBlock; i < mNumberOfBlocks; i++) {
|
|
if (mBlockEndLines[i] >= endLine) {
|
|
lastBlock = i;
|
|
break;
|
|
}
|
|
}
|
|
final int lastBlockEndLine = mBlockEndLines[lastBlock];
|
|
|
|
final boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
|
|
mBlockEndLines[firstBlock - 1] + 1);
|
|
final boolean createBlock = newLineCount > 0;
|
|
final boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
|
|
|
|
int numAddedBlocks = 0;
|
|
if (createBlockBefore) numAddedBlocks++;
|
|
if (createBlock) numAddedBlocks++;
|
|
if (createBlockAfter) numAddedBlocks++;
|
|
|
|
final int numRemovedBlocks = lastBlock - firstBlock + 1;
|
|
final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
|
|
|
|
if (newNumberOfBlocks == 0) {
|
|
// Even when text is empty, there is actually one line and hence one block
|
|
mBlockEndLines[0] = 0;
|
|
mBlockIndices[0] = INVALID_BLOCK_INDEX;
|
|
mNumberOfBlocks = 1;
|
|
return;
|
|
}
|
|
|
|
if (newNumberOfBlocks > mBlockEndLines.length) {
|
|
int[] blockEndLines = ArrayUtils.newUnpaddedIntArray(
|
|
Math.max(mBlockEndLines.length * 2, newNumberOfBlocks));
|
|
int[] blockIndices = new int[blockEndLines.length];
|
|
System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
|
|
System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
|
|
System.arraycopy(mBlockEndLines, lastBlock + 1,
|
|
blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
|
|
System.arraycopy(mBlockIndices, lastBlock + 1,
|
|
blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
|
|
mBlockEndLines = blockEndLines;
|
|
mBlockIndices = blockIndices;
|
|
} else if (numAddedBlocks + numRemovedBlocks != 0) {
|
|
System.arraycopy(mBlockEndLines, lastBlock + 1,
|
|
mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
|
|
System.arraycopy(mBlockIndices, lastBlock + 1,
|
|
mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
|
|
}
|
|
|
|
if (numAddedBlocks + numRemovedBlocks != 0 && mBlocksAlwaysNeedToBeRedrawn != null) {
|
|
final ArraySet<Integer> set = new ArraySet<>();
|
|
final int changedBlockCount = numAddedBlocks - numRemovedBlocks;
|
|
for (int i = 0; i < mBlocksAlwaysNeedToBeRedrawn.size(); i++) {
|
|
Integer block = mBlocksAlwaysNeedToBeRedrawn.valueAt(i);
|
|
if (block < firstBlock) {
|
|
// block index is before firstBlock add it since it did not change
|
|
set.add(block);
|
|
}
|
|
if (block > lastBlock) {
|
|
// block index is after lastBlock, the index reduced to += changedBlockCount
|
|
block += changedBlockCount;
|
|
set.add(block);
|
|
}
|
|
}
|
|
mBlocksAlwaysNeedToBeRedrawn = set;
|
|
}
|
|
|
|
mNumberOfBlocks = newNumberOfBlocks;
|
|
int newFirstChangedBlock;
|
|
final int deltaLines = newLineCount - (endLine - startLine + 1);
|
|
if (deltaLines != 0) {
|
|
// Display list whose index is >= mIndexFirstChangedBlock is valid
|
|
// but it needs to update its drawing location.
|
|
newFirstChangedBlock = firstBlock + numAddedBlocks;
|
|
for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
|
|
mBlockEndLines[i] += deltaLines;
|
|
}
|
|
} else {
|
|
newFirstChangedBlock = mNumberOfBlocks;
|
|
}
|
|
mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
|
|
|
|
int blockIndex = firstBlock;
|
|
if (createBlockBefore) {
|
|
mBlockEndLines[blockIndex] = startLine - 1;
|
|
updateAlwaysNeedsToBeRedrawn(blockIndex);
|
|
mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
|
|
blockIndex++;
|
|
}
|
|
|
|
if (createBlock) {
|
|
mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
|
|
updateAlwaysNeedsToBeRedrawn(blockIndex);
|
|
mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
|
|
blockIndex++;
|
|
}
|
|
|
|
if (createBlockAfter) {
|
|
mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
|
|
updateAlwaysNeedsToBeRedrawn(blockIndex);
|
|
mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is used for test purposes only.
|
|
* @hide
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
|
|
public void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks,
|
|
int totalLines) {
|
|
mBlockEndLines = new int[blockEndLines.length];
|
|
mBlockIndices = new int[blockIndices.length];
|
|
System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
|
|
System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
|
|
mNumberOfBlocks = numberOfBlocks;
|
|
while (mInts.size() < totalLines) {
|
|
mInts.insertAt(mInts.size(), new int[COLUMNS_NORMAL]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public int[] getBlockEndLines() {
|
|
return mBlockEndLines;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public int[] getBlockIndices() {
|
|
return mBlockIndices;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
public int getBlockIndex(int index) {
|
|
return mBlockIndices[index];
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* @param index
|
|
*/
|
|
public void setBlockIndex(int index, int blockIndex) {
|
|
mBlockIndices[index] = blockIndex;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public int getNumberOfBlocks() {
|
|
return mNumberOfBlocks;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public int getIndexFirstChangedBlock() {
|
|
return mIndexFirstChangedBlock;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public void setIndexFirstChangedBlock(int i) {
|
|
mIndexFirstChangedBlock = i;
|
|
}
|
|
|
|
@Override
|
|
public int getLineCount() {
|
|
return mInts.size() - 1;
|
|
}
|
|
|
|
@Override
|
|
public int getLineTop(int line) {
|
|
return mInts.getValue(line, TOP);
|
|
}
|
|
|
|
@Override
|
|
public int getLineDescent(int line) {
|
|
return mInts.getValue(line, DESCENT);
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@Override
|
|
public int getLineExtra(int line) {
|
|
return mInts.getValue(line, EXTRA);
|
|
}
|
|
|
|
@Override
|
|
public int getLineStart(int line) {
|
|
return mInts.getValue(line, START) & START_MASK;
|
|
}
|
|
|
|
@Override
|
|
public boolean getLineContainsTab(int line) {
|
|
return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
|
|
}
|
|
|
|
@Override
|
|
public int getParagraphDirection(int line) {
|
|
return mInts.getValue(line, DIR) >> DIR_SHIFT;
|
|
}
|
|
|
|
@Override
|
|
public final Directions getLineDirections(int line) {
|
|
return mObjects.getValue(line, 0);
|
|
}
|
|
|
|
@Override
|
|
public int getTopPadding() {
|
|
return mTopPadding;
|
|
}
|
|
|
|
@Override
|
|
public int getBottomPadding() {
|
|
return mBottomPadding;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@Override
|
|
public @Paint.StartHyphenEdit int getStartHyphenEdit(int line) {
|
|
return StaticLayout.unpackStartHyphenEdit(mInts.getValue(line, HYPHEN) & HYPHEN_MASK);
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
@Override
|
|
public @Paint.EndHyphenEdit int getEndHyphenEdit(int line) {
|
|
return StaticLayout.unpackEndHyphenEdit(mInts.getValue(line, HYPHEN) & HYPHEN_MASK);
|
|
}
|
|
|
|
private boolean getContentMayProtrudeFromTopOrBottom(int line) {
|
|
return (mInts.getValue(line, MAY_PROTRUDE_FROM_TOP_OR_BOTTOM)
|
|
& MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK) != 0;
|
|
}
|
|
|
|
@Override
|
|
public int getEllipsizedWidth() {
|
|
return mEllipsizedWidth;
|
|
}
|
|
|
|
private static class ChangeWatcher implements TextWatcher, SpanWatcher {
|
|
public ChangeWatcher(DynamicLayout layout) {
|
|
mLayout = new WeakReference<>(layout);
|
|
}
|
|
|
|
private void reflow(CharSequence s, int where, int before, int after) {
|
|
DynamicLayout ml = mLayout.get();
|
|
|
|
if (ml != null) {
|
|
ml.reflow(s, where, before, after);
|
|
} else if (s instanceof Spannable) {
|
|
((Spannable) s).removeSpan(this);
|
|
}
|
|
}
|
|
|
|
public void beforeTextChanged(CharSequence s, int where, int before, int after) {
|
|
final DynamicLayout dynamicLayout = mLayout.get();
|
|
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
|
|
final OffsetMapping transformedText = (OffsetMapping) dynamicLayout.mDisplay;
|
|
if (mTransformedTextUpdate == null) {
|
|
mTransformedTextUpdate = new OffsetMapping.TextUpdate(where, before, after);
|
|
} else {
|
|
mTransformedTextUpdate.where = where;
|
|
mTransformedTextUpdate.before = before;
|
|
mTransformedTextUpdate.after = after;
|
|
}
|
|
// When there is a transformed text, we have to reflow the DynamicLayout based on
|
|
// the transformed indices instead of the range in base text.
|
|
// For example,
|
|
// base text: abcd > abce
|
|
// updated range: where = 3, before = 1, after = 1
|
|
// transformed text: abxxcd > abxxce
|
|
// updated range: where = 5, before = 1, after = 1
|
|
//
|
|
// Because the transformedText is udapted simultaneously with the base text,
|
|
// the range must be transformed before the base text changes.
|
|
transformedText.originalToTransformed(mTransformedTextUpdate);
|
|
}
|
|
}
|
|
|
|
public void onTextChanged(CharSequence s, int where, int before, int after) {
|
|
final DynamicLayout dynamicLayout = mLayout.get();
|
|
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
|
|
if (mTransformedTextUpdate != null && mTransformedTextUpdate.where >= 0) {
|
|
where = mTransformedTextUpdate.where;
|
|
before = mTransformedTextUpdate.before;
|
|
after = mTransformedTextUpdate.after;
|
|
// Set where to -1 so that we know if beforeTextChanged is called.
|
|
mTransformedTextUpdate.where = -1;
|
|
} else {
|
|
// onTextChanged is called without beforeTextChanged. Reflow the entire text.
|
|
where = 0;
|
|
// We can't get the before length from the text, use the line end of the
|
|
// last line instead.
|
|
before = dynamicLayout.getLineEnd(dynamicLayout.getLineCount() - 1);
|
|
after = dynamicLayout.mDisplay.length();
|
|
}
|
|
}
|
|
reflow(s, where, before, after);
|
|
}
|
|
|
|
public void afterTextChanged(Editable s) {
|
|
// Intentionally empty
|
|
}
|
|
|
|
/**
|
|
* Reflow the {@link DynamicLayout} at the given range from {@code start} to the
|
|
* {@code end}.
|
|
* If the display text in this {@link DynamicLayout} is a {@link OffsetMapping} instance
|
|
* (which means it's also a transformed text), it will transform the given range first and
|
|
* then reflow.
|
|
*/
|
|
private void transformAndReflow(Spannable s, int start, int end) {
|
|
final DynamicLayout dynamicLayout = mLayout.get();
|
|
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
|
|
final OffsetMapping transformedText = (OffsetMapping) dynamicLayout.mDisplay;
|
|
start = transformedText.originalToTransformed(start,
|
|
OffsetMapping.MAP_STRATEGY_CHARACTER);
|
|
end = transformedText.originalToTransformed(end,
|
|
OffsetMapping.MAP_STRATEGY_CHARACTER);
|
|
}
|
|
reflow(s, start, end - start, end - start);
|
|
}
|
|
|
|
public void onSpanAdded(Spannable s, Object o, int start, int end) {
|
|
if (o instanceof UpdateLayout) {
|
|
transformAndReflow(s, start, end);
|
|
}
|
|
}
|
|
|
|
public void onSpanRemoved(Spannable s, Object o, int start, int end) {
|
|
if (o instanceof UpdateLayout) {
|
|
if (Flags.insertModeCrashWhenDelete()) {
|
|
final DynamicLayout dynamicLayout = mLayout.get();
|
|
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
|
|
// It's possible that a Span is removed when the text covering it is
|
|
// deleted, in this case, the original start and end of the span might be
|
|
// OOB. So it'll reflow the entire string instead.
|
|
reflow(s, 0, 0, s.length());
|
|
} else {
|
|
reflow(s, start, end - start, end - start);
|
|
}
|
|
} else {
|
|
transformAndReflow(s, start, end);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
|
|
if (o instanceof UpdateLayout) {
|
|
if (start > end) {
|
|
// Bug: 67926915 start cannot be determined, fallback to reflow from start
|
|
// instead of causing an exception
|
|
start = 0;
|
|
}
|
|
if (Flags.insertModeCrashWhenDelete()) {
|
|
final DynamicLayout dynamicLayout = mLayout.get();
|
|
if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
|
|
// When text is changed, it'll also trigger onSpanChanged. In this case we
|
|
// can't determine the updated range in the transformed text. So it'll
|
|
// reflow the entire range instead.
|
|
reflow(s, 0, 0, s.length());
|
|
} else {
|
|
reflow(s, start, end - start, end - start);
|
|
reflow(s, nstart, nend - nstart, nend - nstart);
|
|
}
|
|
} else {
|
|
transformAndReflow(s, start, end);
|
|
transformAndReflow(s, nstart, nend);
|
|
}
|
|
}
|
|
}
|
|
|
|
private WeakReference<DynamicLayout> mLayout;
|
|
private OffsetMapping.TextUpdate mTransformedTextUpdate;
|
|
}
|
|
|
|
@Override
|
|
public int getEllipsisStart(int line) {
|
|
if (mEllipsizeAt == null) {
|
|
return 0;
|
|
}
|
|
|
|
return mInts.getValue(line, ELLIPSIS_START);
|
|
}
|
|
|
|
@Override
|
|
public int getEllipsisCount(int line) {
|
|
if (mEllipsizeAt == null) {
|
|
return 0;
|
|
}
|
|
|
|
return mInts.getValue(line, ELLIPSIS_COUNT);
|
|
}
|
|
|
|
/**
|
|
* Gets the {@link LineBreakConfig} used in this DynamicLayout.
|
|
* Use this only to consult the LineBreakConfig's properties and not
|
|
* to change them.
|
|
*
|
|
* @return The line break config in this DynamicLayout.
|
|
*/
|
|
@NonNull
|
|
public LineBreakConfig getLineBreakConfig() {
|
|
return mLineBreakConfig;
|
|
}
|
|
|
|
private CharSequence mBase;
|
|
private CharSequence mDisplay;
|
|
private ChangeWatcher mWatcher;
|
|
private boolean mIncludePad;
|
|
private boolean mFallbackLineSpacing;
|
|
private boolean mEllipsize;
|
|
private int mEllipsizedWidth;
|
|
private TextUtils.TruncateAt mEllipsizeAt;
|
|
private int mBreakStrategy;
|
|
private int mHyphenationFrequency;
|
|
private int mJustificationMode;
|
|
private LineBreakConfig mLineBreakConfig;
|
|
|
|
private PackedIntVector mInts;
|
|
private PackedObjectVector<Directions> mObjects;
|
|
|
|
/**
|
|
* Value used in mBlockIndices when a block has been created or recycled and indicating that its
|
|
* display list needs to be re-created.
|
|
* @hide
|
|
*/
|
|
public static final int INVALID_BLOCK_INDEX = -1;
|
|
// Stores the line numbers of the last line of each block (inclusive)
|
|
private int[] mBlockEndLines;
|
|
// The indices of this block's display list in TextView's internal display list array or
|
|
// INVALID_BLOCK_INDEX if this block has been invalidated during an edition
|
|
private int[] mBlockIndices;
|
|
// Set of blocks that always need to be redrawn.
|
|
private ArraySet<Integer> mBlocksAlwaysNeedToBeRedrawn;
|
|
// Number of items actually currently being used in the above 2 arrays
|
|
private int mNumberOfBlocks;
|
|
// The first index of the blocks whose locations are changed
|
|
private int mIndexFirstChangedBlock;
|
|
|
|
private int mTopPadding, mBottomPadding;
|
|
|
|
private Rect mTempRect = new Rect();
|
|
|
|
private boolean mUseBoundsForWidth;
|
|
private boolean mShiftDrawingOffsetForStartOverhang;
|
|
@Nullable Paint.FontMetrics mMinimumFontMetrics;
|
|
|
|
@UnsupportedAppUsage
|
|
private static StaticLayout sStaticLayout = null;
|
|
private static StaticLayout.Builder sBuilder = null;
|
|
|
|
private static final Object[] sLock = new Object[0];
|
|
|
|
// START, DIR, and TAB share the same entry.
|
|
private static final int START = 0;
|
|
private static final int DIR = START;
|
|
private static final int TAB = START;
|
|
private static final int TOP = 1;
|
|
private static final int DESCENT = 2;
|
|
private static final int EXTRA = 3;
|
|
// HYPHEN and MAY_PROTRUDE_FROM_TOP_OR_BOTTOM share the same entry.
|
|
private static final int HYPHEN = 4;
|
|
private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM = HYPHEN;
|
|
private static final int COLUMNS_NORMAL = 5;
|
|
|
|
private static final int ELLIPSIS_START = 5;
|
|
private static final int ELLIPSIS_COUNT = 6;
|
|
private static final int COLUMNS_ELLIPSIZE = 7;
|
|
|
|
private static final int START_MASK = 0x1FFFFFFF;
|
|
private static final int DIR_SHIFT = 30;
|
|
private static final int TAB_MASK = 0x20000000;
|
|
private static final int HYPHEN_MASK = 0xFF;
|
|
private static final int MAY_PROTRUDE_FROM_TOP_OR_BOTTOM_MASK = 0x100;
|
|
|
|
private static final int ELLIPSIS_UNDEFINED = 0x80000000;
|
|
}
|