AutoCompleteTextView.java revision f0bc7ecebf8c30732f6de109b9e04dab253c3d08
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.content.res.TypedArray; 21import android.graphics.Rect; 22import android.graphics.drawable.Drawable; 23import android.text.Editable; 24import android.text.Selection; 25import android.text.TextUtils; 26import android.text.TextWatcher; 27import android.util.AttributeSet; 28import android.util.Log; 29import android.view.KeyEvent; 30import android.view.LayoutInflater; 31import android.view.MotionEvent; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.inputmethod.CompletionInfo; 35import android.view.inputmethod.InputMethodManager; 36import android.view.inputmethod.EditorInfo; 37 38import com.android.internal.R; 39 40 41/** 42 * <p>An editable text view that shows completion suggestions automatically 43 * while the user is typing. The list of suggestions is displayed in a drop 44 * down menu from which the user can choose an item to replace the content 45 * of the edit box with.</p> 46 * 47 * <p>The drop down can be dismissed at any time by pressing the back key or, 48 * if no item is selected in the drop down, by pressing the enter/dpad center 49 * key.</p> 50 * 51 * <p>The list of suggestions is obtained from a data adapter and appears 52 * only after a given number of characters defined by 53 * {@link #getThreshold() the threshold}.</p> 54 * 55 * <p>The following code snippet shows how to create a text view which suggests 56 * various countries names while the user is typing:</p> 57 * 58 * <pre class="prettyprint"> 59 * public class CountriesActivity extends Activity { 60 * protected void onCreate(Bundle icicle) { 61 * super.onCreate(icicle); 62 * setContentView(R.layout.countries); 63 * 64 * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, 65 * android.R.layout.simple_dropdown_item_1line, COUNTRIES); 66 * AutoCompleteTextView textView = (AutoCompleteTextView) 67 * findViewById(R.id.countries_list); 68 * textView.setAdapter(adapter); 69 * } 70 * 71 * private static final String[] COUNTRIES = new String[] { 72 * "Belgium", "France", "Italy", "Germany", "Spain" 73 * }; 74 * } 75 * </pre> 76 * 77 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 78 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold 79 * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView 80 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector 81 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 82 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 83 */ 84public class AutoCompleteTextView extends EditText implements Filter.FilterListener { 85 static final boolean DEBUG = false; 86 static final String TAG = "AutoCompleteTextView"; 87 88 private static final int HINT_VIEW_ID = 0x17; 89 90 private CharSequence mHintText; 91 private int mHintResource; 92 93 private ListAdapter mAdapter; 94 private Filter mFilter; 95 private int mThreshold; 96 97 private PopupWindow mPopup; 98 private DropDownListView mDropDownList; 99 private int mDropDownVerticalOffset; 100 private int mDropDownHorizontalOffset; 101 private int mDropDownAnchorId; 102 private View mDropDownAnchorView; // view is retrieved lazily from id once needed 103 private int mDropDownWidth; 104 105 private Drawable mDropDownListHighlight; 106 107 private AdapterView.OnItemClickListener mItemClickListener; 108 private AdapterView.OnItemSelectedListener mItemSelectedListener; 109 110 private final DropDownItemClickListener mDropDownItemClickListener = 111 new DropDownItemClickListener(); 112 113 private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 114 private boolean mOpenBefore; 115 116 private Validator mValidator = null; 117 118 private boolean mBlockCompletion; 119 120 private AutoCompleteTextView.ListSelectorHider mHideSelector; 121 122 // Indicates whether this AutoCompleteTextView is attached to a window or not 123 // The widget is attached to a window when mAttachCount > 0 124 private int mAttachCount; 125 126 public AutoCompleteTextView(Context context) { 127 this(context, null); 128 } 129 130 public AutoCompleteTextView(Context context, AttributeSet attrs) { 131 this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); 132 } 133 134 public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { 135 super(context, attrs, defStyle); 136 137 mPopup = new PopupWindow(context, attrs, 138 com.android.internal.R.attr.autoCompleteTextViewStyle); 139 140 TypedArray a = 141 context.obtainStyledAttributes( 142 attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0); 143 144 mThreshold = a.getInt( 145 R.styleable.AutoCompleteTextView_completionThreshold, 2); 146 147 mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint); 148 149 mDropDownListHighlight = a.getDrawable( 150 R.styleable.AutoCompleteTextView_dropDownSelector); 151 mDropDownVerticalOffset = (int) 152 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f); 153 mDropDownHorizontalOffset = (int) 154 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f); 155 156 // Get the anchor's id now, but the view won't be ready, so wait to actually get the 157 // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. 158 // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return 159 // this TextView, as a default anchoring point. 160 mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor, 161 View.NO_ID); 162 163 // For dropdown width, the developer can specify a specific width, or FILL_PARENT 164 // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). 165 mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, 166 ViewGroup.LayoutParams.WRAP_CONTENT); 167 168 mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, 169 R.layout.simple_dropdown_hint); 170 171 // Always turn on the auto complete input type flag, since it 172 // makes no sense to use this widget without it. 173 int inputType = getInputType(); 174 if ((inputType&EditorInfo.TYPE_MASK_CLASS) 175 == EditorInfo.TYPE_CLASS_TEXT) { 176 inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE; 177 setRawInputType(inputType); 178 } 179 180 a.recycle(); 181 182 setFocusable(true); 183 184 addTextChangedListener(new MyWatcher()); 185 } 186 187 /** 188 * Sets this to be single line; a separate method so 189 * MultiAutoCompleteTextView can skip this. 190 */ 191 /* package */ void finishInit() { 192 setSingleLine(); 193 } 194 195 /** 196 * <p>Sets the optional hint text that is displayed at the bottom of the 197 * the matching list. This can be used as a cue to the user on how to 198 * best use the list, or to provide extra information.</p> 199 * 200 * @param hint the text to be displayed to the user 201 * 202 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 203 */ 204 public void setCompletionHint(CharSequence hint) { 205 mHintText = hint; 206 } 207 208 /** 209 * <p>Returns the current width for the auto-complete drop down list. This can 210 * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or 211 * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> 212 * 213 * @return the width for the drop down list 214 * 215 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 216 */ 217 public int getDropDownWidth() { 218 return mDropDownWidth; 219 } 220 221 /** 222 * <p>Sets the current width for the auto-complete drop down list. This can 223 * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or 224 * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> 225 * 226 * @param width the width to use 227 * 228 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 229 */ 230 public void setDropDownWidth(int width) { 231 mDropDownWidth = width; 232 } 233 234 /** 235 * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p> 236 * 237 * @return the view's id, or {@link View#NO_ID} if none specified 238 * 239 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 240 */ 241 public int getDropDownAnchor() { 242 return mDropDownAnchorId; 243 } 244 245 /** 246 * <p>Sets the view to which the auto-complete drop down list should anchor. The view 247 * corresponding to this id will not be loaded until the next time it is needed to avoid 248 * loading a view which is not yet instantiated.</p> 249 * 250 * @param id the id to anchor the drop down list view to 251 * 252 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 253 */ 254 public void setDropDownAnchor(int id) { 255 mDropDownAnchorId = id; 256 mDropDownAnchorView = null; 257 } 258 259 /** 260 * <p>Gets the background of the auto-complete drop-down list.</p> 261 * 262 * @return the background drawable 263 * 264 * @attr ref android.R.styleable#PopupWindow_popupBackground 265 */ 266 public Drawable getDropDownBackground() { 267 return mPopup.getBackground(); 268 } 269 270 /** 271 * <p>Sets the background of the auto-complete drop-down list.</p> 272 * 273 * @param d the drawable to set as the background 274 * 275 * @attr ref android.R.styleable#PopupWindow_popupBackground 276 */ 277 public void setDropDownBackgroundDrawable(Drawable d) { 278 mPopup.setBackgroundDrawable(d); 279 } 280 281 /** 282 * <p>Sets the background of the auto-complete drop-down list.</p> 283 * 284 * @param id the id of the drawable to set as the background 285 * 286 * @attr ref android.R.styleable#PopupWindow_popupBackground 287 */ 288 public void setDropDownBackgroundResource(int id) { 289 mPopup.setBackgroundDrawable(getResources().getDrawable(id)); 290 } 291 292 /** 293 * <p>Sets the vertical offset used for the auto-complete drop-down list.</p> 294 * 295 * @param offset the vertical offset 296 */ 297 public void setDropDownVerticalOffset(int offset) { 298 mDropDownVerticalOffset = offset; 299 } 300 301 /** 302 * <p>Gets the vertical offset used for the auto-complete drop-down list.</p> 303 * 304 * @return the vertical offset 305 */ 306 public int getDropDownVerticalOffset() { 307 return mDropDownVerticalOffset; 308 } 309 310 /** 311 * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p> 312 * 313 * @param offset the horizontal offset 314 */ 315 public void setDropDownHorizontalOffset(int offset) { 316 mDropDownHorizontalOffset = offset; 317 } 318 319 /** 320 * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p> 321 * 322 * @return the horizontal offset 323 */ 324 public int getDropDownHorizontalOffset() { 325 return mDropDownHorizontalOffset; 326 } 327 328 /** 329 * <p>Returns the number of characters the user must type before the drop 330 * down list is shown.</p> 331 * 332 * @return the minimum number of characters to type to show the drop down 333 * 334 * @see #setThreshold(int) 335 */ 336 public int getThreshold() { 337 return mThreshold; 338 } 339 340 /** 341 * <p>Specifies the minimum number of characters the user has to type in the 342 * edit box before the drop down list is shown.</p> 343 * 344 * <p>When <code>threshold</code> is less than or equals 0, a threshold of 345 * 1 is applied.</p> 346 * 347 * @param threshold the number of characters to type before the drop down 348 * is shown 349 * 350 * @see #getThreshold() 351 * 352 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold 353 */ 354 public void setThreshold(int threshold) { 355 if (threshold <= 0) { 356 threshold = 1; 357 } 358 359 mThreshold = threshold; 360 } 361 362 /** 363 * <p>Sets the listener that will be notified when the user clicks an item 364 * in the drop down list.</p> 365 * 366 * @param l the item click listener 367 */ 368 public void setOnItemClickListener(AdapterView.OnItemClickListener l) { 369 mItemClickListener = l; 370 } 371 372 /** 373 * <p>Sets the listener that will be notified when the user selects an item 374 * in the drop down list.</p> 375 * 376 * @param l the item selected listener 377 */ 378 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) { 379 mItemSelectedListener = l; 380 } 381 382 /** 383 * <p>Returns the listener that is notified whenever the user clicks an item 384 * in the drop down list.</p> 385 * 386 * @return the item click listener 387 * 388 * @deprecated Use {@link #getOnItemClickListener()} intead 389 */ 390 @Deprecated 391 public AdapterView.OnItemClickListener getItemClickListener() { 392 return mItemClickListener; 393 } 394 395 /** 396 * <p>Returns the listener that is notified whenever the user selects an 397 * item in the drop down list.</p> 398 * 399 * @return the item selected listener 400 * 401 * @deprecated Use {@link #getOnItemSelectedListener()} intead 402 */ 403 @Deprecated 404 public AdapterView.OnItemSelectedListener getItemSelectedListener() { 405 return mItemSelectedListener; 406 } 407 408 /** 409 * <p>Returns the listener that is notified whenever the user clicks an item 410 * in the drop down list.</p> 411 * 412 * @return the item click listener 413 */ 414 public AdapterView.OnItemClickListener getOnItemClickListener() { 415 return mItemClickListener; 416 } 417 418 /** 419 * <p>Returns the listener that is notified whenever the user selects an 420 * item in the drop down list.</p> 421 * 422 * @return the item selected listener 423 */ 424 public AdapterView.OnItemSelectedListener getOnItemSelectedListener() { 425 return mItemSelectedListener; 426 } 427 428 /** 429 * <p>Returns a filterable list adapter used for auto completion.</p> 430 * 431 * @return a data adapter used for auto completion 432 */ 433 public ListAdapter getAdapter() { 434 return mAdapter; 435 } 436 437 /** 438 * <p>Changes the list of data used for auto completion. The provided list 439 * must be a filterable list adapter.</p> 440 * 441 * <p>The caller is still responsible for managing any resources used by the adapter. 442 * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified. 443 * A common case is the use of {@link android.widget.CursorAdapter}, which 444 * contains a {@link android.database.Cursor} that must be closed. This can be done 445 * automatically (see 446 * {@link android.app.Activity#startManagingCursor(android.database.Cursor) 447 * startManagingCursor()}), 448 * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p> 449 * 450 * @param adapter the adapter holding the auto completion data 451 * 452 * @see #getAdapter() 453 * @see android.widget.Filterable 454 * @see android.widget.ListAdapter 455 */ 456 public <T extends ListAdapter & Filterable> void setAdapter(T adapter) { 457 mAdapter = adapter; 458 if (mAdapter != null) { 459 //noinspection unchecked 460 mFilter = ((Filterable) mAdapter).getFilter(); 461 } else { 462 mFilter = null; 463 } 464 465 if (mDropDownList != null) { 466 mDropDownList.setAdapter(mAdapter); 467 } 468 } 469 470 @Override 471 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 472 if (isPopupShowing()) { 473 // special case for the back key, we do not even try to send it 474 // to the drop down list but instead, consume it immediately 475 if (keyCode == KeyEvent.KEYCODE_BACK) { 476 dismissDropDown(); 477 return true; 478 } 479 } 480 return super.onKeyPreIme(keyCode, event); 481 } 482 483 @Override 484 public boolean onKeyUp(int keyCode, KeyEvent event) { 485 if (isPopupShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 486 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 487 if (consumed) { 488 switch (keyCode) { 489 // if the list accepts the key events and the key event 490 // was a click, the text view gets the selected item 491 // from the drop down as its content 492 case KeyEvent.KEYCODE_ENTER: 493 case KeyEvent.KEYCODE_DPAD_CENTER: 494 performCompletion(); 495 return true; 496 } 497 } 498 } 499 return super.onKeyUp(keyCode, event); 500 } 501 502 @Override 503 public boolean onKeyDown(int keyCode, KeyEvent event) { 504 // when the drop down is shown, we drive it directly 505 if (isPopupShowing()) { 506 // the key events are forwarded to the list in the drop down view 507 // note that ListView handles space but we don't want that to happen 508 // also if selection is not currently in the drop down, then don't 509 // let center or enter presses go there since that would cause it 510 // to select one of its items 511 if (keyCode != KeyEvent.KEYCODE_SPACE 512 && (mDropDownList.getSelectedItemPosition() >= 0 513 || (keyCode != KeyEvent.KEYCODE_ENTER 514 && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { 515 int curIndex = mDropDownList.getSelectedItemPosition(); 516 boolean consumed; 517 final boolean below = !mPopup.isAboveAnchor(); 518 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= 0) || 519 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= 520 mDropDownList.getAdapter().getCount() - 1)) { 521 // When the selection is at the top, we block the key 522 // event to prevent focus from moving. 523 mDropDownList.hideSelector(); 524 mDropDownList.requestLayout(); 525 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 526 mPopup.update(); 527 return true; 528 } 529 consumed = mDropDownList.onKeyDown(keyCode, event); 530 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" 531 + consumed); 532 if (consumed) { 533 // If it handled the key event, then the user is 534 // navigating in the list, so we should put it in front. 535 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 536 // Here's a little trick we need to do to make sure that 537 // the list view is actually showing its focus indicator, 538 // by ensuring it has focus and getting its window out 539 // of touch mode. 540 mDropDownList.requestFocusFromTouch(); 541 mPopup.update(); 542 543 switch (keyCode) { 544 // avoid passing the focus from the text view to the 545 // next component 546 case KeyEvent.KEYCODE_ENTER: 547 case KeyEvent.KEYCODE_DPAD_CENTER: 548 case KeyEvent.KEYCODE_DPAD_DOWN: 549 case KeyEvent.KEYCODE_DPAD_UP: 550 return true; 551 } 552 } else { 553 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 554 // when the selection is at the bottom, we block the 555 // event to avoid going to the next focusable widget 556 Adapter adapter = mDropDownList.getAdapter(); 557 if (adapter != null && curIndex == adapter.getCount() - 1) { 558 return true; 559 } 560 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex == 0) { 561 return true; 562 } 563 } 564 } 565 } else { 566 switch(keyCode) { 567 case KeyEvent.KEYCODE_DPAD_DOWN: 568 performValidation(); 569 } 570 } 571 572 mLastKeyCode = keyCode; 573 boolean handled = super.onKeyDown(keyCode, event); 574 mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 575 576 if (handled && isPopupShowing() && mDropDownList != null) { 577 clearListSelection(); 578 } 579 580 return handled; 581 } 582 583 /** 584 * Returns <code>true</code> if the amount of text in the field meets 585 * or exceeds the {@link #getThreshold} requirement. You can override 586 * this to impose a different standard for when filtering will be 587 * triggered. 588 */ 589 public boolean enoughToFilter() { 590 if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length() 591 + " threshold=" + mThreshold); 592 return getText().length() >= mThreshold; 593 } 594 595 /** 596 * This is used to watch for edits to the text view. Note that we call 597 * to methods on the auto complete text view class so that we can access 598 * private vars without going through thunks. 599 */ 600 private class MyWatcher implements TextWatcher { 601 public void afterTextChanged(Editable s) { 602 doAfterTextChanged(); 603 } 604 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 605 doBeforeTextChanged(); 606 } 607 public void onTextChanged(CharSequence s, int start, int before, int count) { 608 } 609 } 610 611 void doBeforeTextChanged() { 612 if (mBlockCompletion) return; 613 614 // when text is changed, inserted or deleted, we attempt to show 615 // the drop down 616 mOpenBefore = isPopupShowing(); 617 if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore); 618 } 619 620 void doAfterTextChanged() { 621 if (mBlockCompletion) return; 622 623 // if the list was open before the keystroke, but closed afterwards, 624 // then something in the keystroke processing (an input filter perhaps) 625 // called performCompletion() and we shouldn't do any more processing. 626 if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore 627 + " open=" + isPopupShowing()); 628 if (mOpenBefore && !isPopupShowing()) { 629 return; 630 } 631 632 // the drop down is shown only when a minimum number of characters 633 // was typed in the text view 634 if (enoughToFilter()) { 635 if (mFilter != null) { 636 performFiltering(getText(), mLastKeyCode); 637 } 638 } else { 639 // drop down is automatically dismissed when enough characters 640 // are deleted from the text view 641 dismissDropDown(); 642 if (mFilter != null) { 643 mFilter.filter(null); 644 } 645 } 646 } 647 648 /** 649 * <p>Indicates whether the popup menu is showing.</p> 650 * 651 * @return true if the popup menu is showing, false otherwise 652 */ 653 public boolean isPopupShowing() { 654 return mPopup.isShowing(); 655 } 656 657 /** 658 * <p>Converts the selected item from the drop down list into a sequence 659 * of character that can be used in the edit box.</p> 660 * 661 * @param selectedItem the item selected by the user for completion 662 * 663 * @return a sequence of characters representing the selected suggestion 664 */ 665 protected CharSequence convertSelectionToString(Object selectedItem) { 666 return mFilter.convertResultToString(selectedItem); 667 } 668 669 /** 670 * <p>Clear the list selection. This may only be temporary, as user input will often bring 671 * it back. 672 */ 673 public void clearListSelection() { 674 if (mDropDownList != null) { 675 mDropDownList.hideSelector(); 676 mDropDownList.requestLayout(); 677 } 678 } 679 680 /** 681 * Set the position of the dropdown view selection. 682 * 683 * @param position The position to move the selector to. 684 */ 685 public void setListSelection(int position) { 686 if (mPopup.isShowing() && (mDropDownList != null)) { 687 mDropDownList.setSelection(position); 688 // ListView.setSelection() will call requestLayout() 689 } 690 } 691 692 /** 693 * Get the position of the dropdown view selection, if there is one. Returns 694 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if 695 * there is no selection. 696 * 697 * @return the position of the current selection, if there is one, or 698 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not. 699 * 700 * @see ListView#getSelectedItemPosition() 701 */ 702 public int getListSelection() { 703 if (mPopup.isShowing() && (mDropDownList != null)) { 704 return mDropDownList.getSelectedItemPosition(); 705 } 706 return ListView.INVALID_POSITION; 707 } 708 709 /** 710 * We're changing the adapter and its views so really, really clear everything out 711 * @hide - for SearchDialog only 712 */ 713 public void resetListAndClearViews() { 714 if (mDropDownList != null) { 715 mDropDownList.resetListAndClearViews(); 716 } 717 } 718 719 /** 720 * <p>Starts filtering the content of the drop down list. The filtering 721 * pattern is the content of the edit box. Subclasses should override this 722 * method to filter with a different pattern, for instance a substring of 723 * <code>text</code>.</p> 724 * 725 * @param text the filtering pattern 726 * @param keyCode the last character inserted in the edit box; beware that 727 * this will be null when text is being added through a soft input method. 728 */ 729 @SuppressWarnings({ "UnusedDeclaration" }) 730 protected void performFiltering(CharSequence text, int keyCode) { 731 mFilter.filter(text, this); 732 } 733 734 /** 735 * <p>Performs the text completion by converting the selected item from 736 * the drop down list into a string, replacing the text box's content with 737 * this string and finally dismissing the drop down menu.</p> 738 */ 739 public void performCompletion() { 740 performCompletion(null, -1, -1); 741 } 742 743 @Override 744 public void onCommitCompletion(CompletionInfo completion) { 745 if (isPopupShowing()) { 746 mBlockCompletion = true; 747 replaceText(completion.getText()); 748 mBlockCompletion = false; 749 750 if (mItemClickListener != null) { 751 final DropDownListView list = mDropDownList; 752 // Note that we don't have a View here, so we will need to 753 // supply null. Hopefully no existing apps crash... 754 mItemClickListener.onItemClick(list, null, completion.getPosition(), 755 completion.getId()); 756 } 757 } 758 } 759 760 private void performCompletion(View selectedView, int position, long id) { 761 if (isPopupShowing()) { 762 Object selectedItem; 763 if (position < 0) { 764 selectedItem = mDropDownList.getSelectedItem(); 765 } else { 766 selectedItem = mAdapter.getItem(position); 767 } 768 if (selectedItem == null) { 769 Log.w(TAG, "performCompletion: no selected item"); 770 return; 771 } 772 773 mBlockCompletion = true; 774 replaceText(convertSelectionToString(selectedItem)); 775 mBlockCompletion = false; 776 777 if (mItemClickListener != null) { 778 final DropDownListView list = mDropDownList; 779 780 if (selectedView == null || position < 0) { 781 selectedView = list.getSelectedView(); 782 position = list.getSelectedItemPosition(); 783 id = list.getSelectedItemId(); 784 } 785 mItemClickListener.onItemClick(list, selectedView, position, id); 786 } 787 } 788 789 dismissDropDown(); 790 } 791 792 /** 793 * Identifies whether the view is currently performing a text completion, so subclasses 794 * can decide whether to respond to text changed events. 795 */ 796 public boolean isPerformingCompletion() { 797 return mBlockCompletion; 798 } 799 800 /** 801 * <p>Performs the text completion by replacing the current text by the 802 * selected item. Subclasses should override this method to avoid replacing 803 * the whole content of the edit box.</p> 804 * 805 * @param text the selected suggestion in the drop down list 806 */ 807 protected void replaceText(CharSequence text) { 808 setText(text); 809 // make sure we keep the caret at the end of the text view 810 Editable spannable = getText(); 811 Selection.setSelection(spannable, spannable.length()); 812 } 813 814 public void onFilterComplete(int count) { 815 if (mAttachCount <= 0) return; 816 817 /* 818 * This checks enoughToFilter() again because filtering requests 819 * are asynchronous, so the result may come back after enough text 820 * has since been deleted to make it no longer appropriate 821 * to filter. 822 */ 823 824 if (count > 0 && enoughToFilter()) { 825 if (hasFocus() && hasWindowFocus()) { 826 showDropDown(); 827 } 828 } else { 829 dismissDropDown(); 830 } 831 } 832 833 @Override 834 public void onWindowFocusChanged(boolean hasWindowFocus) { 835 super.onWindowFocusChanged(hasWindowFocus); 836 performValidation(); 837 if (!hasWindowFocus) { 838 dismissDropDown(); 839 } 840 } 841 842 @Override 843 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 844 super.onFocusChanged(focused, direction, previouslyFocusedRect); 845 performValidation(); 846 if (!focused) { 847 dismissDropDown(); 848 } 849 } 850 851 @Override 852 protected void onAttachedToWindow() { 853 super.onAttachedToWindow(); 854 mAttachCount++; 855 } 856 857 @Override 858 protected void onDetachedFromWindow() { 859 dismissDropDown(); 860 mAttachCount--; 861 super.onDetachedFromWindow(); 862 } 863 864 /** 865 * <p>Closes the drop down if present on screen.</p> 866 */ 867 public void dismissDropDown() { 868 InputMethodManager imm = InputMethodManager.peekInstance(); 869 if (imm != null) { 870 imm.displayCompletions(this, null); 871 } 872 mPopup.dismiss(); 873 mPopup.setContentView(null); 874 mDropDownList = null; 875 } 876 877 @Override 878 protected boolean setFrame(int l, int t, int r, int b) { 879 boolean result = super.setFrame(l, t, r, b); 880 881 if (mPopup.isShowing()) { 882 mPopup.update(this, r - l, -1); 883 } 884 885 return result; 886 } 887 888 /** 889 * Set the horizontal offset with respect to {@link #setDropDownAnchor(int)} 890 * @hide pending API council review 891 */ 892 public void setDropDownHorizontalOffset(int horizontalOffset) { 893 mDropDownHorizontalOffset = horizontalOffset; 894 } 895 896 /** 897 * Set the vertical offset with respect to {@link #setDropDownAnchor(int)} 898 * @hide pending API council review 899 */ 900 public void setDropDownVerticalOffset(int verticalOffset) { 901 mDropDownVerticalOffset = verticalOffset; 902 } 903 904 /** 905 * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of 906 * the id is NO_ID or we can't find a view for the given id, we return this TextView as 907 * the default anchoring point.</p> 908 */ 909 private View getDropDownAnchorView() { 910 if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) { 911 mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId); 912 } 913 return mDropDownAnchorView == null ? this : mDropDownAnchorView; 914 } 915 916 /** 917 * <p>Displays the drop down on screen.</p> 918 */ 919 public void showDropDown() { 920 int height = buildDropDown(); 921 if (mPopup.isShowing()) { 922 int widthSpec; 923 if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { 924 // The call to PopupWindow's update method below can accept -1 for any 925 // value you do not want to update. 926 widthSpec = -1; 927 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 928 widthSpec = getDropDownAnchorView().getWidth(); 929 } else { 930 widthSpec = mDropDownWidth; 931 } 932 mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, 933 mDropDownVerticalOffset, widthSpec, height); 934 } else { 935 if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { 936 mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT, 937 ViewGroup.LayoutParams.WRAP_CONTENT); 938 } else { 939 mPopup.setWindowLayoutMode(0, ViewGroup.LayoutParams.WRAP_CONTENT); 940 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 941 mPopup.setWidth(getDropDownAnchorView().getWidth()); 942 } else { 943 mPopup.setWidth(mDropDownWidth); 944 } 945 } 946 mPopup.setHeight(height); 947 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 948 mPopup.setOutsideTouchable(true); 949 mPopup.setTouchInterceptor(new PopupTouchIntercepter()); 950 mPopup.showAsDropDown(getDropDownAnchorView(), 951 mDropDownHorizontalOffset, mDropDownVerticalOffset); 952 mDropDownList.setSelection(ListView.INVALID_POSITION); 953 mDropDownList.hideSelector(); 954 mDropDownList.requestFocus(); 955 post(mHideSelector); 956 } 957 } 958 959 /** 960 * <p>Builds the popup window's content and returns the height the popup 961 * should have. Returns -1 when the content already exists.</p> 962 * 963 * @return the content's height or -1 if content already exists 964 */ 965 private int buildDropDown() { 966 ViewGroup dropDownView; 967 int otherHeights = 0; 968 969 if (mAdapter != null) { 970 InputMethodManager imm = InputMethodManager.peekInstance(); 971 if (imm != null) { 972 int N = mAdapter.getCount(); 973 if (N > 20) N = 20; 974 CompletionInfo[] completions = new CompletionInfo[N]; 975 for (int i = 0; i < N; i++) { 976 Object item = mAdapter.getItem(i); 977 long id = mAdapter.getItemId(i); 978 completions[i] = new CompletionInfo(id, i, 979 convertSelectionToString(item)); 980 } 981 imm.displayCompletions(this, completions); 982 } 983 } 984 985 if (mDropDownList == null) { 986 Context context = getContext(); 987 988 mHideSelector = new ListSelectorHider(); 989 990 mDropDownList = new DropDownListView(context); 991 mDropDownList.setSelector(mDropDownListHighlight); 992 mDropDownList.setAdapter(mAdapter); 993 mDropDownList.setVerticalFadingEdgeEnabled(true); 994 mDropDownList.setOnItemClickListener(mDropDownItemClickListener); 995 mDropDownList.setFocusable(true); 996 mDropDownList.setFocusableInTouchMode(true); 997 998 if (mItemSelectedListener != null) { 999 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1000 } 1001 1002 dropDownView = mDropDownList; 1003 1004 View hintView = getHintView(context); 1005 if (hintView != null) { 1006 // if an hint has been specified, we accomodate more space for it and 1007 // add a text view in the drop down menu, at the bottom of the list 1008 LinearLayout hintContainer = new LinearLayout(context); 1009 hintContainer.setOrientation(LinearLayout.VERTICAL); 1010 1011 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1012 ViewGroup.LayoutParams.FILL_PARENT, 0, 1.0f 1013 ); 1014 hintContainer.addView(dropDownView, hintParams); 1015 hintContainer.addView(hintView); 1016 1017 // measure the hint's height to find how much more vertical space 1018 // we need to add to the drop down's height 1019 int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST); 1020 int heightSpec = MeasureSpec.UNSPECIFIED; 1021 hintView.measure(widthSpec, heightSpec); 1022 1023 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1024 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1025 + hintParams.bottomMargin; 1026 1027 dropDownView = hintContainer; 1028 } 1029 1030 mPopup.setContentView(dropDownView); 1031 } else { 1032 dropDownView = (ViewGroup) mPopup.getContentView(); 1033 final View view = dropDownView.findViewById(HINT_VIEW_ID); 1034 if (view != null) { 1035 LinearLayout.LayoutParams hintParams = 1036 (LinearLayout.LayoutParams) view.getLayoutParams(); 1037 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1038 + hintParams.bottomMargin; 1039 } 1040 } 1041 1042 // Max height available on the screen for a popup anchored to us 1043 final int maxHeight = mPopup.getMaxAvailableHeight(this, mDropDownVerticalOffset); 1044 //otherHeights += dropDownView.getPaddingTop() + dropDownView.getPaddingBottom(); 1045 1046 return mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, 1047 0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights; 1048 } 1049 1050 private View getHintView(Context context) { 1051 if (mHintText != null && mHintText.length() > 0) { 1052 final TextView hintView = (TextView) LayoutInflater.from(context).inflate( 1053 mHintResource, null).findViewById(com.android.internal.R.id.text1); 1054 hintView.setText(mHintText); 1055 hintView.setId(HINT_VIEW_ID); 1056 return hintView; 1057 } else { 1058 return null; 1059 } 1060 } 1061 1062 /** 1063 * Sets the validator used to perform text validation. 1064 * 1065 * @param validator The validator used to validate the text entered in this widget. 1066 * 1067 * @see #getValidator() 1068 * @see #performValidation() 1069 */ 1070 public void setValidator(Validator validator) { 1071 mValidator = validator; 1072 } 1073 1074 /** 1075 * Returns the Validator set with {@link #setValidator}, 1076 * or <code>null</code> if it was not set. 1077 * 1078 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1079 * @see #performValidation() 1080 */ 1081 public Validator getValidator() { 1082 return mValidator; 1083 } 1084 1085 /** 1086 * If a validator was set on this view and the current string is not valid, 1087 * ask the validator to fix it. 1088 * 1089 * @see #getValidator() 1090 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1091 */ 1092 public void performValidation() { 1093 if (mValidator == null) return; 1094 1095 CharSequence text = getText(); 1096 1097 if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { 1098 setText(mValidator.fixText(text)); 1099 } 1100 } 1101 1102 /** 1103 * Returns the Filter obtained from {@link Filterable#getFilter}, 1104 * or <code>null</code> if {@link #setAdapter} was not called with 1105 * a Filterable. 1106 */ 1107 protected Filter getFilter() { 1108 return mFilter; 1109 } 1110 1111 private class ListSelectorHider implements Runnable { 1112 public void run() { 1113 if (mDropDownList != null) { 1114 mDropDownList.hideSelector(); 1115 mDropDownList.requestLayout(); 1116 } 1117 } 1118 } 1119 1120 private class PopupTouchIntercepter implements OnTouchListener { 1121 public boolean onTouch(View v, MotionEvent event) { 1122 if (event.getAction() == MotionEvent.ACTION_DOWN) { 1123 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1124 mPopup.update(); 1125 } 1126 return false; 1127 } 1128 } 1129 1130 private class DropDownItemClickListener implements AdapterView.OnItemClickListener { 1131 public void onItemClick(AdapterView parent, View v, int position, long id) { 1132 performCompletion(v, position, id); 1133 } 1134 } 1135 1136 /** 1137 * <p>Wrapper class for a ListView. This wrapper hijacks the focus to 1138 * make sure the list uses the appropriate drawables and states when 1139 * displayed on screen within a drop down. The focus is never actually 1140 * passed to the drop down; the list only looks focused.</p> 1141 */ 1142 private static class DropDownListView extends ListView { 1143 /** 1144 * <p>Creates a new list view wrapper.</p> 1145 * 1146 * @param context this view's context 1147 */ 1148 public DropDownListView(Context context) { 1149 super(context, null, com.android.internal.R.attr.dropDownListViewStyle); 1150 } 1151 1152 /** 1153 * <p>Avoids jarring scrolling effect by ensuring that list elements 1154 * made of a text view fit on a single line.</p> 1155 * 1156 * @param position the item index in the list to get a view for 1157 * @return the view for the specified item 1158 */ 1159 @Override 1160 protected View obtainView(int position) { 1161 View view = super.obtainView(position); 1162 1163 if (view instanceof TextView) { 1164 ((TextView) view).setHorizontallyScrolling(true); 1165 } 1166 1167 return view; 1168 } 1169 1170 /** 1171 * <p>Returns the top padding of the currently selected view.</p> 1172 * 1173 * @return the height of the top padding for the selection 1174 */ 1175 public int getSelectionPaddingTop() { 1176 return mSelectionTopPadding; 1177 } 1178 1179 /** 1180 * <p>Returns the bottom padding of the currently selected view.</p> 1181 * 1182 * @return the height of the bottom padding for the selection 1183 */ 1184 public int getSelectionPaddingBottom() { 1185 return mSelectionBottomPadding; 1186 } 1187 1188 /** 1189 * <p>Returns the focus state in the drop down.</p> 1190 * 1191 * @return true always 1192 */ 1193 @Override 1194 public boolean hasWindowFocus() { 1195 return true; 1196 } 1197 1198 /** 1199 * <p>Returns the focus state in the drop down.</p> 1200 * 1201 * @return true always 1202 */ 1203 @Override 1204 public boolean isFocused() { 1205 return true; 1206 } 1207 1208 /** 1209 * <p>Returns the focus state in the drop down.</p> 1210 * 1211 * @return true always 1212 */ 1213 @Override 1214 public boolean hasFocus() { 1215 return true; 1216 } 1217 1218 protected int[] onCreateDrawableState(int extraSpace) { 1219 int[] res = super.onCreateDrawableState(extraSpace); 1220 //noinspection ConstantIfStatement 1221 if (false) { 1222 StringBuilder sb = new StringBuilder("Created drawable state: ["); 1223 for (int i=0; i<res.length; i++) { 1224 if (i > 0) sb.append(", "); 1225 sb.append("0x"); 1226 sb.append(Integer.toHexString(res[i])); 1227 } 1228 sb.append("]"); 1229 Log.i(TAG, sb.toString()); 1230 } 1231 return res; 1232 } 1233 } 1234 1235 /** 1236 * This interface is used to make sure that the text entered in this TextView complies to 1237 * a certain format. Since there is no foolproof way to prevent the user from leaving 1238 * this View with an incorrect value in it, all we can do is try to fix it ourselves 1239 * when this happens. 1240 */ 1241 public interface Validator { 1242 /** 1243 * Validates the specified text. 1244 * 1245 * @return true If the text currently in the text editor is valid. 1246 * 1247 * @see #fixText(CharSequence) 1248 */ 1249 boolean isValid(CharSequence text); 1250 1251 /** 1252 * Corrects the specified text to make it valid. 1253 * 1254 * @param invalidText A string that doesn't pass validation: isValid(invalidText) 1255 * returns false 1256 * 1257 * @return A string based on invalidText such as invoking isValid() on it returns true. 1258 * 1259 * @see #isValid(CharSequence) 1260 */ 1261 CharSequence fixText(CharSequence invalidText); 1262 } 1263} 1264