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;
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;
// Documentation from interface
public void clear() {
replace(0, length(), "", 0, 0);
+ mSpanInsertCount = 0;
}
// Documentation from interface
if (mIndexOfSpan != null) {
mIndexOfSpan.clear();
}
+ mSpanInsertCount = 0;
}
// Documentation from interface
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--;
end += mGapLength;
}
- int count = mSpanCount;
- Object[] spans = mSpans;
-
if (mIndexOfSpan != null) {
Integer index = mIndexOfSpan.get(what);
if (index != null) {
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.
*/
@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());
// 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;
}
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) {
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);
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;
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
* begins or ends.
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);
}
}
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
mSpanData = EmptyArray.INT;
if (source instanceof Spanned) {
- Spanned sp = (Spanned) source;
- Object[] spans = sp.getSpans(start, end, Object.class);
+ if (source instanceof SpannableStringInternal) {
+ copySpans((SpannableStringInternal) source, start, end);
+ } else {
+ copySpans((Spanned) source, start, end);
+ }
+ }
+ }
- for (int i = 0; i < spans.length; i++) {
- int st = sp.getSpanStart(spans[i]);
- int en = sp.getSpanEnd(spans[i]);
- int fl = sp.getSpanFlags(spans[i]);
+ /**
+ * Copies another {@link Spanned} object's spans between [start, end] into this object.
+ *
+ * @param src Source object to copy from.
+ * @param start Start index in the source object.
+ * @param end End index in the source object.
+ */
+ private final void copySpans(Spanned src, int start, int end) {
+ Object[] spans = src.getSpans(start, end, Object.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int st = src.getSpanStart(spans[i]);
+ int en = src.getSpanEnd(spans[i]);
+ int fl = src.getSpanFlags(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ setSpan(spans[i], st - start, en - start, fl);
+ }
+ }
- if (st < start)
- st = start;
- if (en > end)
- en = end;
+ /**
+ * Copies a {@link SpannableStringInternal} object's spans between [start, end] into this
+ * object.
+ *
+ * @param src Source object to copy from.
+ * @param start Start index in the source object.
+ * @param end End index in the source object.
+ */
+ private final void copySpans(SpannableStringInternal src, int start, int end) {
+ if (start == 0 && end == src.length()) {
+ mSpans = ArrayUtils.newUnpaddedObjectArray(src.mSpans.length);
+ mSpanData = new int[src.mSpanData.length];
+ mSpanCount = src.mSpanCount;
+ System.arraycopy(src.mSpans, 0, mSpans, 0, src.mSpans.length);
+ System.arraycopy(src.mSpanData, 0, mSpanData, 0, mSpanData.length);
+ } else {
+ int count = 0;
+ int[] srcData = src.mSpanData;
+ int limit = src.mSpanCount;
+ for (int i = 0; i < limit; i++) {
+ int spanStart = srcData[i * COLUMNS + START];
+ int spanEnd = srcData[i * COLUMNS + END];
+ if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue;
+ count++;
+ }
- setSpan(spans[i], st - start, en - start, fl);
+ if (count == 0) return;
+
+ Object[] srcSpans = src.mSpans;
+ mSpanCount = count;
+ mSpans = ArrayUtils.newUnpaddedObjectArray(mSpanCount);
+ mSpanData = new int[mSpanCount * COLUMNS];
+ for (int i = 0, j = 0; i < limit; i++) {
+ int spanStart = srcData[i * COLUMNS + START];
+ int spanEnd = srcData[i * COLUMNS + END];
+ if (isOutOfCopyRange(start, end, spanStart, spanEnd)) continue;
+ if (spanStart < start) spanStart = start;
+ if (spanEnd > end) spanEnd = end;
+
+ mSpans[j] = srcSpans[i];
+ mSpanData[j * COLUMNS + START] = spanStart - start;
+ mSpanData[j * COLUMNS + END] = spanEnd - start;
+ mSpanData[j * COLUMNS + FLAGS] = srcData[i * COLUMNS + FLAGS];
+ j++;
}
}
}
+ /**
+ * Checks if [spanStart, spanEnd] interval is excluded from [start, end].
+ *
+ * @return True if excluded, false if included.
+ */
+ private final boolean isOutOfCopyRange(int start, int end, int spanStart, int spanEnd) {
+ if (spanStart > end || spanEnd < start) return true;
+ if (spanStart != spanEnd && start != end) {
+ if (spanStart == end || spanEnd == start) return true;
+ }
+ return false;
+ }
+
public final int length() {
return mText.length();
}
}
// verify span class as late as possible, since it is expensive
- if (kind != null && !kind.isInstance(spans[i])) {
+ if (kind != null && kind != Object.class && !kind.isInstance(spans[i])) {
continue;
}
--- /dev/null
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.text;
+
+import com.google.caliper.AfterExperiment;
+import com.google.caliper.BeforeExperiment;
+import com.google.caliper.Benchmark;
+import com.google.caliper.Param;
+
+public class SpannableStringBuilderBenchmark {
+
+ @Param({"android.text.style.ImageSpan",
+ "android.text.style.ParagraphStyle",
+ "android.text.style.CharacterStyle",
+ "java.lang.Object"})
+ private String paramType;
+
+ @Param({"1", "4", "16"})
+ private String paramStringMult;
+
+ private Class clazz;
+ private SpannableStringBuilder builder;
+
+ @BeforeExperiment
+ protected void setUp() throws Exception {
+ clazz = Class.forName(paramType);
+ int strSize = Integer.valueOf(paramStringMult);
+ StringBuilder strBuilder = new StringBuilder();
+ for (int i = 0; i < strSize; i++) {
+ strBuilder.append(TEST_STRING);
+ }
+ builder = new SpannableStringBuilder(Html.fromHtml(strBuilder.toString()));
+ }
+
+ @AfterExperiment
+ protected void tearDown() {
+ builder.clear();
+ builder = null;
+ }
+
+ @Benchmark
+ public void timeGetSpans(int reps) throws Exception {
+ for (int i = 0; i < reps; i++) {
+ builder.getSpans(0, builder.length(), clazz);
+ }
+ }
+
+ //contains 0 ImageSpans, 2 ParagraphSpans, 53 CharacterStyleSpans
+ public static String TEST_STRING =
+ "<p><span><a href=\"http://android.com\">some link</a></span></p>\n" +
+ "<h1 style=\"margin: 0.0px 0.0px 10.0px 0.0px; line-height: 64.0px; font: 62.0px " +
+ "'Helvetica Neue Light'; color: #000000; \"><span>some title</span></h1>\n" +
+ "<p><span>by <a href=\"http://android.com\"><span>some name</span></a>\n" +
+ " <a href=\"https://android.com\"><span>some text</span></a></span></p>\n" +
+ "<p><span>some date</span></p>\n" +
+ "<table cellspacing=\"0\" cellpadding=\"0\">\n" +
+ " <tbody><tr><td valign=\"bottom\">\n" +
+ " <p><span><blockquote>a paragraph</blockquote></span><br></p>\n" +
+ " </tbody></tr></td>\n" +
+ "</table>\n" +
+ "<h2 style=\"margin: 0.0px 0.0px 0.0px 0.0px; line-height: 38.0px; font: 26.0px " +
+ "'Helvetica Neue Light'; color: #262626; -webkit-text-stroke: #262626\">" +
+ "<span>some header two</span></h2>\n" +
+ "<p><span>Lorem ipsum dolor concludaturque. </span></p>\n" +
+ "<p><span></span><br></p>\n" +
+ "<p><span>Vix te doctus</span></p>\n" +
+ "<p><span><b>Error mel</b></span><span>, est ei. <a href=\"http://andorid.com\">" +
+ "<span>asda</span></a> ullamcorper eam.</span></p>\n" +
+ "<p><span>adversarium <a href=\"http://android.com\"><span>efficiantur</span></a>, " +
+ "mea te.</span></p>\n" +
+ "<p><span></span><br></p>\n" +
+ "<h1>Testing display of HTML elements</h1>\n" +
+ "<h2>2nd level heading</h2>\n" +
+ "<p>test paragraph.</p>\n" +
+ "<h3>3rd level heading</h3>\n" +
+ "<p>test paragraph.</p>\n" +
+ "<h4>4th level heading</h4>\n" +
+ "<p>test paragraph.</p>\n" +
+ "<h5>5th level heading</h5>\n" +
+ "<p>test paragraph.</p>\n" +
+ "<h6>6th level heading</h6>\n" +
+ "<p>test paragraph.</p>\n" +
+ "<h2>level elements</h2>\n" +
+ "<p>a normap paragraph(<code>p</code> element).\n" +
+ " with some <strong>strong</strong>.</p>\n" +
+ "<div>This is a <code>div</code> element. </div>\n" +
+ "<blockquote><p>This is a block quotation with some <em>style</em></p></blockquote>\n" +
+ "<address>an address element</address>\n" +
+ "<h2>Text-level markup</h2>\n" +
+ "<ul>\n" +
+ " <li> <abbr title=\"Cascading Style Sheets\">CSS</abbr> (an abbreviation;\n" +
+ " <code>abbr</code> markup used)\n" +
+ " <li> <acronym title=\"radio detecting and ranging\">radar</acronym>\n" +
+ " <li> <b>bolded</b>\n" +
+ " <li> <big>big thing</big>\n" +
+ " <li> <font size=6>large size</font>\n" +
+ " <li> <font face=Courier>Courier font</font>\n" +
+ " <li> <font color=red>red text</font>\n" +
+ " <li> <cite>Origin of Species</cite>\n" +
+ " <li> <code>a[i] = b[i] + c[i);</code>\n" +
+ " <li> some <del>deleted</del> text\n" +
+ " <li> an <dfn>octet</dfn> is an\n" +
+ " <li> this is <em>very</em> simple\n" +
+ " <li> <i lang=\"la\">Homo sapiens</i>\n" +
+ " <li> some <ins>inserted</ins> text\n" +
+ " <li> type <kbd>yes</kbd> when\n" +
+ " <li> <q>Hello!</q>\n" +
+ " <li> <q>She said <q>Hello!</q></q>\n" +
+ " <li> <samp>ccc</samp>\n" +
+ " <li> <small>important</small>\n" +
+ " <li> <strike>overstruck</strike>\n" +
+ " <li> <strong>this is highlighted text</strong>\n" +
+ " <li> <code>sub</code> and\n" +
+ " <code>sup</code> x<sub>1</sub> and H<sub>2</sub>O\n" +
+ " M<sup>lle</sup>, 1<sup>st</sup>, e<sup>x</sup>, sin<sup>2</sup> <i>x</i>,\n" +
+ " e<sup>x<sup>2</sup></sup> and f(x)<sup>g(x)<sup>a+b+c</sup></sup>\n" +
+ " (where 2 and a+b+c should appear as exponents of exponents).\n" +
+ " <li> <tt>text in monospace font</tt>\n" +
+ " <li> <u>underlined</u> text\n" +
+ " <li> <code>cat</code> <var>filename</var> displays the\n" +
+ " the <var>filename</var>.\n" +
+ "</ul>\n";
+
+}