/*
* 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 android.annotation.FloatRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
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 java.lang.ref.WeakReference;
/**
* DynamicLayout is a text layout that updates itself as the text is edited.
*
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.
*/
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;
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.
*
* 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;
}
/**
* Build the {@link DynamicLayout} after options have been set.
*
*
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 final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
private static final SynchronizedPool 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, 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,
@Nullable TextUtils.TruncateAt ellipsize,
@IntRange(from = 0) int ellipsizedWidth) {
super(createEllipsizer(ellipsize, display),
paint, width, align, textDir, spacingmult, spacingadd);
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;
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);
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;
mFallbackLineSpacing = b.mFallbackLineSpacing;
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);
final int baseLength = mBase.length();
// Update from 0 characters to whatever the real text is
reflow(mBase, 0, 0, baseLength);
if (mBase instanceof Spannable) {
if (mWatcher == null)
mWatcher = new ChangeWatcher(this);
// Strip out any watchers for other DynamicLayouts.
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, 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 (reflowed == null) {
reflowed = new StaticLayout(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)
.setAddLastLineLineSpacing(!islast);
reflowed.generate(b, false /*includepad*/, true /*trackpad*/);
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 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 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
public int getIndexFirstChangedBlock() {
return mIndexFirstChangedBlock;
}
/**
* @hide
*/
@UnsupportedAppUsage
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) {
// Intentionally empty
}
public void onTextChanged(CharSequence s, int where, int before, int after) {
reflow(s, where, before, after);
}
public void afterTextChanged(Editable s) {
// Intentionally empty
}
public void onSpanAdded(Spannable s, Object o, int start, int end) {
if (o instanceof UpdateLayout)
reflow(s, start, end - start, end - start);
}
public void onSpanRemoved(Spannable s, Object o, int start, int end) {
if (o instanceof UpdateLayout)
reflow(s, start, end - start, end - start);
}
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;
}
reflow(s, start, end - start, end - start);
reflow(s, nstart, nend - nstart, nend - nstart);
}
}
private WeakReference mLayout;
}
@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);
}
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 PackedIntVector mInts;
private PackedObjectVector 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 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();
@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;
}