504 lines
16 KiB
Java
504 lines
16 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 android.graphics.Paint;
|
|
import android.text.style.UpdateLayout;
|
|
import android.text.style.WrapTogetherSpan;
|
|
|
|
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;
|
|
|
|
/**
|
|
* Make a layout for the specified text that will be updated as
|
|
* the text is changed.
|
|
*/
|
|
public DynamicLayout(CharSequence base,
|
|
TextPaint paint,
|
|
int width, Alignment align,
|
|
float spacingmult, float spacingadd,
|
|
boolean includepad) {
|
|
this(base, base, paint, width, align, spacingmult, spacingadd,
|
|
includepad);
|
|
}
|
|
|
|
/**
|
|
* Make a layout for the transformed text (password transformation
|
|
* being the primary example of a transformation)
|
|
* that will be updated as the base text is changed.
|
|
*/
|
|
public DynamicLayout(CharSequence base, CharSequence display,
|
|
TextPaint paint,
|
|
int width, Alignment align,
|
|
float spacingmult, float spacingadd,
|
|
boolean includepad) {
|
|
this(base, display, paint, width, align, spacingmult, spacingadd,
|
|
includepad, null, 0);
|
|
}
|
|
|
|
/**
|
|
* Make a layout for the transformed text (password transformation
|
|
* being the primary example of a transformation)
|
|
* that will be updated as the base text is changed.
|
|
* If ellipsize is non-null, the Layout will ellipsize the text
|
|
* down to ellipsizedWidth.
|
|
*/
|
|
public DynamicLayout(CharSequence base, CharSequence display,
|
|
TextPaint paint,
|
|
int width, Alignment align,
|
|
float spacingmult, float spacingadd,
|
|
boolean includepad,
|
|
TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
|
|
super((ellipsize == null)
|
|
? display
|
|
: (display instanceof Spanned)
|
|
? new SpannedEllipsizer(display)
|
|
: new Ellipsizer(display),
|
|
paint, width, align, 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 = ellipsize;
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
private 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;
|
|
|
|
synchronized (sLock) {
|
|
reflowed = sStaticLayout;
|
|
sStaticLayout = null;
|
|
}
|
|
|
|
if (reflowed == null)
|
|
reflowed = new StaticLayout(true);
|
|
|
|
reflowed.generate(text, where, where + after,
|
|
getPaint(), getWidth(), getAlignment(),
|
|
getSpacingMultiplier(), getSpacingAdd(),
|
|
false, true, mEllipsize,
|
|
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);
|
|
}
|
|
|
|
synchronized (sLock) {
|
|
sStaticLayout = reflowed;
|
|
}
|
|
}
|
|
|
|
private void dump(boolean show) {
|
|
int n = getLineCount();
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
System.out.print("line " + i + ": " + getLineStart(i) + " to " + getLineEnd(i) + " ");
|
|
|
|
if (show) {
|
|
System.out.print(getText().subSequence(getLineStart(i),
|
|
getLineEnd(i)));
|
|
}
|
|
|
|
System.out.println("");
|
|
}
|
|
|
|
System.out.println("");
|
|
}
|
|
|
|
public int getLineCount() {
|
|
return mInts.size() - 1;
|
|
}
|
|
|
|
public int getLineTop(int line) {
|
|
return mInts.getValue(line, TOP);
|
|
}
|
|
|
|
public int getLineDescent(int line) {
|
|
return mInts.getValue(line, DESCENT);
|
|
}
|
|
|
|
public int getLineStart(int line) {
|
|
return mInts.getValue(line, START) & START_MASK;
|
|
}
|
|
|
|
public boolean getLineContainsTab(int line) {
|
|
return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
|
|
}
|
|
|
|
public int getParagraphDirection(int line) {
|
|
return mInts.getValue(line, DIR) >> DIR_SHIFT;
|
|
}
|
|
|
|
public final Directions getLineDirections(int line) {
|
|
return mObjects.getValue(line, 0);
|
|
}
|
|
|
|
public int getTopPadding() {
|
|
return mTopPadding;
|
|
}
|
|
|
|
public int getBottomPadding() {
|
|
return mBottomPadding;
|
|
}
|
|
|
|
@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 = (DynamicLayout) 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) {
|
|
;
|
|
}
|
|
|
|
public void onTextChanged(CharSequence s,
|
|
int where, int before, int after) {
|
|
reflow(s, where, before, after);
|
|
}
|
|
|
|
public void afterTextChanged(Editable s) {
|
|
;
|
|
}
|
|
|
|
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) {
|
|
reflow(s, start, end - start, end - start);
|
|
reflow(s, nstart, nend - nstart, nend - nstart);
|
|
}
|
|
}
|
|
|
|
private WeakReference mLayout;
|
|
}
|
|
|
|
public int getEllipsisStart(int line) {
|
|
if (mEllipsizeAt == null) {
|
|
return 0;
|
|
}
|
|
|
|
return mInts.getValue(line, ELLIPSIS_START);
|
|
}
|
|
|
|
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 mEllipsize;
|
|
private int mEllipsizedWidth;
|
|
private TextUtils.TruncateAt mEllipsizeAt;
|
|
|
|
private PackedIntVector mInts;
|
|
private PackedObjectVector<Directions> mObjects;
|
|
|
|
private int mTopPadding, mBottomPadding;
|
|
|
|
private static StaticLayout sStaticLayout = new StaticLayout(true);
|
|
private static Object sLock = new Object();
|
|
|
|
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 COLUMNS_NORMAL = 3;
|
|
|
|
private static final int ELLIPSIS_START = 3;
|
|
private static final int ELLIPSIS_COUNT = 4;
|
|
private static final int COLUMNS_ELLIPSIZE = 5;
|
|
|
|
private static final int START_MASK = 0x1FFFFFFF;
|
|
private static final int DIR_MASK = 0xC0000000;
|
|
private static final int DIR_SHIFT = 30;
|
|
private static final int TAB_MASK = 0x20000000;
|
|
|
|
private static final int ELLIPSIS_UNDEFINED = 0x80000000;
|
|
}
|