1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.database.DataSetObserver; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.text.Editable; 25import android.text.Selection; 26import android.text.TextUtils; 27import android.text.TextWatcher; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.KeyEvent; 31import android.view.LayoutInflater; 32import android.view.MotionEvent; 33import android.view.View; 34import android.view.ViewGroup; 35import android.view.WindowManager; 36import android.view.inputmethod.CompletionInfo; 37import android.view.inputmethod.InputMethodManager; 38import android.view.inputmethod.EditorInfo; 39 40import com.android.internal.R; 41 42 43/** 44 * <p>An editable text view that shows completion suggestions automatically 45 * while the user is typing. The list of suggestions is displayed in a drop 46 * down menu from which the user can choose an item to replace the content 47 * of the edit box with.</p> 48 * 49 * <p>The drop down can be dismissed at any time by pressing the back key or, 50 * if no item is selected in the drop down, by pressing the enter/dpad center 51 * key.</p> 52 * 53 * <p>The list of suggestions is obtained from a data adapter and appears 54 * only after a given number of characters defined by 55 * {@link #getThreshold() the threshold}.</p> 56 * 57 * <p>The following code snippet shows how to create a text view which suggests 58 * various countries names while the user is typing:</p> 59 * 60 * <pre class="prettyprint"> 61 * public class CountriesActivity extends Activity { 62 * protected void onCreate(Bundle icicle) { 63 * super.onCreate(icicle); 64 * setContentView(R.layout.countries); 65 * 66 * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, 67 * android.R.layout.simple_dropdown_item_1line, COUNTRIES); 68 * AutoCompleteTextView textView = (AutoCompleteTextView) 69 * findViewById(R.id.countries_list); 70 * textView.setAdapter(adapter); 71 * } 72 * 73 * private static final String[] COUNTRIES = new String[] { 74 * "Belgium", "France", "Italy", "Germany", "Spain" 75 * }; 76 * } 77 * </pre> 78 * 79 * @attr ref android.R.styleable#AutoCompleteTextView_completionHint 80 * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold 81 * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView 82 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector 83 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor 84 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth 85 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight 86 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownVerticalOffset 87 * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHorizontalOffset 88 */ 89public class AutoCompleteTextView extends EditText implements Filter.FilterListener { 90 static final boolean DEBUG = false; 91 static final String TAG = "AutoCompleteTextView"; 92 93 private static final int HINT_VIEW_ID = 0x17; 94 95 /** 96 * This value controls the length of time that the user 97 * must leave a pointer down without scrolling to expand 98 * the autocomplete dropdown list to cover the IME. 99 */ 100 private static final int EXPAND_LIST_TIMEOUT = 250; 101 102 private CharSequence mHintText; 103 private int mHintResource; 104 105 private ListAdapter mAdapter; 106 private Filter mFilter; 107 private int mThreshold; 108 109 private PopupWindow mPopup; 110 private DropDownListView mDropDownList; 111 private int mDropDownVerticalOffset; 112 private int mDropDownHorizontalOffset; 113 private int mDropDownAnchorId; 114 private View mDropDownAnchorView; // view is retrieved lazily from id once needed 115 private int mDropDownWidth; 116 private int mDropDownHeight; 117 private final Rect mTempRect = new Rect(); 118 119 private Drawable mDropDownListHighlight; 120 121 private AdapterView.OnItemClickListener mItemClickListener; 122 private AdapterView.OnItemSelectedListener mItemSelectedListener; 123 124 private final DropDownItemClickListener mDropDownItemClickListener = 125 new DropDownItemClickListener(); 126 127 private boolean mDropDownAlwaysVisible = false; 128 129 private boolean mDropDownDismissedOnCompletion = true; 130 131 private boolean mForceIgnoreOutsideTouch = false; 132 133 private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 134 private boolean mOpenBefore; 135 136 private Validator mValidator = null; 137 138 private boolean mBlockCompletion; 139 140 private ListSelectorHider mHideSelector; 141 private Runnable mShowDropDownRunnable; 142 private Runnable mResizePopupRunnable = new ResizePopupRunnable(); 143 144 private PassThroughClickListener mPassThroughClickListener; 145 private PopupDataSetObserver mObserver; 146 147 public AutoCompleteTextView(Context context) { 148 this(context, null); 149 } 150 151 public AutoCompleteTextView(Context context, AttributeSet attrs) { 152 this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle); 153 } 154 155 public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) { 156 super(context, attrs, defStyle); 157 158 mPopup = new PopupWindow(context, attrs, 159 com.android.internal.R.attr.autoCompleteTextViewStyle); 160 mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); 161 162 TypedArray a = 163 context.obtainStyledAttributes( 164 attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0); 165 166 mThreshold = a.getInt( 167 R.styleable.AutoCompleteTextView_completionThreshold, 2); 168 169 mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint); 170 171 mDropDownListHighlight = a.getDrawable( 172 R.styleable.AutoCompleteTextView_dropDownSelector); 173 mDropDownVerticalOffset = (int) 174 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f); 175 mDropDownHorizontalOffset = (int) 176 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f); 177 178 // Get the anchor's id now, but the view won't be ready, so wait to actually get the 179 // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later. 180 // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return 181 // this TextView, as a default anchoring point. 182 mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor, 183 View.NO_ID); 184 185 // For dropdown width, the developer can specify a specific width, or MATCH_PARENT 186 // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). 187 mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, 188 ViewGroup.LayoutParams.WRAP_CONTENT); 189 mDropDownHeight = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight, 190 ViewGroup.LayoutParams.WRAP_CONTENT); 191 192 mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, 193 R.layout.simple_dropdown_hint); 194 195 // Always turn on the auto complete input type flag, since it 196 // makes no sense to use this widget without it. 197 int inputType = getInputType(); 198 if ((inputType&EditorInfo.TYPE_MASK_CLASS) 199 == EditorInfo.TYPE_CLASS_TEXT) { 200 inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE; 201 setRawInputType(inputType); 202 } 203 204 a.recycle(); 205 206 setFocusable(true); 207 208 addTextChangedListener(new MyWatcher()); 209 210 mPassThroughClickListener = new PassThroughClickListener(); 211 super.setOnClickListener(mPassThroughClickListener); 212 } 213 214 @Override 215 public void setOnClickListener(OnClickListener listener) { 216 mPassThroughClickListener.mWrapped = listener; 217 } 218 219 /** 220 * Private hook into the on click event, dispatched from {@link PassThroughClickListener} 221 */ 222 private void onClickImpl() { 223 // If the dropdown is showing, bring the keyboard to the front 224 // when the user touches the text field. 225 if (mPopup.isShowing()) { 226 ensureImeVisible(true); 227 } 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 if (mObserver == null) { 596 mObserver = new PopupDataSetObserver(); 597 } else if (mAdapter != null) { 598 mAdapter.unregisterDataSetObserver(mObserver); 599 } 600 mAdapter = adapter; 601 if (mAdapter != null) { 602 //noinspection unchecked 603 mFilter = ((Filterable) mAdapter).getFilter(); 604 adapter.registerDataSetObserver(mObserver); 605 } else { 606 mFilter = null; 607 } 608 609 if (mDropDownList != null) { 610 mDropDownList.setAdapter(mAdapter); 611 } 612 } 613 614 @Override 615 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 616 if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing() 617 && !mDropDownAlwaysVisible) { 618 // special case for the back key, we do not even try to send it 619 // to the drop down list but instead, consume it immediately 620 if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { 621 getKeyDispatcherState().startTracking(event, this); 622 return true; 623 } else if (event.getAction() == KeyEvent.ACTION_UP) { 624 getKeyDispatcherState().handleUpEvent(event); 625 if (event.isTracking() && !event.isCanceled()) { 626 dismissDropDown(); 627 return true; 628 } 629 } 630 } 631 return super.onKeyPreIme(keyCode, event); 632 } 633 634 @Override 635 public boolean onKeyUp(int keyCode, KeyEvent event) { 636 if (isPopupShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 637 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 638 if (consumed) { 639 switch (keyCode) { 640 // if the list accepts the key events and the key event 641 // was a click, the text view gets the selected item 642 // from the drop down as its content 643 case KeyEvent.KEYCODE_ENTER: 644 case KeyEvent.KEYCODE_DPAD_CENTER: 645 performCompletion(); 646 return true; 647 } 648 } 649 } 650 return super.onKeyUp(keyCode, event); 651 } 652 653 @Override 654 public boolean onKeyDown(int keyCode, KeyEvent event) { 655 // when the drop down is shown, we drive it directly 656 if (isPopupShowing()) { 657 // the key events are forwarded to the list in the drop down view 658 // note that ListView handles space but we don't want that to happen 659 // also if selection is not currently in the drop down, then don't 660 // let center or enter presses go there since that would cause it 661 // to select one of its items 662 if (keyCode != KeyEvent.KEYCODE_SPACE 663 && (mDropDownList.getSelectedItemPosition() >= 0 664 || (keyCode != KeyEvent.KEYCODE_ENTER 665 && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { 666 int curIndex = mDropDownList.getSelectedItemPosition(); 667 boolean consumed; 668 669 final boolean below = !mPopup.isAboveAnchor(); 670 671 final ListAdapter adapter = mAdapter; 672 673 boolean allEnabled; 674 int firstItem = Integer.MAX_VALUE; 675 int lastItem = Integer.MIN_VALUE; 676 677 if (adapter != null) { 678 allEnabled = adapter.areAllItemsEnabled(); 679 firstItem = allEnabled ? 0 : 680 mDropDownList.lookForSelectablePosition(0, true); 681 lastItem = allEnabled ? adapter.getCount() - 1 : 682 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); 683 } 684 685 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || 686 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { 687 // When the selection is at the top, we block the key 688 // event to prevent focus from moving. 689 clearListSelection(); 690 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 691 showDropDown(); 692 return true; 693 } else { 694 // WARNING: Please read the comment where mListSelectionHidden 695 // is declared 696 mDropDownList.mListSelectionHidden = false; 697 } 698 699 consumed = mDropDownList.onKeyDown(keyCode, event); 700 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 701 702 if (consumed) { 703 // If it handled the key event, then the user is 704 // navigating in the list, so we should put it in front. 705 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 706 // Here's a little trick we need to do to make sure that 707 // the list view is actually showing its focus indicator, 708 // by ensuring it has focus and getting its window out 709 // of touch mode. 710 mDropDownList.requestFocusFromTouch(); 711 showDropDown(); 712 713 switch (keyCode) { 714 // avoid passing the focus from the text view to the 715 // next component 716 case KeyEvent.KEYCODE_ENTER: 717 case KeyEvent.KEYCODE_DPAD_CENTER: 718 case KeyEvent.KEYCODE_DPAD_DOWN: 719 case KeyEvent.KEYCODE_DPAD_UP: 720 return true; 721 } 722 } else { 723 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 724 // when the selection is at the bottom, we block the 725 // event to avoid going to the next focusable widget 726 if (curIndex == lastItem) { 727 return true; 728 } 729 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && 730 curIndex == firstItem) { 731 return true; 732 } 733 } 734 } 735 } else { 736 switch(keyCode) { 737 case KeyEvent.KEYCODE_DPAD_DOWN: 738 performValidation(); 739 } 740 } 741 742 mLastKeyCode = keyCode; 743 boolean handled = super.onKeyDown(keyCode, event); 744 mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN; 745 746 if (handled && isPopupShowing() && mDropDownList != null) { 747 clearListSelection(); 748 } 749 750 return handled; 751 } 752 753 /** 754 * Returns <code>true</code> if the amount of text in the field meets 755 * or exceeds the {@link #getThreshold} requirement. You can override 756 * this to impose a different standard for when filtering will be 757 * triggered. 758 */ 759 public boolean enoughToFilter() { 760 if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length() 761 + " threshold=" + mThreshold); 762 return getText().length() >= mThreshold; 763 } 764 765 /** 766 * This is used to watch for edits to the text view. Note that we call 767 * to methods on the auto complete text view class so that we can access 768 * private vars without going through thunks. 769 */ 770 private class MyWatcher implements TextWatcher { 771 public void afterTextChanged(Editable s) { 772 doAfterTextChanged(); 773 } 774 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 775 doBeforeTextChanged(); 776 } 777 public void onTextChanged(CharSequence s, int start, int before, int count) { 778 } 779 } 780 781 void doBeforeTextChanged() { 782 if (mBlockCompletion) return; 783 784 // when text is changed, inserted or deleted, we attempt to show 785 // the drop down 786 mOpenBefore = isPopupShowing(); 787 if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore); 788 } 789 790 void doAfterTextChanged() { 791 if (mBlockCompletion) return; 792 793 // if the list was open before the keystroke, but closed afterwards, 794 // then something in the keystroke processing (an input filter perhaps) 795 // called performCompletion() and we shouldn't do any more processing. 796 if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore 797 + " open=" + isPopupShowing()); 798 if (mOpenBefore && !isPopupShowing()) { 799 return; 800 } 801 802 // the drop down is shown only when a minimum number of characters 803 // was typed in the text view 804 if (enoughToFilter()) { 805 if (mFilter != null) { 806 performFiltering(getText(), mLastKeyCode); 807 } 808 } else { 809 // drop down is automatically dismissed when enough characters 810 // are deleted from the text view 811 if (!mDropDownAlwaysVisible) dismissDropDown(); 812 if (mFilter != null) { 813 mFilter.filter(null); 814 } 815 } 816 } 817 818 /** 819 * <p>Indicates whether the popup menu is showing.</p> 820 * 821 * @return true if the popup menu is showing, false otherwise 822 */ 823 public boolean isPopupShowing() { 824 return mPopup.isShowing(); 825 } 826 827 /** 828 * <p>Converts the selected item from the drop down list into a sequence 829 * of character that can be used in the edit box.</p> 830 * 831 * @param selectedItem the item selected by the user for completion 832 * 833 * @return a sequence of characters representing the selected suggestion 834 */ 835 protected CharSequence convertSelectionToString(Object selectedItem) { 836 return mFilter.convertResultToString(selectedItem); 837 } 838 839 /** 840 * <p>Clear the list selection. This may only be temporary, as user input will often bring 841 * it back. 842 */ 843 public void clearListSelection() { 844 final DropDownListView list = mDropDownList; 845 if (list != null) { 846 // WARNING: Please read the comment where mListSelectionHidden is declared 847 list.mListSelectionHidden = true; 848 list.hideSelector(); 849 list.requestLayout(); 850 } 851 } 852 853 /** 854 * Set the position of the dropdown view selection. 855 * 856 * @param position The position to move the selector to. 857 */ 858 public void setListSelection(int position) { 859 if (mPopup.isShowing() && (mDropDownList != null)) { 860 mDropDownList.mListSelectionHidden = false; 861 mDropDownList.setSelection(position); 862 // ListView.setSelection() will call requestLayout() 863 } 864 } 865 866 /** 867 * Get the position of the dropdown view selection, if there is one. Returns 868 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if 869 * there is no selection. 870 * 871 * @return the position of the current selection, if there is one, or 872 * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not. 873 * 874 * @see ListView#getSelectedItemPosition() 875 */ 876 public int getListSelection() { 877 if (mPopup.isShowing() && (mDropDownList != null)) { 878 return mDropDownList.getSelectedItemPosition(); 879 } 880 return ListView.INVALID_POSITION; 881 } 882 883 /** 884 * <p>Starts filtering the content of the drop down list. The filtering 885 * pattern is the content of the edit box. Subclasses should override this 886 * method to filter with a different pattern, for instance a substring of 887 * <code>text</code>.</p> 888 * 889 * @param text the filtering pattern 890 * @param keyCode the last character inserted in the edit box; beware that 891 * this will be null when text is being added through a soft input method. 892 */ 893 @SuppressWarnings({ "UnusedDeclaration" }) 894 protected void performFiltering(CharSequence text, int keyCode) { 895 mFilter.filter(text, this); 896 } 897 898 /** 899 * <p>Performs the text completion by converting the selected item from 900 * the drop down list into a string, replacing the text box's content with 901 * this string and finally dismissing the drop down menu.</p> 902 */ 903 public void performCompletion() { 904 performCompletion(null, -1, -1); 905 } 906 907 @Override 908 public void onCommitCompletion(CompletionInfo completion) { 909 if (isPopupShowing()) { 910 mBlockCompletion = true; 911 replaceText(completion.getText()); 912 mBlockCompletion = false; 913 914 if (mItemClickListener != null) { 915 final DropDownListView list = mDropDownList; 916 // Note that we don't have a View here, so we will need to 917 // supply null. Hopefully no existing apps crash... 918 mItemClickListener.onItemClick(list, null, completion.getPosition(), 919 completion.getId()); 920 } 921 } 922 } 923 924 private void performCompletion(View selectedView, int position, long id) { 925 if (isPopupShowing()) { 926 Object selectedItem; 927 if (position < 0) { 928 selectedItem = mDropDownList.getSelectedItem(); 929 } else { 930 selectedItem = mAdapter.getItem(position); 931 } 932 if (selectedItem == null) { 933 Log.w(TAG, "performCompletion: no selected item"); 934 return; 935 } 936 937 mBlockCompletion = true; 938 replaceText(convertSelectionToString(selectedItem)); 939 mBlockCompletion = false; 940 941 if (mItemClickListener != null) { 942 final DropDownListView list = mDropDownList; 943 944 if (selectedView == null || position < 0) { 945 selectedView = list.getSelectedView(); 946 position = list.getSelectedItemPosition(); 947 id = list.getSelectedItemId(); 948 } 949 mItemClickListener.onItemClick(list, selectedView, position, id); 950 } 951 } 952 953 if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) { 954 dismissDropDown(); 955 } 956 } 957 958 /** 959 * Identifies whether the view is currently performing a text completion, so subclasses 960 * can decide whether to respond to text changed events. 961 */ 962 public boolean isPerformingCompletion() { 963 return mBlockCompletion; 964 } 965 966 /** 967 * Like {@link #setText(CharSequence)}, except that it can disable filtering. 968 * 969 * @param filter If <code>false</code>, no filtering will be performed 970 * as a result of this call. 971 * 972 * @hide Pending API council approval. 973 */ 974 public void setText(CharSequence text, boolean filter) { 975 if (filter) { 976 setText(text); 977 } else { 978 mBlockCompletion = true; 979 setText(text); 980 mBlockCompletion = false; 981 } 982 } 983 984 /** 985 * <p>Performs the text completion by replacing the current text by the 986 * selected item. Subclasses should override this method to avoid replacing 987 * the whole content of the edit box.</p> 988 * 989 * @param text the selected suggestion in the drop down list 990 */ 991 protected void replaceText(CharSequence text) { 992 clearComposingText(); 993 994 setText(text); 995 // make sure we keep the caret at the end of the text view 996 Editable spannable = getText(); 997 Selection.setSelection(spannable, spannable.length()); 998 } 999 1000 /** {@inheritDoc} */ 1001 public void onFilterComplete(int count) { 1002 updateDropDownForFilter(count); 1003 1004 } 1005 1006 private void updateDropDownForFilter(int count) { 1007 // Not attached to window, don't update drop-down 1008 if (getWindowVisibility() == View.GONE) return; 1009 1010 /* 1011 * This checks enoughToFilter() again because filtering requests 1012 * are asynchronous, so the result may come back after enough text 1013 * has since been deleted to make it no longer appropriate 1014 * to filter. 1015 */ 1016 1017 if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) { 1018 if (hasFocus() && hasWindowFocus()) { 1019 showDropDown(); 1020 } 1021 } else if (!mDropDownAlwaysVisible) { 1022 dismissDropDown(); 1023 } 1024 } 1025 1026 @Override 1027 public void onWindowFocusChanged(boolean hasWindowFocus) { 1028 super.onWindowFocusChanged(hasWindowFocus); 1029 if (!hasWindowFocus && !mDropDownAlwaysVisible) { 1030 dismissDropDown(); 1031 } 1032 } 1033 1034 @Override 1035 protected void onDisplayHint(int hint) { 1036 super.onDisplayHint(hint); 1037 switch (hint) { 1038 case INVISIBLE: 1039 if (!mDropDownAlwaysVisible) { 1040 dismissDropDown(); 1041 } 1042 break; 1043 } 1044 } 1045 1046 @Override 1047 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 1048 super.onFocusChanged(focused, direction, previouslyFocusedRect); 1049 // Perform validation if the view is losing focus. 1050 if (!focused) { 1051 performValidation(); 1052 } 1053 if (!focused && !mDropDownAlwaysVisible) { 1054 dismissDropDown(); 1055 } 1056 } 1057 1058 @Override 1059 protected void onAttachedToWindow() { 1060 super.onAttachedToWindow(); 1061 } 1062 1063 @Override 1064 protected void onDetachedFromWindow() { 1065 dismissDropDown(); 1066 super.onDetachedFromWindow(); 1067 } 1068 1069 /** 1070 * <p>Closes the drop down if present on screen.</p> 1071 */ 1072 public void dismissDropDown() { 1073 InputMethodManager imm = InputMethodManager.peekInstance(); 1074 if (imm != null) { 1075 imm.displayCompletions(this, null); 1076 } 1077 mPopup.dismiss(); 1078 mPopup.setContentView(null); 1079 mDropDownList = null; 1080 } 1081 1082 @Override 1083 protected boolean setFrame(final int l, int t, final int r, int b) { 1084 boolean result = super.setFrame(l, t, r, b); 1085 1086 if (mPopup.isShowing()) { 1087 showDropDown(); 1088 } 1089 1090 return result; 1091 } 1092 1093 /** 1094 * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of 1095 * the id is NO_ID or we can't find a view for the given id, we return this TextView as 1096 * the default anchoring point.</p> 1097 */ 1098 private View getDropDownAnchorView() { 1099 if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) { 1100 mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId); 1101 } 1102 return mDropDownAnchorView == null ? this : mDropDownAnchorView; 1103 } 1104 1105 /** 1106 * Issues a runnable to show the dropdown as soon as possible. 1107 * 1108 * @hide internal used only by SearchDialog 1109 */ 1110 public void showDropDownAfterLayout() { 1111 post(mShowDropDownRunnable); 1112 } 1113 1114 /** 1115 * Ensures that the drop down is not obscuring the IME. 1116 * @param visible whether the ime should be in front. If false, the ime is pushed to 1117 * the background. 1118 * @hide internal used only here and SearchDialog 1119 */ 1120 public void ensureImeVisible(boolean visible) { 1121 mPopup.setInputMethodMode(visible 1122 ? PopupWindow.INPUT_METHOD_NEEDED : PopupWindow.INPUT_METHOD_NOT_NEEDED); 1123 showDropDown(); 1124 } 1125 1126 /** 1127 * @hide internal used only here and SearchDialog 1128 */ 1129 public boolean isInputMethodNotNeeded() { 1130 return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1131 } 1132 1133 /** 1134 * <p>Displays the drop down on screen.</p> 1135 */ 1136 public void showDropDown() { 1137 int height = buildDropDown(); 1138 1139 int widthSpec = 0; 1140 int heightSpec = 0; 1141 1142 boolean noInputMethod = isInputMethodNotNeeded(); 1143 1144 if (mPopup.isShowing()) { 1145 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 1146 // The call to PopupWindow's update method below can accept -1 for any 1147 // value you do not want to update. 1148 widthSpec = -1; 1149 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 1150 widthSpec = getDropDownAnchorView().getWidth(); 1151 } else { 1152 widthSpec = mDropDownWidth; 1153 } 1154 1155 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1156 // The call to PopupWindow's update method below can accept -1 for any 1157 // value you do not want to update. 1158 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; 1159 if (noInputMethod) { 1160 mPopup.setWindowLayoutMode( 1161 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 1162 ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); 1163 } else { 1164 mPopup.setWindowLayoutMode( 1165 mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? 1166 ViewGroup.LayoutParams.MATCH_PARENT : 0, 1167 ViewGroup.LayoutParams.MATCH_PARENT); 1168 } 1169 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 1170 heightSpec = height; 1171 } else { 1172 heightSpec = mDropDownHeight; 1173 } 1174 1175 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 1176 1177 mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, 1178 mDropDownVerticalOffset, widthSpec, heightSpec); 1179 } else { 1180 if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { 1181 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; 1182 } else { 1183 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 1184 mPopup.setWidth(getDropDownAnchorView().getWidth()); 1185 } else { 1186 mPopup.setWidth(mDropDownWidth); 1187 } 1188 } 1189 1190 if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1191 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; 1192 } else { 1193 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 1194 mPopup.setHeight(height); 1195 } else { 1196 mPopup.setHeight(mDropDownHeight); 1197 } 1198 } 1199 1200 mPopup.setWindowLayoutMode(widthSpec, heightSpec); 1201 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 1202 1203 // use outside touchable to dismiss drop down when touching outside of it, so 1204 // only set this if the dropdown is not always visible 1205 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 1206 mPopup.setTouchInterceptor(new PopupTouchInterceptor()); 1207 mPopup.showAsDropDown(getDropDownAnchorView(), 1208 mDropDownHorizontalOffset, mDropDownVerticalOffset); 1209 mDropDownList.setSelection(ListView.INVALID_POSITION); 1210 clearListSelection(); 1211 post(mHideSelector); 1212 } 1213 } 1214 1215 /** 1216 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 1217 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we 1218 * ignore outside touch even when the drop down is not set to always visible. 1219 * 1220 * @hide used only by SearchDialog 1221 */ 1222 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 1223 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; 1224 } 1225 1226 /** 1227 * <p>Builds the popup window's content and returns the height the popup 1228 * should have. Returns -1 when the content already exists.</p> 1229 * 1230 * @return the content's height or -1 if content already exists 1231 */ 1232 private int buildDropDown() { 1233 ViewGroup dropDownView; 1234 int otherHeights = 0; 1235 1236 final ListAdapter adapter = mAdapter; 1237 if (adapter != null) { 1238 InputMethodManager imm = InputMethodManager.peekInstance(); 1239 if (imm != null) { 1240 final int count = Math.min(adapter.getCount(), 20); 1241 CompletionInfo[] completions = new CompletionInfo[count]; 1242 int realCount = 0; 1243 1244 for (int i = 0; i < count; i++) { 1245 if (adapter.isEnabled(i)) { 1246 realCount++; 1247 Object item = adapter.getItem(i); 1248 long id = adapter.getItemId(i); 1249 completions[i] = new CompletionInfo(id, i, 1250 convertSelectionToString(item)); 1251 } 1252 } 1253 1254 if (realCount != count) { 1255 CompletionInfo[] tmp = new CompletionInfo[realCount]; 1256 System.arraycopy(completions, 0, tmp, 0, realCount); 1257 completions = tmp; 1258 } 1259 1260 imm.displayCompletions(this, completions); 1261 } 1262 } 1263 1264 if (mDropDownList == null) { 1265 Context context = getContext(); 1266 1267 mHideSelector = new ListSelectorHider(); 1268 1269 /** 1270 * This Runnable exists for the sole purpose of checking if the view layout has got 1271 * completed and if so call showDropDown to display the drop down. This is used to show 1272 * the drop down as soon as possible after user opens up the search dialog, without 1273 * waiting for the normal UI pipeline to do it's job which is slower than this method. 1274 */ 1275 mShowDropDownRunnable = new Runnable() { 1276 public void run() { 1277 // View layout should be all done before displaying the drop down. 1278 View view = getDropDownAnchorView(); 1279 if (view != null && view.getWindowToken() != null) { 1280 showDropDown(); 1281 } 1282 } 1283 }; 1284 1285 mDropDownList = new DropDownListView(context); 1286 mDropDownList.setSelector(mDropDownListHighlight); 1287 mDropDownList.setAdapter(adapter); 1288 mDropDownList.setVerticalFadingEdgeEnabled(true); 1289 mDropDownList.setOnItemClickListener(mDropDownItemClickListener); 1290 mDropDownList.setFocusable(true); 1291 mDropDownList.setFocusableInTouchMode(true); 1292 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 1293 public void onItemSelected(AdapterView<?> parent, View view, 1294 int position, long id) { 1295 1296 if (position != -1) { 1297 DropDownListView dropDownList = mDropDownList; 1298 1299 if (dropDownList != null) { 1300 dropDownList.mListSelectionHidden = false; 1301 } 1302 } 1303 } 1304 1305 public void onNothingSelected(AdapterView<?> parent) { 1306 } 1307 }); 1308 mDropDownList.setOnScrollListener(new PopupScrollListener()); 1309 1310 if (mItemSelectedListener != null) { 1311 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 1312 } 1313 1314 dropDownView = mDropDownList; 1315 1316 View hintView = getHintView(context); 1317 if (hintView != null) { 1318 // if an hint has been specified, we accomodate more space for it and 1319 // add a text view in the drop down menu, at the bottom of the list 1320 LinearLayout hintContainer = new LinearLayout(context); 1321 hintContainer.setOrientation(LinearLayout.VERTICAL); 1322 1323 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1324 ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f 1325 ); 1326 hintContainer.addView(dropDownView, hintParams); 1327 hintContainer.addView(hintView); 1328 1329 // measure the hint's height to find how much more vertical space 1330 // we need to add to the drop down's height 1331 int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST); 1332 int heightSpec = MeasureSpec.UNSPECIFIED; 1333 hintView.measure(widthSpec, heightSpec); 1334 1335 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1336 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1337 + hintParams.bottomMargin; 1338 1339 dropDownView = hintContainer; 1340 } 1341 1342 mPopup.setContentView(dropDownView); 1343 } else { 1344 dropDownView = (ViewGroup) mPopup.getContentView(); 1345 final View view = dropDownView.findViewById(HINT_VIEW_ID); 1346 if (view != null) { 1347 LinearLayout.LayoutParams hintParams = 1348 (LinearLayout.LayoutParams) view.getLayoutParams(); 1349 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1350 + hintParams.bottomMargin; 1351 } 1352 } 1353 1354 // Max height available on the screen for a popup. 1355 boolean ignoreBottomDecorations = 1356 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1357 final int maxHeight = mPopup.getMaxAvailableHeight( 1358 getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); 1359 1360 // getMaxAvailableHeight() subtracts the padding, so we put it back, 1361 // to get the available height for the whole window 1362 int padding = 0; 1363 Drawable background = mPopup.getBackground(); 1364 if (background != null) { 1365 background.getPadding(mTempRect); 1366 padding = mTempRect.top + mTempRect.bottom; 1367 } 1368 1369 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { 1370 return maxHeight + padding; 1371 } 1372 1373 final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, 1374 0, ListView.NO_POSITION, maxHeight - otherHeights, 2); 1375 // add padding only if the list has items in it, that way we don't show 1376 // the popup if it is not needed 1377 if (listContent > 0) otherHeights += padding; 1378 1379 return listContent + otherHeights; 1380 } 1381 1382 private View getHintView(Context context) { 1383 if (mHintText != null && mHintText.length() > 0) { 1384 final TextView hintView = (TextView) LayoutInflater.from(context).inflate( 1385 mHintResource, null).findViewById(com.android.internal.R.id.text1); 1386 hintView.setText(mHintText); 1387 hintView.setId(HINT_VIEW_ID); 1388 return hintView; 1389 } else { 1390 return null; 1391 } 1392 } 1393 1394 /** 1395 * Sets the validator used to perform text validation. 1396 * 1397 * @param validator The validator used to validate the text entered in this widget. 1398 * 1399 * @see #getValidator() 1400 * @see #performValidation() 1401 */ 1402 public void setValidator(Validator validator) { 1403 mValidator = validator; 1404 } 1405 1406 /** 1407 * Returns the Validator set with {@link #setValidator}, 1408 * or <code>null</code> if it was not set. 1409 * 1410 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1411 * @see #performValidation() 1412 */ 1413 public Validator getValidator() { 1414 return mValidator; 1415 } 1416 1417 /** 1418 * If a validator was set on this view and the current string is not valid, 1419 * ask the validator to fix it. 1420 * 1421 * @see #getValidator() 1422 * @see #setValidator(android.widget.AutoCompleteTextView.Validator) 1423 */ 1424 public void performValidation() { 1425 if (mValidator == null) return; 1426 1427 CharSequence text = getText(); 1428 1429 if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) { 1430 setText(mValidator.fixText(text)); 1431 } 1432 } 1433 1434 /** 1435 * Returns the Filter obtained from {@link Filterable#getFilter}, 1436 * or <code>null</code> if {@link #setAdapter} was not called with 1437 * a Filterable. 1438 */ 1439 protected Filter getFilter() { 1440 return mFilter; 1441 } 1442 1443 private class ListSelectorHider implements Runnable { 1444 public void run() { 1445 clearListSelection(); 1446 } 1447 } 1448 1449 private class ResizePopupRunnable implements Runnable { 1450 public void run() { 1451 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1452 showDropDown(); 1453 } 1454 } 1455 1456 private class PopupTouchInterceptor implements OnTouchListener { 1457 public boolean onTouch(View v, MotionEvent event) { 1458 final int action = event.getAction(); 1459 if (action == MotionEvent.ACTION_DOWN && 1460 mPopup != null && mPopup.isShowing()) { 1461 postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); 1462 } else if (action == MotionEvent.ACTION_UP) { 1463 removeCallbacks(mResizePopupRunnable); 1464 } 1465 return false; 1466 } 1467 } 1468 1469 private class PopupScrollListener implements ListView.OnScrollListener { 1470 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1471 int totalItemCount) { 1472 1473 } 1474 1475 public void onScrollStateChanged(AbsListView view, int scrollState) { 1476 if (scrollState == SCROLL_STATE_TOUCH_SCROLL && 1477 !isInputMethodNotNeeded() && mPopup.getContentView() != null) { 1478 removeCallbacks(mResizePopupRunnable); 1479 mResizePopupRunnable.run(); 1480 } 1481 } 1482 } 1483 1484 private class DropDownItemClickListener implements AdapterView.OnItemClickListener { 1485 public void onItemClick(AdapterView parent, View v, int position, long id) { 1486 performCompletion(v, position, id); 1487 } 1488 } 1489 1490 /** 1491 * <p>Wrapper class for a ListView. This wrapper hijacks the focus to 1492 * make sure the list uses the appropriate drawables and states when 1493 * displayed on screen within a drop down. The focus is never actually 1494 * passed to the drop down; the list only looks focused.</p> 1495 */ 1496 private static class DropDownListView extends ListView { 1497 /* 1498 * WARNING: This is a workaround for a touch mode issue. 1499 * 1500 * Touch mode is propagated lazily to windows. This causes problems in 1501 * the following scenario: 1502 * - Type something in the AutoCompleteTextView and get some results 1503 * - Move down with the d-pad to select an item in the list 1504 * - Move up with the d-pad until the selection disappears 1505 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 1506 * and get new results; you are now in touch mode 1507 * - The selection comes back on the first item in the list, even though 1508 * the list is supposed to be in touch mode 1509 * 1510 * Using the soft keyboard triggers the touch mode change but that change 1511 * is propagated to our window only after the first list layout, therefore 1512 * after the list attempts to resurrect the selection. 1513 * 1514 * The trick to work around this issue is to pretend the list is in touch 1515 * mode when we know that the selection should not appear, that is when 1516 * we know the user moved the selection away from the list. 1517 * 1518 * This boolean is set to true whenever we explicitely hide the list's 1519 * selection and reset to false whenver we know the user moved the 1520 * selection back to the list. 1521 * 1522 * When this boolean is true, isInTouchMode() returns true, otherwise it 1523 * returns super.isInTouchMode(). 1524 */ 1525 private boolean mListSelectionHidden; 1526 1527 /** 1528 * <p>Creates a new list view wrapper.</p> 1529 * 1530 * @param context this view's context 1531 */ 1532 public DropDownListView(Context context) { 1533 super(context, null, com.android.internal.R.attr.dropDownListViewStyle); 1534 } 1535 1536 /** 1537 * <p>Avoids jarring scrolling effect by ensuring that list elements 1538 * made of a text view fit on a single line.</p> 1539 * 1540 * @param position the item index in the list to get a view for 1541 * @return the view for the specified item 1542 */ 1543 @Override 1544 View obtainView(int position, boolean[] isScrap) { 1545 View view = super.obtainView(position, isScrap); 1546 1547 if (view instanceof TextView) { 1548 ((TextView) view).setHorizontallyScrolling(true); 1549 } 1550 1551 return view; 1552 } 1553 1554 @Override 1555 public boolean isInTouchMode() { 1556 // WARNING: Please read the comment where mListSelectionHidden is declared 1557 return mListSelectionHidden || super.isInTouchMode(); 1558 } 1559 1560 /** 1561 * <p>Returns the focus state in the drop down.</p> 1562 * 1563 * @return true always 1564 */ 1565 @Override 1566 public boolean hasWindowFocus() { 1567 return true; 1568 } 1569 1570 /** 1571 * <p>Returns the focus state in the drop down.</p> 1572 * 1573 * @return true always 1574 */ 1575 @Override 1576 public boolean isFocused() { 1577 return true; 1578 } 1579 1580 /** 1581 * <p>Returns the focus state in the drop down.</p> 1582 * 1583 * @return true always 1584 */ 1585 @Override 1586 public boolean hasFocus() { 1587 return true; 1588 } 1589 1590 protected int[] onCreateDrawableState(int extraSpace) { 1591 int[] res = super.onCreateDrawableState(extraSpace); 1592 //noinspection ConstantIfStatement 1593 if (false) { 1594 StringBuilder sb = new StringBuilder("Created drawable state: ["); 1595 for (int i=0; i<res.length; i++) { 1596 if (i > 0) sb.append(", "); 1597 sb.append("0x"); 1598 sb.append(Integer.toHexString(res[i])); 1599 } 1600 sb.append("]"); 1601 Log.i(TAG, sb.toString()); 1602 } 1603 return res; 1604 } 1605 } 1606 1607 /** 1608 * This interface is used to make sure that the text entered in this TextView complies to 1609 * a certain format. Since there is no foolproof way to prevent the user from leaving 1610 * this View with an incorrect value in it, all we can do is try to fix it ourselves 1611 * when this happens. 1612 */ 1613 public interface Validator { 1614 /** 1615 * Validates the specified text. 1616 * 1617 * @return true If the text currently in the text editor is valid. 1618 * 1619 * @see #fixText(CharSequence) 1620 */ 1621 boolean isValid(CharSequence text); 1622 1623 /** 1624 * Corrects the specified text to make it valid. 1625 * 1626 * @param invalidText A string that doesn't pass validation: isValid(invalidText) 1627 * returns false 1628 * 1629 * @return A string based on invalidText such as invoking isValid() on it returns true. 1630 * 1631 * @see #isValid(CharSequence) 1632 */ 1633 CharSequence fixText(CharSequence invalidText); 1634 } 1635 1636 /** 1637 * Allows us a private hook into the on click event without preventing users from setting 1638 * their own click listener. 1639 */ 1640 private class PassThroughClickListener implements OnClickListener { 1641 1642 private View.OnClickListener mWrapped; 1643 1644 /** {@inheritDoc} */ 1645 public void onClick(View v) { 1646 onClickImpl(); 1647 1648 if (mWrapped != null) mWrapped.onClick(v); 1649 } 1650 } 1651 1652 private class PopupDataSetObserver extends DataSetObserver { 1653 @Override 1654 public void onChanged() { 1655 if (isPopupShowing()) { 1656 // This will resize the popup to fit the new adapter's content 1657 showDropDown(); 1658 } else if (mAdapter != null) { 1659 // If the popup is not showing already, showing it will cause 1660 // the list of data set observers attached to the adapter to 1661 // change. We can't do it from here, because we are in the middle 1662 // of iterating throught he list of observers. 1663 post(new Runnable() { 1664 public void run() { 1665 final ListAdapter adapter = mAdapter; 1666 if (adapter != null) { 1667 updateDropDownForFilter(adapter.getCount()); 1668 } 1669 } 1670 }); 1671 } 1672 } 1673 1674 @Override 1675 public void onInvalidated() { 1676 if (!mDropDownAlwaysVisible) { 1677 // There's no data to display so make sure we're not showing 1678 // the drop down and its list 1679 dismissDropDown(); 1680 } 1681 } 1682 } 1683} 1684