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.view.accessibility.AccessibilityEvent; 27 import android.view.accessibility.AccessibilityNodeInfo; 28 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> 34 * You must provide a {@link Tokenizer} to distinguish the 35 * 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); 45 * 46 * ArrayAdapter<String> adapter = new ArrayAdapter<String>(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 59 public 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 70 public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { 71 super(context, attrs, defStyle); 72 } 73 74 /* package */ void finishInit() { } 75 76 /** 77 * Sets the Tokenizer that will be used to determine the relevant 78 * range of the text where the user is typing. 79 */ 80 public void setTokenizer(Tokenizer t) { 81 mTokenizer = t; 82 } 83 84 /** 85 * Instead of filtering on the entire contents of the edit box, 86 * this subclass method filters on the range from 87 * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} 88 * if the length of that range meets or exceeds {@link #getThreshold}. 89 */ 90 @Override 91 protected void performFiltering(CharSequence text, int keyCode) { 92 if (enoughToFilter()) { 93 int end = getSelectionEnd(); 94 int start = mTokenizer.findTokenStart(text, end); 95 96 performFiltering(text, start, end, keyCode); 97 } else { 98 dismissDropDown(); 99 100 Filter f = getFilter(); 101 if (f != null) { 102 f.filter(null); 103 } 104 } 105 } 106 107 /** 108 * Instead of filtering whenever the total length of the text 109 * exceeds the threshhold, this subclass filters only when the 110 * length of the range from 111 * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} 112 * meets or exceeds {@link #getThreshold}. 113 */ 114 @Override 115 public boolean enoughToFilter() { 116 Editable text = getText(); 117 118 int end = getSelectionEnd(); 119 if (end < 0 || mTokenizer == null) { 120 return false; 121 } 122 123 int start = mTokenizer.findTokenStart(text, end); 124 125 if (end - start >= getThreshold()) { 126 return true; 127 } else { 128 return false; 129 } 130 } 131 132 /** 133 * Instead of validating the entire text, this subclass method validates 134 * each token of the text individually. Empty tokens are removed. 135 */ 136 @Override 137 public void performValidation() { 138 Validator v = getValidator(); 139 140 if (v == null || mTokenizer == null) { 141 return; 142 } 143 144 Editable e = getText(); 145 int i = getText().length(); 146 while (i > 0) { 147 int start = mTokenizer.findTokenStart(e, i); 148 int end = mTokenizer.findTokenEnd(e, start); 149 150 CharSequence sub = e.subSequence(start, end); 151 if (TextUtils.isEmpty(sub)) { 152 e.replace(start, i, ""); 153 } else if (!v.isValid(sub)) { 154 e.replace(start, i, 155 mTokenizer.terminateToken(v.fixText(sub))); 156 } 157 158 i = start; 159 } 160 } 161 162 /** 163 * <p>Starts filtering the content of the drop down list. The filtering 164 * pattern is the specified range of text from the edit box. Subclasses may 165 * override this method to filter with a different pattern, for 166 * instance a smaller substring of <code>text</code>.</p> 167 */ 168 protected void performFiltering(CharSequence text, int start, int end, 169 int keyCode) { 170 getFilter().filter(text.subSequence(start, end), this); 171 } 172 173 /** 174 * <p>Performs the text completion by replacing the range from 175 * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the 176 * the result of passing <code>text</code> through 177 * {@link Tokenizer#terminateToken}. 178 * In addition, the replaced region will be marked as an AutoText 179 * substition so that if the user immediately presses DEL, the 180 * completion will be undone. 181 * Subclasses may override this method to do some different 182 * insertion of the content into the edit box.</p> 183 * 184 * @param text the selected suggestion in the drop down list 185 */ 186 @Override 187 protected void replaceText(CharSequence text) { 188 clearComposingText(); 189 190 int end = getSelectionEnd(); 191 int start = mTokenizer.findTokenStart(getText(), end); 192 193 Editable editable = getText(); 194 String original = TextUtils.substring(editable, start, end); 195 196 QwertyKeyListener.markAsReplaced(editable, start, end, original); 197 editable.replace(start, end, mTokenizer.terminateToken(text)); 198 } 199 200 @Override 201 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 202 super.onInitializeAccessibilityEvent(event); 203 event.setClassName(MultiAutoCompleteTextView.class.getName()); 204 } 205 206 @Override 207 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 208 super.onInitializeAccessibilityNodeInfo(info); 209 info.setClassName(MultiAutoCompleteTextView.class.getName()); 210 } 211 212 public static interface Tokenizer { 213 /** 214 * Returns the start of the token that ends at offset 215 * <code>cursor</code> within <code>text</code>. 216 */ 217 public int findTokenStart(CharSequence text, int cursor); 218 219 /** 220 * Returns the end of the token (minus trailing punctuation) 221 * that begins at offset <code>cursor</code> within <code>text</code>. 222 */ 223 public int findTokenEnd(CharSequence text, int cursor); 224 225 /** 226 * Returns <code>text</code>, modified, if necessary, to ensure that 227 * it ends with a token terminator (for example a space or comma). 228 */ 229 public CharSequence terminateToken(CharSequence text); 230 } 231 232 /** 233 * This simple Tokenizer can be used for lists where the items are 234 * separated by a comma and one or more spaces. 235 */ 236 public static class CommaTokenizer implements Tokenizer { 237 public int findTokenStart(CharSequence text, int cursor) { 238 int i = cursor; 239 240 while (i > 0 && text.charAt(i - 1) != ',') { 241 i--; 242 } 243 while (i < cursor && text.charAt(i) == ' ') { 244 i++; 245 } 246 247 return i; 248 } 249 250 public int findTokenEnd(CharSequence text, int cursor) { 251 int i = cursor; 252 int len = text.length(); 253 254 while (i < len) { 255 if (text.charAt(i) == ',') { 256 return i; 257 } else { 258 i++; 259 } 260 } 261 262 return len; 263 } 264 265 public CharSequence terminateToken(CharSequence text) { 266 int i = text.length(); 267 268 while (i > 0 && text.charAt(i - 1) == ' ') { 269 i--; 270 } 271 272 if (i > 0 && text.charAt(i - 1) == ',') { 273 return text; 274 } else { 275 if (text instanceof Spanned) { 276 SpannableString sp = new SpannableString(text + ", "); 277 TextUtils.copySpansFrom((Spanned) text, 0, text.length(), 278 Object.class, sp, 0); 279 return sp; 280 } else { 281 return text + ", "; 282 } 283 } 284 } 285 } 286 } 287