blob: cbd01b0ba9c5c9adda9cc00004c48df7817257bc [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080020import android.text.Editable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.text.SpannableString;
Gilles Debunnea4a57582011-02-08 18:21:01 -080022import android.text.Spanned;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.text.TextUtils;
24import android.text.method.QwertyKeyListener;
25import android.util.AttributeSet;
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -080026import android.view.accessibility.AccessibilityEvent;
27import android.view.accessibility.AccessibilityNodeInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028
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>
Daisuke Miyakawa979154f2011-05-10 17:18:55 -070034 * You must provide a {@link Tokenizer} to distinguish the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035 * 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);
Daisuke Miyakawa979154f2011-05-10 17:18:55 -070045 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046 * 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
59public 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
Alan Viverette617feb92013-09-09 18:09:13 -070070 public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
71 this(context, attrs, defStyleAttr, 0);
72 }
73
74 public MultiAutoCompleteTextView(
75 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
76 super(context, attrs, defStyleAttr, defStyleRes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080077 }
78
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 */
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
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
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 */
Daisuke Miyakawa979154f2011-05-10 17:18:55 -0700141 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800142 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 */
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
192 protected void replaceText(CharSequence text) {
Daisuke Miyakawac1d27482009-05-25 17:37:41 +0900193 clearComposingText();
194
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800195 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
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800205 @Override
206 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
207 super.onInitializeAccessibilityEvent(event);
208 event.setClassName(MultiAutoCompleteTextView.class.getName());
209 }
210
211 @Override
212 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
213 super.onInitializeAccessibilityNodeInfo(info);
214 info.setClassName(MultiAutoCompleteTextView.class.getName());
215 }
216
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800217 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 */
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 */
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 */
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 {
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
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
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}