• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
MultiAutoCompleteTextView(Context context)62     public MultiAutoCompleteTextView(Context context) {
63         this(context, null);
64     }
65 
MultiAutoCompleteTextView(Context context, AttributeSet attrs)66     public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
67         this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
68     }
69 
MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle)70     public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
71         super(context, attrs, defStyle);
72     }
73 
finishInit()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      */
setTokenizer(Tokenizer t)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
performFiltering(CharSequence text, int keyCode)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
enoughToFilter()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
performValidation()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      */
performFiltering(CharSequence text, int start, int end, int keyCode)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
replaceText(CharSequence text)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
onInitializeAccessibilityEvent(AccessibilityEvent event)201     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
202         super.onInitializeAccessibilityEvent(event);
203         event.setClassName(MultiAutoCompleteTextView.class.getName());
204     }
205 
206     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)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          */
findTokenStart(CharSequence text, int cursor)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          */
findTokenEnd(CharSequence text, int cursor)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          */
terminateToken(CharSequence text)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 {
findTokenStart(CharSequence text, int cursor)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 
findTokenEnd(CharSequence text, int cursor)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 
terminateToken(CharSequence text)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