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