DynamicLayoutpublic class DynamicLayout extends Layout 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. |
Fields Summary |
---|
private static final int | PRIORITY | private static final int | BLOCK_MINIMUM_CHARACTER_LENGTH | private CharSequence | mBase | private CharSequence | mDisplay | private ChangeWatcher | mWatcher | private boolean | mIncludePad | private boolean | mEllipsize | private int | mEllipsizedWidth | private TextUtils.TruncateAt | mEllipsizeAt | private PackedIntVector | mInts | private PackedObjectVector | mObjects | public static final int | INVALID_BLOCK_INDEXValue used in mBlockIndices when a block has been created or recycled and indicating that its
display list needs to be re-created. | private int[] | mBlockEndLines | private int[] | mBlockIndices | private int | mNumberOfBlocks | private int | mIndexFirstChangedBlock | private int | mTopPadding | private int | mBottomPadding | private static StaticLayout | sStaticLayout | private static final Object[] | sLock | private static final int | START | private static final int | DIR | private static final int | TAB | private static final int | TOP | private static final int | DESCENT | private static final int | COLUMNS_NORMAL | private static final int | ELLIPSIS_START | private static final int | ELLIPSIS_COUNT | private static final int | COLUMNS_ELLIPSIZE | private static final int | START_MASK | private static final int | DIR_SHIFT | private static final int | TAB_MASK | private static final int | ELLIPSIS_UNDEFINED |
Constructors Summary |
---|
public DynamicLayout(CharSequence base, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad)Make a layout for the specified text that will be updated as
the text is changed.
this(base, base, paint, width, align, spacingmult, spacingadd,
includepad);
| public DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean 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.
this(base, display, paint, width, align, spacingmult, spacingadd,
includepad, null, 0);
| public DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int 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.
this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth);
| public DynamicLayout(CharSequence base, CharSequence display, TextPaint paint, int width, Alignment align, TextDirectionHeuristic textDir, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int 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
super((ellipsize == null)
? display
: (display instanceof Spanned)
? new SpannedEllipsizer(display)
: new Ellipsizer(display),
paint, width, align, textDir, spacingmult, spacingadd);
mBase = base;
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;
/*
* 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();
e.mLayout = this;
e.mWidth = ellipsizedWidth;
e.mMethod = ellipsize;
mEllipsize = true;
}
// 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) {
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 };
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int asc = fm.ascent;
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 real text is
reflow(base, 0, 0, base.length());
if (base 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++)
sp.removeSpan(spans[i]);
sp.setSpan(mWatcher, 0, base.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE |
(PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
}
|
Methods Summary |
---|
private void | addBlockAtOffset(int offset)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.
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;
mNumberOfBlocks++;
return;
}
final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
if (line > previousBlockEndLine) {
mBlockEndLines = GrowingArrayUtils.append(mBlockEndLines, mNumberOfBlocks, line);
mNumberOfBlocks++;
}
| private void | createBlocks()Create the initial block structure, cutting the text into blocks of at least
BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
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;
}
| public int[] | getBlockEndLines()
return mBlockEndLines;
| public int[] | getBlockIndices()
return mBlockIndices;
| public int | getBottomPadding()
return mBottomPadding;
| public int | getEllipsisCount(int line)
if (mEllipsizeAt == null) {
return 0;
}
return mInts.getValue(line, ELLIPSIS_COUNT);
| public int | getEllipsisStart(int line)
if (mEllipsizeAt == null) {
return 0;
}
return mInts.getValue(line, ELLIPSIS_START);
| public int | getEllipsizedWidth()
return mEllipsizedWidth;
| public int | getIndexFirstChangedBlock()
return mIndexFirstChangedBlock;
| public boolean | getLineContainsTab(int line)
return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
| public int | getLineCount()
return mInts.size() - 1;
| public int | getLineDescent(int line)
return mInts.getValue(line, DESCENT);
| public final Directions | getLineDirections(int line)
return mObjects.getValue(line, 0);
| public int | getLineStart(int line)
return mInts.getValue(line, START) & START_MASK;
| public int | getLineTop(int line)
return mInts.getValue(line, TOP);
| public int | getNumberOfBlocks()
return mNumberOfBlocks;
| public int | getParagraphDirection(int line)
return mInts.getValue(line, DIR) >> DIR_SHIFT;
| public int | getTopPadding()
return mTopPadding;
| private void | reflow(java.lang.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;
synchronized (sLock) {
reflowed = sStaticLayout;
sStaticLayout = null;
}
if (reflowed == null) {
reflowed = new StaticLayout(null);
} else {
reflowed.prepare();
}
reflowed.generate(text, where, where + after,
getPaint(), getWidth(), getTextDirectionHeuristic(), getSpacingMultiplier(),
getSpacingAdd(), false,
true, mEllipsizedWidth, mEllipsizeAt);
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++) {
ints[START] = reflowed.getLineStart(i) |
(reflowed.getParagraphDirection(i) << DIR_SHIFT) |
(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;
objects[0] = reflowed.getLineDirections(i);
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);
synchronized (sLock) {
sStaticLayout = reflowed;
reflowed.finish();
}
| void | setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks)This package private method is used for test purposes only
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;
| public void | setIndexFirstChangedBlock(int i)
mIndexFirstChangedBlock = i;
| void | updateBlocks(int startLine, int endLine, int newLineCount)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.
This method is package private and not private so that it can be tested.
if (mBlockEndLines == null) {
createBlocks();
return;
}
int firstBlock = -1;
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];
boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
mBlockEndLines[firstBlock - 1] + 1);
boolean createBlock = newLineCount > 0;
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 {
System.arraycopy(mBlockEndLines, lastBlock + 1,
mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
System.arraycopy(mBlockIndices, lastBlock + 1,
mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
}
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;
mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
blockIndex++;
}
if (createBlock) {
mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
blockIndex++;
}
if (createBlockAfter) {
mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
}
|
|