summaryrefslogtreecommitdiffstats
path: root/core/java/android/text/DynamicLayout.java
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/text/DynamicLayout.java
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_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.java503
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;
+}