Merge "Internationalize InputFilter.AllCaps"
This commit is contained in:
committed by
Android (Google) Code Review
commit
aa8380fa22
@@ -41102,6 +41102,7 @@ package android.text {
|
||||
|
||||
public static class InputFilter.AllCaps implements android.text.InputFilter {
|
||||
ctor public InputFilter.AllCaps();
|
||||
ctor public InputFilter.AllCaps(java.util.Locale);
|
||||
method public java.lang.CharSequence filter(java.lang.CharSequence, int, int, android.text.Spanned, int, int);
|
||||
}
|
||||
|
||||
|
||||
@@ -44647,6 +44647,7 @@ package android.text {
|
||||
|
||||
public static class InputFilter.AllCaps implements android.text.InputFilter {
|
||||
ctor public InputFilter.AllCaps();
|
||||
ctor public InputFilter.AllCaps(java.util.Locale);
|
||||
method public java.lang.CharSequence filter(java.lang.CharSequence, int, int, android.text.Spanned, int, int);
|
||||
}
|
||||
|
||||
|
||||
@@ -41329,6 +41329,7 @@ package android.text {
|
||||
|
||||
public static class InputFilter.AllCaps implements android.text.InputFilter {
|
||||
ctor public InputFilter.AllCaps();
|
||||
ctor public InputFilter.AllCaps(java.util.Locale);
|
||||
method public java.lang.CharSequence filter(java.lang.CharSequence, int, int, android.text.Spanned, int, int);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package android.text;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* InputFilters can be attached to {@link Editable}s to constrain the
|
||||
* changes that can be made to them.
|
||||
@@ -33,40 +37,122 @@ public interface InputFilter
|
||||
* as this is what happens when you delete text. Also beware that
|
||||
* you should not attempt to make any changes to <code>dest</code>
|
||||
* from this method; you may only examine it for context.
|
||||
*
|
||||
*
|
||||
* Note: If <var>source</var> is an instance of {@link Spanned} or
|
||||
* {@link Spannable}, the span objects in the <var>source</var> should be
|
||||
* copied into the filtered result (i.e. the non-null return value).
|
||||
* {@link TextUtils#copySpansFrom} can be used for convenience.
|
||||
* {@link Spannable}, the span objects in the <var>source</var> should be
|
||||
* copied into the filtered result (i.e. the non-null return value).
|
||||
* {@link TextUtils#copySpansFrom} can be used for convenience if the
|
||||
* span boundary indices would be remaining identical relative to the source.
|
||||
*/
|
||||
public CharSequence filter(CharSequence source, int start, int end,
|
||||
Spanned dest, int dstart, int dend);
|
||||
|
||||
/**
|
||||
* This filter will capitalize all the lower case letters that are added
|
||||
* through edits.
|
||||
* This filter will capitalize all the lowercase and titlecase letters that are added
|
||||
* through edits. (Note that if there are no lowercase or titlecase letters in the input, the
|
||||
* text would not be transformed, even if the result of capitalization of the string is
|
||||
* different from the string.)
|
||||
*/
|
||||
public static class AllCaps implements InputFilter {
|
||||
private final Locale mLocale;
|
||||
|
||||
public AllCaps() {
|
||||
mLocale = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a locale-specific AllCaps filter, to make sure capitalization rules of that
|
||||
* locale are used for transforming the sequence.
|
||||
*/
|
||||
public AllCaps(@Nullable Locale locale) {
|
||||
mLocale = locale;
|
||||
}
|
||||
|
||||
public CharSequence filter(CharSequence source, int start, int end,
|
||||
Spanned dest, int dstart, int dend) {
|
||||
for (int i = start; i < end; i++) {
|
||||
if (Character.isLowerCase(source.charAt(i))) {
|
||||
char[] v = new char[end - start];
|
||||
TextUtils.getChars(source, start, end, v, 0);
|
||||
String s = new String(v).toUpperCase();
|
||||
final CharSequence wrapper = new CharSequenceWrapper(source, start, end);
|
||||
|
||||
if (source instanceof Spanned) {
|
||||
SpannableString sp = new SpannableString(s);
|
||||
TextUtils.copySpansFrom((Spanned) source,
|
||||
start, end, null, sp, 0);
|
||||
return sp;
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
boolean lowerOrTitleFound = false;
|
||||
final int length = end - start;
|
||||
for (int i = 0, cp; i < length; i += Character.charCount(cp)) {
|
||||
// We access 'wrapper' instead of 'source' to make sure no code unit beyond 'end' is
|
||||
// ever accessed.
|
||||
cp = Character.codePointAt(wrapper, i);
|
||||
if (Character.isLowerCase(cp) || Character.isTitleCase(cp)) {
|
||||
lowerOrTitleFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!lowerOrTitleFound) {
|
||||
return null; // keep original
|
||||
}
|
||||
|
||||
return null; // keep original
|
||||
final boolean copySpans = source instanceof Spanned;
|
||||
final CharSequence upper = TextUtils.toUpperCase(mLocale, wrapper, copySpans);
|
||||
if (upper == wrapper) {
|
||||
// Nothing was changed in the uppercasing operation. This is weird, since
|
||||
// we had found at least one lowercase or titlecase character. But we can't
|
||||
// do anything better than keeping the original in this case.
|
||||
return null; // keep original
|
||||
}
|
||||
// Return a SpannableString or String for backward compatibility.
|
||||
return copySpans ? new SpannableString(upper) : upper.toString();
|
||||
}
|
||||
|
||||
private static class CharSequenceWrapper implements CharSequence, Spanned {
|
||||
private final CharSequence mSource;
|
||||
private final int mStart, mEnd;
|
||||
private final int mLength;
|
||||
|
||||
CharSequenceWrapper(CharSequence source, int start, int end) {
|
||||
mSource = source;
|
||||
mStart = start;
|
||||
mEnd = end;
|
||||
mLength = end - start;
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return mLength;
|
||||
}
|
||||
|
||||
public char charAt(int index) {
|
||||
if (index < 0 || index >= mLength) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
return mSource.charAt(mStart + index);
|
||||
}
|
||||
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
if (start < 0 || end < 0 || end > mLength || start > end) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
return new CharSequenceWrapper(mSource, mStart + start, mStart + end);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return mSource.subSequence(mStart, mEnd).toString();
|
||||
}
|
||||
|
||||
public <T> T[] getSpans(int start, int end, Class<T> type) {
|
||||
return ((Spanned) mSource).getSpans(mStart + start, mStart + end, type);
|
||||
}
|
||||
|
||||
public int getSpanStart(Object tag) {
|
||||
return ((Spanned) mSource).getSpanStart(tag) - mStart;
|
||||
}
|
||||
|
||||
public int getSpanEnd(Object tag) {
|
||||
return ((Spanned) mSource).getSpanEnd(tag) - mStart;
|
||||
}
|
||||
|
||||
public int getSpanFlags(Object tag) {
|
||||
return ((Spanned) mSource).getSpanFlags(tag);
|
||||
}
|
||||
|
||||
public int nextSpanTransition(int start, int limit, Class type) {
|
||||
return ((Spanned) mSource).nextSpanTransition(mStart + start, mStart + limit, type)
|
||||
- mStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import android.annotation.PluralsRes;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.icu.lang.UCharacter;
|
||||
import android.icu.text.CaseMap;
|
||||
import android.icu.text.Edits;
|
||||
import android.icu.util.ULocale;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
@@ -1072,6 +1074,75 @@ public class TextUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a CharSequences to uppercase, copying the sources spans and keeping them spans as
|
||||
* much as possible close to their relative original places. In the case the the uppercase
|
||||
* string is identical to the sources, the source itself is returned instead of being copied.
|
||||
*
|
||||
* If copySpans is set, source must be an instance of Spanned.
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
@NonNull
|
||||
public static CharSequence toUpperCase(@Nullable Locale locale, @NonNull CharSequence source,
|
||||
boolean copySpans) {
|
||||
final Edits edits = new Edits();
|
||||
if (!copySpans) { // No spans. Just uppercase the characters.
|
||||
final StringBuilder result = CaseMap.toUpper().apply(
|
||||
locale, source, new StringBuilder(), edits);
|
||||
return edits.hasChanges() ? result : source;
|
||||
}
|
||||
|
||||
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 int sourceLength = source.length();
|
||||
final Spanned spanned = (Spanned) source;
|
||||
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 indices are not at the end of the string, since in that case
|
||||
// iterator.findSourceIndex() would fail.
|
||||
final int destStart = sourceStart == sourceLength ? result.length() :
|
||||
toUpperMapToDest(iterator, sourceStart);
|
||||
final int destEnd = sourceEnd == sourceLength ? result.length() :
|
||||
toUpperMapToDest(iterator, sourceEnd);
|
||||
result.setSpan(span, destStart, destEnd, flags);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// helper method for toUpperCase()
|
||||
private static int toUpperMapToDest(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());
|
||||
}
|
||||
}
|
||||
|
||||
public enum TruncateAt {
|
||||
START,
|
||||
MIDDLE,
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
*/
|
||||
package android.text.method;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
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.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
@@ -38,12 +38,12 @@ public class AllCapsTransformationMethod implements TransformationMethod2 {
|
||||
private boolean mEnabled;
|
||||
private Locale mLocale;
|
||||
|
||||
public AllCapsTransformationMethod(Context context) {
|
||||
public AllCapsTransformationMethod(@NonNull Context context) {
|
||||
mLocale = context.getResources().getConfiguration().getLocales().get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getTransformation(CharSequence source, View view) {
|
||||
public CharSequence getTransformation(@Nullable CharSequence source, View view) {
|
||||
if (!mEnabled) {
|
||||
Log.w(TAG, "Caller did not enable length changes; not transforming text");
|
||||
return source;
|
||||
@@ -60,61 +60,8 @@ public class AllCapsTransformationMethod implements TransformationMethod2 {
|
||||
if (locale == null) {
|
||||
locale = mLocale;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
final boolean copySpans = source instanceof Spanned;
|
||||
return TextUtils.toUpperCase(locale, source, copySpans);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,26 +20,28 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import com.google.android.collect.Lists;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.support.test.filters.LargeTest;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.test.MoreAsserts;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.util.Rfc822Token;
|
||||
import android.text.util.Rfc822Tokenizer;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.collect.Lists;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* TextUtilsTest tests {@link TextUtils}.
|
||||
@@ -580,4 +582,84 @@ public class TextUtilsTest {
|
||||
assertEquals(View.LAYOUT_DIRECTION_RTL,
|
||||
TextUtils.getLayoutDirectionFromLocale(Locale.forLanguageTag("tr-Arab")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToUpperCase() {
|
||||
{
|
||||
final CharSequence result = TextUtils.toUpperCase(null, "abc", false);
|
||||
assertEquals(StringBuilder.class, result.getClass());
|
||||
assertEquals("ABC", result.toString());
|
||||
}
|
||||
{
|
||||
final SpannableString str = new SpannableString("abc");
|
||||
Object span = new Object();
|
||||
str.setSpan(span, 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
|
||||
final CharSequence result = TextUtils.toUpperCase(null, str, true /* copySpans */);
|
||||
assertEquals(SpannableStringBuilder.class, result.getClass());
|
||||
assertEquals("ABC", result.toString());
|
||||
final Spanned spanned = (Spanned) result;
|
||||
final Object[] resultSpans = spanned.getSpans(0, result.length(), Object.class);
|
||||
assertEquals(1, resultSpans.length);
|
||||
assertSame(span, resultSpans[0]);
|
||||
assertEquals(1, spanned.getSpanStart(span));
|
||||
assertEquals(2, spanned.getSpanEnd(span));
|
||||
assertEquals(Spanned.SPAN_INCLUSIVE_INCLUSIVE, spanned.getSpanFlags(span));
|
||||
}
|
||||
{
|
||||
final Locale turkish = new Locale("tr", "TR");
|
||||
final CharSequence result = TextUtils.toUpperCase(turkish, "i", false);
|
||||
assertEquals(StringBuilder.class, result.getClass());
|
||||
assertEquals("İ", result.toString());
|
||||
}
|
||||
{
|
||||
final String str = "ABC";
|
||||
assertSame(str, TextUtils.toUpperCase(null, str, false));
|
||||
}
|
||||
{
|
||||
final SpannableString str = new SpannableString("ABC");
|
||||
str.setSpan(new Object(), 1, 2, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
assertSame(str, TextUtils.toUpperCase(null, str, true /* copySpans */));
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from cts/tests/tests/widget/src/android/widget/cts/TextViewTest.java and modified
|
||||
// for the TextUtils.toUpperCase method.
|
||||
@Test
|
||||
public void testToUpperCase_SpansArePreserved() {
|
||||
final Locale greek = new Locale("el", "GR");
|
||||
final String lowerString = "ι\u0301ριδα"; // ίριδα with first letter decomposed
|
||||
final String upperString = "ΙΡΙΔΑ"; // uppercased
|
||||
// expected lowercase to uppercase index map
|
||||
final int[] indexMap = {0, 1, 1, 2, 3, 4, 5};
|
||||
final int flags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;
|
||||
|
||||
final Spannable source = new SpannableString(lowerString);
|
||||
source.setSpan(new Object(), 0, 1, flags);
|
||||
source.setSpan(new Object(), 1, 2, flags);
|
||||
source.setSpan(new Object(), 2, 3, flags);
|
||||
source.setSpan(new Object(), 3, 4, flags);
|
||||
source.setSpan(new Object(), 4, 5, flags);
|
||||
source.setSpan(new Object(), 5, 6, flags);
|
||||
source.setSpan(new Object(), 0, 2, flags);
|
||||
source.setSpan(new Object(), 1, 3, flags);
|
||||
source.setSpan(new Object(), 2, 4, flags);
|
||||
source.setSpan(new Object(), 0, 6, flags);
|
||||
final Object[] sourceSpans = source.getSpans(0, source.length(), Object.class);
|
||||
|
||||
final CharSequence uppercase = TextUtils.toUpperCase(greek, source, true /* copySpans */);
|
||||
assertEquals(SpannableStringBuilder.class, uppercase.getClass());
|
||||
final Spanned result = (Spanned) uppercase;
|
||||
|
||||
assertEquals(upperString, result.toString());
|
||||
final Object[] resultSpans = result.getSpans(0, result.length(), Object.class);
|
||||
assertEquals(sourceSpans.length, resultSpans.length);
|
||||
for (int i = 0; i < sourceSpans.length; i++) {
|
||||
assertSame(sourceSpans[i], resultSpans[i]);
|
||||
final Object span = sourceSpans[i];
|
||||
assertEquals(indexMap[source.getSpanStart(span)], result.getSpanStart(span));
|
||||
assertEquals(indexMap[source.getSpanEnd(span)], result.getSpanEnd(span));
|
||||
assertEquals(source.getSpanFlags(span), result.getSpanFlags(span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user