summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorRaph Levien <raph@google.com>2015-02-12 20:34:44 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2015-02-12 20:34:46 +0000
commit80955e105d6e71f7b71a63b391fd5a8adcded55e (patch)
tree0d9b6836674240734839867c8c891e872e983172 /core
parentd12940797cab9156a43992468ca076932b574f41 (diff)
parentd7c020e9e525f1db44da1d0428d87a53babb3927 (diff)
downloadframeworks_base-80955e105d6e71f7b71a63b391fd5a8adcded55e.zip
frameworks_base-80955e105d6e71f7b71a63b391fd5a8adcded55e.tar.gz
frameworks_base-80955e105d6e71f7b71a63b391fd5a8adcded55e.tar.bz2
Merge "Interval tree for SpannableStringBuilder"
Diffstat (limited to 'core')
-rw-r--r--core/java/android/text/SpannableStringBuilder.java550
1 files changed, 361 insertions, 189 deletions
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index 06df683..7ce44e1 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -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];
-
- if (start > mGapStart)
- start -= mGapLength;
- if (start > where)
- start += mGapLength;
- else if (start == where) {
- int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
+ // 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 (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);
+
+ if (flag == POINT || (atEnd && flag == PARAGRAPH))
+ end += mGapLength;
+ }
- mSpanStarts[i] = start;
- mSpanEnds[i] = end;
+ 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 <= 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;
+ }
- if (spanStart != spanEnd && queryStart != queryEnd) {
- if (spanStart == queryEnd)
- continue;
- if (spanEnd == queryStart)
- continue;
+ @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;
}
-
- // 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 (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;