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