Merge "Interval tree for SpannableStringBuilder"

This commit is contained in:
Raph Levien
2015-02-12 20:34:44 +00:00
committed by Android (Google) Code Review

View File

@@ -26,6 +26,7 @@ import com.android.internal.util.GrowingArrayUtils;
import libcore.util.EmptyArray;
import java.lang.reflect.Array;
import java.util.IdentityHashMap;
/**
* This is the class for text whose content and markup can both be changed.
@@ -68,6 +69,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
mSpanStarts = EmptyArray.INT;
mSpanEnds = EmptyArray.INT;
mSpanFlags = EmptyArray.INT;
mSpanMax = EmptyArray.INT;
if (text instanceof Spanned) {
Spanned sp = (Spanned) text;
@@ -94,6 +96,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
setSpan(false, spans[i], st, en, fl);
}
restoreInvariants();
}
}
@@ -147,9 +150,12 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
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;
if (mSpanCount != 0) {
for (int i = 0; i < mSpanCount; i++) {
if (mSpanStarts[i] > mGapStart) mSpanStarts[i] += delta;
if (mSpanEnds[i] > mGapStart) mSpanEnds[i] += delta;
}
calcMax(treeRoot());
}
}
@@ -167,35 +173,38 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
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];
// TODO: be more clever (although the win really isn't that big)
if (mSpanCount != 0) {
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))
if (start > mGapStart)
start -= mGapLength;
if (start > where)
start += mGapLength;
}
else if (start == where) {
int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
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))
start += mGapLength;
}
if (flag == POINT || (atEnd && flag == PARAGRAPH))
if (end > mGapStart)
end -= mGapLength;
if (end > where)
end += mGapLength;
}
else if (end == where) {
int flag = (mSpanFlags[i] & END_MASK);
mSpanStarts[i] = start;
mSpanEnds[i] = end;
if (flag == POINT || (atEnd && flag == PARAGRAPH))
end += mGapLength;
}
mSpanStarts[i] = start;
mSpanEnds[i] = end;
}
calcMax(treeRoot());
}
mGapStart = where;
@@ -243,6 +252,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
sendSpanRemoved(what, ostart, oend);
}
if (mIndexOfSpan != null) {
mIndexOfSpan.clear();
}
}
// Documentation from interface
@@ -277,12 +289,39 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
return append(String.valueOf(text));
}
// Returns true if a node was removed (so we can restart search from root)
private boolean removeSpansForChange(int start, int end, boolean textIsRemoved, int i) {
if ((i & 1) != 0) {
// internal tree node
if (resolveGap(mSpanMax[i]) >= start &&
removeSpansForChange(start, end, textIsRemoved, leftChild(i))) {
return true;
}
}
if (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 &&
// The following condition indicates that the span would become empty
(textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) {
mIndexOfSpan.remove(mSpans[i]);
removeSpan(i);
return true;
}
return resolveGap(mSpanStarts[i]) <= end && (i & 1) != 0 &&
removeSpansForChange(start, end, textIsRemoved, rightChild(i));
}
return false;
}
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;
boolean changed = false;
for (int i = mSpanCount - 1; i >= 0; i--) {
int spanStart = mSpanStarts[i];
if (spanStart > mGapStart)
@@ -309,8 +348,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
break;
}
if (spanStart != ost || spanEnd != oen)
if (spanStart != ost || spanEnd != oen) {
setSpan(false, mSpans[i], spanStart, spanEnd, mSpanFlags[i]);
changed = true;
}
}
int flags = 0;
@@ -320,6 +361,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
else if (spanEnd == end + nbNewChars) flags |= SPAN_END_AT_END;
mSpanFlags[i] |= flags;
}
if (changed) {
restoreInvariants();
}
moveGapTo(end);
@@ -331,23 +375,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
// 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++;
while (mSpanCount > 0 &&
removeSpansForChange(start, end, textIsRemoved, treeRoot())) {
// keep deleting spans as needed, and restart from root after every deletion
// because deletion can invalidate an index.
}
}
@@ -360,6 +391,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
TextUtils.getChars(cs, csStart, csEnd, mText, start);
if (replacedLength > 0) { // no need for span fixup on pure insertion
// TODO potential optimization: only update bounds on intersecting spans
final boolean atEnd = (mGapStart + mGapLength == mText.length);
for (int i = 0; i < mSpanCount; i++) {
@@ -371,10 +403,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
mSpanEnds[i] = updatedIntervalBound(mSpanEnds[i], start, nbNewChars, endFlag,
atEnd, textIsRemoved);
}
// TODO potential optimization: only fix up invariants when bounds actually changed
restoreInvariants();
}
mSpanCountBeforeAdd = mSpanCount;
if (cs instanceof Spanned) {
Spanned sp = (Spanned) cs;
Object[] spans = sp.getSpans(csStart, csEnd, Object.class);
@@ -389,9 +421,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
// 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]));
sp.getSpanFlags(spans[i]) | SPAN_ADDED);
}
}
restoreInvariants();
}
}
@@ -427,6 +460,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
return offset;
}
// Note: caller is responsible for removing the mIndexOfSpan entry.
private void removeSpan(int i) {
Object object = mSpans[i];
@@ -444,8 +478,12 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
mSpanCount--;
invalidateIndex(i);
mSpans[mSpanCount] = null;
// Invariants must be restored before sending span removed notifications.
restoreInvariants();
sendSpanRemoved(object, start, end);
}
@@ -496,10 +534,12 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
change(start, end, tb, tbstart, tbend);
if (adjustSelection) {
boolean changed = false;
if (selectionStart > start && selectionStart < end) {
final int offset = (selectionStart - start) * newLen / origLen;
selectionStart = start + offset;
changed = true;
setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart,
Spanned.SPAN_POINT_POINT);
}
@@ -507,9 +547,13 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
final int offset = (selectionEnd - start) * newLen / origLen;
selectionEnd = start + offset;
changed = true;
setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd,
Spanned.SPAN_POINT_POINT);
}
if (changed) {
restoreInvariants();
}
}
sendTextChanged(textWatchers, start, origLen, newLen);
@@ -536,12 +580,15 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
}
private void sendToSpanWatchers(int replaceStart, int replaceEnd, int nbNewChars) {
for (int i = 0; i < mSpanCountBeforeAdd; i++) {
for (int i = 0; i < mSpanCount; i++) {
int spanFlags = mSpanFlags[i];
// This loop handles only modified (not added) spans.
if ((spanFlags & SPAN_ADDED) != 0) continue;
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;
@@ -588,13 +635,17 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
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);
// Handle added spans
for (int i = 0; i < mSpanCount; i++) {
int spanFlags = mSpanFlags[i];
if ((spanFlags & SPAN_ADDED) != 0) {
mSpanFlags[i] &= ~SPAN_ADDED;
int spanStart = mSpanStarts[i];
int spanEnd = mSpanEnds[i];
if (spanStart > mGapStart) spanStart -= mGapLength;
if (spanEnd > mGapStart) spanEnd -= mGapLength;
sendSpanAdded(mSpans[i], spanStart, spanEnd);
}
}
}
@@ -607,6 +658,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
setSpan(true, what, start, end, flags);
}
// Note: if send is false, then it is the caller's responsibility to restore
// invariants. If send is false and the span already exists, then this method
// will not change the index of any spans.
private void setSpan(boolean send, Object what, int start, int end, int flags) {
checkRange("setSpan", start, end);
@@ -661,8 +715,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
int count = mSpanCount;
Object[] spans = mSpans;
for (int i = 0; i < count; i++) {
if (spans[i] == what) {
if (mIndexOfSpan != null) {
Integer index = mIndexOfSpan.get(what);
if (index != null) {
int i = index;
int ostart = mSpanStarts[i];
int oend = mSpanEnds[i];
@@ -675,7 +731,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
mSpanEnds[i] = end;
mSpanFlags[i] = flags;
if (send) sendSpanChanged(what, ostart, oend, nstart, nend);
if (send) {
restoreInvariants();
sendSpanChanged(what, ostart, oend, nstart, nend);
}
return;
}
@@ -685,43 +744,48 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start);
mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end);
mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags);
invalidateIndex(mSpanCount);
mSpanCount++;
// Make sure there is enough room for empty interior nodes.
// This magic formula computes the size of the smallest perfect binary
// tree no smaller than mSpanCount.
int sizeOfMax = 2 * treeRoot() + 1;
if (mSpanMax.length < sizeOfMax) {
mSpanMax = new int[sizeOfMax];
}
if (send) sendSpanAdded(what, nstart, nend);
if (send) {
restoreInvariants();
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;
}
if (mIndexOfSpan == null) return;
Integer i = mIndexOfSpan.remove(what);
if (i != null) {
removeSpan(i.intValue());
}
}
/**
* Return externally visible offset given offset into gapped buffer.
*/
private int resolveGap(int i) {
return i > mGapStart ? i - mGapLength : i;
}
/**
* 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;
if (mIndexOfSpan == null) return -1;
Integer i = mIndexOfSpan.get(what);
return i == null ? -1 : resolveGap(mSpanStarts[i]);
}
/**
@@ -729,21 +793,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
* 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;
if (mIndexOfSpan == null) return -1;
Integer i = mIndexOfSpan.get(what);
return i == null ? -1 : resolveGap(mSpanEnds[i]);
}
/**
@@ -751,16 +803,9 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
* 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;
if (mIndexOfSpan == null) return 0;
Integer i = mIndexOfSpan.get(what);
return i == null ? 0 : mSpanFlags[i];
}
/**
@@ -770,59 +815,84 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
*/
@SuppressWarnings("unchecked")
public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
if (kind == null) return ArrayUtils.emptyArray(kind);
if (kind == null || mSpanCount == 0) return ArrayUtils.emptyArray(kind);
int count = countSpans(queryStart, queryEnd, kind, treeRoot());
if (count == 0) {
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;
// Safe conversion, but requires a suppressWarning
T[] ret = (T[]) Array.newInstance(kind, count);
getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, 0);
return ret;
}
private int countSpans(int queryStart, int queryEnd, Class kind, int i) {
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 ((i & 1) != 0) {
// internal tree node
int left = leftChild(i);
int spanMax = mSpanMax[left];
if (spanMax > mGapStart) {
spanMax -= mGapLength;
}
if (spanStart > queryEnd) {
continue;
if (spanMax >= queryStart) {
count = countSpans(queryStart, queryEnd, kind, left);
}
int spanEnd = ends[i];
if (spanEnd > gapstart) {
spanEnd -= gaplen;
}
if (i < mSpanCount) {
int spanStart = mSpanStarts[i];
if (spanStart > mGapStart) {
spanStart -= mGapLength;
}
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;
if (spanStart <= queryEnd) {
int spanEnd = mSpanEnds[i];
if (spanEnd > mGapStart) {
spanEnd -= mGapLength;
}
if (spanEnd >= queryStart &&
(spanStart == spanEnd || queryStart == queryEnd ||
(spanStart != queryEnd && spanEnd != queryStart)) &&
kind.isInstance(mSpans[i])) {
count++;
}
if ((i & 1) != 0) {
count += countSpans(queryStart, queryEnd, kind, rightChild(i));
}
}
}
return count;
}
int prio = flags[i] & SPAN_PRIORITY;
@SuppressWarnings("unchecked")
private <T> int getSpansRec(int queryStart, int queryEnd, Class<T> kind,
int i, T[] ret, int count) {
if ((i & 1) != 0) {
// internal tree node
int left = leftChild(i);
int spanMax = mSpanMax[left];
if (spanMax > mGapStart) {
spanMax -= mGapLength;
}
if (spanMax >= queryStart) {
count = getSpansRec(queryStart, queryEnd, kind, left, ret, count);
}
}
if (i >= mSpanCount) return count;
int spanStart = mSpanStarts[i];
if (spanStart > mGapStart) {
spanStart -= mGapLength;
}
if (spanStart <= queryEnd) {
int spanEnd = mSpanEnds[i];
if (spanEnd > mGapStart) {
spanEnd -= mGapLength;
}
if (spanEnd >= queryStart &&
(spanStart == spanEnd || queryStart == queryEnd ||
(spanStart != queryEnd && spanEnd != queryStart)) &&
kind.isInstance(mSpans[i])) {
int prio = mSpanFlags[i] & SPAN_PRIORITY;
if (prio != 0) {
int j;
@@ -836,32 +906,18 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
System.arraycopy(ret, j, ret, j + 1, count - j);
// Safe conversion thanks to the isInstance test above
ret[j] = (T) spans[i];
count++;
ret[j] = (T) mSpans[i];
} else {
// Safe conversion thanks to the isInstance test above
ret[count++] = (T) spans[i];
ret[count] = (T) mSpans[i];
}
count++;
}
if (count < ret.length && (i & 1) != 0) {
count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, count);
}
}
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 count;
}
/**
@@ -870,30 +926,31 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
* 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 (mSpanCount == 0) return limit;
if (kind == null) {
kind = Object.class;
}
return nextSpanTransitionRec(start, limit, kind, treeRoot());
}
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]))
private int nextSpanTransitionRec(int start, int limit, Class kind, int i) {
if ((i & 1) != 0) {
// internal tree node
int left = leftChild(i);
if (resolveGap(mSpanMax[left]) > start) {
limit = nextSpanTransitionRec(start, limit, kind, left);
}
}
if (i < mSpanCount) {
int st = resolveGap(mSpanStarts[i]);
int en = resolveGap(mSpanEnds[i]);
if (st > start && st < limit && kind.isInstance(mSpans[i]))
limit = st;
if (en > start && en < limit && kind.isInstance(spans[i]))
if (en > start && en < limit && kind.isInstance(mSpans[i]))
limit = en;
if (st < limit && (i & 1) != 0) {
limit = nextSpanTransitionRec(start, limit, kind, rightChild(i));
}
}
return limit;
@@ -1339,6 +1396,118 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
return hash;
}
// Primitives for treating span list as binary tree
// The spans (along with start and end offsets and flags) are stored in linear arrays sorted
// by start offset. For fast searching, there is a binary search structure imposed over these
// arrays. This structure is inorder traversal of a perfect binary tree, a slightly unusual
// but advantageous approach.
// The value-containing nodes are indexed 0 <= i < n (where n = mSpanCount), thus preserving
// logic that accesses the values as a contiguous array. Other balanced binary tree approaches
// (such as a complete binary tree) would require some shuffling of node indices.
// Basic properties of this structure: For a perfect binary tree of height m:
// The tree has 2^(m+1) - 1 total nodes.
// The root of the tree has index 2^m - 1.
// All leaf nodes have even index, all interior nodes odd.
// The height of a node of index i is the number of trailing ones in i's binary representation.
// The left child of a node i of height h is i - 2^(h - 1).
// The right child of a node i of height h is i + 2^(h - 1).
// Note that for arbitrary n, interior nodes of this tree may be >= n. Thus, the general
// structure of a recursive traversal of node i is:
// * traverse left child if i is an interior node
// * process i if i < n
// * traverse right child if i is an interior node and i < n
private int treeRoot() {
return Integer.highestOneBit(mSpanCount) - 1;
}
// (i+1) & ~i is equal to 2^(the number of trailing ones in i)
private static int leftChild(int i) {
return i - (((i + 1) & ~i) >> 1);
}
private static int rightChild(int i) {
return i + (((i + 1) & ~i) >> 1);
}
// The span arrays are also augmented by an mSpanMax[] array that represents an interval tree
// over the binary tree structure described above. For each node, the mSpanMax[] array contains
// the maximum value of mSpanEnds of that node and its descendants. Thus, traversals can
// easily reject subtrees that contain no spans overlapping the area of interest.
// Note that mSpanMax[] also has a valid valuefor interior nodes of index >= n, but which have
// descendants of index < n. In these cases, it simply represents the maximum span end of its
// descendants. This is a consequence of the perfect binary tree structure.
private int calcMax(int i) {
int max = 0;
if ((i & 1) != 0) {
// internal tree node
max = calcMax(leftChild(i));
}
if (i < mSpanCount) {
max = Math.max(max, mSpanEnds[i]);
if ((i & 1) != 0) {
max = Math.max(max, calcMax(rightChild(i)));
}
}
mSpanMax[i] = max;
return max;
}
// restores binary interval tree invariants after any mutation of span structure
private void restoreInvariants() {
if (mSpanCount == 0) return;
// invariant 1: span starts are nondecreasing
// This is a simple insertion sort because we expect it to be mostly sorted.
for (int i = 1; i < mSpanCount; i++) {
if (mSpanStarts[i] < mSpanStarts[i - 1]) {
Object span = mSpans[i];
int start = mSpanStarts[i];
int end = mSpanEnds[i];
int flags = mSpanFlags[i];
int j = i;
do {
mSpans[j] = mSpans[j - 1];
mSpanStarts[j] = mSpanStarts[j - 1];
mSpanEnds[j] = mSpanEnds[j - 1];
mSpanFlags[j] = mSpanFlags[j - 1];
j--;
} while (j > 0 && start < mSpanStarts[j - 1]);
mSpans[j] = span;
mSpanStarts[j] = start;
mSpanEnds[j] = end;
mSpanFlags[j] = flags;
invalidateIndex(j);
}
}
// invariant 2: max is max span end for each node and its descendants
calcMax(treeRoot());
// invariant 3: mIndexOfSpan maps spans back to indices
if (mIndexOfSpan == null) {
mIndexOfSpan = new IdentityHashMap<Object, Integer>();
}
for (int i = mLowWaterMark; i < mSpanCount; i++) {
Integer existing = mIndexOfSpan.get(mSpans[i]);
if (existing == null || existing != i) {
mIndexOfSpan.put(mSpans[i], i);
}
}
mLowWaterMark = Integer.MAX_VALUE;
}
// Call this on any update to mSpans[], so that mIndexOfSpan can be updated
private void invalidateIndex(int i) {
mLowWaterMark = Math.min(i, mLowWaterMark);
}
private static final InputFilter[] NO_FILTERS = new InputFilter[0];
private InputFilter[] mFilters = NO_FILTERS;
@@ -1349,9 +1518,11 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
private Object[] mSpans;
private int[] mSpanStarts;
private int[] mSpanEnds;
private int[] mSpanMax; // see calcMax() for an explanation of what this array stores
private int[] mSpanFlags;
private int mSpanCount;
private int mSpanCountBeforeAdd;
private IdentityHashMap<Object, Integer> mIndexOfSpan;
private int mLowWaterMark; // indices below this have not been touched
// TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned}
private static final int MARK = 1;
@@ -1363,6 +1534,7 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
private static final int START_SHIFT = 4;
// These bits are not (currently) used by SPANNED flags
private static final int SPAN_ADDED = 0x800;
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;