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