MultiAutoCompleteTextView.java revision 9066cfe9886ac131c34d59ed0e2d287b0e3c0087
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
17package android.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Rect;
22import android.graphics.drawable.Drawable;
23import android.text.Editable;
24import android.text.Selection;
25import android.text.Spanned;
26import android.text.Spannable;
27import android.text.SpannableString;
28import android.text.TextUtils;
29import android.text.method.QwertyKeyListener;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.KeyEvent;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36
37import com.android.internal.R;
38
39/**
40 * An editable text view, extending {@link AutoCompleteTextView}, that
41 * can show completion suggestions for the substring of the text where
42 * the user is typing instead of necessarily for the entire thing.
43 * <p>
44 * You must must provide a {@link Tokenizer} to distinguish the
45 * various substrings.
46 *
47 * <p>The following code snippet shows how to create a text view which suggests
48 * various countries names while the user is typing:</p>
49 *
50 * <pre class="prettyprint">
51 * public class CountriesActivity extends Activity {
52 *     protected void onCreate(Bundle savedInstanceState) {
53 *         super.onCreate(savedInstanceState);
54 *         setContentView(R.layout.autocomplete_7);
55 *
56 *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
57 *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
58 *         MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit);
59 *         textView.setAdapter(adapter);
60 *         textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
61 *     }
62 *
63 *     private static final String[] COUNTRIES = new String[] {
64 *         "Belgium", "France", "Italy", "Germany", "Spain"
65 *     };
66 * }</pre>
67 */
68
69public class MultiAutoCompleteTextView extends AutoCompleteTextView {
70    private Tokenizer mTokenizer;
71
72    public MultiAutoCompleteTextView(Context context) {
73        this(context, null);
74    }
75
76    public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
77        this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
78    }
79
80    public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
81        super(context, attrs, defStyle);
82    }
83
84    /* package */ void finishInit() { }
85
86    /**
87     * Sets the Tokenizer that will be used to determine the relevant
88     * range of the text where the user is typing.
89     */
90    public void setTokenizer(Tokenizer t) {
91        mTokenizer = t;
92    }
93
94    /**
95     * Instead of filtering on the entire contents of the edit box,
96     * this subclass method filters on the range from
97     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
98     * if the length of that range meets or exceeds {@link #getThreshold}.
99     */
100    @Override
101    protected void performFiltering(CharSequence text, int keyCode) {
102        if (enoughToFilter()) {
103            int end = getSelectionEnd();
104            int start = mTokenizer.findTokenStart(text, end);
105
106            performFiltering(text, start, end, keyCode);
107        } else {
108            dismissDropDown();
109
110            Filter f = getFilter();
111            if (f != null) {
112                f.filter(null);
113            }
114        }
115    }
116
117    /**
118     * Instead of filtering whenever the total length of the text
119     * exceeds the threshhold, this subclass filters only when the
120     * length of the range from
121     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
122     * meets or exceeds {@link #getThreshold}.
123     */
124    @Override
125    public boolean enoughToFilter() {
126        Editable text = getText();
127
128        int end = getSelectionEnd();
129        if (end < 0 || mTokenizer == null) {
130            return false;
131        }
132
133        int start = mTokenizer.findTokenStart(text, end);
134
135        if (end - start >= getThreshold()) {
136            return true;
137        } else {
138            return false;
139        }
140    }
141
142    /**
143     * Instead of validating the entire text, this subclass method validates
144     * each token of the text individually.  Empty tokens are removed.
145     */
146    @Override
147    public void performValidation() {
148        Validator v = getValidator();
149
150        if (v == null || mTokenizer == null) {
151            return;
152        }
153
154        Editable e = getText();
155        int i = getText().length();
156        while (i > 0) {
157            int start = mTokenizer.findTokenStart(e, i);
158            int end = mTokenizer.findTokenEnd(e, start);
159
160            CharSequence sub = e.subSequence(start, end);
161            if (TextUtils.isEmpty(sub)) {
162                e.replace(start, i, "");
163            } else if (!v.isValid(sub)) {
164                e.replace(start, i,
165                          mTokenizer.terminateToken(v.fixText(sub)));
166            }
167
168            i = start;
169        }
170    }
171
172    /**
173     * <p>Starts filtering the content of the drop down list. The filtering
174     * pattern is the specified range of text from the edit box. Subclasses may
175     * override this method to filter with a different pattern, for
176     * instance a smaller substring of <code>text</code>.</p>
177     */
178    protected void performFiltering(CharSequence text, int start, int end,
179                                    int keyCode) {
180        getFilter().filter(text.subSequence(start, end), this);
181    }
182
183    /**
184     * <p>Performs the text completion by replacing the range from
185     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the
186     * the result of passing <code>text</code> through
187     * {@link Tokenizer#terminateToken}.
188     * In addition, the replaced region will be marked as an AutoText
189     * substition so that if the user immediately presses DEL, the
190     * completion will be undone.
191     * Subclasses may override this method to do some different
192     * insertion of the content into the edit box.</p>
193     *
194     * @param text the selected suggestion in the drop down list
195     */
196    @Override
197    protected void replaceText(CharSequence text) {
198        int end = getSelectionEnd();
199        int start = mTokenizer.findTokenStart(getText(), end);
200
201        Editable editable = getText();
202        String original = TextUtils.substring(editable, start, end);
203
204        QwertyKeyListener.markAsReplaced(editable, start, end, original);
205        editable.replace(start, end, mTokenizer.terminateToken(text));
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         */
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         */
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         */
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 {
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
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
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