/* * 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) { this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingmult, spacingadd, includepad, 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 */ 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) { 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)); } } 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(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); } synchronized (sLock) { sStaticLayout = reflowed; reflowed.finish(); } } @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); } @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; } @Override public int getEllipsizedWidth() { return mEllipsizedWidth; } private static class ChangeWatcher implements TextWatcher, SpanWatcher { public ChangeWatcher(DynamicLayout layout) { mLayout = new WeakReference<DynamicLayout>(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) { } 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<DynamicLayout> 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 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(null); private static final Object[] sLock = new Object[0]; 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_SHIFT = 30; private static final int TAB_MASK = 0x20000000; private static final int ELLIPSIS_UNDEFINED = 0x80000000; }