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.text.Editable; 21import android.text.SpannableString; 22import android.text.Spanned; 23import android.text.TextUtils; 24import android.text.method.QwertyKeyListener; 25import android.util.AttributeSet; 26 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> 32 * You must provide a {@link Tokenizer} to distinguish the 33 * 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); 43 * 44 * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, 45 * android.R.layout.simple_dropdown_item_1line, COUNTRIES); 46 * MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit); 47 * 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 68 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); 75 } 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 */ 139 @Override 140 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) { 191 clearComposingText(); 192 193 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 203 @Override 204 public CharSequence getAccessibilityClassName() { 205 return MultiAutoCompleteTextView.class.getName(); 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