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