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