FileDocCategorySizeDatePackage
SpannableStringBuilder.javaAPI DocAndroid 5.1 API47950Thu Mar 12 22:22:10 GMT 2015android.text

SpannableStringBuilder

public class SpannableStringBuilder extends Object implements GraphicsOperations, Appendable, CharSequence, Editable, GetChars, Spannable
This is the class for text whose content and markup can both be changed.

Fields Summary
private static final String
TAG
private static final InputFilter[]
NO_FILTERS
private InputFilter[]
mFilters
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
private static final int
MARK
private static final int
POINT
private static final int
PARAGRAPH
private static final int
START_MASK
private static final int
END_MASK
private static final int
START_SHIFT
private static final int
SPAN_START_AT_START
private static final int
SPAN_START_AT_END
private static final int
SPAN_END_AT_START
private static final int
SPAN_END_AT_END
private static final int
SPAN_START_END_MASK
Constructors Summary
public SpannableStringBuilder()
Create a new SpannableStringBuilder with empty contents

                
      
        this("");
    
public SpannableStringBuilder(CharSequence text)
Create a new SpannableStringBuilder containing a copy of the specified text, including its spans if any.

        this(text, 0, text.length());
    
public SpannableStringBuilder(CharSequence text, int start, int end)
Create a new SpannableStringBuilder containing a copy of the specified slice of the specified text, including its spans if any.

        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);
            }
        }
    
Methods Summary
public android.text.SpannableStringBuilderappend(java.lang.CharSequence text)

        int length = length();
        return replace(length, length, text, 0, text.length());
    
public android.text.SpannableStringBuilderappend(java.lang.CharSequence text, java.lang.Object what, int flags)
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}.

        int start = length();
        append(text);
        setSpan(what, start, length(), flags);
        return this;
    
public android.text.SpannableStringBuilderappend(java.lang.CharSequence text, int start, int end)

        int length = length();
        return replace(length, length, text, start, end);
    
public android.text.SpannableStringBuilderappend(char text)

        return append(String.valueOf(text));
    
private voidchange(int start, int end, java.lang.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]));
                }
            }
        }
    
public charcharAt(int where)
Return the char at the specified offset within the buffer.

        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];
    
private voidcheckRange(java.lang.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");
        }
    
public voidclear()

        replace(0, length(), "", 0, 0);
    
public voidclearSpans()

        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);
        }
    
public android.text.SpannableStringBuilderdelete(int start, int end)

        SpannableStringBuilder ret = replace(start, end, "", 0, 0);

        if (mGapLength > 2 * length())
            resizeFor(length());

        return ret; // == this
    
public voiddrawText(android.graphics.Canvas c, int start, int end, float x, float y, android.graphics.Paint p)
Don't call this yourself -- exists for Canvas to use internally. {@hide}

        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);
        }
    
public voiddrawTextRun(android.graphics.Canvas c, int start, int end, int contextStart, int contextEnd, float x, float y, boolean isRtl, android.graphics.Paint p)
Don't call this yourself -- exists for Canvas to use internally. {@hide}

        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);
        }
    
public booleanequals(java.lang.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;
    
public voidgetChars(int start, int end, char[] dest, int destoff)
Copy the specified range of chars from this buffer into the specified array, beginning at the specified offset.

        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);
        }
    
public InputFilter[]getFilters()

        return mFilters;
    
public intgetSpanEnd(java.lang.Object what)
Return the buffer offset of the end of the specified markup object, or -1 if it is not attached to this buffer.

        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;
    
public intgetSpanFlags(java.lang.Object what)
Return the flags of the end of the specified markup object, or 0 if it is not attached to this buffer.

        int count = mSpanCount;
        Object[] spans = mSpans;

        for (int i = count - 1; i >= 0; i--) {
            if (spans[i] == what) {
                return mSpanFlags[i];
            }
        }

        return 0;
    
public intgetSpanStart(java.lang.Object what)
Return the buffer offset of the beginning of the specified markup object, or -1 if it is not attached to this buffer.

        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;
    
