AutoCompleteTextView.java revision d25eb35b6b80b6f8065ab39b1cf1abb1fd801234
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.Configuration; 21import android.content.res.TypedArray; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.graphics.drawable.GradientDrawable.Orientation; 25import android.text.Editable; 26import android.text.Selection; 27import android.text.TextUtils; 28import android.text.TextWatcher; 29import android.util.AttributeSet; 30import android.util.Log; 31import android.view.KeyEvent; 32import android.view.LayoutInflater; 33import android.view.MotionEvent; 34import android.view.View; 35import android.view.ViewGroup; 36import android.view.WindowManager; 37import android.view.inputmethod.CompletionInfo; 38import android.view.inputmethod.InputMethodManager; 39import android.view.inputmethod.EditorInfo; 40 41import com.android.internal.R; 42 43 44/** 45 * <p>An editable text view that shows completion suggestions automatically 46 * while the user is typing. The list of suggestions is displayed in a drop 47 * down menu from which the user can choose an item to replace the content 48 * of the edit box with.</p> 49 * 50 * <p>The drop down can be dismissed at any time by pressing the back key or, 51 * if no item is selected in the drop down, by pressing the enter/dpad center 52 * key.</p> 53 * 54 * <p>The list of suggestions is obtained from a data adapter and appears 55 * only after a given number of characters defined by 56 * {@link #getThreshold() the threshold}.</p> 57 * 58 * <p>The following code snippet shows how to create a text view which suggests 59 * various countries names while the user is typing:</p> 60 * 61 * <pre class="prettyprint"> 62 * public class CountriesActivity extends Activity { 63 * protected void onCreate(Bundle icicle) { 64 * super.onCreate(icicle); 65 * setContentView(R.layout.countries); 66 * 67 * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, 68 * android.R.layout.simple_dropdown_item_1line, COUNTRIES); 69 * AutoCompleteTextView textView = (AutoCompleteTextView) 70 * findViewById(R.id.countries_list); 71 * textView.setAdapter(adapter); 72 * } 73 * 74 * private static final String[] COUNTRIES = new String[] { 75 * "Belgium", "France", "Italy", "Germany", "Spain" 76 * }; 77 * } 78 * </pre> 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#AutoCompleteTextView_dropDownVerticalOffset 88 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHorizontalOffset 89 */ 90public class AutoCompleteTextView extends EditText implements Filter.FilterListener { 91 static final boolean DEBUG = false; 92 static final String TAG = "AutoCompleteTextView"; 93 94 private static final int HINT_VIEW_ID = 0x17; 95 96 private CharSequence mHintText; 97 private int mHintResource; 98 99 private ListAdapter mAdapter; 100 private Filter mFilter; 101 private int mThreshold; 102 103 private PopupWindow mPopup; 104 private DropDownListView mDropDownList; 105 private int mDropDownVerticalOffset; 106 private int mDropDownHorizontalOffset; 107 private int mDropDownAnchorId; 108 private View mDropDownAnchorView; // view is retrieved lazily from id once needed 109 private int mDropDownWidth; 110 private int mDropDownHeight; 111 private final Rect mTempRect = new Rect(); 112 113 private Drawable mDropDownListHighlight; 114 115 private AdapterView.OnItemClickListener mItemClickListener; 116 private AdapterView.OnItemSelectedListener mItemSelectedListener; 117 118 private final DropDownItemClickListener mDropDownItemClickListener = 119 new DropDownItemClickListener(); 120 121 private boolean mDropDownAlwaysVisible = false; 122 123 private boolean mDropDownDismissedOnCompletion = true; 124 125 private boolean mForceIgnoreOutsideTouch = false; 126 127 private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 128 private boolean mOpenBefore; 129 130 private Validator mValidator = null; 131 132 private boolean mBlockCompletion; 133 134 private AutoCompleteTextView.ListSelectorHider mHideSelector; 135 private Runnable mShowDropDownRunnable; 136 137 private AutoCompleteTextView.PassThroughClickListener mPassThroughClickListener; 138 139 public AutoCompleteTextView(Context context) { 140 this(context, null); 141 } 142 143 public AutoCompleteTextView(Context context, AttributeSet attrs) { 144 this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); 145 } 146 147 public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { 148 super(context, attrs, defStyle); 149 150 mPopup = new PopupWindow(context, attrs, 151 com.android.internal.R.attr.autoCompleteTextViewStyle); 152 mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); 153 154 TypedArray a = 155 context.obtainStyledAttributes( 156 attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0); 157 158 mThreshold = a.getInt( 159 R.styleable.AutoCompleteTextView_completionThreshold, 2); 160 161 mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint); 162 163 mDropDownListHighlight = a.getDrawable( 164 R.styleable.AutoCompleteTextView_dropDownSelector); 165 mDropDownVerticalOffset = (int) 166 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f); 167 mDropDownHorizontalOffset = (int) 168 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f); 169 170 // Get the anchor's id now, but the view won't be ready, so wait to actually get the 171 // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. 172 // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return 173 // this TextView, as a default anchoring point. 174 mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor, 175 View.NO_ID); 176 177 // For dropdown width, the developer can specify a specific width, or MATCH_PARENT 178 // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). 179 mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, 180 ViewGroup.LayoutParams.WRAP_CONTENT); 181 mDropDownHeight = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight, 182 ViewGroup.LayoutParams.WRAP_CONTENT); 183 184 mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, 185 R.layout.simple_dropdown_hint); 186 187 // Always turn on the auto complete input type flag, since it 188 // makes no sense to use this widget without it. 189 int inputType = getInputType(); 190 if ((inputType&EditorInfo.TYPE_MASK_CLASS) 191 == EditorInfo.TYPE_CLASS_TEXT) { 192 inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE; 193 setRawInputType(inputType); 194 } 195 196 a.recycle(); 197 198 setFocusable(true); 199 200 addTextChangedListener(new MyWatcher()); 201 202 mPassThroughClickListener = new PassThroughClickListener(); 203 super.setOnClickListener(mPassThroughClickListener); 204 } 205 206 @Override 207 public void setOnClickListener(OnClickListener listener) { 208 mPassThroughClickListener.mWrapped = listener; 209 } 210 211 /** 212 * Private hook into the on click event, dispatched from {@link PassThroughClickListener} 213 */ 214 private void onClickImpl() { 215 // If the dropdown is showing, bring the keyboard to the front 216 // when the user touches the text field. 217 if (mPopup.isShowing()) { 218 ensureImeVisible(true); 219 } 220 } 221 222 /** 223 * Sets this to be single line; a separate method so 224 * MultiAutoCompleteTextView can skip this. 225 */ 226 /* package */ void finishInit() { 227 setSingleLine(); 228 } 229 230 /** 231 * <p>Sets the optional hint text that is displayed at the bottom of the 232 * the matching list. This can be used as a cue to the user on how to 233 * best use the list, or to provide extra information.</p> 234 * 235 * @param hint the text to be displayed to the user 236 * 237 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 238 */ 239 public void setCompletionHint(CharSequence hint) { 240 mHintText = hint; 241 } 242 243 /** 244 * <p>Returns the current width for the auto-complete drop down list. This can 245 * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or 246 * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> 247 * 248 * @return the width for the drop down list 249 * 250 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 251 */ 252 public int getDropDownWidth() { 253 return mDropDownWidth; 254 } 255 256 /** 257 * <p>Sets 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 * @param width the width to use 262 * 263 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 264 */ 265 public void setDropDownWidth(int width) { 266 mDropDownWidth = width; 267 } 268 269 /** 270 * <p>Returns the current height for the auto-complete drop down list. This can 271 * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill 272 * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height 273 * of the drop down's content.</p> 274 * 275 * @return the height for the drop down list 276 * 277 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight 278 */ 279 public int getDropDownHeight() { 280 return mDropDownHeight; 281 } 282 283 /** 284 * <p>Sets the current height for the auto-complete drop down list. This can 285 * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill 286 * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height 287 * of the drop down's content.</p> 288 * 289 * @param height the height to use 290 * 291 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight 292 */ 293 public void setDropDownHeight(int height) { 294 mDropDownHeight = height; 295 } 296 297 /** 298 * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p> 299 * 300 * @return the view's id, or {@link View#NO_ID} if none specified 301 * 302 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 303 */ 304 public int getDropDownAnchor() { 305 return mDropDownAnchorId; 306 } 307 308 /** 309 * <p>Sets the view to which the auto-complete drop down list should anchor. The view 310 * corresponding to this id will not be loaded until the next time it is needed to avoid 311 * loading a view which is not yet instantiated.</p> 312 * 313 * @param id the id to anchor the drop down list view to 314 * 315 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 316 */ 317 public void setDropDownAnchor(int id) { 318 mDropDownAnchorId = id; 319 mDropDownAnchorView = null; 320 } 321 322 /** 323 * <p>Gets the background of the auto-complete drop-down list.</p> 324 * 325 * @return the background drawable 326 * 327 * @attr ref android.R.styleable#PopupWindow_popupBackground 328 */ 329 public Drawable getDropDownBackground() { 330 return mPopup.getBackground(); 331 } 332 333 /** 334 * <p>Sets the background of the auto-complete drop-down list.</p> 335 * 336 * @param d the drawable to set as the background 337 * 338 * @attr ref android.R.styleable#PopupWindow_popupBackground 339 */ 340 public void setDropDownBackgroundDrawable(Drawable d) { 341 mPopup.setBackgroundDrawable(d); 342 } 343 344 /** 345 * <p>Sets the background of the auto-complete drop-down list.</p> 346 * 347 * @param id the id of the drawable to set as the background 348 * 349 * @attr ref android.R.styleable#PopupWindow_popupBackground 350 */ 351 public void setDropDownBackgroundResource(int id) { 352 mPopup.setBackgroundDrawable(getResources().getDrawable(id)); 353 } 354 355 /** 356 * <p>Sets the vertical offset used for the auto-complete drop-down list.</p> 357 * 358 * @param offset the vertical offset 359 */ 360 public void setDropDownVerticalOffset(int offset) { 361 mDropDownVerticalOffset = offset; 362 } 363 364 /** 365 * <p>Gets the vertical offset used for the auto-complete drop-down list.</p> 366 * 367 * @return the vertical offset 368 */ 369 public int getDropDownVerticalOffset() { 370 return mDropDownVerticalOffset; 371 } 372 373 /** 374 * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p> 375 * 376 * @param offset the horizontal offset 377 */ 378 public void setDropDownHorizontalOffset(int offset) { 379 mDropDownHorizontalOffset = offset; 380 } 381 382 /** 383 * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p> 384 * 385 * @return the horizontal offset 386 */ 387 public int getDropDownHorizontalOffset() { 388 return mDropDownHorizontalOffset; 389 } 390 391 /** 392 * <p>Sets the animation style of the auto-complete drop-down list.</p> 393 * 394 * <p>If the drop-down is showing, calling this method will take effect only 395 * the next time the drop-down is shown.</p> 396 * 397 * @param animationStyle animation style to use when the drop-down appears 398 * and disappears. Set to -1 for the default animation, 0 for no 399 * animation, or a resource identifier for an explicit animation. 400 * 401 * @hide Pending API council approval 402 */ 403 public void setDropDownAnimationStyle(int animationStyle) { 404 mPopup.setAnimationStyle(animationStyle); 405 } 406 407 /** 408 * <p>Returns the animation style that is used when the drop-down list appears and disappears 409 * </p> 410 * 411 * @return the animation style that is used when the drop-down list appears and disappears 412 * 413 * @hide Pending API council approval 414 */ 415 public int getDropDownAnimationStyle() { 416 return mPopup.getAnimationStyle(); 417 } 418 419 /** 420 * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()} 421 * 422 * @hide Pending API council approval 423 */ 424 public boolean isDropDownAlwaysVisible() { 425 return mDropDownAlwaysVisible; 426 } 427 428 /** 429 * Sets whether the drop-down should remain visible as long as there is there is 430 * {@link #enoughToFilter()}. This is useful if an unknown number of results are expected 431 * to show up in the adapter sometime in the future. 432 * 433 * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless 434 * of the size or content of the list. {@link #getDropDownBackground()} will fill any space 435 * that is not used by the list. 436 * 437 * @param dropDownAlwaysVisible Whether to keep the drop-down visible. 438 * 439 * @hide Pending API council approval 440 */ 441 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { 442 mDropDownAlwaysVisible = dropDownAlwaysVisible; 443 } 444 445 /** 446 * Checks whether the drop-down is dismissed when a suggestion is clicked. 447 * 448 * @hide Pending API council approval 449 */ 450 public boolean isDropDownDismissedOnCompletion() { 451 return mDropDownDismissedOnCompletion; 452 } 453 454 /** 455 * Sets whether the drop-down is dismissed when a suggestion is clicked. This is 456 * true by default. 457 * 458 * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down. 459 * 460 * @hide Pending API council approval 461 */ 462 public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) { 463 mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion; 464 } 465 466 /** 467 * <p>Returns the number of characters the user must type before the drop 468 * down list is shown.</p> 469 * 470 * @return the minimum number of characters to type to show the drop down 471 * 472 * @see #setThreshold(int) 473 */ 474 public int getThreshold() { 475 return mThreshold; 476 } 477 478 /** 479 * <p>Specifies the minimum number of characters the user has to type in the 480 * edit box before the drop down list is shown.</p> 481 * 482 * <p>When <code>threshold</code> is less than or equals 0, a threshold of 483 * 1 is applied.</p> 484 * 485 * @param threshold the number of characters to type before the drop down 486 * is shown 487 * 488 * @see #getThreshold() 489 * 490 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold 491 */ 492 public void setThreshold(int threshold) { 493 if (threshold <= 0) { 494 threshold = 1; 495 } 496 497 mThreshold = threshold; 498 } 499 500 /** 501 * <p>Sets the listener that will be notified when the user clicks an item 502 * in the drop down list.</p> 503 * 504 * @param l the item click listener 505 */ 506 public void setOnItemClickListener(AdapterView.OnItemClickListener l) { 507 mItemClickListener = l; 508 } 509 510 /** 511 * <p>Sets the listener that will be notified when the user selects an item 512 * in the drop down list.</p> 513 * 514 * @param l the item selected listener 515 */ 516 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) { 517 mItemSelectedListener = l; 518 } 519 520 /** 521 * <p>Returns the listener that is notified whenever the user clicks an item 522 * in the drop down list.</p> 523 * 524 * @return the item click listener 525 * 526 * @deprecated Use {@link #getOnItemClickListener()} intead 527 */ 528 @Deprecated 529 public AdapterView.OnItemClickListener getItemClickListener() { 530 return mItemClickListener; 531 } 532 533 /** 534 * <p>Returns the listener that is notified whenever the user selects an 535 * item in the drop down list.</p> 536 * 537 * @return the item selected listener 538 * 539 * @deprecated Use {@link #getOnItemSelectedListener()} intead 540 */ 541 @Deprecated 542 public AdapterView.OnItemSelectedListener getItemSelectedListener() { 543 return mItemSelectedListener; 544 } 545 546 /** 547 * <p>Returns the listener that is notified whenever the user clicks an item 548 * in the drop down list.</p> 549 * 550 * @return the item click listener 551 */ 552 public AdapterView.OnItemClickListener getOnItemClickListener() { 553 return mItemClickListener; 554 } 555 556 /** 557 * <p>Returns the listener that is notified whenever the user selects an 558 * item in the drop down list.</p> 559 * 560 * @return the item selected listener 561 */ 562 public AdapterView.OnItemSelectedListener getOnItemSelectedListener() { 563 return mItemSelectedListener; 564 } 565 566 /** 567 * <p>Returns a filterable list adapter used for auto completion.</p> 568 * 569 * @return a data adapter used for auto completion 570 */ 571 public ListAdapter getAdapter() { 572 return mAdapter; 573 } 574 575 /** 576 * <p>Changes the list of data used for auto completion. The provided list 577 * must be a filterable list adapter.</p> 578 * 579 * <p>The caller is still responsible for managing any resources used by the adapter. 580 * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified. 581 * A common case is the use of {@link android.widget.CursorAdapter}, which 582 * contains a {@link android.database.Cursor} that must be closed. This can be done 583 * automatically (see 584 * {@link android.app.Activity#startManagingCursor(android.database.Cursor) 585 * startManagingCursor()}), 586 * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p> 587 * 588 * @param adapter the adapter holding the auto completion data 589 * 590 * @see #getAdapter() 591 * @see android.widget.Filterable 592 * @see android.widget.ListAdapter 593 */ 594 public <T extends ListAdapter & Filterable> void setAdapter(T adapter) { 595 mAdapter = adapter; 596 if (mAdapter != null) { 597 //noinspection unchecked 598 mFilter = ((Filterable) mAdapter).getFilter(); 599 } else { 600 mFilter = null; 601 } 602 603 if (mDropDownList != null) { 604 mDropDownList.setAdapter(mAdapter); 605 } 606 } 607 608 @Override 609 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 610 if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing() 611 && !mDropDownAlwaysVisible) { 612 // special case for the back key, we do not even try to send it 613 // to the drop down list but instead, consume it immediately 614 if (event.getAction() == KeyEvent.ACTION_DOWN 615 && event.getRepeatCount() == 0) { 616 getKeyDispatcherState().startTracking(event, this); 617 return true; 618 } else if (event.getAction() == KeyEvent.ACTION_UP) { 619 getKeyDispatcherState().handleUpEvent(event); 620 if (event.isTracking() && !event.isCanceled()) { 621 dismissDropDown(); 622 return true; 623 } 624 } 625 } 626 return super.onKeyPreIme(keyCode, event); 627 } 628 629 @Override 630 public boolean onKeyUp(int keyCode, KeyEvent event) { 631 if (isPopupShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 632 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 633 if (consumed) { 634 switch (keyCode) { 635 // if the list accepts the key events and the key event 636 // was a click, the text view gets the selected item 637 // from the drop down as its content 638 case KeyEvent.KEYCODE_ENTER: 639 case KeyEvent.KEYCODE_DPAD_CENTER: 640 performCompletion(); 641 return true; 642 } 643 } 644 } 645 return super.onKeyUp(keyCode, event); 646 } 647 648 @Override 649 public boolean onKeyDown(int keyCode, KeyEvent event) { 650 // when the drop down is shown, we drive it directly 651 if (isPopupShowing()) { 652 // the key events are forwarded to the list in the drop down view 653 // note that ListView handles space but we don't want that to happen 654 // also if selection is not currently in the drop down, then don't 655 // let center or enter presses go there since that would cause it 656 // to select one of its items 657 if (keyCode != KeyEvent.KEYCODE_SPACE 658 && (mDropDownList.getSelectedItemPosition() >= 0 659 || (keyCode != KeyEvent.KEYCODE_ENTER 660 && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { 661 int curIndex = mDropDownList.getSelectedItemPosition(); 662 boolean consumed; 663 final boolean below = !mPopup.isAboveAnchor(); 664 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= 0) || 665 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= 666 mDropDownList.getAdapter().getCount() - 1)) { 667 // When the selection is at the top, we block the key 668 // event to prevent focus from moving. 669 clearListSelection(); 670 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 671 showDropDown(); 672 return true; 673 } else { 674 // WARNING: Please read the comment where mListSelectionHidden 675 // is declared 676 mDropDownList.mListSelectionHidden = false; 677 } 678 679 consumed = mDropDownList.onKeyDown(keyCode, event); 680 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 681 682 if (consumed) { 683 // If it handled the key event, then the user is 684 // navigating in the list, so we should put it in front. 685 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 686 // Here's a little trick we need to do to make sure that 687 // the list view is actually showing its focus indicator, 688 // by ensuring it has focus and getting its window out 689 // of touch mode. 690 mDropDownList.requestFocusFromTouch(); 691 showDropDown(); 692 693 switch (keyCode) { 694 // avoid passing the focus from the text view to the 695 // next component 696 case KeyEvent.KEYCODE_ENTER: 697 case KeyEvent.KEYCODE_DPAD_CENTER: 698 case KeyEvent.KEYCODE_DPAD_DOWN: 699 case KeyEvent.KEYCODE_DPAD_UP: 700 return true; 701 } 702 } else { 703 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 704 // when the selection is at the bottom, we block the 705 // event to avoid going to the next focusable widget 706 Adapter adapter = mDropDownList.getAdapter(); 707 if (adapter != null && curIndex == adapter.getCount() - 1) { 708 return true; 709 } 710 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex == 0) { 711 return true; 712 } 713 } 714 } 715 } else { 716 switch(keyCode) { 717 case KeyEvent.KEYCODE_DPAD_DOWN: 718 performValidation(); 719 } 720 } 721 722 mLastKeyCode = keyCode; 723 boolean handled = super.onKeyDown(keyCode, event); 724 mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 725 726 if (handled && isPopupShowing() && mDropDownList != null) { 727 clearListSelection(); 728 } 729 730 return handled; 731 } 732 733 /** 734 * Returns <code>true</code> if the amount of text in the field meets 735 * or exceeds the {@link #getThreshold} requirement. You can override 736 * this to impose a different standard for when filtering will be 737 * triggered. 738 */ 739 public boolean enoughToFilter() { 740 if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length() 741 + " threshold=" + mThreshold); 742 return getText().length() >= mThreshold; 743 } 744 745 /** 746 * This is used to watch for edits to the text view. Note that we call 747 * to methods on the auto complete text view class so that we can access 748 * private vars without going through thunks. 749 */ 750 private class MyWatcher implements TextWatcher { 751 public void afterTextChanged(Editable s) { 752 doAfterTextChanged(); 753 } 754 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 755 doBeforeTextChanged(); 756 } 757 public void onTextChanged(CharSequence s, int start, int before, int count) { 758 } 759 } 760 761 void doBeforeTextChanged() { 762 if (mBlockCompletion) return; 763 764 // when text is changed, inserted or deleted, we attempt to show 765 // the drop down 766 mOpenBefore = isPopupShowing(); 767 if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore); 768 } 769 770 void doAfterTextChanged() { 771 if (mBlockCompletion) return; 772 773 // if the list was open before the keystroke, but closed afterwards, 774 // then something in the keystroke processing (an input filter perhaps) 775 // called performCompletion() and we shouldn't do any more processing. 776 if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore 777 + " open=" + isPopupShowing()); 778 if (mOpenBefore && !isPopupShowing()) { 779 return; 780 } 781 782 // the drop down is shown only when a minimum number of characters 783 // was typed in the text view 784 if (enoughToFilter()) { 785 if (mFilter != null) { 786 performFiltering(getText(), mLastKeyCode); 787 } 788 } else { 789 // drop down is automatically dismissed when enough characters 790 // are deleted from the text view 791 if (!mDropDownAlwaysVisible) dismissDropDown(); 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 */ 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 */ 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 */ 823 public void clearListSelection() { 824 final DropDownListView list = mDropDownList; 825 if (list != null) { 826 // WARNING: Please read the comment where mListSelectionHidden is declared 827 list.mListSelectionHidden = true; 828 list.hideSelector(); 829 list.requestLayout(); 830 } 831 } 832 833 /** 834 * Set the position of the dropdown view selection. 835 * 836 * @param position The position to move the selector to. 837 */ 838 public void setListSelection(int position) { 839 if (mPopup.isShowing() && (mDropDownList != null)) { 840 mDropDownList.mListSelectionHidden = false; 841 mDropDownList.setSelection(position); 842 // ListView.setSelection() will call requestLayout() 843 } 844 } 845 846 /** 847 * Get the position of the dropdown view selection, if there is one. Returns 848 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if 849 * there is no selection. 850 * 851 * @return the position of the current selection, if there is one, or 852 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not. 853 * 854 * @see ListView#getSelectedItemPosition() 855 */ 856 public int getListSelection() { 857 if (mPopup.isShowing() && (mDropDownList != null)) { 858 return mDropDownList.getSelectedItemPosition(); 859 } 860 return ListView.INVALID_POSITION; 861 } 862 863 864 /** 865 * @hide 866 * @return {@link android.widget.ListView#getChildCount()} of the drop down if it is showing, 867 * otherwise 0. 868 */ 869 protected int getDropDownChildCount() { 870 return mDropDownList == null ? 0 : mDropDownList.getChildCount(); 871 } 872 873 /** 874 * <p>Starts filtering the content of the drop down list. The filtering 875 * pattern is the content of the edit box. Subclasses should override this 876 * method to filter with a different pattern, for instance a substring of 877 * <code>text</code>.</p> 878 * 879 * @param text the filtering pattern 880 * @param keyCode the last character inserted in the edit box; beware that 881 * this will be null when text is being added through a soft input method. 882 */ 883 @SuppressWarnings({ "UnusedDeclaration" }) 884 protected void performFiltering(CharSequence text, int keyCode) { 885 mFilter.filter(text, this); 886 } 887 888 /** 889 * <p>Performs the text completion by converting the selected item from 890 * the drop down list into a string, replacing the text box's content with 891 * this string and finally dismissing the drop down menu.</p> 892 */ 893 public void performCompletion() { 894 performCompletion(null, -1, -1); 895 } 896 897 @Override 898 public void onCommitCompletion(CompletionInfo completion) { 899 if (isPopupShowing()) { 900 mBlockCompletion = true; 901 replaceText(completion.getText()); 902 mBlockCompletion = false; 903 904 if (mItemClickListener != null) { 905 final DropDownListView list = mDropDownList; 906 // Note that we don't have a View here, so we will need to 907 // supply null. Hopefully no existing apps crash... 908 mItemClickListener.onItemClick(list, null, completion.getPosition(), 909 completion.getId()); 910 } 911 } 912 } 913 914 private void performCompletion(View selectedView, int position, long id) { 915 if (isPopupShowing()) { 916 Object selectedItem; 917 if (position < 0) { 918 selectedItem = mDropDownList.getSelectedItem(); 919 } else { 920 selectedItem = mAdapter.getItem(position); 921 } 922 if (selectedItem == null) { 923 Log.w(TAG, "performCompletion: no selected item"); 924 return; 925 } 926 927 mBlockCompletion = true; 928 replaceText(convertSelectionToString(selectedItem)); 929 mBlockCompletion = false; 930 931 if (mItemClickListener != null) { 932 final DropDownListView list = mDropDownList; 933 934 if (selectedView == null || position < 0) { 935 selectedView = list.getSelectedView(); 936 position = list.getSelectedItemPosition(); 937 id = list.getSelectedItemId(); 938 } 939 mItemClickListener.onItemClick(list, selectedView, position, id); 940 } 941 } 942 943 if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) { 944 dismissDropDown(); 945 } 946 } 947 948 /** 949 * Identifies whether the view is currently performing a text completion, so subclasses 950 * can decide whether to respond to text changed events. 951 */ 952 public boolean isPerformingCompletion() { 953 return mBlockCompletion; 954 } 955 956 /** 957 * Like {@link #setText(CharSequence)}, except that it can disable filtering. 958 * 959 * @param filter If <code>false</code>, no filtering will be performed 960 * as a result of this call. 961 * 962 * @hide Pending API council approval. 963 */ 964 public void setText(CharSequence text, boolean filter) { 965 if (filter) { 966 setText(text); 967 } else { 968 mBlockCompletion = true; 969 setText(text); 970 mBlockCompletion = false; 971 } 972 } 973 974 /** 975 * Like {@link #setTextKeepState(CharSequence)}, except that it can disable filtering. 976 * 977 * @param filter If <code>false</code>, no filtering will be performed 978 * as a result of this call. 979 * 980 * @hide Pending API council approval. 981 */ 982 public void setTextKeepState(CharSequence text, boolean filter) { 983 if (filter) { 984 setTextKeepState(text); 985 } else { 986 mBlockCompletion = true; 987 setTextKeepState(text); 988 mBlockCompletion = false; 989 } 990 } 991 992 /** 993 * <p>Performs the text completion by replacing the current text by the 994 * selected item. Subclasses should override this method to avoid replacing 995 * the whole content of the edit box.</p> 996 * 997 * @param text the selected suggestion in the drop down list 998 */ 999 protected void replaceText(CharSequence text) { 1000 clearComposingText(); 1001 1002 setText(text); 1003 // make sure we keep the caret at the end of the text view 1004 Editable spannable = getText(); 1005 Selection.setSelection(spannable, spannable.length()); 1006 } 1007 1008 /** {@inheritDoc} */ 1009 public void onFilterComplete(int count) { 1010 // Not attached to window, don't update drop-down 1011 if (getWindowVisibility() == View.GONE) return; 1012 1013 /* 1014 * This checks enoughToFilter() again because filtering requests 1015 * are asynchronous, so the result may come back after enough text 1016 * has since been deleted to make it no longer appropriate 1017 * to filter. 1018 */ 1019 1020 if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) { 1021 if (hasFocus() && hasWindowFocus()) { 1022 showDropDown(); 1023 } 1024 } else if (!mDropDownAlwaysVisible) { 1025 dismissDropDown(); 1026 } 1027 } 1028 1029 @Override 1030 public void onWindowFocusChanged(boolean hasWindowFocus) { 1031 super.onWindowFocusChanged(hasWindowFocus); 1032 if (!hasWindowFocus && !mDropDownAlwaysVisible) { 1033 dismissDropDown(); 1034 } 1035 } 1036 1037 @Override 1038 protected void onDisplayHint(int hint) { 1039 super.onDisplayHint(hint); 1040 switch (hint) { 1041 case INVISIBLE: 1042 if (!mDropDownAlwaysVisible) { 1043 dismissDropDown(); 1044 } 1045 break; 1046 } 1047 } 1048 1049 @Override 1050 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 1051 super.onFocusChanged(focused, direction, previouslyFocusedRect); 1052 // Perform validation if the view is losing focus. 1053 if (!focused) { 1054 performValidation(); 1055 } 1056 if (!focused && !mDropDownAlwaysVisible) { 1057 dismissDropDown(); 1058 } 1059 } 1060 1061 @Override 1062 protected void onAttachedToWindow() { 1063 super.onAttachedToWindow(); 1064 } 1065 1066 @Override 1067 protected void onDetachedFromWindow() { 1068 dismissDropDown(); 1069 super.onDetachedFromWindow(); 1070 } 1071 1072 /** 1073 * <p>Closes the drop down if present on screen.</p> 1074 */ 1075 public void dismissDropDown() { 1076 InputMethodManager imm = InputMethodManager.peekInstance(); 1077 if (imm != null) { 1078 imm.displayCompletions(this, null); 1079 } 1080 mPopup.dismiss(); 1081 mPopup.setContentView(null); 1082 mDropDownList = null; 1083 } 1084 1085 @Override 1086 protected boolean setFrame(final int l, int t, final int r, int b) { 1087 boolean result = super.setFrame(l, t, r, b); 1088 1089 if (mPopup.isShowing()) { 1090 showDropDown(); 1091 } 1092 1093 return result; 1094 } 1095 1096 /** 1097 * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of 1098 * the id is NO_ID or we can't find a view for the given id, we return this TextView as 1099 * the default anchoring point.</p> 1100 */ 1101 private View getDropDownAnchorView() { 1102 if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) { 1103 mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId); 1104 } 1105 return mDropDownAnchorView == null ? this : mDropDownAnchorView; 1106 } 1107 1108 /** 1109 * Issues a runnable to show the dropdown as soon as possible. 1110 * 1111 * @hide internal used only by SearchDialog 1112 */ 1113 public void showDropDownAfterLayout() { 1114 post(mShowDropDownRunnable); 1115 } 1116 1117 /** 1118 * Ensures that the drop down is not obscuring the IME. 1119 * @param visible whether the ime should be in front. If false, the ime is pushed to 1120 * the background. 1121 * @hide internal used only here and SearchDialog 1122 */ 1123 public void ensureImeVisible(boolean visible) { 1124 mPopup.setInputMethodMode(visible 1125 ? PopupWindow.INPUT_METHOD_NEEDED : PopupWindow.INPUT_METHOD_NOT_NEEDED); 1126 showDropDown(); 1127 } 1128 1129 /** 1130 * @hide internal used only here and SearchDialog 1131 */ 1132 public boolean isInputMethodNotNeeded() { 1133 return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1134 } 1135 1136 /** 1137 * <p>Displays the drop down on screen.</p> 1138 */ 1139 public void showDropDown() { 1140 int height = buildDropDown(); 1141 1142 int widthSpec = 0; 1143 int heightSpec = 0; 1144 1145 boolean noInputMethod = isInputMethodNotNeeded(); 1146 1147 if (mPopup.isShowing()) { 1148 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 1149 // The call to PopupWindow's update method below can accept -1 for any 1150 // value you do not want to update. 1151 widthSpec = -1; 1152 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 1153 widthSpec = getDropDownAnchorView().getWidth(); 1154 } else { 1155 widthSpec = mDropDownWidth; 1156 } 1157 1158 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1159 // The call to PopupWindow's update method below can accept -1 for any 1160 // value you do not want to update. 1161 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; 1162 if (noInputMethod) { 1163 mPopup.setWindowLayoutMode( 1164 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 1165 ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); 1166 } else { 1167 mPopup.setWindowLayoutMode( 1168 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 1169 ViewGroup.LayoutParams.MATCH_PARENT : 0, 1170 ViewGroup.LayoutParams.MATCH_PARENT); 1171 } 1172 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 1173 heightSpec = height; 1174 } else { 1175 heightSpec = mDropDownHeight; 1176 } 1177 1178 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 1179 1180 mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, 1181 mDropDownVerticalOffset, widthSpec, heightSpec); 1182 } else { 1183 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 1184 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; 1185 } else { 1186 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 1187 mPopup.setWidth(getDropDownAnchorView().getWidth()); 1188 } else { 1189 mPopup.setWidth(mDropDownWidth); 1190 } 1191 } 1192 1193 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1194 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; 1195 } else { 1196 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 1197 mPopup.setHeight(height); 1198 } else { 1199 mPopup.setHeight(mDropDownHeight); 1200 } 1201 } 1202 1203 mPopup.setWindowLayoutMode(widthSpec, heightSpec); 1204 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 1205 1206 // use outside touchable to dismiss drop down when touching outside of it, so 1207 // only set this if the dropdown is not always visible 1208 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 1209 mPopup.setTouchInterceptor(new PopupTouchInterceptor()); 1210 mPopup.showAsDropDown(getDropDownAnchorView(), 1211 mDropDownHorizontalOffset, mDropDownVerticalOffset); 1212 mDropDownList.setSelection(ListView.INVALID_POSITION); 1213 clearListSelection(); 1214 post(mHideSelector); 1215 } 1216 } 1217 1218 /** 1219 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 1220 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we 1221 * ignore outside touch even when the drop down is not set to always visible. 1222 * 1223 * @hide used only by SearchDialog 1224 */ 1225 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 1226 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; 1227 } 1228 1229 /** 1230 * <p>Builds the popup window's content and returns the height the popup 1231 * should have. Returns -1 when the content already exists.</p> 1232 * 1233 * @return the content's height or -1 if content already exists 1234 */ 1235 private int buildDropDown() { 1236 ViewGroup dropDownView; 1237 int otherHeights = 0; 1238 1239 if (mAdapter != null) { 1240 InputMethodManager imm = InputMethodManager.peekInstance(); 1241 if (imm != null) { 1242 int N = mAdapter.getCount(); 1243 if (N > 20) N = 20; 1244 CompletionInfo[] completions = new CompletionInfo[N]; 1245 for (int i = 0; i < N; i++) { 1246 Object item = mAdapter.getItem(i); 1247 long id = mAdapter.getItemId(i); 1248 completions[i] = new CompletionInfo(id, i, 1249 convertSelectionToString(item)); 1250 } 1251 imm.displayCompletions(this, completions); 1252 } 1253 } 1254 1255 if (mDropDownList == null) { 1256 Context context = getContext(); 1257 1258 mHideSelector = new ListSelectorHider(); 1259 1260 /** 1261 * This Runnable exists for the sole purpose of checking if the view layout has got 1262 * completed and if so call showDropDown to display the drop down. This is used to show 1263 * the drop down as soon as possible after user opens up the search dialog, without 1264 * waiting for the normal UI pipeline to do it's job which is slower than this method. 1265 */ 1266 mShowDropDownRunnable = new Runnable() { 1267 public void run() { 1268 // View layout should be all done before displaying the drop down. 1269 View view = getDropDownAnchorView(); 1270 if (view != null && view.getWindowToken() != null) { 1271 showDropDown(); 1272 } 1273 } 1274 }; 1275 1276 mDropDownList = new DropDownListView(context); 1277 mDropDownList.setSelector(mDropDownListHighlight); 1278 mDropDownList.setAdapter(mAdapter); 1279 mDropDownList.setVerticalFadingEdgeEnabled(true); 1280 mDropDownList.setOnItemClickListener(mDropDownItemClickListener); 1281 mDropDownList.setFocusable(true); 1282 mDropDownList.setFocusableInTouchMode(true); 1283 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 1284 public void onItemSelected(AdapterView<?> parent, View view, 1285 int position, long id) { 1286 1287 if (position != -1) { 1288 DropDownListView dropDownList = mDropDownList; 1289 1290 if (dropDownList != null) { 1291 dropDownList.mListSelectionHidden = false; 1292 } 1293 } 1294 } 1295 1296 public void onNothingSelected(AdapterView<?> parent) { 1297 } 1298 }); 1299 1300 if (mItemSelectedListener != null) { 1301 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1302 } 1303 1304 dropDownView = mDropDownList; 1305 1306 View hintView = getHintView(context); 1307 if (hintView != null) { 1308 // if an hint has been specified, we accomodate more space for it and 1309 // add a text view in the drop down menu, at the bottom of the list 1310 LinearLayout hintContainer = new LinearLayout(context); 1311 hintContainer.setOrientation(LinearLayout.VERTICAL); 1312 1313 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1314 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f 1315 ); 1316 hintContainer.addView(dropDownView, hintParams); 1317 hintContainer.addView(hintView); 1318 1319 // measure the hint's height to find how much more vertical space 1320 // we need to add to the drop down's height 1321 int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST); 1322 int heightSpec = MeasureSpec.UNSPECIFIED; 1323 hintView.measure(widthSpec, heightSpec); 1324 1325 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1326 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1327 + hintParams.bottomMargin; 1328 1329 dropDownView = hintContainer; 1330 } 1331 1332 mPopup.setContentView(dropDownView); 1333 } else { 1334 dropDownView = (ViewGroup) mPopup.getContentView(); 1335 final View view = dropDownView.findViewById(HINT_VIEW_ID); 1336 if (view != null) { 1337 LinearLayout.LayoutParams hintParams = 1338 (LinearLayout.LayoutParams) view.getLayoutParams(); 1339 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1340 + hintParams.bottomMargin; 1341 } 1342 } 1343 1344 // Max height available on the screen for a popup. 1345 boolean ignoreBottomDecorations = 1346 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1347 final int maxHeight = mPopup.getMaxAvailableHeight( 1348 getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); 1349 1350 // getMaxAvailableHeight() subtracts the padding, so we put it back, 1351 // to get the available height for the whole window 1352 int padding = 0; 1353 Drawable background = mPopup.getBackground(); 1354 if (background != null) { 1355 background.getPadding(mTempRect); 1356 padding = mTempRect.top + mTempRect.bottom; 1357 } 1358 1359 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1360 return maxHeight + padding; 1361 } 1362 1363 final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, 1364 0, ListView.NO_POSITION, maxHeight - otherHeights, 2); 1365 // add padding only if the list has items in it, that way we don't show 1366 // the popup if it is not needed 1367 if (listContent > 0) otherHeights += padding; 1368 1369 return listContent + otherHeights; 1370 } 1371 1372 private View getHintView(Context context) { 1373 if (mHintText != null && mHintText.length() > 0) { 1374 final TextView hintView = (TextView) LayoutInflater.from(context).inflate( 1375 mHintResource, null).findViewById(com.android.internal.R.id.text1); 1376 hintView.setText(mHintText); 1377 hintView.setId(HINT_VIEW_ID); 1378 return hintView; 1379 } else { 1380 return null; 1381 } 1382 } 1383 1384 /** 1385 * Sets the validator used to perform text validation. 1386 * 1387 * @param validator The validator used to validate the text entered in this widget. 1388 * 1389 * @see #getValidator() 1390 * @see #performValidation() 1391 */ 1392 public void setValidator(Validator validator) { 1393 mValidator = validator; 1394 } 1395 1396 /** 1397 * Returns the Validator set with {@link #setValidator}, 1398 * or <code>null</code> if it was not set. 1399 * 1400 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1401 * @see #performValidation() 1402 */ 1403 public Validator getValidator() { 1404 return mValidator; 1405 } 1406 1407 /** 1408 * If a validator was set on this view and the current string is not valid, 1409 * ask the validator to fix it. 1410 * 1411 * @see #getValidator() 1412 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1413 */ 1414 public void performValidation() { 1415 if (mValidator == null) return; 1416 1417 CharSequence text = getText(); 1418 1419 if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { 1420 setText(mValidator.fixText(text)); 1421 } 1422 } 1423 1424 /** 1425 * Returns the Filter obtained from {@link Filterable#getFilter}, 1426 * or <code>null</code> if {@link #setAdapter} was not called with 1427 * a Filterable. 1428 */ 1429 protected Filter getFilter() { 1430 return mFilter; 1431 } 1432 1433 private class ListSelectorHider implements Runnable { 1434 public void run() { 1435 clearListSelection(); 1436 } 1437 } 1438 1439 private class PopupTouchInterceptor implements OnTouchListener { 1440 public boolean onTouch(View v, MotionEvent event) { 1441 if (event.getAction() == MotionEvent.ACTION_DOWN && 1442 mPopup != null && mPopup.isShowing()) { 1443 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1444 showDropDown(); 1445 } 1446 return false; 1447 } 1448 } 1449 1450 private class DropDownItemClickListener implements AdapterView.OnItemClickListener { 1451 public void onItemClick(AdapterView parent, View v, int position, long id) { 1452 performCompletion(v, position, id); 1453 } 1454 } 1455 1456 /** 1457 * <p>Wrapper class for a ListView. This wrapper hijacks the focus to 1458 * make sure the list uses the appropriate drawables and states when 1459 * displayed on screen within a drop down. The focus is never actually 1460 * passed to the drop down; the list only looks focused.</p> 1461 */ 1462 private static class DropDownListView extends ListView { 1463 /* 1464 * WARNING: This is a workaround for a touch mode issue. 1465 * 1466 * Touch mode is propagated lazily to windows. This causes problems in 1467 * the following scenario: 1468 * - Type something in the AutoCompleteTextView and get some results 1469 * - Move down with the d-pad to select an item in the list 1470 * - Move up with the d-pad until the selection disappears 1471 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 1472 * and get new results; you are now in touch mode 1473 * - The selection comes back on the first item in the list, even though 1474 * the list is supposed to be in touch mode 1475 * 1476 * Using the soft keyboard triggers the touch mode change but that change 1477 * is propagated to our window only after the first list layout, therefore 1478 * after the list attempts to resurrect the selection. 1479 * 1480 * The trick to work around this issue is to pretend the list is in touch 1481 * mode when we know that the selection should not appear, that is when 1482 * we know the user moved the selection away from the list. 1483 * 1484 * This boolean is set to true whenever we explicitely hide the list's 1485 * selection and reset to false whenver we know the user moved the 1486 * selection back to the list. 1487 * 1488 * When this boolean is true, isInTouchMode() returns true, otherwise it 1489 * returns super.isInTouchMode(). 1490 */ 1491 private boolean mListSelectionHidden; 1492 1493 /** 1494 * <p>Creates a new list view wrapper.</p> 1495 * 1496 * @param context this view's context 1497 */ 1498 public DropDownListView(Context context) { 1499 super(context, null, com.android.internal.R.attr.dropDownListViewStyle); 1500 } 1501 1502 /** 1503 * <p>Avoids jarring scrolling effect by ensuring that list elements 1504 * made of a text view fit on a single line.</p> 1505 * 1506 * @param position the item index in the list to get a view for 1507 * @return the view for the specified item 1508 */ 1509 @Override 1510 View obtainView(int position, boolean[] isScrap) { 1511 View view = super.obtainView(position, isScrap); 1512 1513 if (view instanceof TextView) { 1514 ((TextView) view).setHorizontallyScrolling(true); 1515 } 1516 1517 return view; 1518 } 1519 1520 /** 1521 * <p>Returns the top padding of the currently selected view.</p> 1522 * 1523 * @return the height of the top padding for the selection 1524 */ 1525 public int getSelectionPaddingTop() { 1526 return mSelectionTopPadding; 1527 } 1528 1529 /** 1530 * <p>Returns the bottom padding of the currently selected view.</p> 1531 * 1532 * @return the height of the bottom padding for the selection 1533 */ 1534 public int getSelectionPaddingBottom() { 1535 return mSelectionBottomPadding; 1536 } 1537 1538 @Override 1539 public boolean isInTouchMode() { 1540 // WARNING: Please read the comment where mListSelectionHidden is declared 1541 return mListSelectionHidden || super.isInTouchMode(); 1542 } 1543 1544 /** 1545 * <p>Returns the focus state in the drop down.</p> 1546 * 1547 * @return true always 1548 */ 1549 @Override 1550 public boolean hasWindowFocus() { 1551 return true; 1552 } 1553 1554 /** 1555 * <p>Returns the focus state in the drop down.</p> 1556 * 1557 * @return true always 1558 */ 1559 @Override 1560 public boolean isFocused() { 1561 return true; 1562 } 1563 1564 /** 1565 * <p>Returns the focus state in the drop down.</p> 1566 * 1567 * @return true always 1568 */ 1569 @Override 1570 public boolean hasFocus() { 1571 return true; 1572 } 1573 1574 protected int[] onCreateDrawableState(int extraSpace) { 1575 int[] res = super.onCreateDrawableState(extraSpace); 1576 //noinspection ConstantIfStatement 1577 if (false) { 1578 StringBuilder sb = new StringBuilder("Created drawable state: ["); 1579 for (int i=0; i<res.length; i++) { 1580 if (i > 0) sb.append(", "); 1581 sb.append("0x"); 1582 sb.append(Integer.toHexString(res[i])); 1583 } 1584 sb.append("]"); 1585 Log.i(TAG, sb.toString()); 1586 } 1587 return res; 1588 } 1589 } 1590 1591 /** 1592 * This interface is used to make sure that the text entered in this TextView complies to 1593 * a certain format. Since there is no foolproof way to prevent the user from leaving 1594 * this View with an incorrect value in it, all we can do is try to fix it ourselves 1595 * when this happens. 1596 */ 1597 public interface Validator { 1598 /** 1599 * Validates the specified text. 1600 * 1601 * @return true If the text currently in the text editor is valid. 1602 * 1603 * @see #fixText(CharSequence) 1604 */ 1605 boolean isValid(CharSequence text); 1606 1607 /** 1608 * Corrects the specified text to make it valid. 1609 * 1610 * @param invalidText A string that doesn't pass validation: isValid(invalidText) 1611 * returns false 1612 * 1613 * @return A string based on invalidText such as invoking isValid() on it returns true. 1614 * 1615 * @see #isValid(CharSequence) 1616 */ 1617 CharSequence fixText(CharSequence invalidText); 1618 } 1619 1620 /** 1621 * Allows us a private hook into the on click event without preventing users from setting 1622 * their own click listener. 1623 */ 1624 private class PassThroughClickListener implements OnClickListener { 1625 1626 private View.OnClickListener mWrapped; 1627 1628 /** {@inheritDoc} */ 1629 public void onClick(View v) { 1630 onClickImpl(); 1631 1632 if (mWrapped != null) mWrapped.onClick(v); 1633 } 1634 } 1635 1636} 1637