diff options
-rw-r--r-- | core/java/android/text/StaticLayout.java | 2 | ||||
-rw-r--r-- | core/java/android/text/TextLine.java | 21 | ||||
-rw-r--r-- | core/java/android/text/TextUtils.java | 69 | ||||
-rw-r--r-- | core/tests/coretests/src/android/text/TextUtilsTest.java | 119 |
4 files changed, 180 insertions, 31 deletions
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index a826a97..9e48eff 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -235,6 +235,8 @@ public class StaticLayout extends Layout { } else { MetricAffectingSpan[] spans = spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, spanned, + MetricAffectingSpan.class); measured.addStyleRun(paint, spans, spanLen, fm); } } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 90279d1..1b7f2f3 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -127,12 +127,12 @@ class TextLine { boolean hasReplacement = false; if (text instanceof Spanned) { mSpanned = (Spanned) text; - hasReplacement = mSpanned.getSpans(start, limit, - ReplacementSpan.class).length > 0; + ReplacementSpan[] spans = mSpanned.getSpans(start, limit, ReplacementSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class); + hasReplacement = spans.length > 0; } - mCharsValid = hasReplacement || hasTabs || - directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; + mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; if (mCharsValid) { if (mChars == null || mChars.length < mLen) { @@ -147,10 +147,11 @@ class TextLine { // zero-width characters. char[] chars = mChars; for (int i = start, inext; i < limit; i = inext) { - inext = mSpanned.nextSpanTransition(i, limit, - ReplacementSpan.class); - if (mSpanned.getSpans(i, inext, ReplacementSpan.class) - .length > 0) { // transition into a span + inext = mSpanned.nextSpanTransition(i, limit, ReplacementSpan.class); + ReplacementSpan[] spans = mSpanned.getSpans(i, inext, ReplacementSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class); + if (spans.length > 0) { + // transition into a span chars[i - start] = '\ufffc'; for (int j = i - start + 1, e = inext - start; j < e; ++j) { chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip @@ -197,7 +198,6 @@ class TextLine { boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; int segstart = runStart; - char[] chars = mChars; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { int codept = 0; Bitmap bm = null; @@ -629,6 +629,7 @@ class TextLine { MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, mStart + spanLimit, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); if (spans.length > 0) { ReplacementSpan replacement = null; @@ -835,6 +836,7 @@ class TextLine { mlimit = inext < measureLimit ? inext : measureLimit; MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i, mStart + mlimit, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); if (spans.length > 0) { ReplacementSpan replacement = null; @@ -868,6 +870,7 @@ class TextLine { CharacterStyle[] spans = mSpanned.getSpans(mStart + j, mStart + jnext, CharacterStyle.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, CharacterStyle.class); wp.set(mPaint); for (int k = 0; k < spans.length; k++) { diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index d5010c6..30a1f48 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -44,6 +44,7 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Printer; +import java.lang.reflect.Array; import java.util.Iterator; import java.util.regex.Pattern; @@ -54,7 +55,7 @@ public class TextUtils { public static void getChars(CharSequence s, int start, int end, char[] dest, int destoff) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) ((String) s).getChars(start, end, dest, destoff); @@ -75,7 +76,7 @@ public class TextUtils { } public static int indexOf(CharSequence s, char ch, int start) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) return ((String) s).indexOf(ch, start); @@ -84,7 +85,7 @@ public class TextUtils { } public static int indexOf(CharSequence s, char ch, int start, int end) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (s instanceof GetChars || c == StringBuffer.class || c == StringBuilder.class || c == String.class) { @@ -125,7 +126,7 @@ public class TextUtils { } public static int lastIndexOf(CharSequence s, char ch, int last) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) return ((String) s).lastIndexOf(ch, last); @@ -142,7 +143,7 @@ public class TextUtils { int end = last + 1; - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (s instanceof GetChars || c == StringBuffer.class || c == StringBuilder.class || c == String.class) { @@ -499,6 +500,7 @@ public class TextUtils { return new String(buf); } + @Override public String toString() { return subSequence(0, length()).toString(); } @@ -621,7 +623,7 @@ public class TextUtils { * Read and return a new CharSequence, possibly with styles, * from the parcel. */ - public CharSequence createFromParcel(Parcel p) { + public CharSequence createFromParcel(Parcel p) { int kind = p.readInt(); if (kind == 1) @@ -760,7 +762,7 @@ public class TextUtils { if (where >= 0) tb.setSpan(sources[i], where, where + sources[i].length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } for (int i = 0; i < sources.length; i++) { @@ -1114,7 +1116,6 @@ public class TextUtils { int remaining = commaCount + 1; int ok = 0; - int okRemaining = remaining; String okFormat = ""; int w = 0; @@ -1146,7 +1147,6 @@ public class TextUtils { if (w + moreWid <= avail) { ok = i + 1; - okRemaining = remaining; okFormat = format; } } @@ -1179,6 +1179,7 @@ public class TextUtils { MetricAffectingSpan.class); MetricAffectingSpan[] spans = sp.getSpans( spanStart, spanEnd, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class); width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); } } @@ -1537,6 +1538,56 @@ public class TextUtils { return false; } + /** + * Removes empty spans from the <code>spans</code> array. + * + * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans + * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by + * one of these transitions will (correctly) include the empty overlapping span. + * + * However, these empty spans should not be taken into account when layouting or rendering the + * string and this method provides a way to filter getSpans' results accordingly. + * + * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from + * the <code>spanned</code> + * @param spanned The Spanned from which spans were extracted + * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} == + * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved + * @hide + */ + @SuppressWarnings("unchecked") + public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) { + T[] copy = null; + int count = 0; + + for (int i = 0; i < spans.length; i++) { + final T span = spans[i]; + final int start = spanned.getSpanStart(span); + final int end = spanned.getSpanEnd(span); + + if (start == end) { + if (copy == null) { + copy = (T[]) Array.newInstance(klass, spans.length - 1); + System.arraycopy(spans, 0, copy, 0, i); + count = i; + } + } else { + if (copy != null) { + copy[count] = span; + count++; + } + } + } + + if (copy != null) { + T[] result = (T[]) Array.newInstance(klass, count); + System.arraycopy(copy, 0, result, 0, count); + return result; + } else { + return spans; + } + } + private static Object sLock = new Object(); private static char[] sTemp = null; } diff --git a/core/tests/coretests/src/android/text/TextUtilsTest.java b/core/tests/coretests/src/android/text/TextUtilsTest.java index e111662..e8e56de 100644 --- a/core/tests/coretests/src/android/text/TextUtilsTest.java +++ b/core/tests/coretests/src/android/text/TextUtilsTest.java @@ -16,28 +16,20 @@ package android.text; -import android.graphics.Paint; +import com.google.android.collect.Lists; + +import android.test.MoreAsserts; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.SmallTest; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.SpannedString; -import android.text.TextPaint; -import android.text.TextUtils; import android.text.style.StyleSpan; import android.text.util.Rfc822Token; import android.text.util.Rfc822Tokenizer; -import android.test.MoreAsserts; -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; +import java.util.ArrayList; +import java.util.List; import junit.framework.TestCase; -import java.util.List; -import java.util.Map; - /** * TextUtilsTest tests {@link TextUtils}. */ @@ -354,6 +346,7 @@ public class TextUtilsTest extends TestCase { return mString.charAt(off); } + @Override public String toString() { return mString.toString(); } @@ -362,4 +355,104 @@ public class TextUtilsTest extends TestCase { return new Wrapper(mString.subSequence(start, end)); } } + + @LargeTest + public void testRemoveEmptySpans() { + MockSpanned spanned = new MockSpanned(); + + spanned.test(); + spanned.addSpan().test(); + spanned.addSpan().test(); + spanned.addSpan().test(); + spanned.addEmptySpan().test(); + spanned.addSpan().test(); + spanned.addEmptySpan().test(); + spanned.addEmptySpan().test(); + spanned.addSpan().test(); + + spanned.clear(); + spanned.addEmptySpan().test(); + spanned.addEmptySpan().test(); + spanned.addEmptySpan().test(); + spanned.addSpan().test(); + spanned.addEmptySpan().test(); + spanned.addSpan().test(); + + spanned.clear(); + spanned.addSpan().test(); + spanned.addEmptySpan().test(); + spanned.addSpan().test(); + spanned.addEmptySpan().test(); + spanned.addSpan().test(); + spanned.addSpan().test(); + } + + protected static class MockSpanned implements Spanned { + + private List<Object> allSpans = new ArrayList<Object>(); + private List<Object> nonEmptySpans = new ArrayList<Object>(); + + public void clear() { + allSpans.clear(); + nonEmptySpans.clear(); + } + + public MockSpanned addSpan() { + Object o = new Object(); + allSpans.add(o); + nonEmptySpans.add(o); + return this; + } + + public MockSpanned addEmptySpan() { + Object o = new Object(); + allSpans.add(o); + return this; + } + + public void test() { + Object[] nonEmpty = TextUtils.removeEmptySpans(allSpans.toArray(), this, Object.class); + assertEquals("Mismatched array size", nonEmptySpans.size(), nonEmpty.length); + for (int i=0; i<nonEmpty.length; i++) { + assertEquals("Span differ", nonEmptySpans.get(i), nonEmpty[i]); + } + } + + public char charAt(int arg0) { + return 0; + } + + public int length() { + return 0; + } + + public CharSequence subSequence(int arg0, int arg1) { + return null; + } + + @Override + public <T> T[] getSpans(int start, int end, Class<T> type) { + return null; + } + + @Override + public int getSpanStart(Object tag) { + return 0; + } + + @Override + public int getSpanEnd(Object tag) { + return nonEmptySpans.contains(tag) ? 1 : 0; + } + + @Override + public int getSpanFlags(Object tag) { + return 0; + } + + @Override + public int nextSpanTransition(int start, int limit, Class type) { + return 0; + } + } } |