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