OSDN Git Service

Keep spans while transforming to uppercase
authorRoozbeh Pournader <roozbeh@google.com>
Thu, 6 Apr 2017 01:26:17 +0000 (18:26 -0700)
committerRoozbeh Pournader <roozbeh@google.com>
Tue, 11 Apr 2017 20:49:18 +0000 (13:49 -0700)
We now use ICU's new functionality for casing edits to preserve
the spans while applying AllCapsTransformationMethod.

Test: Manual
Test: CTS tests added and pass
Bug: 7198872
Bug: 19047649
Bug: 19713814
Bug: 34853618
Bug: https://code.google.com/p/android/issues/detail?id=67509
Change-Id: I0df388e79894961efb272d497f1dcc0570a6c085

core/java/android/text/method/AllCapsTransformationMethod.java

index 0cea821..15f40d5 100644 (file)
@@ -17,6 +17,10 @@ package android.text.method;
 
 import android.content.Context;
 import android.graphics.Rect;
+import android.icu.text.CaseMap;
+import android.icu.text.Edits;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
 import android.util.Log;
 import android.view.View;
 import android.widget.TextView;
@@ -35,7 +39,7 @@ public class AllCapsTransformationMethod implements TransformationMethod2 {
     private Locale mLocale;
 
     public AllCapsTransformationMethod(Context context) {
-        mLocale = context.getResources().getConfiguration().locale;
+        mLocale = context.getResources().getConfiguration().getLocales().get(0);
     }
 
     @Override
@@ -56,7 +60,61 @@ public class AllCapsTransformationMethod implements TransformationMethod2 {
         if (locale == null) {
             locale = mLocale;
         }
-        return source.toString().toUpperCase(locale);
+
+        if (!(source instanceof Spanned)) { // No spans
+            return CaseMap.toUpper().apply(
+                    locale, source, new StringBuilder(),
+                    null /* we don't need the edits */);
+        }
+
+        final Edits edits = new Edits();
+        final SpannableStringBuilder result = CaseMap.toUpper().apply(
+                locale, source, new SpannableStringBuilder(), edits);
+        if (!edits.hasChanges()) {
+            // No changes happened while capitalizing. We can return the source as it was.
+            return source;
+        }
+
+        final Edits.Iterator iterator = edits.getFineIterator();
+        final Spanned spanned = (Spanned) source;
+        final int sourceLength = source.length();
+        final Object[] spans = spanned.getSpans(0, sourceLength, Object.class);
+        for (Object span : spans) {
+            final int sourceStart = spanned.getSpanStart(span);
+            final int sourceEnd = spanned.getSpanEnd(span);
+            final int flags = spanned.getSpanFlags(span);
+            // Make sure the indexes are not at the end of the string, since in that case
+            // iterator.findSourceIndex() would fail.
+            final int destStart = sourceStart == sourceLength ? result.length() :
+                    mapToDest(iterator, sourceStart);
+            final int destEnd = sourceEnd == sourceLength ? result.length() :
+                    mapToDest(iterator, sourceEnd);
+            result.setSpan(span, destStart, destEnd, flags);
+        }
+        return result;
+    }
+
+    private static int mapToDest(Edits.Iterator iterator, int sourceIndex) {
+        // Guaranteed to succeed if sourceIndex < source.length().
+        iterator.findSourceIndex(sourceIndex);
+        if (sourceIndex == iterator.sourceIndex()) {
+            return iterator.destinationIndex();
+        }
+        // We handle the situation differently depending on if we are in the changed slice or an
+        // unchanged one: In an unchanged slice, we can find the exact location the span
+        // boundary was before and map there.
+        //
+        // But in a changed slice, we need to treat the whole destination slice as an atomic unit.
+        // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent
+        // spans in the source overlapping in the result. (The choice for the end vs the beginning
+        // is somewhat arbitrary, but was taken because we except to see slightly more spans only
+        // affecting a base character compared to spans only affecting a combining character.)
+        if (iterator.hasChange()) {
+            return iterator.destinationIndex() + iterator.newLength();
+        } else {
+            // Move the index 1:1 along with this unchanged piece of text.
+            return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex());
+        }
     }
 
     @Override