public T[]getSpans(int queryStart, int queryEnd, java.lang.Class kind)
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.

        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;
    
public floatgetTextRunAdvances(int start, int end, int contextStart, int contextEnd, boolean isRtl, float[] advances, int advancesPos, android.graphics.Paint p)
Don't call this yourself -- exists for Paint to use internally. {@hide}


        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;
    
public intgetTextRunCursor(int contextStart, int contextEnd, int dir, int offset, int cursorOpt, android.graphics.Paint p)
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


        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;
    
public intgetTextWidths(int start, int end, float[] widths, android.graphics.Paint p)
Don't call this yourself -- exists for Paint to use internally. {@hide}

        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;
    
private static booleanhasNonExclusiveExclusiveSpanAt(java.lang.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;
    
public inthashCode()

        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;
    
public android.text.SpannableStringBuilderinsert(int where, java.lang.CharSequence tb)

        return replace(where, where, tb, 0, tb.length());
    
public android.text.SpannableStringBuilderinsert(int where, java.lang.CharSequence tb, int start, int end)

        return replace(where, where, tb, start, end);
    
public intlength()
Return the number of chars in the buffer.

        return mText.length - mGapLength;
    
public floatmeasureText(int start, int end, android.graphics.Paint p)
Don't call this yourself -- exists for Paint to use internally. {@hide}

        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;
    
private voidmoveGapTo(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;
    
public intnextSpanTransition(int start, int limit, java.lang.Class kind)
Return the next offset after start but less than or equal to limit where a span of the specified type begins or ends.

        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;
    
private static java.lang.Stringregion(int start, int end)

        return "(" + start + " ... " + end + ")";
    
private voidremoveSpan(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);
    
public voidremoveSpan(java.lang.Object what)
Remove the specified markup object from the buffer.

        for (int i = mSpanCount - 1; i >= 0; i--) {
            if (mSpans[i] == what) {
                removeSpan(i);
                return;
            }
        }
    
public android.text.SpannableStringBuilderreplace(int start, int end, java.lang.CharSequence tb)

        return replace(start, end, tb, 0, tb.length());
    
public android.text.SpannableStringBuilderreplace(int start, int end, java.lang.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 voidresizeFor(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 voidsendAfterTextChanged(TextWatcher[] watchers)

        int n = watchers.length;

        for (int i = 0; i < n; i++) {
            watchers[i].afterTextChanged(this);
        }
    
private voidsendBeforeTextChanged(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 voidsendSpanAdded(java.lang.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 voidsendSpanChanged(java.lang.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 voidsendSpanRemoved(java.lang.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 voidsendTextChanged(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 voidsendToSpanWatchers(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);
        }
    
public voidsetFilters(InputFilter[] filters)

        if (filters == null) {
            throw new IllegalArgumentException();
        }

        mFilters = filters;
    
public voidsetSpan(java.lang.Object what, int start, int end, int flags)
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.

        setSpan(true, what, start, end, flags);
    
private voidsetSpan(boolean send, java.lang.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);
    
public java.lang.CharSequencesubSequence(int start, int end)
Return a new CharSequence containing a copy of the specified range of this buffer, including the overlapping spans.

        return new SpannableStringBuilder(this, start, end);
    
public java.lang.Stringsubstring(int start, int end)
Return a String containing a copy of the chars in this buffer, limited to the [start, end[ range.

hide

        char[] buf = new char[end - start];
        getChars(start, end, buf, 0);
        return new String(buf);
    
public java.lang.StringtoString()
Return a String containing a copy of the chars in this buffer.

        int len = length();
        char[] buf = new char[len];

        getChars(0, len, buf, 0);
        return new String(buf);
    
private intupdatedIntervalBound(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;
    
public static android.text.SpannableStringBuildervalueOf(java.lang.CharSequence source)

        if (source instanceof SpannableStringBuilder) {
            return (SpannableStringBuilder) source;
        } else {
            return new SpannableStringBuilder(source);
        }