• 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 defStyleAttr)70     public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
71         this(context, attrs, defStyleAttr, 0);
72     }
73 
MultiAutoCompleteTextView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)74     public MultiAutoCompleteTextView(
75             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
76         super(context, attrs, defStyleAttr, defStyleRes);
77     }
78 
finishInit()79     /* package */ void finishInit() { }
80 
81     /**
82      * Sets the Tokenizer that will be used to determine the relevant
83      * range of the text where the user is typing.
84      */
setTokenizer(Tokenizer t)85     public void setTokenizer(Tokenizer t) {
86         mTokenizer = t;
87     }
88 
89     /**
90      * Instead of filtering on the entire contents of the edit box,
91      * this subclass method filters on the range from
92      * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
93      * if the length of that range meets or exceeds {@link #getThreshold}.
94      */
95     @Override
performFiltering(CharSequence text, int keyCode)96     protected void performFiltering(CharSequence text, int keyCode) {
97         if (enoughToFilter()) {
98             int end = getSelectionEnd();
99             int start = mTokenizer.findTokenStart(text, end);
100 
101             performFiltering(text, start, end, keyCode);
102         } else {
103             dismissDropDown();
104 
105             Filter f = getFilter();
106             if (f != null) {
107                 f.filter(null);
108             }
109         }
110     }
111 
112     /**
113      * Instead of filtering whenever the total length of the text
114      * exceeds the threshhold, this subclass filters only when the
115      * length of the range from
116      * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
117      * meets or exceeds {@link #getThreshold}.
118      */
119     @Override
enoughToFilter()120     public boolean enoughToFilter() {
121         Editable text = getText();
122 
123         int end = getSelectionEnd();
124         if (end < 0 || mTokenizer == null) {
125             return false;
126         }
127 
128         int start = mTokenizer.findTokenStart(text, end);
129 
130         if (end - start >= getThreshold()) {
131             return true;
132         } else {
133             return false;
134         }
135     }
136 
137     /**
138      * Instead of validating the entire text, this subclass method validates
139      * each token of the text individually.  Empty tokens are removed.
140      */
141     @Override
performValidation()142     public void performValidation() {
143         Validator v = getValidator();
144 
145         if (v == null || mTokenizer == null) {
146             return;
147         }
148 
149         Editable e = getText();
150         int i = getText().length();
151         while (i > 0) {
152             int start = mTokenizer.findTokenStart(e, i);
153             int end = mTokenizer.findTokenEnd(e, start);
154 
155             CharSequence sub = e.subSequence(start, end);
156             if (TextUtils.isEmpty(sub)) {
157                 e.replace(start, i, "");
158             } else if (!v.isValid(sub)) {
159                 e.replace(start, i,
160                           mTokenizer.terminateToken(v.fixText(sub)));
161             }
162 
163             i = start;
164         }
165     }
166 
167     /**
168      * <p>Starts filtering the content of the drop down list. The filtering
169      * pattern is the specified range of text from the edit box. Subclasses may
170      * override this method to filter with a different pattern, for
171      * instance a smaller substring of <code>text</code>.</p>
172      */
performFiltering(CharSequence text, int start, int end, int keyCode)173     protected void performFiltering(CharSequence text, int start, int end,
174                                     int keyCode) {
175         getFilter().filter(text.subSequence(start, end), this);
176     }
177 
178     /**
179      * <p>Performs the text completion by replacing the range from
180      * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the
181      * the result of passing <code>text</code> through
182      * {@link Tokenizer#terminateToken}.
183      * In addition, the replaced region will be marked as an AutoText
184      * substition so that if the user immediately presses DEL, the
185      * completion will be undone.
186      * Subclasses may override this method to do some different
187      * insertion of the content into the edit box.</p>
188      *
189      * @param text the selected suggestion in the drop down list
190      */
191     @Override
replaceText(CharSequence text)192     protected void replaceText(CharSequence text) {
193         clearComposingText();
194 
195         int end = getSelectionEnd();
196         int start = mTokenizer.findTokenStart(getText(), end);
197 
198         Editable editable = getText();
199         String original = TextUtils.substring(editable, start, end);
200 
201         QwertyKeyListener.markAsReplaced(editable, start, end, original);
202         editable.replace(start, end, mTokenizer.terminateToken(text));
203     }
204 
205     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)206     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
207         super.onInitializeAccessibilityEvent(event);
208         event.setClassName(MultiAutoCompleteTextView.class.getName());
209     }
210 
211     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)212     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
213         super.onInitializeAccessibilityNodeInfo(info);
214         info.setClassName(MultiAutoCompleteTextView.class.getName());
215     }
216 
217     public static interface Tokenizer {
218         /**
219          * Returns the start of the token that ends at offset
220          * <code>cursor</code> within <code>text</code>.
221          */
findTokenStart(CharSequence text, int cursor)222         public int findTokenStart(CharSequence text, int cursor);
223 
224         /**
225          * Returns the end of the token (minus trailing punctuation)
226          * that begins at offset <code>cursor</code> within <code>text</code>.
227          */
findTokenEnd(CharSequence text, int cursor)228         public int findTokenEnd(CharSequence text, int cursor);
229 
230         /**
231          * Returns <code>text</code>, modified, if necessary, to ensure that
232          * it ends with a token terminator (for example a space or comma).
233          */
terminateToken(CharSequence text)234         public CharSequence terminateToken(CharSequence text);
235     }
236 
237     /**
238      * This simple Tokenizer can be used for lists where the items are
239      * separated by a comma and one or more spaces.
240      */
241     public static class CommaTokenizer implements Tokenizer {
findTokenStart(CharSequence text, int cursor)242         public int findTokenStart(CharSequence text, int cursor) {
243             int i = cursor;
244 
245             while (i > 0 && text.charAt(i - 1) != ',') {
246                 i--;
247             }
248             while (i < cursor && text.charAt(i) == ' ') {
249                 i++;
250             }
251 
252             return i;
253         }
254 
findTokenEnd(CharSequence text, int cursor)255         public int findTokenEnd(CharSequence text, int cursor) {
256             int i = cursor;
257             int len = text.length();
258 
259             while (i < len) {
260                 if (text.charAt(i) == ',') {
261                     return i;
262                 } else {
263                     i++;
264                 }
265             }
266 
267             return len;
268         }
269 
terminateToken(CharSequence text)270         public CharSequence terminateToken(CharSequence text) {
271             int i = text.length();
272 
273             while (i > 0 && text.charAt(i - 1) == ' ') {
274                 i--;
275             }
276 
277             if (i > 0 && text.charAt(i - 1) == ',') {
278                 return text;
279             } else {
280                 if (text instanceof Spanned) {
281                     SpannableString sp = new SpannableString(text + ", ");
282                     TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
283                                             Object.class, sp, 0);
284                     return sp;
285                 } else {
286                     return text + ", ";
287                 }
288             }
289         }
290     }
291 }
292