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