SuggestionSpan.java revision c9fd978da60f76c0576150c55629a034e1fa19fb
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 17package android.text.style; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Color; 22import android.os.Parcel; 23import android.os.Parcelable; 24import android.os.SystemClock; 25import android.text.ParcelableSpan; 26import android.text.TextPaint; 27import android.text.TextUtils; 28import android.widget.TextView; 29 30import java.util.Arrays; 31import java.util.Locale; 32 33/** 34 * Holds suggestion candidates for the text enclosed in this span. 35 * 36 * When such a span is edited in an EditText, double tapping on the text enclosed in this span will 37 * display a popup dialog listing suggestion replacement for that text. The user can then replace 38 * the original text by one of the suggestions. 39 * 40 * These spans should typically be created by the input method to provide correction and alternates 41 * for the text. 42 * 43 * @see TextView#isSuggestionsEnabled() 44 */ 45public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { 46 47 /** 48 * Sets this flag if the suggestions should be easily accessible with few interactions. 49 * This flag should be set for every suggestions that the user is likely to use. 50 */ 51 public static final int FLAG_EASY_CORRECT = 0x0001; 52 53 /** 54 * Sets this flag if the suggestions apply to a misspelled word/text. This type of suggestion is 55 * rendered differently to highlight the error. 56 */ 57 public static final int FLAG_MISSPELLED = 0x0002; 58 59 public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED"; 60 public static final String SUGGESTION_SPAN_PICKED_AFTER = "after"; 61 public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before"; 62 public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode"; 63 64 public static final int SUGGESTIONS_MAX_SIZE = 5; 65 66 /* 67 * TODO: Needs to check the validity and add a feature that TextView will change 68 * the current IME to the other IME which is specified in SuggestionSpan. 69 * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan. 70 * And the current IME might want to specify any IME as the target IME including other IMEs. 71 */ 72 73 private int mFlags; 74 private final String[] mSuggestions; 75 private final String mLocaleString; 76 private final String mNotificationTargetClassName; 77 private final int mHashCode; 78 79 private float mEasyCorrectUnderlineThickness; 80 private int mEasyCorrectUnderlineColor; 81 82 private float mMisspelledUnderlineThickness; 83 private int mMisspelledUnderlineColor; 84 85 /* 86 * TODO: If switching IME is required, needs to add parameters for ids of InputMethodInfo 87 * and InputMethodSubtype. 88 */ 89 90 /** 91 * @param context Context for the application 92 * @param suggestions Suggestions for the string under the span 93 * @param flags Additional flags indicating how this span is handled in TextView 94 */ 95 public SuggestionSpan(Context context, String[] suggestions, int flags) { 96 this(context, null, suggestions, flags, null); 97 } 98 99 /** 100 * @param locale Locale of the suggestions 101 * @param suggestions Suggestions for the string under the span 102 * @param flags Additional flags indicating how this span is handled in TextView 103 */ 104 public SuggestionSpan(Locale locale, String[] suggestions, int flags) { 105 this(null, locale, suggestions, flags, null); 106 } 107 108 /** 109 * @param context Context for the application 110 * @param locale locale Locale of the suggestions 111 * @param suggestions Suggestions for the string under the span. Only the first up to 112 * {@link SuggestionSpan#SUGGESTIONS_MAX_SIZE} will be considered. 113 * @param flags Additional flags indicating how this span is handled in TextView 114 * @param notificationTargetClass if not null, this class will get notified when the user 115 * selects one of the suggestions. 116 */ 117 public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, 118 Class<?> notificationTargetClass) { 119 final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length); 120 mSuggestions = Arrays.copyOf(suggestions, N); 121 mFlags = flags; 122 if (context != null && locale == null) { 123 mLocaleString = context.getResources().getConfiguration().locale.toString(); 124 } else { 125 mLocaleString = locale.toString(); 126 } 127 128 if (notificationTargetClass != null) { 129 mNotificationTargetClassName = notificationTargetClass.getCanonicalName(); 130 } else { 131 mNotificationTargetClassName = ""; 132 } 133 mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName); 134 135 initStyle(context); 136 } 137 138 private void initStyle(Context context) { 139 int defStyle = com.android.internal.R.attr.textAppearanceMisspelledSuggestion; 140 TypedArray typedArray = context.obtainStyledAttributes( 141 null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); 142 mMisspelledUnderlineThickness = typedArray.getDimension( 143 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 144 mMisspelledUnderlineColor = typedArray.getColor( 145 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 146 147 defStyle = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion; 148 149 typedArray = context.obtainStyledAttributes( 150 null, com.android.internal.R.styleable.SuggestionSpan, defStyle, 0); 151 152 mEasyCorrectUnderlineThickness = typedArray.getDimension( 153 com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0); 154 mEasyCorrectUnderlineColor = typedArray.getColor( 155 com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK); 156 } 157 158 public SuggestionSpan(Parcel src) { 159 mSuggestions = src.readStringArray(); 160 mFlags = src.readInt(); 161 mLocaleString = src.readString(); 162 mNotificationTargetClassName = src.readString(); 163 mHashCode = src.readInt(); 164 mEasyCorrectUnderlineColor = src.readInt(); 165 mEasyCorrectUnderlineThickness = src.readFloat(); 166 mMisspelledUnderlineColor = src.readInt(); 167 mMisspelledUnderlineThickness = src.readFloat(); 168 } 169 170 /** 171 * @return an array of suggestion texts for this span 172 */ 173 public String[] getSuggestions() { 174 return mSuggestions; 175 } 176 177 /** 178 * @return the locale of the suggestions 179 */ 180 public String getLocale() { 181 return mLocaleString; 182 } 183 184 /** 185 * @return The name of the class to notify. The class of the original IME package will receive 186 * a notification when the user selects one of the suggestions. The notification will include 187 * the original string, the suggested replacement string as well as the hashCode of this span. 188 * The class will get notified by an intent that has those information. 189 * This is an internal API because only the framework should know the class name. 190 * 191 * @hide 192 */ 193 public String getNotificationTargetClassName() { 194 return mNotificationTargetClassName; 195 } 196 197 public int getFlags() { 198 return mFlags; 199 } 200 201 public void setFlags(int flags) { 202 mFlags = flags; 203 } 204 205 @Override 206 public int describeContents() { 207 return 0; 208 } 209 210 @Override 211 public void writeToParcel(Parcel dest, int flags) { 212 dest.writeStringArray(mSuggestions); 213 dest.writeInt(mFlags); 214 dest.writeString(mLocaleString); 215 dest.writeString(mNotificationTargetClassName); 216 dest.writeInt(mHashCode); 217 dest.writeInt(mEasyCorrectUnderlineColor); 218 dest.writeFloat(mEasyCorrectUnderlineThickness); 219 dest.writeInt(mMisspelledUnderlineColor); 220 dest.writeFloat(mMisspelledUnderlineThickness); 221 } 222 223 @Override 224 public int getSpanTypeId() { 225 return TextUtils.SUGGESTION_SPAN; 226 } 227 228 @Override 229 public boolean equals(Object o) { 230 if (o instanceof SuggestionSpan) { 231 return ((SuggestionSpan)o).hashCode() == mHashCode; 232 } 233 return false; 234 } 235 236 @Override 237 public int hashCode() { 238 return mHashCode; 239 } 240 241 private static int hashCodeInternal(String[] suggestions, String locale, 242 String notificationTargetClassName) { 243 return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, 244 locale, notificationTargetClassName}); 245 } 246 247 public static final Parcelable.Creator<SuggestionSpan> CREATOR = 248 new Parcelable.Creator<SuggestionSpan>() { 249 @Override 250 public SuggestionSpan createFromParcel(Parcel source) { 251 return new SuggestionSpan(source); 252 } 253 254 @Override 255 public SuggestionSpan[] newArray(int size) { 256 return new SuggestionSpan[size]; 257 } 258 }; 259 260 @Override 261 public void updateDrawState(TextPaint tp) { 262 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 263 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 264 if (easy) { 265 if (!misspelled) { 266 tp.setUnderlineText(mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness); 267 } else if (tp.underlineColor == 0) { 268 // Spans are rendered in an arbitrary order. Since misspelled is less prioritary 269 // than just easy, do not apply misspelled if an easy (or a mispelled) has been set 270 tp.setUnderlineText(mMisspelledUnderlineColor, mMisspelledUnderlineThickness); 271 } 272 } 273 } 274 275 /** 276 * @return The color of the underline for that span, or 0 if there is no underline 277 * 278 * @hide 279 */ 280 public int getUnderlineColor() { 281 // The order here should match what is used in updateDrawState 282 final boolean misspelled = (mFlags & FLAG_MISSPELLED) != 0; 283 final boolean easy = (mFlags & FLAG_EASY_CORRECT) != 0; 284 if (easy) { 285 if (!misspelled) { 286 return mEasyCorrectUnderlineColor; 287 } else { 288 return mMisspelledUnderlineColor; 289 } 290 } 291 return 0; 292 } 293} 294