Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 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.widget;
     18 
     19 import android.content.Context;
     20 import android.text.Editable;
     21 import android.text.SpannableString;
     22 import android.text.Spanned;
     23 import android.text.TextUtils;
     24 import android.text.method.QwertyKeyListener;
     25 import android.util.AttributeSet;
     26 import android.widget.MultiAutoCompleteTextView.Tokenizer;
     27 
     28 /**
     29  * An editable text view, extending {@link AutoCompleteTextView}, that
     30  * can show completion suggestions for the substring of the text where
     31  * the user is typing instead of necessarily for the entire thing.
     32  * <p>
     33  * You must provide a {@link Tokenizer} to distinguish the
     34  * various substrings.
     35  *
     36  * <p>The following code snippet shows how to create a text view which suggests
     37  * various countries names while the user is typing:</p>
     38  *
     39  * <pre class="prettyprint">
     40  * public class CountriesActivity extends Activity {
     41  *     protected void onCreate(Bundle savedInstanceState) {
     42  *         super.onCreate(savedInstanceState);
     43  *         setContentView(R.layout.autocomplete_7);
     44  *
     45  *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
     46  *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
     47  *         MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit);
     48  *         textView.setAdapter(adapter);
     49  *         textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
     50  *     }
     51  *
     52  *     private static final String[] COUNTRIES = new String[] {
     53  *         "Belgium", "France", "Italy", "Germany", "Spain"
     54  *     };
     55  * }</pre>
     56  */
     57 
     58 public class MultiAutoCompleteTextView extends AutoCompleteTextView {
     59     private Tokenizer mTokenizer;
     60 
     61     public MultiAutoCompleteTextView(Context context) {
     62         this(context, null);
     63     }
     64 
     65     public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
     66         this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
     67     }
     68 
     69     public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
     70         super(context, attrs, defStyle);
     71     }
     72 
     73     /* package */ void finishInit() { }
     74 
     75     /**
     76      * Sets the Tokenizer that will be used to determine the relevant
     77      * range of the text where the user is typing.
     78      */
     79     public void setTokenizer(Tokenizer t) {
     80         mTokenizer = t;
     81     }
     82 
     83     /**
     84      * Instead of filtering on the entire contents of the edit box,
     85      * this subclass method filters on the range from
     86      * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
     87      * if the length of that range meets or exceeds {@link #getThreshold}.
     88      */
     89     @Override
     90     protected void performFiltering(CharSequence text, int keyCode) {
     91         if (enoughToFilter()) {
     92             int end = getSelectionEnd();
     93             int start = mTokenizer.findTokenStart(text, end);
     94 
     95             performFiltering(text, start, end, keyCode);
     96         } else {
     97             dismissDropDown();
     98 
     99             Filter f = getFilter();
    100             if (f != null) {
    101                 f.filter(null);
    102             }
    103         }
    104     }
    105 
    106     /**
    107      * Instead of filtering whenever the total length of the text
    108      * exceeds the threshhold, this subclass filters only when the
    109      * length of the range from
    110      * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
    111      * meets or exceeds {@link #getThreshold}.
    112      */
    113     @Override
    114     public boolean enoughToFilter() {
    115         Editable text = getText();
    116 
    117         int end = getSelectionEnd();
    118         if (end < 0 || mTokenizer == null) {
    119             return false;
    120         }
    121 
    122         int start = mTokenizer.findTokenStart(text, end);
    123 
    124         if (end - start >= getThreshold()) {
    125             return true;
    126         } else {
    127             return false;
    128         }
    129     }
    130 
    131     /**
    132      * Instead of validating the entire text, this subclass method validates
    133      * each token of the text individually.  Empty tokens are removed.
    134      */
    135     @Override
    136     public void performValidation() {
    137         Validator v = getValidator();
    138 
    139         if (v == null || mTokenizer == null) {
    140             return;
    141         }
    142 
    143         Editable e = getText();
    144         int i = getText().length();
    145         while (i > 0) {
    146             int start = mTokenizer.findTokenStart(e, i);
    147             int end = mTokenizer.findTokenEnd(e, start);
    148 
    149             CharSequence sub = e.subSequence(start, end);
    150             if (TextUtils.isEmpty(sub)) {
    151                 e.replace(start, i, "");
    152             } else if (!v.isValid(sub)) {
    153                 e.replace(start, i,
    154                           mTokenizer.terminateToken(v.fixText(sub)));
    155             }
    156 
    157             i = start;
    158         }
    159     }
    160 
    161     /**
    162      * <p>Starts filtering the content of the drop down list. The filtering
    163      * pattern is the specified range of text from the edit box. Subclasses may
    164      * override this method to filter with a different pattern, for
    165      * instance a smaller substring of <code>text</code>.</p>
    166      */
    167     protected void performFiltering(CharSequence text, int start, int end,
    168                                     int keyCode) {
    169         getFilter().filter(text.subSequence(start, end), this);
    170     }
    171 
    172     /**
    173      * <p>Performs the text completion by replacing the range from
    174      * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the
    175      * the result of passing <code>text</code> through
    176      * {@link Tokenizer#terminateToken}.
    177      * In addition, the replaced region will be marked as an AutoText
    178      * substition so that if the user immediately presses DEL, the
    179      * completion will be undone.
    180      * Subclasses may override this method to do some different
    181      * insertion of the content into the edit box.</p>
    182      *
    183      * @param text the selected suggestion in the drop down list
    184      */
    185     @Override
    186     protected void replaceText(CharSequence text) {
    187         clearComposingText();
    188 
    189         int end = getSelectionEnd();
    190         int start = mTokenizer.findTokenStart(getText(), end);
    191 
    192         Editable editable = getText();
    193         String original = TextUtils.substring(editable, start, end);
    194 
    195         QwertyKeyListener.markAsReplaced(editable, start, end, original);
    196         editable.replace(start, end, mTokenizer.terminateToken(text));
    197     }
    198 
    199     public static interface Tokenizer {
    200         /**
    201          * Returns the start of the token that ends at offset
    202          * <code>cursor</code> within <code>text</code>.
    203          */
    204         public int findTokenStart(CharSequence text, int cursor);
    205 
    206         /**
    207          * Returns the end of the token (minus trailing punctuation)
    208          * that begins at offset <code>cursor</code> within <code>text</code>.
    209          */
    210         public int findTokenEnd(CharSequence text, int cursor);
    211 
    212         /**
    213          * Returns <code>text</code>, modified, if necessary, to ensure that
    214          * it ends with a token terminator (for example a space or comma).
    215          */
    216         public CharSequence terminateToken(CharSequence text);
    217     }
    218 
    219     /**
    220      * This simple Tokenizer can be used for lists where the items are
    221      * separated by a comma and one or more spaces.
    222      */
    223     public static class CommaTokenizer implements Tokenizer {
    224         public int findTokenStart(CharSequence text, int cursor) {
    225             int i = cursor;
    226 
    227             while (i > 0 && text.charAt(i - 1) != ',') {
    228                 i--;
    229             }
    230             while (i < cursor && text.charAt(i) == ' ') {
    231                 i++;
    232             }
    233 
    234             return i;
    235         }
    236 
    237         public int findTokenEnd(CharSequence text, int cursor) {
    238             int i = cursor;
    239             int len = text.length();
    240 
    241             while (i < len) {
    242                 if (text.charAt(i) == ',') {
    243                     return i;
    244                 } else {
    245                     i++;
    246                 }
    247             }
    248 
    249             return len;
    250         }
    251 
    252         public CharSequence terminateToken(CharSequence text) {
    253             int i = text.length();
    254 
    255             while (i > 0 && text.charAt(i - 1) == ' ') {
    256                 i--;
    257             }
    258 
    259             if (i > 0 && text.charAt(i - 1) == ',') {
    260                 return text;
    261             } else {
    262                 if (text instanceof Spanned) {
    263                     SpannableString sp = new SpannableString(text + ", ");
    264                     TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
    265                                             Object.class, sp, 0);
    266                     return sp;
    267                 } else {
    268                     return text + ", ";
    269                 }
    270             }
    271         }
    272     }
    273 }
    274