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 import android.widget.MultiAutoCompleteTextView.Tokenizer; 27 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 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<String> adapter = new ArrayAdapter<String>(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 58 public 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) { 187 clearComposingText(); 188 189 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 } 274