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.database.DataSetObserver; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.text.Editable; 25import android.text.Selection; 26import android.text.TextUtils; 27import android.text.TextWatcher; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.KeyEvent; 31import android.view.LayoutInflater; 32import android.view.View; 33import android.view.ViewGroup; 34import android.view.WindowManager; 35import android.view.inputmethod.CompletionInfo; 36import android.view.inputmethod.EditorInfo; 37import android.view.inputmethod.InputMethodManager; 38import com.android.internal.R; 39import java.lang.ref.WeakReference; 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 * <p>See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a> 78 * guide.</p> 79 * 80 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 81 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold 82 * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView 83 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector 84 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 85 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 86 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight 87 * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset 88 * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset 89 */ 90public class AutoCompleteTextView extends EditText implements Filter.FilterListener { 91 static final boolean DEBUG = false; 92 static final String TAG = "AutoCompleteTextView"; 93 94 static final int EXPAND_MAX = 3; 95 96 private CharSequence mHintText; 97 private TextView mHintView; 98 private int mHintResource; 99 100 private ListAdapter mAdapter; 101 private Filter mFilter; 102 private int mThreshold; 103 104 private ListPopupWindow mPopup; 105 private int mDropDownAnchorId; 106 107 private AdapterView.OnItemClickListener mItemClickListener; 108 private AdapterView.OnItemSelectedListener mItemSelectedListener; 109 110 private boolean mDropDownDismissedOnCompletion = true; 111 112 private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 113 private boolean mOpenBefore; 114 115 private Validator mValidator = null; 116 117 // Set to true when text is set directly and no filtering shall be performed 118 private boolean mBlockCompletion; 119 120 // When set, an update in the underlying adapter will update the result list popup. 121 // Set to false when the list is hidden to prevent asynchronous updates to popup the list again. 122 private boolean mPopupCanBeUpdated = true; 123 124 private PassThroughClickListener mPassThroughClickListener; 125 private PopupDataSetObserver mObserver; 126 127 public AutoCompleteTextView(Context context) { 128 this(context, null); 129 } 130 131 public AutoCompleteTextView(Context context, AttributeSet attrs) { 132 this(context, attrs, R.attr.autoCompleteTextViewStyle); 133 } 134 135 public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { 136 this(context, attrs, defStyleAttr, 0); 137 } 138 139 public AutoCompleteTextView( 140 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 141 super(context, attrs, defStyleAttr, defStyleRes); 142 143 mPopup = new ListPopupWindow(context, attrs, defStyleAttr, defStyleRes); 144 mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); 145 mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW); 146 147 final TypedArray a = context.obtainStyledAttributes( 148 attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes); 149 150 mThreshold = a.getInt(R.styleable.AutoCompleteTextView_completionThreshold, 2); 151 152 mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector)); 153 154 // Get the anchor's id now, but the view won't be ready, so wait to actually get the 155 // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. 156 // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return 157 // this TextView, as a default anchoring point. 158 mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor, 159 View.NO_ID); 160 161 // For dropdown width, the developer can specify a specific width, or MATCH_PARENT 162 // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). 163 mPopup.setWidth(a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, 164 ViewGroup.LayoutParams.WRAP_CONTENT)); 165 mPopup.setHeight(a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight, 166 ViewGroup.LayoutParams.WRAP_CONTENT)); 167 168 mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, 169 R.layout.simple_dropdown_hint); 170 171 mPopup.setOnItemClickListener(new DropDownItemClickListener()); 172 setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint)); 173 174 // Always turn on the auto complete input type flag, since it 175 // makes no sense to use this widget without it. 176 int inputType = getInputType(); 177 if ((inputType&EditorInfo.TYPE_MASK_CLASS) 178 == EditorInfo.TYPE_CLASS_TEXT) { 179 inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE; 180 setRawInputType(inputType); 181 } 182 183 a.recycle(); 184 185 setFocusable(true); 186 187 addTextChangedListener(new MyWatcher()); 188 189 mPassThroughClickListener = new PassThroughClickListener(); 190 super.setOnClickListener(mPassThroughClickListener); 191 } 192 193 @Override 194 public void setOnClickListener(OnClickListener listener) { 195 mPassThroughClickListener.mWrapped = listener; 196 } 197 198 /** 199 * Private hook into the on click event, dispatched from {@link PassThroughClickListener} 200 */ 201 private void onClickImpl() { 202 // If the dropdown is showing, bring the keyboard to the front 203 // when the user touches the text field. 204 if (isPopupShowing()) { 205 ensureImeVisible(true); 206 } 207 } 208 209 /** 210 * <p>Sets the optional hint text that is displayed at the bottom of the 211 * the matching list. This can be used as a cue to the user on how to 212 * best use the list, or to provide extra information.</p> 213 * 214 * @param hint the text to be displayed to the user 215 * 216 * @see #getCompletionHint() 217 * 218 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 219 */ 220 public void setCompletionHint(CharSequence hint) { 221 mHintText = hint; 222 if (hint != null) { 223 if (mHintView == null) { 224 final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate( 225 mHintResource, null).findViewById(com.android.internal.R.id.text1); 226 hintView.setText(mHintText); 227 mHintView = hintView; 228 mPopup.setPromptView(hintView); 229 } else { 230 mHintView.setText(hint); 231 } 232 } else { 233 mPopup.setPromptView(null); 234 mHintView = null; 235 } 236 } 237 238 /** 239 * Gets the optional hint text displayed at the bottom of the the matching list. 240 * 241 * @return The hint text, if any 242 * 243 * @see #setCompletionHint(CharSequence) 244 * 245 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 246 */ 247 public CharSequence getCompletionHint() { 248 return mHintText; 249 } 250 251 /** 252 * <p>Returns the current width for the auto-complete drop down list. This can 253 * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or 254 * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> 255 * 256 * @return the width for the drop down list 257 * 258 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 259 */ 260 public int getDropDownWidth() { 261 return mPopup.getWidth(); 262 } 263 264 /** 265 * <p>Sets the current width for the auto-complete drop down list. This can 266 * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or 267 * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> 268 * 269 * @param width the width to use 270 * 271 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 272 */ 273 public void setDropDownWidth(int width) { 274 mPopup.setWidth(width); 275 } 276 277 /** 278 * <p>Returns the current height for the auto-complete drop down list. This can 279 * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill 280 * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height 281 * of the drop down's content.</p> 282 * 283 * @return the height for the drop down list 284 * 285 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight 286 */ 287 public int getDropDownHeight() { 288 return mPopup.getHeight(); 289 } 290 291 /** 292 * <p>Sets the current height for the auto-complete drop down list. This can 293 * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill 294 * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height 295 * of the drop down's content.</p> 296 * 297 * @param height the height to use 298 * 299 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight 300 */ 301 public void setDropDownHeight(int height) { 302 mPopup.setHeight(height); 303 } 304 305 /** 306 * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p> 307 * 308 * @return the view's id, or {@link View#NO_ID} if none specified 309 * 310 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 311 */ 312 public int getDropDownAnchor() { 313 return mDropDownAnchorId; 314 } 315 316 /** 317 * <p>Sets the view to which the auto-complete drop down list should anchor. The view 318 * corresponding to this id will not be loaded until the next time it is needed to avoid 319 * loading a view which is not yet instantiated.</p> 320 * 321 * @param id the id to anchor the drop down list view to 322 * 323 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 324 */ 325 public void setDropDownAnchor(int id) { 326 mDropDownAnchorId = id; 327 mPopup.setAnchorView(null); 328 } 329 330 /** 331 * <p>Gets the background of the auto-complete drop-down list.</p> 332 * 333 * @return the background drawable 334 * 335 * @attr ref android.R.styleable#PopupWindow_popupBackground 336 */ 337 public Drawable getDropDownBackground() { 338 return mPopup.getBackground(); 339 } 340 341 /** 342 * <p>Sets the background of the auto-complete drop-down list.</p> 343 * 344 * @param d the drawable to set as the background 345 * 346 * @attr ref android.R.styleable#PopupWindow_popupBackground 347 */ 348 public void setDropDownBackgroundDrawable(Drawable d) { 349 mPopup.setBackgroundDrawable(d); 350 } 351 352 /** 353 * <p>Sets the background of the auto-complete drop-down list.</p> 354 * 355 * @param id the id of the drawable to set as the background 356 * 357 * @attr ref android.R.styleable#PopupWindow_popupBackground 358 */ 359 public void setDropDownBackgroundResource(int id) { 360 mPopup.setBackgroundDrawable(getContext().getDrawable(id)); 361 } 362 363 /** 364 * <p>Sets the vertical offset used for the auto-complete drop-down list.</p> 365 * 366 * @param offset the vertical offset 367 * 368 * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset 369 */ 370 public void setDropDownVerticalOffset(int offset) { 371 mPopup.setVerticalOffset(offset); 372 } 373 374 /** 375 * <p>Gets the vertical offset used for the auto-complete drop-down list.</p> 376 * 377 * @return the vertical offset 378 * 379 * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset 380 */ 381 public int getDropDownVerticalOffset() { 382 return mPopup.getVerticalOffset(); 383 } 384 385 /** 386 * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p> 387 * 388 * @param offset the horizontal offset 389 * 390 * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset 391 */ 392 public void setDropDownHorizontalOffset(int offset) { 393 mPopup.setHorizontalOffset(offset); 394 } 395 396 /** 397 * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p> 398 * 399 * @return the horizontal offset 400 * 401 * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset 402 */ 403 public int getDropDownHorizontalOffset() { 404 return mPopup.getHorizontalOffset(); 405 } 406 407 /** 408 * <p>Sets the animation style of the auto-complete drop-down list.</p> 409 * 410 * <p>If the drop-down is showing, calling this method will take effect only 411 * the next time the drop-down is shown.</p> 412 * 413 * @param animationStyle animation style to use when the drop-down appears 414 * and disappears. Set to -1 for the default animation, 0 for no 415 * animation, or a resource identifier for an explicit animation. 416 * 417 * @hide Pending API council approval 418 */ 419 public void setDropDownAnimationStyle(int animationStyle) { 420 mPopup.setAnimationStyle(animationStyle); 421 } 422 423 /** 424 * <p>Returns the animation style that is used when the drop-down list appears and disappears 425 * </p> 426 * 427 * @return the animation style that is used when the drop-down list appears and disappears 428 * 429 * @hide Pending API council approval 430 */ 431 public int getDropDownAnimationStyle() { 432 return mPopup.getAnimationStyle(); 433 } 434 435 /** 436 * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()} 437 * 438 * @hide Pending API council approval 439 */ 440 public boolean isDropDownAlwaysVisible() { 441 return mPopup.isDropDownAlwaysVisible(); 442 } 443 444 /** 445 * Sets whether the drop-down should remain visible as long as there is there is 446 * {@link #enoughToFilter()}. This is useful if an unknown number of results are expected 447 * to show up in the adapter sometime in the future. 448 * 449 * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless 450 * of the size or content of the list. {@link #getDropDownBackground()} will fill any space 451 * that is not used by the list. 452 * 453 * @param dropDownAlwaysVisible Whether to keep the drop-down visible. 454 * 455 * @hide Pending API council approval 456 */ 457 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { 458 mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible); 459 } 460 461 /** 462 * Checks whether the drop-down is dismissed when a suggestion is clicked. 463 * 464 * @hide Pending API council approval 465 */ 466 public boolean isDropDownDismissedOnCompletion() { 467 return mDropDownDismissedOnCompletion; 468 } 469 470 /** 471 * Sets whether the drop-down is dismissed when a suggestion is clicked. This is 472 * true by default. 473 * 474 * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down. 475 * 476 * @hide Pending API council approval 477 */ 478 public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) { 479 mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion; 480 } 481 482 /** 483 * <p>Returns the number of characters the user must type before the drop 484 * down list is shown.</p> 485 * 486 * @return the minimum number of characters to type to show the drop down 487 * 488 * @see #setThreshold(int) 489 * 490 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold 491 */ 492 public int getThreshold() { 493 return mThreshold; 494 } 495 496 /** 497 * <p>Specifies the minimum number of characters the user has to type in the 498 * edit box before the drop down list is shown.</p> 499 * 500 * <p>When <code>threshold</code> is less than or equals 0, a threshold of 501 * 1 is applied.</p> 502 * 503 * @param threshold the number of characters to type before the drop down 504 * is shown 505 * 506 * @see #getThreshold() 507 * 508 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold 509 */ 510 public void setThreshold(int threshold) { 511 if (threshold <= 0) { 512 threshold = 1; 513 } 514 515 mThreshold = threshold; 516 } 517 518 /** 519 * <p>Sets the listener that will be notified when the user clicks an item 520 * in the drop down list.</p> 521 * 522 * @param l the item click listener 523 */ 524 public void setOnItemClickListener(AdapterView.OnItemClickListener l) { 525 mItemClickListener = l; 526 } 527 528 /** 529 * <p>Sets the listener that will be notified when the user selects an item 530 * in the drop down list.</p> 531 * 532 * @param l the item selected listener 533 */ 534 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) { 535 mItemSelectedListener = l; 536 } 537 538 /** 539 * <p>Returns the listener that is notified whenever the user clicks an item 540 * in the drop down list.</p> 541 * 542 * @return the item click listener 543 * 544 * @deprecated Use {@link #getOnItemClickListener()} intead 545 */ 546 @Deprecated 547 public AdapterView.OnItemClickListener getItemClickListener() { 548 return mItemClickListener; 549 } 550 551 /** 552 * <p>Returns the listener that is notified whenever the user selects an 553 * item in the drop down list.</p> 554 * 555 * @return the item selected listener 556 * 557 * @deprecated Use {@link #getOnItemSelectedListener()} intead 558 */ 559 @Deprecated 560 public AdapterView.OnItemSelectedListener getItemSelectedListener() { 561 return mItemSelectedListener; 562 } 563 564 /** 565 * <p>Returns the listener that is notified whenever the user clicks an item 566 * in the drop down list.</p> 567 * 568 * @return the item click listener 569 */ 570 public AdapterView.OnItemClickListener getOnItemClickListener() { 571 return mItemClickListener; 572 } 573 574 /** 575 * <p>Returns the listener that is notified whenever the user selects an 576 * item in the drop down list.</p> 577 * 578 * @return the item selected listener 579 */ 580 public AdapterView.OnItemSelectedListener getOnItemSelectedListener() { 581 return mItemSelectedListener; 582 } 583 584 /** 585 * Set a listener that will be invoked whenever the AutoCompleteTextView's 586 * list of completions is dismissed. 587 * @param dismissListener Listener to invoke when completions are dismissed 588 */ 589 public void setOnDismissListener(final OnDismissListener dismissListener) { 590 PopupWindow.OnDismissListener wrappedListener = null; 591 if (dismissListener != null) { 592 wrappedListener = new PopupWindow.OnDismissListener() { 593 @Override public void onDismiss() { 594 dismissListener.onDismiss(); 595 } 596 }; 597 } 598 mPopup.setOnDismissListener(wrappedListener); 599 } 600 601 /** 602 * <p>Returns a filterable list adapter used for auto completion.</p> 603 * 604 * @return a data adapter used for auto completion 605 */ 606 public ListAdapter getAdapter() { 607 return mAdapter; 608 } 609 610 /** 611 * <p>Changes the list of data used for auto completion. The provided list 612 * must be a filterable list adapter.</p> 613 * 614 * <p>The caller is still responsible for managing any resources used by the adapter. 615 * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified. 616 * A common case is the use of {@link android.widget.CursorAdapter}, which 617 * contains a {@link android.database.Cursor} that must be closed. This can be done 618 * automatically (see 619 * {@link android.app.Activity#startManagingCursor(android.database.Cursor) 620 * startManagingCursor()}), 621 * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p> 622 * 623 * @param adapter the adapter holding the auto completion data 624 * 625 * @see #getAdapter() 626 * @see android.widget.Filterable 627 * @see android.widget.ListAdapter 628 */ 629 public <T extends ListAdapter & Filterable> void setAdapter(T adapter) { 630 if (mObserver == null) { 631 mObserver = new PopupDataSetObserver(this); 632 } else if (mAdapter != null) { 633 mAdapter.unregisterDataSetObserver(mObserver); 634 } 635 mAdapter = adapter; 636 if (mAdapter != null) { 637 //noinspection unchecked 638 mFilter = ((Filterable) mAdapter).getFilter(); 639 adapter.registerDataSetObserver(mObserver); 640 } else { 641 mFilter = null; 642 } 643 644 mPopup.setAdapter(mAdapter); 645 } 646 647 @Override 648 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 649 if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing() 650 && !mPopup.isDropDownAlwaysVisible()) { 651 // special case for the back key, we do not even try to send it 652 // to the drop down list but instead, consume it immediately 653 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 654 KeyEvent.DispatcherState state = getKeyDispatcherState(); 655 if (state != null) { 656 state.startTracking(event, this); 657 } 658 return true; 659 } else if (event.getAction() == KeyEvent.ACTION_UP) { 660 KeyEvent.DispatcherState state = getKeyDispatcherState(); 661 if (state != null) { 662 state.handleUpEvent(event); 663 } 664 if (event.isTracking() && !event.isCanceled()) { 665 dismissDropDown(); 666 return true; 667 } 668 } 669 } 670 return super.onKeyPreIme(keyCode, event); 671 } 672 673 @Override 674 public boolean onKeyUp(int keyCode, KeyEvent event) { 675 boolean consumed = mPopup.onKeyUp(keyCode, event); 676 if (consumed) { 677 switch (keyCode) { 678 // if the list accepts the key events and the key event 679 // was a click, the text view gets the selected item 680 // from the drop down as its content 681 case KeyEvent.KEYCODE_ENTER: 682 case KeyEvent.KEYCODE_DPAD_CENTER: 683 case KeyEvent.KEYCODE_TAB: 684 if (event.hasNoModifiers()) { 685 performCompletion(); 686 } 687 return true; 688 } 689 } 690 691 if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) { 692 performCompletion(); 693 return true; 694 } 695 696 return super.onKeyUp(keyCode, event); 697 } 698 699 @Override 700 public boolean onKeyDown(int keyCode, KeyEvent event) { 701 if (mPopup.onKeyDown(keyCode, event)) { 702 return true; 703 } 704 705 if (!isPopupShowing()) { 706 switch(keyCode) { 707 case KeyEvent.KEYCODE_DPAD_DOWN: 708 if (event.hasNoModifiers()) { 709 performValidation(); 710 } 711 } 712 } 713 714 if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) { 715 return true; 716 } 717 718 mLastKeyCode = keyCode; 719 boolean handled = super.onKeyDown(keyCode, event); 720 mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 721 722 if (handled && isPopupShowing()) { 723 clearListSelection(); 724 } 725 726 return handled; 727 } 728 729 /** 730 * Returns <code>true</code> if the amount of text in the field meets 731 * or exceeds the {@link #getThreshold} requirement. You can override 732 * this to impose a different standard for when filtering will be 733 * triggered. 734 */ 735 public boolean enoughToFilter() { 736 if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length() 737 + " threshold=" + mThreshold); 738 return getText().length() >= mThreshold; 739 } 740 741 /** 742 * This is used to watch for edits to the text view. Note that we call 743 * to methods on the auto complete text view class so that we can access 744 * private vars without going through thunks. 745 */ 746 private class MyWatcher implements TextWatcher { 747 public void afterTextChanged(Editable s) { 748 doAfterTextChanged(); 749 } 750 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 751 doBeforeTextChanged(); 752 } 753 public void onTextChanged(CharSequence s, int start, int before, int count) { 754 } 755 } 756 757 void doBeforeTextChanged() { 758 if (mBlockCompletion) return; 759 760 // when text is changed, inserted or deleted, we attempt to show 761 // the drop down 762 mOpenBefore = isPopupShowing(); 763 if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore); 764 } 765 766 void doAfterTextChanged() { 767 if (mBlockCompletion) return; 768 769 // if the list was open before the keystroke, but closed afterwards, 770 // then something in the keystroke processing (an input filter perhaps) 771 // called performCompletion() and we shouldn't do any more processing. 772 if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore 773 + " open=" + isPopupShowing()); 774 if (mOpenBefore && !isPopupShowing()) { 775 return; 776 } 777 778 // the drop down is shown only when a minimum number of characters 779 // was typed in the text view 780 if (enoughToFilter()) { 781 if (mFilter != null) { 782 mPopupCanBeUpdated = true; 783 performFiltering(getText(), mLastKeyCode); 784 } 785 } else { 786 // drop down is automatically dismissed when enough characters 787 // are deleted from the text view 788 if (!mPopup.isDropDownAlwaysVisible()) { 789 dismissDropDown(); 790 } 791 if (mFilter != null) { 792 mFilter.filter(null); 793 } 794 } 795 } 796 797 /** 798 * <p>Indicates whether the popup menu is showing.</p> 799 * 800 * @return true if the popup menu is showing, false otherwise 801 */ 802 public boolean isPopupShowing() { 803 return mPopup.isShowing(); 804 } 805 806 /** 807 * <p>Converts the selected item from the drop down list into a sequence 808 * of character that can be used in the edit box.</p> 809 * 810 * @param selectedItem the item selected by the user for completion 811 * 812 * @return a sequence of characters representing the selected suggestion 813 */ 814 protected CharSequence convertSelectionToString(Object selectedItem) { 815 return mFilter.convertResultToString(selectedItem); 816 } 817 818 /** 819 * <p>Clear the list selection. This may only be temporary, as user input will often bring 820 * it back. 821 */ 822 public void clearListSelection() { 823 mPopup.clearListSelection(); 824 } 825 826 /** 827 * Set the position of the dropdown view selection. 828 * 829 * @param position The position to move the selector to. 830 */ 831 public void setListSelection(int position) { 832 mPopup.setSelection(position); 833 } 834 835 /** 836 * Get the position of the dropdown view selection, if there is one. Returns 837 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if 838 * there is no selection. 839 * 840 * @return the position of the current selection, if there is one, or 841 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not. 842 * 843 * @see ListView#getSelectedItemPosition() 844 */ 845 public int getListSelection() { 846 return mPopup.getSelectedItemPosition(); 847 } 848 849 /** 850 * <p>Starts filtering the content of the drop down list. The filtering 851 * pattern is the content of the edit box. Subclasses should override this 852 * method to filter with a different pattern, for instance a substring of 853 * <code>text</code>.</p> 854 * 855 * @param text the filtering pattern 856 * @param keyCode the last character inserted in the edit box; beware that 857 * this will be null when text is being added through a soft input method. 858 */ 859 @SuppressWarnings({ "UnusedDeclaration" }) 860 protected void performFiltering(CharSequence text, int keyCode) { 861 mFilter.filter(text, this); 862 } 863 864 /** 865 * <p>Performs the text completion by converting the selected item from 866 * the drop down list into a string, replacing the text box's content with 867 * this string and finally dismissing the drop down menu.</p> 868 */ 869 public void performCompletion() { 870 performCompletion(null, -1, -1); 871 } 872 873 @Override 874 public void onCommitCompletion(CompletionInfo completion) { 875 if (isPopupShowing()) { 876 mPopup.performItemClick(completion.getPosition()); 877 } 878 } 879 880 private void performCompletion(View selectedView, int position, long id) { 881 if (isPopupShowing()) { 882 Object selectedItem; 883 if (position < 0) { 884 selectedItem = mPopup.getSelectedItem(); 885 } else { 886 selectedItem = mAdapter.getItem(position); 887 } 888 if (selectedItem == null) { 889 Log.w(TAG, "performCompletion: no selected item"); 890 return; 891 } 892 893 mBlockCompletion = true; 894 replaceText(convertSelectionToString(selectedItem)); 895 mBlockCompletion = false; 896 897 if (mItemClickListener != null) { 898 final ListPopupWindow list = mPopup; 899 900 if (selectedView == null || position < 0) { 901 selectedView = list.getSelectedView(); 902 position = list.getSelectedItemPosition(); 903 id = list.getSelectedItemId(); 904 } 905 mItemClickListener.onItemClick(list.getListView(), selectedView, position, id); 906 } 907 } 908 909 if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) { 910 dismissDropDown(); 911 } 912 } 913 914 /** 915 * Identifies whether the view is currently performing a text completion, so subclasses 916 * can decide whether to respond to text changed events. 917 */ 918 public boolean isPerformingCompletion() { 919 return mBlockCompletion; 920 } 921 922 /** 923 * Like {@link #setText(CharSequence)}, except that it can disable filtering. 924 * 925 * @param filter If <code>false</code>, no filtering will be performed 926 * as a result of this call. 927 */ 928 public void setText(CharSequence text, boolean filter) { 929 if (filter) { 930 setText(text); 931 } else { 932 mBlockCompletion = true; 933 setText(text); 934 mBlockCompletion = false; 935 } 936 } 937 938 /** 939 * <p>Performs the text completion by replacing the current text by the 940 * selected item. Subclasses should override this method to avoid replacing 941 * the whole content of the edit box.</p> 942 * 943 * @param text the selected suggestion in the drop down list 944 */ 945 protected void replaceText(CharSequence text) { 946 clearComposingText(); 947 948 setText(text); 949 // make sure we keep the caret at the end of the text view 950 Editable spannable = getText(); 951 Selection.setSelection(spannable, spannable.length()); 952 } 953 954 /** {@inheritDoc} */ 955 public void onFilterComplete(int count) { 956 updateDropDownForFilter(count); 957 } 958 959 private void updateDropDownForFilter(int count) { 960 // Not attached to window, don't update drop-down 961 if (getWindowVisibility() == View.GONE) return; 962 963 /* 964 * This checks enoughToFilter() again because filtering requests 965 * are asynchronous, so the result may come back after enough text 966 * has since been deleted to make it no longer appropriate 967 * to filter. 968 */ 969 970 final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible(); 971 final boolean enoughToFilter = enoughToFilter(); 972 if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter) { 973 if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) { 974 showDropDown(); 975 } 976 } else if (!dropDownAlwaysVisible && isPopupShowing()) { 977 dismissDropDown(); 978 // When the filter text is changed, the first update from the adapter may show an empty 979 // count (when the query is being performed on the network). Future updates when some 980 // content has been retrieved should still be able to update the list. 981 mPopupCanBeUpdated = true; 982 } 983 } 984 985 @Override 986 public void onWindowFocusChanged(boolean hasWindowFocus) { 987 super.onWindowFocusChanged(hasWindowFocus); 988 if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) { 989 dismissDropDown(); 990 } 991 } 992 993 @Override 994 protected void onDisplayHint(int hint) { 995 super.onDisplayHint(hint); 996 switch (hint) { 997 case INVISIBLE: 998 if (!mPopup.isDropDownAlwaysVisible()) { 999 dismissDropDown(); 1000 } 1001 break; 1002 } 1003 } 1004 1005 @Override 1006 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 1007 super.onFocusChanged(focused, direction, previouslyFocusedRect); 1008 // Perform validation if the view is losing focus. 1009 if (!focused) { 1010 performValidation(); 1011 } 1012 if (!focused && !mPopup.isDropDownAlwaysVisible()) { 1013 dismissDropDown(); 1014 } 1015 } 1016 1017 @Override 1018 protected void onAttachedToWindow() { 1019 super.onAttachedToWindow(); 1020 } 1021 1022 @Override 1023 protected void onDetachedFromWindow() { 1024 dismissDropDown(); 1025 super.onDetachedFromWindow(); 1026 } 1027 1028 /** 1029 * <p>Closes the drop down if present on screen.</p> 1030 */ 1031 public void dismissDropDown() { 1032 InputMethodManager imm = InputMethodManager.peekInstance(); 1033 if (imm != null) { 1034 imm.displayCompletions(this, null); 1035 } 1036 mPopup.dismiss(); 1037 mPopupCanBeUpdated = false; 1038 } 1039 1040 @Override 1041 protected boolean setFrame(final int l, int t, final int r, int b) { 1042 boolean result = super.setFrame(l, t, r, b); 1043 1044 if (isPopupShowing()) { 1045 showDropDown(); 1046 } 1047 1048 return result; 1049 } 1050 1051 /** 1052 * Issues a runnable to show the dropdown as soon as possible. 1053 * 1054 * @hide internal used only by SearchDialog 1055 */ 1056 public void showDropDownAfterLayout() { 1057 mPopup.postShow(); 1058 } 1059 1060 /** 1061 * Ensures that the drop down is not obscuring the IME. 1062 * @param visible whether the ime should be in front. If false, the ime is pushed to 1063 * the background. 1064 * @hide internal used only here and SearchDialog 1065 */ 1066 public void ensureImeVisible(boolean visible) { 1067 mPopup.setInputMethodMode(visible 1068 ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1069 if (mPopup.isDropDownAlwaysVisible() || (mFilter != null && enoughToFilter())) { 1070 showDropDown(); 1071 } 1072 } 1073 1074 /** 1075 * @hide internal used only here and SearchDialog 1076 */ 1077 public boolean isInputMethodNotNeeded() { 1078 return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED; 1079 } 1080 1081 /** 1082 * <p>Displays the drop down on screen.</p> 1083 */ 1084 public void showDropDown() { 1085 buildImeCompletions(); 1086 1087 if (mPopup.getAnchorView() == null) { 1088 if (mDropDownAnchorId != View.NO_ID) { 1089 mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId)); 1090 } else { 1091 mPopup.setAnchorView(this); 1092 } 1093 } 1094 if (!isPopupShowing()) { 1095 // Make sure the list does not obscure the IME when shown for the first time. 1096 mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED); 1097 mPopup.setListItemExpandMax(EXPAND_MAX); 1098 } 1099 mPopup.show(); 1100 mPopup.getListView().setOverScrollMode(View.OVER_SCROLL_ALWAYS); 1101 } 1102 1103 /** 1104 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 1105 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we 1106 * ignore outside touch even when the drop down is not set to always visible. 1107 * 1108 * @hide used only by SearchDialog 1109 */ 1110 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 1111 mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch); 1112 } 1113 1114 private void buildImeCompletions() { 1115 final ListAdapter adapter = mAdapter; 1116 if (adapter != null) { 1117 InputMethodManager imm = InputMethodManager.peekInstance(); 1118 if (imm != null) { 1119 final int count = Math.min(adapter.getCount(), 20); 1120 CompletionInfo[] completions = new CompletionInfo[count]; 1121 int realCount = 0; 1122 1123 for (int i = 0; i < count; i++) { 1124 if (adapter.isEnabled(i)) { 1125 Object item = adapter.getItem(i); 1126 long id = adapter.getItemId(i); 1127 completions[realCount] = new CompletionInfo(id, realCount, 1128 convertSelectionToString(item)); 1129 realCount++; 1130 } 1131 } 1132 1133 if (realCount != count) { 1134 CompletionInfo[] tmp = new CompletionInfo[realCount]; 1135 System.arraycopy(completions, 0, tmp, 0, realCount); 1136 completions = tmp; 1137 } 1138 1139 imm.displayCompletions(this, completions); 1140 } 1141 } 1142 } 1143 1144 /** 1145 * Sets the validator used to perform text validation. 1146 * 1147 * @param validator The validator used to validate the text entered in this widget. 1148 * 1149 * @see #getValidator() 1150 * @see #performValidation() 1151 */ 1152 public void setValidator(Validator validator) { 1153 mValidator = validator; 1154 } 1155 1156 /** 1157 * Returns the Validator set with {@link #setValidator}, 1158 * or <code>null</code> if it was not set. 1159 * 1160 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1161 * @see #performValidation() 1162 */ 1163 public Validator getValidator() { 1164 return mValidator; 1165 } 1166 1167 /** 1168 * If a validator was set on this view and the current string is not valid, 1169 * ask the validator to fix it. 1170 * 1171 * @see #getValidator() 1172 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1173 */ 1174 public void performValidation() { 1175 if (mValidator == null) return; 1176 1177 CharSequence text = getText(); 1178 1179 if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { 1180 setText(mValidator.fixText(text)); 1181 } 1182 } 1183 1184 /** 1185 * Returns the Filter obtained from {@link Filterable#getFilter}, 1186 * or <code>null</code> if {@link #setAdapter} was not called with 1187 * a Filterable. 1188 */ 1189 protected Filter getFilter() { 1190 return mFilter; 1191 } 1192 1193 private class DropDownItemClickListener implements AdapterView.OnItemClickListener { 1194 public void onItemClick(AdapterView parent, View v, int position, long id) { 1195 performCompletion(v, position, id); 1196 } 1197 } 1198 1199 /** 1200 * This interface is used to make sure that the text entered in this TextView complies to 1201 * a certain format. Since there is no foolproof way to prevent the user from leaving 1202 * this View with an incorrect value in it, all we can do is try to fix it ourselves 1203 * when this happens. 1204 */ 1205 public interface Validator { 1206 /** 1207 * Validates the specified text. 1208 * 1209 * @return true If the text currently in the text editor is valid. 1210 * 1211 * @see #fixText(CharSequence) 1212 */ 1213 boolean isValid(CharSequence text); 1214 1215 /** 1216 * Corrects the specified text to make it valid. 1217 * 1218 * @param invalidText A string that doesn't pass validation: isValid(invalidText) 1219 * returns false 1220 * 1221 * @return A string based on invalidText such as invoking isValid() on it returns true. 1222 * 1223 * @see #isValid(CharSequence) 1224 */ 1225 CharSequence fixText(CharSequence invalidText); 1226 } 1227 1228 /** 1229 * Listener to respond to the AutoCompleteTextView's completion list being dismissed. 1230 * @see AutoCompleteTextView#setOnDismissListener(OnDismissListener) 1231 */ 1232 public interface OnDismissListener { 1233 /** 1234 * This method will be invoked whenever the AutoCompleteTextView's list 1235 * of completion options has been dismissed and is no longer available 1236 * for user interaction. 1237 */ 1238 void onDismiss(); 1239 } 1240 1241 /** 1242 * Allows us a private hook into the on click event without preventing users from setting 1243 * their own click listener. 1244 */ 1245 private class PassThroughClickListener implements OnClickListener { 1246 1247 private View.OnClickListener mWrapped; 1248 1249 /** {@inheritDoc} */ 1250 public void onClick(View v) { 1251 onClickImpl(); 1252 1253 if (mWrapped != null) mWrapped.onClick(v); 1254 } 1255 } 1256 1257 /** 1258 * Static inner listener that keeps a WeakReference to the actual AutoCompleteTextView. 1259 * <p> 1260 * This way, if adapter has a longer life span than the View, we won't leak the View, instead 1261 * we will just leak a small Observer with 1 field. 1262 */ 1263 private static class PopupDataSetObserver extends DataSetObserver { 1264 private final WeakReference<AutoCompleteTextView> mViewReference; 1265 1266 private PopupDataSetObserver(AutoCompleteTextView view) { 1267 mViewReference = new WeakReference<AutoCompleteTextView>(view); 1268 } 1269 1270 @Override 1271 public void onChanged() { 1272 final AutoCompleteTextView textView = mViewReference.get(); 1273 if (textView != null && textView.mAdapter != null) { 1274 // If the popup is not showing already, showing it will cause 1275 // the list of data set observers attached to the adapter to 1276 // change. We can't do it from here, because we are in the middle 1277 // of iterating through the list of observers. 1278 textView.post(updateRunnable); 1279 } 1280 } 1281 1282 private final Runnable updateRunnable = new Runnable() { 1283 @Override 1284 public void run() { 1285 final AutoCompleteTextView textView = mViewReference.get(); 1286 if (textView == null) { 1287 return; 1288 } 1289 final ListAdapter adapter = textView.mAdapter; 1290 if (adapter == null) { 1291 return; 1292 } 1293 textView.updateDropDownForFilter(adapter.getCount()); 1294 } 1295 }; 1296 } 1297} 1298