Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1e3ac18e authored by Gilles Debunne's avatar Gilles Debunne
Browse files

Empty spans are not considered in text layout/rendering process.

Bug http://code.google.com/p/android/issues/detail?id=14786

Empty spans are affecting the text before and after them. See the
comment in TextUtils.removeEmptySpans for details.

Change-Id: I40376c32fd56a17efde6219f9b2593e4b4ab1ba1
parent e888b25f
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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);
                    }
                }
+12 −9
Original line number Diff line number Diff line
@@ -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++) {
+60 −9
Original line number Diff line number Diff line
@@ -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();
        }
@@ -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;
}
+106 −13
Original line number Diff line number Diff line
@@ -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;
        }
    }
}