Loading core/java/android/text/StaticLayout.java +2 −0 Original line number Diff line number Diff line Loading @@ -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); } } Loading core/java/android/text/TextLine.java +12 −9 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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++) { Loading core/java/android/text/TextUtils.java +60 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); Loading @@ -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); Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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) { Loading Loading @@ -499,6 +500,7 @@ public class TextUtils { return new String(buf); } @Override public String toString() { return subSequence(0, length()).toString(); } Loading Loading @@ -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++) { Loading Loading @@ -1114,7 +1116,6 @@ public class TextUtils { int remaining = commaCount + 1; int ok = 0; int okRemaining = remaining; String okFormat = ""; int w = 0; Loading Loading @@ -1146,7 +1147,6 @@ public class TextUtils { if (w + moreWid <= avail) { ok = i + 1; okRemaining = remaining; okFormat = format; } } Loading Loading @@ -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); } } Loading Loading @@ -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; } core/tests/coretests/src/android/text/TextUtilsTest.java +106 −13 Original line number Diff line number Diff line Loading @@ -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}. */ Loading Loading @@ -354,6 +346,7 @@ public class TextUtilsTest extends TestCase { return mString.charAt(off); } @Override public String toString() { return mString.toString(); } Loading @@ -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; } } } Loading
core/java/android/text/StaticLayout.java +2 −0 Original line number Diff line number Diff line Loading @@ -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); } } Loading
core/java/android/text/TextLine.java +12 −9 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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 Loading Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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++) { Loading
core/java/android/text/TextUtils.java +60 −9 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); Loading @@ -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); Loading @@ -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) { Loading Loading @@ -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); Loading @@ -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) { Loading Loading @@ -499,6 +500,7 @@ public class TextUtils { return new String(buf); } @Override public String toString() { return subSequence(0, length()).toString(); } Loading Loading @@ -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++) { Loading Loading @@ -1114,7 +1116,6 @@ public class TextUtils { int remaining = commaCount + 1; int ok = 0; int okRemaining = remaining; String okFormat = ""; int w = 0; Loading Loading @@ -1146,7 +1147,6 @@ public class TextUtils { if (w + moreWid <= avail) { ok = i + 1; okRemaining = remaining; okFormat = format; } } Loading Loading @@ -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); } } Loading Loading @@ -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; }
core/tests/coretests/src/android/text/TextUtilsTest.java +106 −13 Original line number Diff line number Diff line Loading @@ -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}. */ Loading Loading @@ -354,6 +346,7 @@ public class TextUtilsTest extends TestCase { return mString.charAt(off); } @Override public String toString() { return mString.toString(); } Loading @@ -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; } } }