Skip to content
This repository has been archived by the owner on Nov 8, 2023. It is now read-only.

Commit

Permalink
Sort the result of SpannableStringBuilder.getSpans
Browse files Browse the repository at this point in the history
SpannableStringBuilder used to return the result of getSpans in the
order of start indices because of the interval tree it uses. However,
style spans has to be applied in the order of insertion to get
predictable results. Sorted the results of getSpans ordered first by
priority flag and then by insertion time. Moreover improved the
performance of SpannableStringInternal copy constructor.

Bug: 26240132
Change-Id: I0b0fa7bb30a3bd9ca37dedca66d8993718586027
  • Loading branch information
siyamed committed Jan 21, 2016
1 parent 3d5278b commit fa05ba0
Show file tree
Hide file tree
Showing 5 changed files with 472 additions and 36 deletions.
6 changes: 5 additions & 1 deletion core/java/android/text/Layout.java
Original file line number Diff line number Diff line change
Expand Up @@ -1826,7 +1826,11 @@ protected final boolean isSpanned() {
return ArrayUtils.emptyArray(type);
}

return text.getSpans(start, end, type);
if(text instanceof SpannableStringBuilder) {
return ((SpannableStringBuilder) text).getSpans(start, end, type, false);
} else {
return text.getSpans(start, end, type);
}
}

