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