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