/* * 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.Canvas; import android.graphics.Paint; import android.util.Log; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import libcore.util.EmptyArray; import java.lang.reflect.Array; /** * This is the class for text whose content and markup can both be changed. */ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable, Editable, Appendable, GraphicsOperations { private final static String TAG = "SpannableStringBuilder"; /** * Create a new SpannableStringBuilder with empty contents */ public SpannableStringBuilder() { this(""); } /** * Create a new SpannableStringBuilder containing a copy of the * specified text, including its spans if any. */ public SpannableStringBuilder(CharSequence text) { this(text, 0, text.length()); } /** * Create a new SpannableStringBuilder containing a copy of the * specified slice of the specified text, including its spans if any. */ public SpannableStringBuilder(CharSequence text, int start, int end) { int srclen = end - start; if (srclen < 0) throw new StringIndexOutOfBoundsException(); mText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(srclen)); mGapStart = srclen; mGapLength = mText.length - srclen; TextUtils.getChars(text, start, end, mText, 0); mSpanCount = 0; mSpans = EmptyArray.OBJECT; mSpanStarts = EmptyArray.INT; mSpanEnds = EmptyArray.INT; mSpanFlags = EmptyArray.INT; if (text instanceof Spanned) { Spanned sp = (Spanned) text; Object[] spans = sp.getSpans(start, end, Object.class); for (int i = 0; i < spans.length; i++) { if (spans[i] instanceof NoCopySpan) { continue; } int st = sp.getSpanStart(spans[i]) - start; int en = sp.getSpanEnd(spans[i]) - start; int fl = sp.getSpanFlags(spans[i]); if (st < 0) st = 0; if (st > end - start) st = end - start; if (en < 0) en = 0; if (en > end - start) en = end - start; setSpan(false, spans[i], st, en, fl); } } } public static SpannableStringBuilder valueOf(CharSequence source) { if (source instanceof SpannableStringBuilder) { return (SpannableStringBuilder) source; } else { return new SpannableStringBuilder(source); } } /** * Return the char at the specified offset within the buffer. */ public char charAt(int where) { int len = length(); if (where < 0) { throw new IndexOutOfBoundsException("charAt: " + where + " < 0"); } else if (where >= len) { throw new IndexOutOfBoundsException("charAt: " + where + " >= length " + len); } if (where >= mGapStart) return mText[where + mGapLength]; else return mText[where]; } /** * Return the number of chars in the buffer. */ public int length() { return mText.length - mGapLength; } private void resizeFor(int size) { final int oldLength = mText.length; if (size + 1 <= oldLength) { return; } char[] newText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(size)); System.arraycopy(mText, 0, newText, 0, mGapStart); final int newLength = newText.length; final int delta = newLength - oldLength; final int after = oldLength - (mGapStart + mGapLength); System.arraycopy(mText, oldLength - after, newText, newLength - after, after); mText = newText; mGapLength += delta; if (mGapLength < 1) new Exception("mGapLength < 1").printStackTrace(); for (int i = 0; i < mSpanCount; i++) { if (mSpanStarts[i] > mGapStart) mSpanStarts[i] += delta; if (mSpanEnds[i] > mGapStart) mSpanEnds[i] += delta; } } private void moveGapTo(int where) { if (where == mGapStart) return; boolean atEnd = (where == length()); if (where < mGapStart) { int overlap = mGapStart - where; System.arraycopy(mText, where, mText, mGapStart + mGapLength - overlap, overlap); } else /* where > mGapStart */ { int overlap = where - mGapStart; System.arraycopy(mText, where + mGapLength - overlap, mText, mGapStart, overlap); } // XXX be more clever for (int i = 0; i < mSpanCount; i++) { int start = mSpanStarts[i]; int end = mSpanEnds[i]; if (start > mGapStart) start -= mGapLength; if (start > where) start += mGapLength; else if (start == where) { int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; if (flag == POINT || (atEnd && flag == PARAGRAPH)) start += mGapLength; } if (end > mGapStart) end -= mGapLength; if (end > where) end += mGapLength; else if (end == where) { int flag = (mSpanFlags[i] & END_MASK); if (flag == POINT || (atEnd && flag == PARAGRAPH)) end += mGapLength; } mSpanStarts[i] = start; mSpanEnds[i] = end; } mGapStart = where; } // Documentation from interface public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) { return replace(where, where, tb, start, end); } // Documentation from interface public SpannableStringBuilder insert(int where, CharSequence tb) { return replace(where, where, tb, 0, tb.length()); } // Documentation from interface public SpannableStringBuilder delete(int start, int end) { SpannableStringBuilder ret = replace(start, end, "", 0, 0); if (mGapLength > 2 * length()) resizeFor(length()); return ret; // == this } // Documentation from interface public void clear() { replace(0, length(), "", 0, 0); } // Documentation from interface public void clearSpans() { for (int i = mSpanCount - 1; i >= 0; i--) { Object what = mSpans[i]; int ostart = mSpanStarts[i]; int oend = mSpanEnds[i]; if (ostart > mGapStart) ostart -= mGapLength; if (oend > mGapStart) oend -= mGapLength; mSpanCount = i; mSpans[i] = null; sendSpanRemoved(what, ostart, oend); } } // Documentation from interface public SpannableStringBuilder append(CharSequence text) { int length = length(); return replace(length, length, text, 0, text.length()); } /** * Appends the character sequence {@code text} and spans {@code what} over the appended part. * See {@link Spanned} for an explanation of what the flags mean. * @param text the character sequence to append. * @param what the object to be spanned over the appended text. * @param flags see {@link Spanned}. * @return this {@code SpannableStringBuilder}. */ public SpannableStringBuilder append(CharSequence text, Object what, int flags) { int start = length(); append(text); setSpan(what, start, length(), flags); return this; } // Documentation from interface public SpannableStringBuilder append(CharSequence text, int start, int end) { int length = length(); return replace(length, length, text, start, end); } // Documentation from interface public SpannableStringBuilder append(char text) { return append(String.valueOf(text)); } private void change(int start, int end, CharSequence cs, int csStart, int csEnd) { // Can be negative final int replacedLength = end - start; final int replacementLength = csEnd - csStart; final int nbNewChars = replacementLength - replacedLength; for (int i = mSpanCount - 1; i >= 0; i--) { int spanStart = mSpanStarts[i]; if (spanStart > mGapStart) spanStart -= mGapLength; int spanEnd = mSpanEnds[i]; if (spanEnd > mGapStart) spanEnd -= mGapLength; if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { int ost = spanStart; int oen = spanEnd; int clen = length(); if (spanStart > start && spanStart <= end) { for (spanStart = end; spanStart < clen; spanStart++) if (spanStart > end && charAt(spanStart - 1) == '\n') break; } if (spanEnd > start && spanEnd <= end) { for (spanEnd = end; spanEnd < clen; spanEnd++) if (spanEnd > end && charAt(spanEnd - 1) == '\n') break; } if (spanStart != ost || spanEnd != oen) setSpan(false, mSpans[i], spanStart, spanEnd, mSpanFlags[i]); } int flags = 0; if (spanStart == start) flags |= SPAN_START_AT_START; else if (spanStart == end + nbNewChars) flags |= SPAN_START_AT_END; if (spanEnd == start) flags |= SPAN_END_AT_START; else if (spanEnd == end + nbNewChars) flags |= SPAN_END_AT_END; mSpanFlags[i] |= flags; } moveGapTo(end); if (nbNewChars >= mGapLength) { resizeFor(mText.length + nbNewChars - mGapLength); } final boolean textIsRemoved = replacementLength == 0; // The removal pass needs to be done before the gap is updated in order to broadcast the // correct previous positions to the correct intersecting SpanWatchers if (replacedLength > 0) { // no need for span fixup on pure insertion // A for loop will not work because the array is being modified // Do not iterate in reverse to keep the SpanWatchers notified in ordering // Also, a removed SpanWatcher should not get notified of removed spans located // further in the span array. int i = 0; while (i < mSpanCount) { if ((mSpanFlags[i] & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE && mSpanStarts[i] >= start && mSpanStarts[i] < mGapStart + mGapLength && mSpanEnds[i] >= start && mSpanEnds[i] < mGapStart + mGapLength && // This condition indicates that the span would become empty (textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) { removeSpan(i); continue; // do not increment i, spans will be shifted left in the array } i++; } } mGapStart += nbNewChars; mGapLength -= nbNewChars; if (mGapLength < 1) new Exception("mGapLength < 1").printStackTrace(); TextUtils.getChars(cs, csStart, csEnd, mText, start); if (replacedLength > 0) { // no need for span fixup on pure insertion final boolean atEnd = (mGapStart + mGapLength == mText.length); for (int i = 0; i < mSpanCount; i++) { final int startFlag = (mSpanFlags[i] & START_MASK) >> START_SHIFT; mSpanStarts[i] = updatedIntervalBound(mSpanStarts[i], start, nbNewChars, startFlag, atEnd, textIsRemoved); final int endFlag = (mSpanFlags[i] & END_MASK); mSpanEnds[i] = updatedIntervalBound(mSpanEnds[i], start, nbNewChars, endFlag, atEnd, textIsRemoved); } } mSpanCountBeforeAdd = mSpanCount; if (cs instanceof Spanned) { Spanned sp = (Spanned) cs; Object[] spans = sp.getSpans(csStart, csEnd, Object.class); for (int i = 0; i < spans.length; i++) { int st = sp.getSpanStart(spans[i]); int en = sp.getSpanEnd(spans[i]); if (st < csStart) st = csStart; if (en > csEnd) en = csEnd; // Add span only if this object is not yet used as a span in this string if (getSpanStart(spans[i]) < 0) { setSpan(false, spans[i], st - csStart + start, en - csStart + start, sp.getSpanFlags(spans[i])); } } } } private int updatedIntervalBound(int offset, int start, int nbNewChars, int flag, boolean atEnd, boolean textIsRemoved) { if (offset >= start && offset < mGapStart + mGapLength) { if (flag == POINT) { // A POINT located inside the replaced range should be moved to the end of the // replaced text. // The exception is when the point is at the start of the range and we are doing a // text replacement (as opposed to a deletion): the point stays there. if (textIsRemoved || offset > start) { return mGapStart + mGapLength; } } else { if (flag == PARAGRAPH) { if (atEnd) { return mGapStart + mGapLength; } } else { // MARK // MARKs should be moved to the start, with the exception of a mark located at // the end of the range (which will be < mGapStart + mGapLength since mGapLength // is > 0, which should stay 'unchanged' at the end of the replaced text. if (textIsRemoved || offset < mGapStart - nbNewChars) { return start; } else { // Move to the end of replaced text (needed if nbNewChars != 0) return mGapStart; } } } } return offset; } private void removeSpan(int i) { Object object = mSpans[i]; int start = mSpanStarts[i]; int end = mSpanEnds[i]; if (start > mGapStart) start -= mGapLength; if (end > mGapStart) end -= mGapLength; int count = mSpanCount - (i + 1); System.arraycopy(mSpans, i + 1, mSpans, i, count); System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count); System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count); System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count); mSpanCount--; mSpans[mSpanCount] = null; sendSpanRemoved(object, start, end); } // Documentation from interface public SpannableStringBuilder replace(int start, int end, CharSequence tb) { return replace(start, end, tb, 0, tb.length()); } // Documentation from interface public SpannableStringBuilder replace(final int start, final int end, CharSequence tb, int tbstart, int tbend) { checkRange("replace", start, end); int filtercount = mFilters.length; for (int i = 0; i < filtercount; i++) { CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end); if (repl != null) { tb = repl; tbstart = 0; tbend = repl.length(); } } final int origLen = end - start; final int newLen = tbend - tbstart; if (origLen == 0 && newLen == 0 && !hasNonExclusiveExclusiveSpanAt(tb, tbstart)) { // This is a no-op iif there are no spans in tb that would be added (with a 0-length) // Early exit so that the text watchers do not get notified return this; } TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class); sendBeforeTextChanged(textWatchers, start, origLen, newLen); // Try to keep the cursor / selection at the same relative position during // a text replacement. If replaced or replacement text length is zero, this // is already taken care of. boolean adjustSelection = origLen != 0 && newLen != 0; int selectionStart = 0; int selectionEnd = 0; if (adjustSelection) { selectionStart = Selection.getSelectionStart(this); selectionEnd = Selection.getSelectionEnd(this); } change(start, end, tb, tbstart, tbend); if (adjustSelection) { if (selectionStart > start && selectionStart < end) { final int offset = (selectionStart - start) * newLen / origLen; selectionStart = start + offset; setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart, Spanned.SPAN_POINT_POINT); } if (selectionEnd > start && selectionEnd < end) { final int offset = (selectionEnd - start) * newLen / origLen; selectionEnd = start + offset; setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd, Spanned.SPAN_POINT_POINT); } } sendTextChanged(textWatchers, start, origLen, newLen); sendAfterTextChanged(textWatchers); // Span watchers need to be called after text watchers, which may update the layout sendToSpanWatchers(start, end, newLen - origLen); return this; } private static boolean hasNonExclusiveExclusiveSpanAt(CharSequence text, int offset) { if (text instanceof Spanned) { Spanned spanned = (Spanned) text; Object[] spans = spanned.getSpans(offset, offset, Object.class); final int length = spans.length; for (int i = 0; i < length; i++) { Object span = spans[i]; int flags = spanned.getSpanFlags(span); if (flags != Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) return true; } } return false; } private void sendToSpanWatchers(int replaceStart, int replaceEnd, int nbNewChars) { for (int i = 0; i < mSpanCountBeforeAdd; i++) { int spanStart = mSpanStarts[i]; int spanEnd = mSpanEnds[i]; if (spanStart > mGapStart) spanStart -= mGapLength; if (spanEnd > mGapStart) spanEnd -= mGapLength; int spanFlags = mSpanFlags[i]; int newReplaceEnd = replaceEnd + nbNewChars; boolean spanChanged = false; int previousSpanStart = spanStart; if (spanStart > newReplaceEnd) { if (nbNewChars != 0) { previousSpanStart -= nbNewChars; spanChanged = true; } } else if (spanStart >= replaceStart) { // No change if span start was already at replace interval boundaries before replace if ((spanStart != replaceStart || ((spanFlags & SPAN_START_AT_START) != SPAN_START_AT_START)) && (spanStart != newReplaceEnd || ((spanFlags & SPAN_START_AT_END) != SPAN_START_AT_END))) { // TODO A correct previousSpanStart cannot be computed at this point. // It would require to save all the previous spans' positions before the replace // Using an invalid -1 value to convey this would break the broacast range spanChanged = true; } } int previousSpanEnd = spanEnd; if (spanEnd > newReplaceEnd) { if (nbNewChars != 0) { previousSpanEnd -= nbNewChars; spanChanged = true; } } else if (spanEnd >= replaceStart) { // No change if span start was already at replace interval boundaries before replace if ((spanEnd != replaceStart || ((spanFlags & SPAN_END_AT_START) != SPAN_END_AT_START)) && (spanEnd != newReplaceEnd || ((spanFlags & SPAN_END_AT_END) != SPAN_END_AT_END))) { // TODO same as above for previousSpanEnd spanChanged = true; } } if (spanChanged) { sendSpanChanged(mSpans[i], previousSpanStart, previousSpanEnd, spanStart, spanEnd); } mSpanFlags[i] &= ~SPAN_START_END_MASK; } // The spans starting at mIntermediateSpanCount were added from the replacement text for (int i = mSpanCountBeforeAdd; i < mSpanCount; i++) { int spanStart = mSpanStarts[i]; int spanEnd = mSpanEnds[i]; if (spanStart > mGapStart) spanStart -= mGapLength; if (spanEnd > mGapStart) spanEnd -= mGapLength; sendSpanAdded(mSpans[i], spanStart, spanEnd); } } /** * Mark the specified range of text with the specified object. * The flags determine how the span will behave when text is * inserted at the start or end of the span's range. */ public void setSpan(Object what, int start, int end, int flags) { setSpan(true, what, start, end, flags); } private void setSpan(boolean send, Object what, int start, int end, int flags) { checkRange("setSpan", start, end); int flagsStart = (flags & START_MASK) >> START_SHIFT; if (flagsStart == PARAGRAPH) { if (start != 0 && start != length()) { char c = charAt(start - 1); if (c != '\n') throw new RuntimeException("PARAGRAPH span must start at paragraph boundary"); } } int flagsEnd = flags & END_MASK; if (flagsEnd == PARAGRAPH) { if (end != 0 && end != length()) { char c = charAt(end - 1); if (c != '\n') throw new RuntimeException("PARAGRAPH span must end at paragraph boundary"); } } // 0-length Spanned.SPAN_EXCLUSIVE_EXCLUSIVE if (flagsStart == POINT && flagsEnd == MARK && start == end) { if (send) { Log.e(TAG, "SPAN_EXCLUSIVE_EXCLUSIVE spans cannot have a zero length"); } // Silently ignore invalid spans when they are created from this class. // This avoids the duplication of the above test code before all the // calls to setSpan that are done in this class return; } int nstart = start; int nend = end; if (start > mGapStart) { start += mGapLength; } else if (start == mGapStart) { if (flagsStart == POINT || (flagsStart == PARAGRAPH && start == length())) start += mGapLength; } if (end > mGapStart) { end += mGapLength; } else if (end == mGapStart) { if (flagsEnd == POINT || (flagsEnd == PARAGRAPH && end == length())) end += mGapLength; } int count = mSpanCount; Object[] spans = mSpans; for (int i = 0; i < count; i++) { if (spans[i] == what) { int ostart = mSpanStarts[i]; int oend = mSpanEnds[i]; if (ostart > mGapStart) ostart -= mGapLength; if (oend > mGapStart) oend -= mGapLength; mSpanStarts[i] = start; mSpanEnds[i] = end; mSpanFlags[i] = flags; if (send) sendSpanChanged(what, ostart, oend, nstart, nend); return; } } mSpans = GrowingArrayUtils.append(mSpans, mSpanCount, what); mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start); mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end); mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags); mSpanCount++; if (send) sendSpanAdded(what, nstart, nend); } /** * Remove the specified markup object from the buffer. */ public void removeSpan(Object what) { for (int i = mSpanCount - 1; i >= 0; i--) { if (mSpans[i] == what) { removeSpan(i); return; } } } /** * Return the buffer offset of the beginning of the specified * markup object, or -1 if it is not attached to this buffer. */ public int getSpanStart(Object what) { int count = mSpanCount; Object[] spans = mSpans; for (int i = count - 1; i >= 0; i--) { if (spans[i] == what) { int where = mSpanStarts[i]; if (where > mGapStart) where -= mGapLength; return where; } } return -1; } /** * Return the buffer offset of the end of the specified * markup object, or -1 if it is not attached to this buffer. */ public int getSpanEnd(Object what) { int count = mSpanCount; Object[] spans = mSpans; for (int i = count - 1; i >= 0; i--) { if (spans[i] == what) { int where = mSpanEnds[i]; if (where > mGapStart) where -= mGapLength; return where; } } return -1; } /** * Return the flags of the end of the specified * markup object, or 0 if it is not attached to this buffer. */ public int getSpanFlags(Object what) { int count = mSpanCount; Object[] spans = mSpans; for (int i = count - 1; i >= 0; i--) { if (spans[i] == what) { return mSpanFlags[i]; } } return 0; } /** * Return an array of the spans of the specified type that overlap * the specified range of the buffer. The kind may be Object.class to get * a list of all the spans regardless of type. */ @SuppressWarnings("unchecked") public T[] getSpans(int queryStart, int queryEnd, Class kind) { if (kind == null) return ArrayUtils.emptyArray(kind); int spanCount = mSpanCount; Object[] spans = mSpans; int[] starts = mSpanStarts; int[] ends = mSpanEnds; int[] flags = mSpanFlags; int gapstart = mGapStart; int gaplen = mGapLength; int count = 0; T[] ret = null; T ret1 = null; for (int i = 0; i < spanCount; i++) { int spanStart = starts[i]; if (spanStart > gapstart) { spanStart -= gaplen; } if (spanStart > queryEnd) { continue; } int spanEnd = ends[i]; if (spanEnd > gapstart) { spanEnd -= gaplen; } if (spanEnd < queryStart) { continue; } if (spanStart != spanEnd && queryStart != queryEnd) { if (spanStart == queryEnd) continue; if (spanEnd == queryStart) continue; } // Expensive test, should be performed after the previous tests if (!kind.isInstance(spans[i])) continue; if (count == 0) { // Safe conversion thanks to the isInstance test above ret1 = (T) spans[i]; count++; } else { if (count == 1) { // Safe conversion, but requires a suppressWarning ret = (T[]) Array.newInstance(kind, spanCount - i + 1); ret[0] = ret1; } int prio = flags[i] & SPAN_PRIORITY; if (prio != 0) { int j; for (j = 0; j < count; j++) { int p = getSpanFlags(ret[j]) & SPAN_PRIORITY; if (prio > p) { break; } } System.arraycopy(ret, j, ret, j + 1, count - j); // Safe conversion thanks to the isInstance test above ret[j] = (T) spans[i]; count++; } else { // Safe conversion thanks to the isInstance test above ret[count++] = (T) spans[i]; } } } if (count == 0) { return ArrayUtils.emptyArray(kind); } if (count == 1) { // Safe conversion, but requires a suppressWarning ret = (T[]) Array.newInstance(kind, 1); ret[0] = ret1; return ret; } if (count == ret.length) { return ret; } // Safe conversion, but requires a suppressWarning T[] nret = (T[]) Array.newInstance(kind, count); System.arraycopy(ret, 0, nret, 0, count); return nret; } /** * Return the next offset after start but less than or * equal to limit where a span of the specified type * begins or ends. */ public int nextSpanTransition(int start, int limit, Class kind) { int count = mSpanCount; Object[] spans = mSpans; int[] starts = mSpanStarts; int[] ends = mSpanEnds; int gapstart = mGapStart; int gaplen = mGapLength; if (kind == null) { kind = Object.class; } for (int i = 0; i < count; i++) { int st = starts[i]; int en = ends[i]; if (st > gapstart) st -= gaplen; if (en > gapstart) en -= gaplen; if (st > start && st < limit && kind.isInstance(spans[i])) limit = st; if (en > start && en < limit && kind.isInstance(spans[i])) limit = en; } return limit; } /** * Return a new CharSequence containing a copy of the specified * range of this buffer, including the overlapping spans. */ public CharSequence subSequence(int start, int end) { return new SpannableStringBuilder(this, start, end); } /** * Copy the specified range of chars from this buffer into the * specified array, beginning at the specified offset. */ public void getChars(int start, int end, char[] dest, int destoff) { checkRange("getChars", start, end); if (end <= mGapStart) { System.arraycopy(mText, start, dest, destoff, end - start); } else if (start >= mGapStart) { System.arraycopy(mText, start + mGapLength, dest, destoff, end - start); } else { System.arraycopy(mText, start, dest, destoff, mGapStart - start); System.arraycopy(mText, mGapStart + mGapLength, dest, destoff + (mGapStart - start), end - mGapStart); } } /** * Return a String containing a copy of the chars in this buffer. */ @Override public String toString() { int len = length(); char[] buf = new char[len]; getChars(0, len, buf, 0); return new String(buf); } /** * Return a String containing a copy of the chars in this buffer, limited to the * [start, end[ range. * @hide */ public String substring(int start, int end) { char[] buf = new char[end - start]; getChars(start, end, buf, 0); return new String(buf); } private void sendBeforeTextChanged(TextWatcher[] watchers, int start, int before, int after) { int n = watchers.length; for (int i = 0; i < n; i++) { watchers[i].beforeTextChanged(this, start, before, after); } } private void sendTextChanged(TextWatcher[] watchers, int start, int before, int after) { int n = watchers.length; for (int i = 0; i < n; i++) { watchers[i].onTextChanged(this, start, before, after); } } private void sendAfterTextChanged(TextWatcher[] watchers) { int n = watchers.length; for (int i = 0; i < n; i++) { watchers[i].afterTextChanged(this); } } private void sendSpanAdded(Object what, int start, int end) { SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class); int n = recip.length; for (int i = 0; i < n; i++) { recip[i].onSpanAdded(this, what, start, end); } } private void sendSpanRemoved(Object what, int start, int end) { SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class); int n = recip.length; for (int i = 0; i < n; i++) { recip[i].onSpanRemoved(this, what, start, end); } } private void sendSpanChanged(Object what, int oldStart, int oldEnd, int start, int end) { // The bounds of a possible SpanWatcher are guaranteed to be set before this method is // called, so that the order of the span does not affect this broadcast. SpanWatcher[] spanWatchers = getSpans(Math.min(oldStart, start), Math.min(Math.max(oldEnd, end), length()), SpanWatcher.class); int n = spanWatchers.length; for (int i = 0; i < n; i++) { spanWatchers[i].onSpanChanged(this, what, oldStart, oldEnd, start, end); } } private static String region(int start, int end) { return "(" + start + " ... " + end + ")"; } private void checkRange(final String operation, int start, int end) { if (end < start) { throw new IndexOutOfBoundsException(operation + " " + region(start, end) + " has end before start"); } int len = length(); if (start > len || end > len) { throw new IndexOutOfBoundsException(operation + " " + region(start, end) + " ends beyond length " + len); } if (start < 0 || end < 0) { throw new IndexOutOfBoundsException(operation + " " + region(start, end) + " starts before 0"); } } /* private boolean isprint(char c) { // XXX if (c >= ' ' && c <= '~') return true; else return false; } private static final int startFlag(int flag) { return (flag >> 4) & 0x0F; } private static final int endFlag(int flag) { return flag & 0x0F; } public void dump() { // XXX for (int i = 0; i < mGapStart; i++) { System.out.print('|'); System.out.print(' '); System.out.print(isprint(mText[i]) ? mText[i] : '.'); System.out.print(' '); } for (int i = mGapStart; i < mGapStart + mGapLength; i++) { System.out.print('|'); System.out.print('('); System.out.print(isprint(mText[i]) ? mText[i] : '.'); System.out.print(')'); } for (int i = mGapStart + mGapLength; i < mText.length; i++) { System.out.print('|'); System.out.print(' '); System.out.print(isprint(mText[i]) ? mText[i] : '.'); System.out.print(' '); } System.out.print('\n'); for (int i = 0; i < mText.length + 1; i++) { int found = 0; int wfound = 0; for (int j = 0; j < mSpanCount; j++) { if (mSpanStarts[j] == i) { found = 1; wfound = j; break; } if (mSpanEnds[j] == i) { found = 2; wfound = j; break; } } if (found == 1) { if (startFlag(mSpanFlags[wfound]) == MARK) System.out.print("( "); if (startFlag(mSpanFlags[wfound]) == PARAGRAPH) System.out.print("< "); else System.out.print("[ "); } else if (found == 2) { if (endFlag(mSpanFlags[wfound]) == POINT) System.out.print(") "); if (endFlag(mSpanFlags[wfound]) == PARAGRAPH) System.out.print("> "); else System.out.print("] "); } else { System.out.print(" "); } } System.out.print("\n"); } */ /** * Don't call this yourself -- exists for Canvas to use internally. * {@hide} */ public void drawText(Canvas c, int start, int end, float x, float y, Paint p) { checkRange("drawText", start, end); if (end <= mGapStart) { c.drawText(mText, start, end - start, x, y, p); } else if (start >= mGapStart) { c.drawText(mText, start + mGapLength, end - start, x, y, p); } else { char[] buf = TextUtils.obtain(end - start); getChars(start, end, buf, 0); c.drawText(buf, 0, end - start, x, y, p); TextUtils.recycle(buf); } } /** * Don't call this yourself -- exists for Canvas to use internally. * {@hide} */ public void drawTextRun(Canvas c, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint p) { checkRange("drawTextRun", start, end); int contextLen = contextEnd - contextStart; int len = end - start; if (contextEnd <= mGapStart) { c.drawTextRun(mText, start, len, contextStart, contextLen, x, y, isRtl, p); } else if (contextStart >= mGapStart) { c.drawTextRun(mText, start + mGapLength, len, contextStart + mGapLength, contextLen, x, y, isRtl, p); } else { char[] buf = TextUtils.obtain(contextLen); getChars(contextStart, contextEnd, buf, 0); c.drawTextRun(buf, start - contextStart, len, 0, contextLen, x, y, isRtl, p); TextUtils.recycle(buf); } } /** * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ public float measureText(int start, int end, Paint p) { checkRange("measureText", start, end); float ret; if (end <= mGapStart) { ret = p.measureText(mText, start, end - start); } else if (start >= mGapStart) { ret = p.measureText(mText, start + mGapLength, end - start); } else { char[] buf = TextUtils.obtain(end - start); getChars(start, end, buf, 0); ret = p.measureText(buf, 0, end - start); TextUtils.recycle(buf); } return ret; } /** * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ public int getTextWidths(int start, int end, float[] widths, Paint p) { checkRange("getTextWidths", start, end); int ret; if (end <= mGapStart) { ret = p.getTextWidths(mText, start, end - start, widths); } else if (start >= mGapStart) { ret = p.getTextWidths(mText, start + mGapLength, end - start, widths); } else { char[] buf = TextUtils.obtain(end - start); getChars(start, end, buf, 0); ret = p.getTextWidths(buf, 0, end - start, widths); TextUtils.recycle(buf); } return ret; } /** * Don't call this yourself -- exists for Paint to use internally. * {@hide} */ public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, boolean isRtl, float[] advances, int advancesPos, Paint p) { float ret; int contextLen = contextEnd - contextStart; int len = end - start; if (end <= mGapStart) { ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen, isRtl, advances, advancesPos); } else if (start >= mGapStart) { ret = p.getTextRunAdvances(mText, start + mGapLength, len, contextStart + mGapLength, contextLen, isRtl, advances, advancesPos); } else { char[] buf = TextUtils.obtain(contextLen); getChars(contextStart, contextEnd, buf, 0); ret = p.getTextRunAdvances(buf, start - contextStart, len, 0, contextLen, isRtl, advances, advancesPos); TextUtils.recycle(buf); } return ret; } /** * Returns the next cursor position in the run. This avoids placing the cursor between * surrogates, between characters that form conjuncts, between base characters and combining * marks, or within a reordering cluster. * *

The context is the shaping context for cursor movement, generally the bounds of the metric * span enclosing the cursor in the direction of movement. * contextStart, contextEnd and offset are relative to * the start of the string.

* *

If cursorOpt is CURSOR_AT and the offset is not a valid cursor position, * this returns -1. Otherwise this will never return a value before contextStart or after * contextEnd.

* * @param contextStart the start index of the context * @param contextEnd the (non-inclusive) end index of the context * @param dir either DIRECTION_RTL or DIRECTION_LTR * @param offset the cursor position to move from * @param cursorOpt how to move the cursor, one of CURSOR_AFTER, * CURSOR_AT_OR_AFTER, CURSOR_BEFORE, * CURSOR_AT_OR_BEFORE, or CURSOR_AT * @param p the Paint object that is requesting this information * @return the offset of the next position, or -1 * @deprecated This is an internal method, refrain from using it in your code */ @Deprecated public int getTextRunCursor(int contextStart, int contextEnd, int dir, int offset, int cursorOpt, Paint p) { int ret; int contextLen = contextEnd - contextStart; if (contextEnd <= mGapStart) { ret = p.getTextRunCursor(mText, contextStart, contextLen, dir, offset, cursorOpt); } else if (contextStart >= mGapStart) { ret = p.getTextRunCursor(mText, contextStart + mGapLength, contextLen, dir, offset + mGapLength, cursorOpt) - mGapLength; } else { char[] buf = TextUtils.obtain(contextLen); getChars(contextStart, contextEnd, buf, 0); ret = p.getTextRunCursor(buf, 0, contextLen, dir, offset - contextStart, cursorOpt) + contextStart; TextUtils.recycle(buf); } return ret; } // Documentation from interface public void setFilters(InputFilter[] filters) { if (filters == null) { throw new IllegalArgumentException(); } mFilters = filters; } // Documentation from interface public InputFilter[] getFilters() { return mFilters; } // Same as SpannableStringInternal @Override public boolean equals(Object o) { if (o instanceof Spanned && toString().equals(o.toString())) { Spanned other = (Spanned) o; // Check span data Object[] otherSpans = other.getSpans(0, other.length(), Object.class); if (mSpanCount == otherSpans.length) { for (int i = 0; i < mSpanCount; ++i) { Object thisSpan = mSpans[i]; Object otherSpan = otherSpans[i]; if (thisSpan == this) { if (other != otherSpan || getSpanStart(thisSpan) != other.getSpanStart(otherSpan) || getSpanEnd(thisSpan) != other.getSpanEnd(otherSpan) || getSpanFlags(thisSpan) != other.getSpanFlags(otherSpan)) { return false; } } else if (!thisSpan.equals(otherSpan) || getSpanStart(thisSpan) != other.getSpanStart(otherSpan) || getSpanEnd(thisSpan) != other.getSpanEnd(otherSpan) || getSpanFlags(thisSpan) != other.getSpanFlags(otherSpan)) { return false; } } return true; } } return false; } // Same as SpannableStringInternal @Override public int hashCode() { int hash = toString().hashCode(); hash = hash * 31 + mSpanCount; for (int i = 0; i < mSpanCount; ++i) { Object span = mSpans[i]; if (span != this) { hash = hash * 31 + span.hashCode(); } hash = hash * 31 + getSpanStart(span); hash = hash * 31 + getSpanEnd(span); hash = hash * 31 + getSpanFlags(span); } return hash; } private static final InputFilter[] NO_FILTERS = new InputFilter[0]; private InputFilter[] mFilters = NO_FILTERS; private char[] mText; private int mGapStart; private int mGapLength; private Object[] mSpans; private int[] mSpanStarts; private int[] mSpanEnds; private int[] mSpanFlags; private int mSpanCount; private int mSpanCountBeforeAdd; // TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned} private static final int MARK = 1; private static final int POINT = 2; private static final int PARAGRAPH = 3; private static final int START_MASK = 0xF0; private static final int END_MASK = 0x0F; private static final int START_SHIFT = 4; // These bits are not (currently) used by SPANNED flags private static final int SPAN_START_AT_START = 0x1000; private static final int SPAN_START_AT_END = 0x2000; private static final int SPAN_END_AT_START = 0x4000; private static final int SPAN_END_AT_END = 0x8000; private static final int SPAN_START_END_MASK = 0xF000; }