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