private char getEllipsisChar(TextUtils.TruncateAt method) {
Expand Down
204 changes: 181 additions & 23 deletions core/java/android/text/SpannableStringBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.style.ParagraphStyle;
import android.util.Log;

import com.android.internal.util.ArrayUtils;
Expand Down Expand Up @@ -66,11 +67,15 @@ public SpannableStringBuilder(CharSequence text, int start, int end) {
TextUtils.getChars(text, start, end, mText, 0);

mSpanCount = 0;
mSpanInsertCount = 0;
mSpans = EmptyArray.OBJECT;
mSpanStarts = EmptyArray.INT;
mSpanEnds = EmptyArray.INT;
mSpanFlags = EmptyArray.INT;
mSpanMax = EmptyArray.INT;
mSpanOrder = EmptyArray.INT;
mPrioSortBuffer = EmptyArray.INT;
mOrderSortBuffer = EmptyArray.INT;

if (text instanceof Spanned) {
Spanned sp = (Spanned) text;
Expand Down Expand Up @@ -234,6 +239,7 @@ public SpannableStringBuilder delete(int start, int end) {
// Documentation from interface
public void clear() {
replace(0, length(), "", 0, 0);
mSpanInsertCount = 0;
}

// Documentation from interface
Expand All @@ -256,6 +262,7 @@ public void clearSpans() {
if (mIndexOfSpan != null) {
mIndexOfSpan.clear();
}
mSpanInsertCount = 0;
}

// Documentation from interface
Expand Down Expand Up @@ -485,6 +492,7 @@ private void removeSpan(int i) {
System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count);
System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count);
System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count);
System.arraycopy(mSpanOrder, i + 1, mSpanOrder, i, count);

mSpanCount--;

Expand Down Expand Up @@ -712,9 +720,6 @@ private void setSpan(boolean send, Object what, int start, int end, int flags) {
end += mGapLength;
}

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

if (mIndexOfSpan != null) {
Integer index = mIndexOfSpan.get(what);
if (index != null) {
Expand Down Expand Up @@ -744,8 +749,10 @@ private void setSpan(boolean send, Object what, int start, int end, int flags) {
mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start);
mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end);
mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags);
mSpanOrder = GrowingArrayUtils.append(mSpanOrder, mSpanCount, mSpanInsertCount);
invalidateIndex(mSpanCount);
mSpanCount++;
mSpanInsertCount++;
// 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.
Expand Down Expand Up @@ -837,6 +844,25 @@ public int getSpanFlags(Object what) {
*/
@SuppressWarnings("unchecked")
public <T> T[] getSpans(int queryStart, int queryEnd, @Nullable Class<T> kind) {
return getSpans(queryStart, queryEnd, kind, true);
}

/**
* 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.
*
* @param queryStart Start index.
* @param queryEnd End index.
* @param kind Class type to search for.
* @param sort If true the results are sorted by the insertion order.
* @param <T>
* @return Array of the spans. Empty array if no results are found.
*
* @hide
*/
public <T> T[] getSpans(int queryStart, int queryEnd, @Nullable Class<T> kind,
boolean sort) {
if (kind == null) return (T[]) ArrayUtils.emptyArray(Object.class);
if (mSpanCount == 0) return ArrayUtils.emptyArray(kind);
int count = countSpans(queryStart, queryEnd, kind, treeRoot());
Expand All @@ -846,7 +872,13 @@ public <T> T[] getSpans(int queryStart, int queryEnd, @Nullable Class<T> kind) {

// Safe conversion, but requires a suppressWarning
T[] ret = (T[]) Array.newInstance(kind, count);
getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, 0);
if (sort) {
mPrioSortBuffer = checkSortBuffer(mPrioSortBuffer, count);
mOrderSortBuffer = checkSortBuffer(mOrderSortBuffer, count);
}
getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, mPrioSortBuffer,
mOrderSortBuffer, 0, sort);
if (sort) sort(ret, mPrioSortBuffer, mOrderSortBuffer);
return ret;
}

Expand Down Expand Up @@ -876,7 +908,7 @@ private int countSpans(int queryStart, int queryEnd, Class kind, int i) {
if (spanEnd >= queryStart &&
(spanStart == spanEnd || queryStart == queryEnd ||
(spanStart != queryEnd && spanEnd != queryStart)) &&
kind.isInstance(mSpans[i])) {
(Object.class == kind || kind.isInstance(mSpans[i]))) {
count++;
}
if ((i & 1) != 0) {
Expand All @@ -887,9 +919,25 @@ private int countSpans(int queryStart, int queryEnd, Class kind, int i) {
return count;
}

/**
* Fills the result array with the spans found under the current interval tree node.
*
* @param queryStart Start index for the interval query.
* @param queryEnd End index for the interval query.
* @param kind Class type to search for.
* @param i Index of the current tree node.
* @param ret Array to be filled with results.
* @param priority Buffer to keep record of the priorities of spans found.
* @param insertionOrder Buffer to keep record of the insertion orders of spans found.
* @param count The number of found spans.
* @param sort Flag to fill the priority and insertion order buffers. If false then
* the spans with priority flag will be sorted in the result array.
* @param <T>
* @return The total number of spans found.
*/
@SuppressWarnings("unchecked")
private <T> int getSpansRec(int queryStart, int queryEnd, Class<T> kind,
int i, T[] ret, int count) {
int i, T[] ret, int[] priority, int[] insertionOrder, int count, boolean sort) {
if ((i & 1) != 0) {
// internal tree node
int left = leftChild(i);
Expand All @@ -898,7 +946,8 @@ private <T> int getSpansRec(int queryStart, int queryEnd, Class<T> kind,
spanMax -= mGapLength;
}
if (spanMax >= queryStart) {
count = getSpansRec(queryStart, queryEnd, kind, left, ret, count);
count = getSpansRec(queryStart, queryEnd, kind, left, ret, priority,
insertionOrder, count, sort);
}
}
if (i >= mSpanCount) return count;
Expand All @@ -914,35 +963,136 @@ private <T> int getSpansRec(int queryStart, int queryEnd, Class<T> kind,
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;

for (j = 0; j < count; j++) {
(Object.class == kind || kind.isInstance(mSpans[i]))) {
int spanPriority = mSpanFlags[i] & SPAN_PRIORITY;
if(sort) {
ret[count] = (T) mSpans[i];
priority[count] = spanPriority;
insertionOrder[count] = mSpanOrder[i];
} else if (spanPriority != 0) {
//insertion sort for elements with priority
int j = 0;
for (; j < count; j++) {
int p = getSpanFlags(ret[j]) & SPAN_PRIORITY;

if (prio > p) {
break;
}
if (spanPriority > p) break;
}

System.arraycopy(ret, j, ret, j + 1, count - j);
// Safe conversion thanks to the isInstance test above
ret[j] = (T) mSpans[i];
} else {
// Safe conversion thanks to the isInstance test above
ret[count] = (T) mSpans[i];
}
count++;
}
if (count < ret.length && (i & 1) != 0) {
count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, count);
count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, priority,
insertionOrder, count, sort);
}
}
return count;
}

/**
* Check the size of the buffer and grow if required.
*
* @param buffer Buffer to be checked.
* @param size Required size.
* @return Same buffer instance if the current size is greater than required size. Otherwise a
* new instance is created and returned.
*/
private final int[] checkSortBuffer(int[] buffer, int size) {
if(size > buffer.length) {
return ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(size));
}
return buffer;
}

/**
* An iterative heap sort implementation. It will sort the spans using first their priority
* then insertion order. A span with higher priority will be before a span with lower
* priority. If priorities are the same, the spans will be sorted with insertion order. A
* span with a lower insertion order will be before a span with a higher insertion order.
*
* @param array Span array to be sorted.
* @param priority Priorities of the spans
* @param insertionOrder Insertion orders of the spans
* @param <T> Span object type.
* @param <T>
*/
private final <T> void sort(T[] array, int[] priority, int[] insertionOrder) {
int size = array.length;
for (int i = size / 2 - 1; i >= 0; i--) {
siftDown(i, array, size, priority, insertionOrder);
}

for (int i = size - 1; i > 0; i--) {
T v = array[0];
int prio = priority[0];
int insertOrder = insertionOrder[0];
array[0] = array[i];
priority[0] = priority[i];
insertionOrder[0] = insertionOrder[i];
siftDown(0, array, i, priority, insertionOrder);
array[i] = v;
priority[i] = prio;
insertionOrder[i] = insertOrder;
}
}

/**
* Helper function for heap sort.
*
* @param index Index of the element to sift down.
* @param array Span array to be sorted.
* @param size Current heap size.
* @param priority Priorities of the spans
* @param insertionOrder Insertion orders of the spans
* @param <T> Span object type.
*/
private final <T> void siftDown(int index, T[] array, int size, int[] priority,
int[] insertionOrder) {
T v = array[index];
int prio = priority[index];
int insertOrder = insertionOrder[index];

int left = 2 * index + 1;
while (left < size) {
if (left < size - 1 && compareSpans(left, left + 1, priority, insertionOrder) < 0) {
left++;
}
if (compareSpans(index, left, priority, insertionOrder) >= 0) {
break;
}
array[index] = array[left];
priority[index] = priority[left];
insertionOrder[index] = insertionOrder[left];
index = left;
left = 2 * index + 1;
}
array[index] = v;
priority[index] = prio;
insertionOrder[index] = insertOrder;
}

/**
* Compare two span elements in an array. Comparison is based first on the priority flag of
* the span, and then the insertion order of the span.
*
* @param left Index of the element to compare.
* @param right Index of the other element to compare.
* @param priority Priorities of the spans
* @param insertionOrder Insertion orders of the spans
* @return
*/
private final int compareSpans(int left, int right, int[] priority,
int[] insertionOrder) {
int priority1 = priority[left];
int priority2 = priority[right];
if (priority1 == priority2) {
return Integer.compare(insertionOrder[left], insertionOrder[right]);
}
// since high priority has to be before a lower priority, the arguments to compare are
// opposite of the insertion order check.
return Integer.compare(priority2, priority1);
}

/**
* Return the next offset after <code>start</code> but less than or
* equal to <code>limit</code> where a span of the specified type
Expand Down Expand Up @@ -1509,18 +1659,21 @@ private void restoreInvariants() {
int start = mSpanStarts[i];
int end = mSpanEnds[i];
int flags = mSpanFlags[i];
int insertionOrder = mSpanOrder[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];
mSpanOrder[j] = mSpanOrder[j - 1];
j--;
} while (j > 0 && start < mSpanStarts[j - 1]);
mSpans[j] = span;
mSpanStarts[j] = start;
mSpanEnds[j] = end;
mSpanFlags[j] = flags;
mSpanOrder[j] = insertionOrder;
invalidateIndex(j);
}
}
Expand Down Expand Up @@ -1558,6 +1711,11 @@ private void invalidateIndex(int i) {
private int[] mSpanEnds;
private int[] mSpanMax; // see calcMax() for an explanation of what this array stores
private int[] mSpanFlags;
private int[] mSpanOrder; // store the order of span insertion
private int mSpanInsertCount; // counter for the span insertion
private int[] mPrioSortBuffer; // buffer used to sort getSpans result
private int[] mOrderSortBuffer; // buffer used to sort getSpans result

private int mSpanCount;
private IdentityHashMap<Object, Integer> mIndexOfSpan;
private int mLowWaterMark; // indices below this have not been touched
Expand Down
Loading

0 comments on commit fa05ba0

Please sign in to comment.