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