blob: f348d73c1b42be064efe632fcab45dd91daff548 [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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026
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>
Daisuke Miyakawa979154f2011-05-10 17:18:55 -070032 * You must provide a {@link Tokenizer} to distinguish the
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033 * 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);
Daisuke Miyakawa979154f2011-05-10 17:18:55 -070043 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080044 * ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
45 * android.R.layout.simple_dropdown_item_1line, COUNTRIES);
Alan Viverette51efddb2017-04-05 10:00:01 -040046 * MultiAutoCompleteTextView textView = findViewById(R.id.edit);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047 * 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
57public class MultiAutoCompleteTextView extends AutoCompleteTextView {
58 private Tokenizer mTokenizer;
59
60 public MultiAutoCompleteTextView(Context context) {
61 this(context, null);
62 }
63
64 public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
65 this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
66 }
67
Alan Viverette617feb92013-09-09 18:09:13 -070068 public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
69 this(context, attrs, defStyleAttr, 0);
70 }
71
72 public MultiAutoCompleteTextView(
73 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
74 super(context, attrs, defStyleAttr, defStyleRes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080075 }
76
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 */
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
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
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 */
Daisuke Miyakawa979154f2011-05-10 17:18:55 -0700139 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800140 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 */
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
190 protected void replaceText(CharSequence text) {
Daisuke Miyakawac1d27482009-05-25 17:37:41 +0900191 clearComposingText();
192
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800193 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
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800203 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -0800204 public CharSequence getAccessibilityClassName() {
205 return MultiAutoCompleteTextView.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800206 }
207
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800208 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}