1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.text.method; 17 18 import android.content.Context; 19 import android.graphics.Rect; 20 import android.icu.text.CaseMap; 21 import android.icu.text.Edits; 22 import android.text.SpannableStringBuilder; 23 import android.text.Spanned; 24 import android.util.Log; 25 import android.view.View; 26 import android.widget.TextView; 27 28 import java.util.Locale; 29 30 /** 31 * Transforms source text into an ALL CAPS string, locale-aware. 32 * 33 * @hide 34 */ 35 public class AllCapsTransformationMethod implements TransformationMethod2 { 36 private static final String TAG = "AllCapsTransformationMethod"; 37 38 private boolean mEnabled; 39 private Locale mLocale; 40 AllCapsTransformationMethod(Context context)41 public AllCapsTransformationMethod(Context context) { 42 mLocale = context.getResources().getConfiguration().getLocales().get(0); 43 } 44 45 @Override getTransformation(CharSequence source, View view)46 public CharSequence getTransformation(CharSequence source, View view) { 47 if (!mEnabled) { 48 Log.w(TAG, "Caller did not enable length changes; not transforming text"); 49 return source; 50 } 51 52 if (source == null) { 53 return null; 54 } 55 56 Locale locale = null; 57 if (view instanceof TextView) { 58 locale = ((TextView)view).getTextLocale(); 59 } 60 if (locale == null) { 61 locale = mLocale; 62 } 63 64 if (!(source instanceof Spanned)) { // No spans 65 return CaseMap.toUpper().apply( 66 locale, source, new StringBuilder(), 67 null /* we don't need the edits */); 68 } 69 70 final Edits edits = new Edits(); 71 final SpannableStringBuilder result = CaseMap.toUpper().apply( 72 locale, source, new SpannableStringBuilder(), edits); 73 if (!edits.hasChanges()) { 74 // No changes happened while capitalizing. We can return the source as it was. 75 return source; 76 } 77 78 final Edits.Iterator iterator = edits.getFineIterator(); 79 final Spanned spanned = (Spanned) source; 80 final int sourceLength = source.length(); 81 final Object[] spans = spanned.getSpans(0, sourceLength, Object.class); 82 for (Object span : spans) { 83 final int sourceStart = spanned.getSpanStart(span); 84 final int sourceEnd = spanned.getSpanEnd(span); 85 final int flags = spanned.getSpanFlags(span); 86 // Make sure the indexes are not at the end of the string, since in that case 87 // iterator.findSourceIndex() would fail. 88 final int destStart = sourceStart == sourceLength ? result.length() : 89 mapToDest(iterator, sourceStart); 90 final int destEnd = sourceEnd == sourceLength ? result.length() : 91 mapToDest(iterator, sourceEnd); 92 result.setSpan(span, destStart, destEnd, flags); 93 } 94 return result; 95 } 96 mapToDest(Edits.Iterator iterator, int sourceIndex)97 private static int mapToDest(Edits.Iterator iterator, int sourceIndex) { 98 // Guaranteed to succeed if sourceIndex < source.length(). 99 iterator.findSourceIndex(sourceIndex); 100 if (sourceIndex == iterator.sourceIndex()) { 101 return iterator.destinationIndex(); 102 } 103 // We handle the situation differently depending on if we are in the changed slice or an 104 // unchanged one: In an unchanged slice, we can find the exact location the span 105 // boundary was before and map there. 106 // 107 // But in a changed slice, we need to treat the whole destination slice as an atomic unit. 108 // We adjust the span boundary to the end of that slice to reduce of the chance of adjacent 109 // spans in the source overlapping in the result. (The choice for the end vs the beginning 110 // is somewhat arbitrary, but was taken because we except to see slightly more spans only 111 // affecting a base character compared to spans only affecting a combining character.) 112 if (iterator.hasChange()) { 113 return iterator.destinationIndex() + iterator.newLength(); 114 } else { 115 // Move the index 1:1 along with this unchanged piece of text. 116 return iterator.destinationIndex() + (sourceIndex - iterator.sourceIndex()); 117 } 118 } 119 120 @Override onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect)121 public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, 122 Rect previouslyFocusedRect) { 123 } 124 125 @Override setLengthChangesAllowed(boolean allowLengthChanges)126 public void setLengthChangesAllowed(boolean allowLengthChanges) { 127 mEnabled = allowLengthChanges; 128 } 129 130 } 131