diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/text/DynamicLayout.java | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/text/DynamicLayout.java')
-rw-r--r-- | core/java/android/text/DynamicLayout.java | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java new file mode 100644 index 0000000..14e5655 --- /dev/null +++ b/core/java/android/text/DynamicLayout.java @@ -0,0 +1,503 @@ +/* + * 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; +} |