ListPopupWindow.java revision e6072e2d918169bd827cf7431347fb648124c227
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.internal.widget; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.database.DataSetObserver; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.os.Handler; 25import android.support.v7.appcompat.R; 26import android.text.TextUtils; 27import android.util.AttributeSet; 28import android.util.Log; 29import android.util.SparseArray; 30import android.view.KeyEvent; 31import android.view.MotionEvent; 32import android.view.View; 33import android.view.View.MeasureSpec; 34import android.view.View.OnTouchListener; 35import android.view.ViewGroup; 36import android.view.ViewParent; 37import android.widget.*; 38 39import java.lang.reflect.Field; 40import java.util.ArrayList; 41import java.util.List; 42import java.util.Locale; 43 44/** 45 * A ListPopupWindow anchors itself to a host view and displays a list of choices. 46 * 47 * <p>ListPopupWindow contains a number of tricky behaviors surrounding positioning, scrolling 48 * parents to fit the dropdown, interacting sanely with the IME if present, and others. 49 * 50 * @see android.widget.AutoCompleteTextView 51 * @see android.widget.Spinner 52 */ 53public class ListPopupWindow { 54 55 private static final String TAG = "ListPopupWindow"; 56 private static final boolean DEBUG = false; 57 58 /** 59 * This value controls the length of time that the user must leave a pointer down without 60 * scrolling to expand the autocomplete dropdown list to cover the IME. 61 */ 62 private static final int EXPAND_LIST_TIMEOUT = 250; 63 64 private Context mContext; 65 private PopupWindow mPopup; 66 private ListAdapter mAdapter; 67 private DropDownListView mDropDownList; 68 69 private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT; 70 private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT; 71 private int mDropDownHorizontalOffset; 72 private int mDropDownVerticalOffset; 73 private boolean mDropDownVerticalOffsetSet; 74 75 private boolean mDropDownAlwaysVisible = false; 76 private boolean mForceIgnoreOutsideTouch = false; 77 int mListItemExpandMaximum = Integer.MAX_VALUE; 78 79 private View mPromptView; 80 private int mPromptPosition = POSITION_PROMPT_ABOVE; 81 82 private DataSetObserver mObserver; 83 84 private View mDropDownAnchorView; 85 86 private Drawable mDropDownListHighlight; 87 88 private AdapterView.OnItemClickListener mItemClickListener; 89 private AdapterView.OnItemSelectedListener mItemSelectedListener; 90 91 private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable(); 92 private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor(); 93 private final PopupScrollListener mScrollListener = new PopupScrollListener(); 94 private final ListSelectorHider mHideSelector = new ListSelectorHider(); 95 private Runnable mShowDropDownRunnable; 96 97 private Handler mHandler = new Handler(); 98 99 private Rect mTempRect = new Rect(); 100 101 private boolean mModal; 102 103 private int mLayoutDirection; 104 105 /** 106 * The provided prompt view should appear above list content. 107 * 108 * @see #setPromptPosition(int) 109 * @see #getPromptPosition() 110 * @see #setPromptView(View) 111 */ 112 public static final int POSITION_PROMPT_ABOVE = 0; 113 114 /** 115 * The provided prompt view should appear below list content. 116 * 117 * @see #setPromptPosition(int) 118 * @see #getPromptPosition() 119 * @see #setPromptView(View) 120 */ 121 public static final int POSITION_PROMPT_BELOW = 1; 122 123 /** 124 * Alias for {@link ViewGroup.LayoutParams#FILL_PARENT}. If used to specify a popup width, the 125 * popup will match the width of the anchor view. If used to specify a popup height, the popup 126 * will fill available space. 127 */ 128 public static final int FILL_PARENT = ViewGroup.LayoutParams.FILL_PARENT; 129 130 /** 131 * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}. If used to specify a popup width, the 132 * popup will use the width of its content. 133 */ 134 public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; 135 136 /** 137 * Mode for {@link #setInputMethodMode(int)}: the requirements for the input method should be 138 * based on the focusability of the popup. That is if it is focusable than it needs to work 139 * with the input method, else it doesn't. 140 */ 141 public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE; 142 143 /** 144 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to work with an input 145 * method, regardless of whether it is focusable. This means that it will always be displayed 146 * so that the user can also operate the input method while it is shown. 147 */ 148 public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED; 149 150 /** 151 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to work with an input 152 * method, regardless of whether it is focusable. This means that it will always be displayed 153 * to use as much space on the screen as needed, regardless of whether this covers the input 154 * method. 155 */ 156 public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED; 157 158 /** 159 * Create a new, empty popup window capable of displaying items from a ListAdapter. Backgrounds 160 * should be set using {@link #setBackgroundDrawable(Drawable)}. 161 * 162 * @param context Context used for contained views. 163 */ 164 public ListPopupWindow(Context context) { 165 this(context, null, R.attr.listPopupWindowStyle); 166 } 167 168 /** 169 * Create a new, empty popup window capable of displaying items from a ListAdapter. Backgrounds 170 * should be set using {@link #setBackgroundDrawable(Drawable)}. 171 * 172 * @param context Context used for contained views. 173 * @param attrs Attributes from inflating parent views used to style the popup. 174 */ 175 public ListPopupWindow(Context context, AttributeSet attrs) { 176 this(context, attrs, R.attr.listPopupWindowStyle); 177 } 178 179 /** 180 * Create a new, empty popup window capable of displaying items from a ListAdapter. Backgrounds 181 * should be set using {@link #setBackgroundDrawable(Drawable)}. 182 * 183 * @param context Context used for contained views. 184 * @param attrs Attributes from inflating parent views used to style the popup. 185 * @param defStyleAttr Default style attribute to use for popup content. 186 */ 187 public ListPopupWindow(Context context, AttributeSet attrs, int defStyleAttr) { 188 mContext = context; 189 mPopup = new PopupWindow(context, attrs, defStyleAttr); 190 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 191 // Set the default layout direction to match the default locale one 192 final Locale locale = mContext.getResources().getConfiguration().locale; 193 194 } 195 196 /** 197 * Sets the adapter that provides the data and the views to represent the data in this popup 198 * window. 199 * 200 * @param adapter The adapter to use to create this window's content. 201 */ 202 public void setAdapter(ListAdapter adapter) { 203 if (mObserver == null) { 204 mObserver = new PopupDataSetObserver(); 205 } else if (mAdapter != null) { 206 mAdapter.unregisterDataSetObserver(mObserver); 207 } 208 mAdapter = adapter; 209 if (mAdapter != null) { 210 adapter.registerDataSetObserver(mObserver); 211 } 212 213 if (mDropDownList != null) { 214 mDropDownList.setAdapter(mAdapter); 215 } 216 } 217 218 /** 219 * Set where the optional prompt view should appear. The default is {@link 220 * #POSITION_PROMPT_ABOVE}. 221 * 222 * @param position A position constant declaring where the prompt should be displayed. 223 * @see #POSITION_PROMPT_ABOVE 224 * @see #POSITION_PROMPT_BELOW 225 */ 226 public void setPromptPosition(int position) { 227 mPromptPosition = position; 228 } 229 230 /** 231 * @return Where the optional prompt view should appear. 232 * @see #POSITION_PROMPT_ABOVE 233 * @see #POSITION_PROMPT_BELOW 234 */ 235 public int getPromptPosition() { 236 return mPromptPosition; 237 } 238 239 /** 240 * Set whether this window should be modal when shown. 241 * 242 * <p>If a popup window is modal, it will receive all touch and key input. If the user touches 243 * outside the popup window's content area the popup window will be dismissed. 244 * 245 * @param modal {@code true} if the popup window should be modal, {@code false} otherwise. 246 */ 247 public void setModal(boolean modal) { 248 mModal = true; 249 mPopup.setFocusable(modal); 250 } 251 252 /** 253 * Returns whether the popup window will be modal when shown. 254 * 255 * @return {@code true} if the popup window will be modal, {@code false} otherwise. 256 */ 257 public boolean isModal() { 258 return mModal; 259 } 260 261 /** 262 * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is 263 * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we ignore 264 * outside touch even when the drop down is not set to always visible. 265 * 266 * @hide Used only by AutoCompleteTextView to handle some internal special cases. 267 */ 268 public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) { 269 mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch; 270 } 271 272 /** 273 * Sets whether the drop-down should remain visible under certain conditions. 274 * 275 * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless of the 276 * size or content of the list. {@link #getBackground()} will fill any space that is not used 277 * by the list. 278 * 279 * @param dropDownAlwaysVisible Whether to keep the drop-down visible. 280 * @hide Only used by AutoCompleteTextView under special conditions. 281 */ 282 public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) { 283 mDropDownAlwaysVisible = dropDownAlwaysVisible; 284 } 285 286 /** 287 * @return Whether the drop-down is visible under special conditions. 288 * @hide Only used by AutoCompleteTextView under special conditions. 289 */ 290 public boolean isDropDownAlwaysVisible() { 291 return mDropDownAlwaysVisible; 292 } 293 294 /** 295 * Sets the operating mode for the soft input area. 296 * 297 * @param mode The desired mode, see {@link android.view.WindowManager.LayoutParams#softInputMode} 298 * for the full list 299 * @see android.view.WindowManager.LayoutParams#softInputMode 300 * @see #getSoftInputMode() 301 */ 302 public void setSoftInputMode(int mode) { 303 mPopup.setSoftInputMode(mode); 304 } 305 306 /** 307 * Returns the current value in {@link #setSoftInputMode(int)}. 308 * 309 * @see #setSoftInputMode(int) 310 * @see android.view.WindowManager.LayoutParams#softInputMode 311 */ 312 public int getSoftInputMode() { 313 return mPopup.getSoftInputMode(); 314 } 315 316 /** 317 * Sets a drawable to use as the list item selector. 318 * 319 * @param selector List selector drawable to use in the popup. 320 */ 321 public void setListSelector(Drawable selector) { 322 mDropDownListHighlight = selector; 323 } 324 325 /** 326 * @return The background drawable for the popup window. 327 */ 328 public Drawable getBackground() { 329 return mPopup.getBackground(); 330 } 331 332 /** 333 * Sets a drawable to be the background for the popup window. 334 * 335 * @param d A drawable to set as the background. 336 */ 337 public void setBackgroundDrawable(Drawable d) { 338 mPopup.setBackgroundDrawable(d); 339 } 340 341 /** 342 * Set an animation style to use when the popup window is shown or dismissed. 343 * 344 * @param animationStyle Animation style to use. 345 */ 346 public void setAnimationStyle(int animationStyle) { 347 mPopup.setAnimationStyle(animationStyle); 348 } 349 350 /** 351 * Returns the animation style that will be used when the popup window is shown or dismissed. 352 * 353 * @return Animation style that will be used. 354 */ 355 public int getAnimationStyle() { 356 return mPopup.getAnimationStyle(); 357 } 358 359 /** 360 * Returns the view that will be used to anchor this popup. 361 * 362 * @return The popup's anchor view 363 */ 364 public View getAnchorView() { 365 return mDropDownAnchorView; 366 } 367 368 /** 369 * Sets the popup's anchor view. This popup will always be positioned relative to the anchor 370 * view when shown. 371 * 372 * @param anchor The view to use as an anchor. 373 */ 374 public void setAnchorView(View anchor) { 375 mDropDownAnchorView = anchor; 376 } 377 378 /** 379 * @return The horizontal offset of the popup from its anchor in pixels. 380 */ 381 public int getHorizontalOffset() { 382 return mDropDownHorizontalOffset; 383 } 384 385 /** 386 * Set the horizontal offset of this popup from its anchor view in pixels. 387 * 388 * @param offset The horizontal offset of the popup from its anchor. 389 */ 390 public void setHorizontalOffset(int offset) { 391 mDropDownHorizontalOffset = offset; 392 } 393 394 /** 395 * @return The vertical offset of the popup from its anchor in pixels. 396 */ 397 public int getVerticalOffset() { 398 if (!mDropDownVerticalOffsetSet) { 399 return 0; 400 } 401 return mDropDownVerticalOffset; 402 } 403 404 /** 405 * Set the vertical offset of this popup from its anchor view in pixels. 406 * 407 * @param offset The vertical offset of the popup from its anchor. 408 */ 409 public void setVerticalOffset(int offset) { 410 mDropDownVerticalOffset = offset; 411 mDropDownVerticalOffsetSet = true; 412 } 413 414 /** 415 * @return The width of the popup window in pixels. 416 */ 417 public int getWidth() { 418 return mDropDownWidth; 419 } 420 421 /** 422 * Sets the width of the popup window in pixels. Can also be {@link #FILL_PARENT} or {@link 423 * #WRAP_CONTENT}. 424 * 425 * @param width Width of the popup window. 426 */ 427 public void setWidth(int width) { 428 mDropDownWidth = width; 429 } 430 431 /** 432 * Sets the width of the popup window by the size of its content. The final width may be larger 433 * to accommodate styled window dressing. 434 * 435 * @param width Desired width of content in pixels. 436 */ 437 public void setContentWidth(int width) { 438 Drawable popupBackground = mPopup.getBackground(); 439 if (popupBackground != null) { 440 popupBackground.getPadding(mTempRect); 441 mDropDownWidth = mTempRect.left + mTempRect.right + width; 442 } else { 443 setWidth(width); 444 } 445 } 446 447 /** 448 * @return The height of the popup window in pixels. 449 */ 450 public int getHeight() { 451 return mDropDownHeight; 452 } 453 454 /** 455 * Sets the height of the popup window in pixels. Can also be {@link #FILL_PARENT}. 456 * 457 * @param height Height of the popup window. 458 */ 459 public void setHeight(int height) { 460 mDropDownHeight = height; 461 } 462 463 /** 464 * Sets a listener to receive events when a list item is clicked. 465 * 466 * @param clickListener Listener to register 467 * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener) 468 */ 469 public void setOnItemClickListener(AdapterView.OnItemClickListener clickListener) { 470 mItemClickListener = clickListener; 471 } 472 473 /** 474 * Sets a listener to receive events when a list item is selected. 475 * 476 * @param selectedListener Listener to register. 477 * @see ListView#setOnItemSelectedListener(android.widget.AdapterView.OnItemSelectedListener) 478 */ 479 public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener selectedListener) { 480 mItemSelectedListener = selectedListener; 481 } 482 483 /** 484 * Set a view to act as a user prompt for this popup window. Where the prompt view will appear 485 * is controlled by {@link #setPromptPosition(int)}. 486 * 487 * @param prompt View to use as an informational prompt. 488 */ 489 public void setPromptView(View prompt) { 490 boolean showing = isShowing(); 491 if (showing) { 492 removePromptView(); 493 } 494 mPromptView = prompt; 495 if (showing) { 496 show(); 497 } 498 } 499 500 /** 501 * Post a {@link #show()} call to the UI thread. 502 */ 503 public void postShow() { 504 mHandler.post(mShowDropDownRunnable); 505 } 506 507 /** 508 * Show the popup list. If the list is already showing, this method will recalculate the popup's 509 * size and position. 510 */ 511 public void show() { 512 int height = buildDropDown(); 513 514 int widthSpec = 0; 515 int heightSpec = 0; 516 517 boolean noInputMethod = isInputMethodNotNeeded(); 518 // TODO(anirudhd) : Test and replicate this behaviour 519 // mPopup.setAllowScrollingAnchorParent(!noInputMethod); 520 521 if (mPopup.isShowing()) { 522 if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { 523 // The call to PopupWindow's update method below can accept -1 for any 524 // value you do not want to update. 525 widthSpec = -1; 526 } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 527 widthSpec = getAnchorView().getWidth(); 528 } else { 529 widthSpec = mDropDownWidth; 530 } 531 532 if (mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { 533 // The call to PopupWindow's update method below can accept -1 for any 534 // value you do not want to update. 535 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.FILL_PARENT; 536 if (noInputMethod) { 537 mPopup.setWindowLayoutMode( 538 mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT ? 539 ViewGroup.LayoutParams.FILL_PARENT : 0, 0); 540 } else { 541 mPopup.setWindowLayoutMode( 542 mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT ? 543 ViewGroup.LayoutParams.FILL_PARENT : 0, 544 ViewGroup.LayoutParams.FILL_PARENT); 545 } 546 } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 547 heightSpec = height; 548 } else { 549 heightSpec = mDropDownHeight; 550 } 551 552 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 553 554 mPopup.update(getAnchorView(), mDropDownHorizontalOffset, 555 mDropDownVerticalOffset, widthSpec, heightSpec); 556 } else { 557 if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { 558 widthSpec = ViewGroup.LayoutParams.FILL_PARENT; 559 } else { 560 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { 561 mPopup.setWidth(getAnchorView().getWidth()); 562 } else { 563 mPopup.setWidth(mDropDownWidth); 564 } 565 } 566 567 if (mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { 568 heightSpec = ViewGroup.LayoutParams.FILL_PARENT; 569 } else { 570 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { 571 mPopup.setHeight(height); 572 } else { 573 mPopup.setHeight(mDropDownHeight); 574 } 575 } 576 577 mPopup.setWindowLayoutMode(widthSpec, heightSpec); 578 setClipToScreenEnabled(mPopup, true); 579 580 // use outside touchable to dismiss drop down when touching outside of it, so 581 // only set this if the dropdown is not always visible 582 mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible); 583 mPopup.setTouchInterceptor(mTouchInterceptor); 584 mPopup.showAsDropDown(getAnchorView(), 585 mDropDownHorizontalOffset, mDropDownVerticalOffset); 586 mDropDownList.setSelection(ListView.INVALID_POSITION); 587 588 if (!mModal || mDropDownList.isInTouchMode()) { 589 clearListSelection(); 590 } 591 if (!mModal) { 592 mHandler.post(mHideSelector); 593 } 594 } 595 } 596 597 public static void setClipToScreenEnabled(PopupWindow mPopup, boolean enabled) { 598 try { 599 Field f = mPopup.getClass().getDeclaredField("mClipToScreen"); 600 f.setAccessible(true); 601 f.setBoolean(mPopup, enabled); 602 } catch (NoSuchFieldException e) { 603 // No field 604 } catch (IllegalAccessException e) { 605 e.printStackTrace(); 606 } 607 mPopup.setClippingEnabled(!enabled); 608 } 609 610 /** 611 * Dismiss the popup window. 612 */ 613 public void dismiss() { 614 mPopup.dismiss(); 615 removePromptView(); 616 mPopup.setContentView(null); 617 mDropDownList = null; 618 mHandler.removeCallbacks(mResizePopupRunnable); 619 } 620 621 /** 622 * Set a listener to receive a callback when the popup is dismissed. 623 * 624 * @param listener Listener that will be notified when the popup is dismissed. 625 */ 626 public void setOnDismissListener(PopupWindow.OnDismissListener listener) { 627 mPopup.setOnDismissListener(listener); 628 } 629 630 private void removePromptView() { 631 if (mPromptView != null) { 632 final ViewParent parent = mPromptView.getParent(); 633 if (parent instanceof ViewGroup) { 634 final ViewGroup group = (ViewGroup) parent; 635 group.removeView(mPromptView); 636 } 637 } 638 } 639 640 /** 641 * Control how the popup operates with an input method: one of {@link 642 * #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, or {@link 643 * #INPUT_METHOD_NOT_NEEDED}. 644 * 645 * <p>If the popup is showing, calling this method will take effect only the next time the popup 646 * is shown or through a manual call to the {@link #show()} method.</p> 647 * 648 * @see #getInputMethodMode() 649 * @see #show() 650 */ 651 public void setInputMethodMode(int mode) { 652 mPopup.setInputMethodMode(mode); 653 } 654 655 /** 656 * Return the current value in {@link #setInputMethodMode(int)}. 657 * 658 * @see #setInputMethodMode(int) 659 */ 660 public int getInputMethodMode() { 661 return mPopup.getInputMethodMode(); 662 } 663 664 /** 665 * Set the selected position of the list. Only valid when {@link #isShowing()} == {@code true}. 666 * 667 * @param position List position to set as selected. 668 */ 669 public void setSelection(int position) { 670 DropDownListView list = mDropDownList; 671 if (isShowing() && list != null) { 672 list.mListSelectionHidden = false; 673 list.setSelection(position); 674 if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) { 675 list.setItemChecked(position, true); 676 } 677 } 678 } 679 680 /** 681 * Clear any current list selection. Only valid when {@link #isShowing()} == {@code true}. 682 */ 683 public void clearListSelection() { 684 final DropDownListView list = mDropDownList; 685 if (list != null) { 686 // WARNING: Please read the comment where mListSelectionHidden is declared 687 list.mListSelectionHidden = true; 688 //list.hideSelector(); 689 list.requestLayout(); 690 } 691 } 692 693 /** 694 * @return {@code true} if the popup is currently showing, {@code false} otherwise. 695 */ 696 public boolean isShowing() { 697 return mPopup.isShowing(); 698 } 699 700 /** 701 * @return {@code true} if this popup is configured to assume the user does not need to interact 702 * with the IME while it is showing, {@code false} otherwise. 703 */ 704 public boolean isInputMethodNotNeeded() { 705 return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED; 706 } 707 708 /** 709 * Perform an item click operation on the specified list adapter position. 710 * 711 * @param position Adapter position for performing the click 712 * @return true if the click action could be performed, false if not. (e.g. if the popup was not 713 * showing, this method would return false.) 714 */ 715 public boolean performItemClick(int position) { 716 if (isShowing()) { 717 if (mItemClickListener != null) { 718 final DropDownListView list = mDropDownList; 719 final View child = list.getChildAt(position - list.getFirstVisiblePosition()); 720 final ListAdapter adapter = list.getAdapter(); 721 mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position)); 722 } 723 return true; 724 } 725 return false; 726 } 727 728 /** 729 * @return The currently selected item or null if the popup is not showing. 730 */ 731 public Object getSelectedItem() { 732 if (!isShowing()) { 733 return null; 734 } 735 return mDropDownList.getSelectedItem(); 736 } 737 738 /** 739 * @return The position of the currently selected item or {@link ListView#INVALID_POSITION} if 740 * {@link #isShowing()} == {@code false}. 741 * @see ListView#getSelectedItemPosition() 742 */ 743 public int getSelectedItemPosition() { 744 if (!isShowing()) { 745 return ListView.INVALID_POSITION; 746 } 747 return mDropDownList.getSelectedItemPosition(); 748 } 749 750 /** 751 * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID} if {@link 752 * #isShowing()} == {@code false}. 753 * @see ListView#getSelectedItemId() 754 */ 755 public long getSelectedItemId() { 756 if (!isShowing()) { 757 return ListView.INVALID_ROW_ID; 758 } 759 return mDropDownList.getSelectedItemId(); 760 } 761 762 /** 763 * @return The View for the currently selected item or null if {@link #isShowing()} == {@code 764 * false}. 765 * @see ListView#getSelectedView() 766 */ 767 public View getSelectedView() { 768 if (!isShowing()) { 769 return null; 770 } 771 return mDropDownList.getSelectedView(); 772 } 773 774 /** 775 * @return The {@link ListView} displayed within the popup window. Only valid when {@link 776 * #isShowing()} == {@code true}. 777 */ 778 public ListView getListView() { 779 return mDropDownList; 780 } 781 782 /** 783 * The maximum number of list items that can be visible and still have the list expand when 784 * touched. 785 * 786 * @param max Max number of items that can be visible and still allow the list to expand. 787 */ 788 void setListItemExpandMax(int max) { 789 mListItemExpandMaximum = max; 790 } 791 792 /** 793 * Filter key down events. By forwarding key down events to this function, views using non-modal 794 * ListPopupWindow can have it handle key selection of items. 795 * 796 * @param keyCode keyCode param passed to the host view's onKeyDown 797 * @param event event param passed to the host view's onKeyDown 798 * @return true if the event was handled, false if it was ignored. 799 * @see #setModal(boolean) 800 */ 801 public boolean onKeyDown(int keyCode, KeyEvent event) { 802 // when the drop down is shown, we drive it directly 803 if (isShowing()) { 804 // the key events are forwarded to the list in the drop down view 805 // note that ListView handles space but we don't want that to happen 806 // also if selection is not currently in the drop down, then don't 807 // let center or enter presses go there since that would cause it 808 // to select one of its items 809 if (keyCode != KeyEvent.KEYCODE_SPACE 810 && (mDropDownList.getSelectedItemPosition() >= 0 811 || (keyCode != KeyEvent.KEYCODE_ENTER 812 && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) { 813 int curIndex = mDropDownList.getSelectedItemPosition(); 814 boolean consumed; 815 816 final boolean below = !mPopup.isAboveAnchor(); 817 818 final ListAdapter adapter = mAdapter; 819 820 boolean allEnabled; 821 int firstItem = Integer.MAX_VALUE; 822 int lastItem = Integer.MIN_VALUE; 823 824 if (adapter != null) { 825 allEnabled = adapter.areAllItemsEnabled(); 826 firstItem = allEnabled ? 0 : 827 mDropDownList.lookForSelectablePosition(0, true); 828 lastItem = allEnabled ? adapter.getCount() - 1 : 829 mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false); 830 } 831 832 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) || 833 (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) { 834 // When the selection is at the top, we block the key 835 // event to prevent focus from moving. 836 clearListSelection(); 837 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 838 show(); 839 return true; 840 } else { 841 // WARNING: Please read the comment where mListSelectionHidden 842 // is declared 843 mDropDownList.mListSelectionHidden = false; 844 } 845 846 consumed = mDropDownList.onKeyDown(keyCode, event); 847 if (DEBUG) { 848 Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); 849 } 850 851 if (consumed) { 852 // If it handled the key event, then the user is 853 // navigating in the list, so we should put it in front. 854 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 855 // Here's a little trick we need to do to make sure that 856 // the list view is actually showing its focus indicator, 857 // by ensuring it has focus and getting its window out 858 // of touch mode. 859 mDropDownList.requestFocusFromTouch(); 860 show(); 861 862 switch (keyCode) { 863 // avoid passing the focus from the text view to the 864 // next component 865 case KeyEvent.KEYCODE_ENTER: 866 case KeyEvent.KEYCODE_DPAD_CENTER: 867 case KeyEvent.KEYCODE_DPAD_DOWN: 868 case KeyEvent.KEYCODE_DPAD_UP: 869 return true; 870 } 871 } else { 872 if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { 873 // when the selection is at the bottom, we block the 874 // event to avoid going to the next focusable widget 875 if (curIndex == lastItem) { 876 return true; 877 } 878 } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP && 879 curIndex == firstItem) { 880 return true; 881 } 882 } 883 } 884 } 885 886 return false; 887 } 888 889 /** 890 * Filter key down events. By forwarding key up events to this function, views using non-modal 891 * ListPopupWindow can have it handle key selection of items. 892 * 893 * @param keyCode keyCode param passed to the host view's onKeyUp 894 * @param event event param passed to the host view's onKeyUp 895 * @return true if the event was handled, false if it was ignored. 896 * @see #setModal(boolean) 897 */ 898 public boolean onKeyUp(int keyCode, KeyEvent event) { 899 if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) { 900 boolean consumed = mDropDownList.onKeyUp(keyCode, event); 901 if (consumed) { 902 switch (keyCode) { 903 // if the list accepts the key events and the key event 904 // was a click, the text view gets the selected item 905 // from the drop down as its content 906 case KeyEvent.KEYCODE_ENTER: 907 case KeyEvent.KEYCODE_DPAD_CENTER: 908 dismiss(); 909 break; 910 } 911 } 912 return consumed; 913 } 914 return false; 915 } 916 917 /** 918 * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)} events to 919 * this function, views using ListPopupWindow can have it dismiss the popup when the back key is 920 * pressed. 921 * 922 * @param keyCode keyCode param passed to the host view's onKeyPreIme 923 * @param event event param passed to the host view's onKeyPreIme 924 * @return true if the event was handled, false if it was ignored. 925 * @see #setModal(boolean) 926 */ 927 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 928 if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) { 929 // special case for the back key, we do not even try to send it 930 // to the drop down list but instead, consume it immediately 931 //TODO(anirudhd): No dispatcher APIs in eclair 932 } 933 return false; 934 } 935 936 /** 937 * <p>Builds the popup window's content and returns the height the popup should have. Returns -1 938 * when the content already exists.</p> 939 * 940 * @return the content's height or -1 if content already exists 941 */ 942 private int buildDropDown() { 943 ViewGroup dropDownView; 944 int otherHeights = 0; 945 946 if (mDropDownList == null) { 947 Context context = mContext; 948 949 /** 950 * This Runnable exists for the sole purpose of checking if the view layout has got 951 * completed and if so call showDropDown to display the drop down. This is used to show 952 * the drop down as soon as possible after user opens up the search dialog, without 953 * waiting for the normal UI pipeline to do it's job which is slower than this method. 954 */ 955 mShowDropDownRunnable = new Runnable() { 956 public void run() { 957 // View layout should be all done before displaying the drop down. 958 View view = getAnchorView(); 959 if (view != null && view.getWindowToken() != null) { 960 show(); 961 } 962 } 963 }; 964 965 mDropDownList = new DropDownListView(context, !mModal); 966 if (mDropDownListHighlight != null) { 967 mDropDownList.setSelector(mDropDownListHighlight); 968 } 969 mDropDownList.setAdapter(mAdapter); 970 mDropDownList.setOnItemClickListener(mItemClickListener); 971 mDropDownList.setFocusable(true); 972 mDropDownList.setFocusableInTouchMode(true); 973 mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 974 public void onItemSelected(AdapterView<?> parent, View view, 975 int position, long id) { 976 977 if (position != -1) { 978 DropDownListView dropDownList = mDropDownList; 979 980 if (dropDownList != null) { 981 dropDownList.mListSelectionHidden = false; 982 } 983 } 984 } 985 986 public void onNothingSelected(AdapterView<?> parent) { 987 } 988 }); 989 mDropDownList.setOnScrollListener(mScrollListener); 990 991 if (mItemSelectedListener != null) { 992 mDropDownList.setOnItemSelectedListener(mItemSelectedListener); 993 } 994 995 dropDownView = mDropDownList; 996 997 View hintView = mPromptView; 998 if (hintView != null) { 999 // if a hint has been specified, we accomodate more space for it and 1000 // add a text view in the drop down menu, at the bottom of the list 1001 LinearLayout hintContainer = new LinearLayout(context); 1002 hintContainer.setOrientation(LinearLayout.VERTICAL); 1003 1004 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( 1005 ViewGroup.LayoutParams.FILL_PARENT, 0, 1.0f 1006 ); 1007 1008 switch (mPromptPosition) { 1009 case POSITION_PROMPT_BELOW: 1010 hintContainer.addView(dropDownView, hintParams); 1011 hintContainer.addView(hintView); 1012 break; 1013 1014 case POSITION_PROMPT_ABOVE: 1015 hintContainer.addView(hintView); 1016 hintContainer.addView(dropDownView, hintParams); 1017 break; 1018 1019 default: 1020 Log.e(TAG, "Invalid hint position " + mPromptPosition); 1021 break; 1022 } 1023 1024 // measure the hint's height to find how much more vertical space 1025 // we need to add to the drop down's height 1026 int widthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.AT_MOST); 1027 int heightSpec = MeasureSpec.UNSPECIFIED; 1028 hintView.measure(widthSpec, heightSpec); 1029 1030 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams(); 1031 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin 1032 + hintParams.bottomMargin; 1033 1034 dropDownView = hintContainer; 1035 } 1036 1037 mPopup.setContentView(dropDownView); 1038 } else { 1039 dropDownView = (ViewGroup) mPopup.getContentView(); 1040 final View view = mPromptView; 1041 if (view != null) { 1042 LinearLayout.LayoutParams hintParams = 1043 (LinearLayout.LayoutParams) view.getLayoutParams(); 1044 otherHeights = view.getMeasuredHeight() + hintParams.topMargin 1045 + hintParams.bottomMargin; 1046 } 1047 } 1048 1049 // getMaxAvailableHeight() subtracts the padding, so we put it back 1050 // to get the available height for the whole window 1051 int padding = 0; 1052 Drawable background = mPopup.getBackground(); 1053 if (background != null) { 1054 background.getPadding(mTempRect); 1055 padding = mTempRect.top + mTempRect.bottom; 1056 1057 // If we don't have an explicit vertical offset, determine one from the window 1058 // background so that content will line up. 1059 if (!mDropDownVerticalOffsetSet) { 1060 mDropDownVerticalOffset = -mTempRect.top; 1061 } 1062 } else { 1063 mTempRect.setEmpty(); 1064 } 1065 1066 // Max height available on the screen for a popup. 1067 boolean ignoreBottomDecorations = 1068 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; 1069 final int maxHeight = getMaxAvailableHeight( 1070 getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); 1071 1072 if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { 1073 return maxHeight + padding; 1074 } 1075 1076 final int childWidthSpec; 1077 switch (mDropDownWidth) { 1078 case ViewGroup.LayoutParams.WRAP_CONTENT: 1079 childWidthSpec = MeasureSpec.makeMeasureSpec( 1080 mContext.getResources().getDisplayMetrics().widthPixels - 1081 (mTempRect.left + mTempRect.right), 1082 MeasureSpec.AT_MOST); 1083 break; 1084 case ViewGroup.LayoutParams.FILL_PARENT: 1085 childWidthSpec = MeasureSpec.makeMeasureSpec( 1086 mContext.getResources().getDisplayMetrics().widthPixels - 1087 (mTempRect.left + mTempRect.right), 1088 MeasureSpec.EXACTLY); 1089 break; 1090 default: 1091 childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY); 1092 break; 1093 } 1094 1095 final int listContent = mDropDownList.measureHeightOfChildrenCompat(childWidthSpec, 1096 0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1); 1097 // add padding only if the list has items in it, that way we don't show 1098 // the popup if it is not needed 1099 if (listContent > 0) { 1100 otherHeights += padding; 1101 } 1102 1103 return listContent + otherHeights; 1104 } 1105 1106 /** 1107 * Copied from PopupWindow.java of JB 1108 * 1109 * Returns the maximum height that is available for the popup to be completely shown, optionally 1110 * ignoring any bottom decorations such as the input method. It is recommended that this height 1111 * be the maximum for the popup's height, otherwise it is possible that the popup will be 1112 * clipped. 1113 * 1114 * @param anchor The view on which the popup window must be anchored. 1115 * @param yOffset y offset from the view's bottom edge 1116 * @param ignoreBottomDecorations if true, the height returned will be all the way to the bottom 1117 * of the display, ignoring any bottom decorations 1118 * @return The maximum available height for the popup to be completely shown. 1119 * @hide Pending API council approval. 1120 */ 1121 public int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) { 1122 final Rect displayFrame = new Rect(); 1123 anchor.getWindowVisibleDisplayFrame(displayFrame); 1124 1125 int[] mDrawingLocation = new int[2]; 1126 final int[] anchorPos = mDrawingLocation; 1127 anchor.getLocationOnScreen(anchorPos); 1128 1129 int bottomEdge = displayFrame.bottom; 1130 if (ignoreBottomDecorations) { 1131 Resources res = anchor.getContext().getResources(); 1132 bottomEdge = res.getDisplayMetrics().heightPixels; 1133 } 1134 final int distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset; 1135 final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset; 1136 1137 // anchorPos[1] is distance from anchor to top of screen 1138 int returnedHeight = Math.max(distanceToBottom, distanceToTop); 1139 if (mPopup.getBackground() != null) { 1140 mPopup.getBackground().getPadding(mTempRect); 1141 returnedHeight -= mTempRect.top + mTempRect.bottom; 1142 } 1143 1144 return returnedHeight; 1145 } 1146 1147 /** 1148 * Set the layout direction for this popup. Should be a resolved direction as the popup as no 1149 * capacity to do the resolution on his own. 1150 * 1151 * @param layoutDirection One of {@link View#LAYOUT_DIRECTION_LTR}, {@link 1152 * View#LAYOUT_DIRECTION_RTL}, 1153 */ 1154 public void setLayoutDirection(int layoutDirection) { 1155 mLayoutDirection = layoutDirection; 1156 if (mDropDownList != null) { 1157 // BiDi support 1158 // mDropDownList.setLayoutDirection(mLayoutDirection); 1159 } 1160 } 1161 1162 /** 1163 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to make sure the list uses 1164 * the appropriate drawables and states when displayed on screen within a drop down. The focus 1165 * is never actually passed to the drop down in this mode; the list only looks focused.</p> 1166 */ 1167 private static class DropDownListView extends ListView { 1168 1169 private static final String TAG = ListPopupWindow.TAG + ".DropDownListView"; 1170 1171 /* 1172 * WARNING: This is a workaround for a touch mode issue. 1173 * 1174 * Touch mode is propagated lazily to windows. This causes problems in 1175 * the following scenario: 1176 * - Type something in the AutoCompleteTextView and get some results 1177 * - Move down with the d-pad to select an item in the list 1178 * - Move up with the d-pad until the selection disappears 1179 * - Type more text in the AutoCompleteTextView *using the soft keyboard* 1180 * and get new results; you are now in touch mode 1181 * - The selection comes back on the first item in the list, even though 1182 * the list is supposed to be in touch mode 1183 * 1184 * Using the soft keyboard triggers the touch mode change but that change 1185 * is propagated to our window only after the first list layout, therefore 1186 * after the list attempts to resurrect the selection. 1187 * 1188 * The trick to work around this issue is to pretend the list is in touch 1189 * mode when we know that the selection should not appear, that is when 1190 * we know the user moved the selection away from the list. 1191 * 1192 * This boolean is set to true whenever we explicitly hide the list's 1193 * selection and reset to false whenever we know the user moved the 1194 * selection back to the list. 1195 * 1196 * When this boolean is true, isInTouchMode() returns true, otherwise it 1197 * returns super.isInTouchMode(). 1198 */ 1199 private boolean mListSelectionHidden; 1200 1201 1202 public static final int INVALID_POSITION = -1; 1203 1204 static final int NO_POSITION = -1; 1205 1206 1207 /** 1208 * True if this wrapper should fake focus. 1209 */ 1210 private boolean mHijackFocus; 1211 1212 /** 1213 * <p>Creates a new list view wrapper.</p> 1214 * 1215 * @param context this view's context 1216 */ 1217 public DropDownListView(Context context, boolean hijackFocus) { 1218 super(context, null, R.attr.dropDownListViewStyle); 1219 mHijackFocus = hijackFocus; 1220 // TODO: Add an API to control this 1221 setCacheColorHint(0); // Transparent, since the background drawable could be anything. 1222 } 1223 1224 /** 1225 * Find a position that can be selected (i.e., is not a separator). 1226 * 1227 * @param position The starting position to look at. 1228 * @param lookDown Whether to look down for other positions. 1229 * @return The next selectable position starting at position and then searching either up or 1230 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 1231 */ 1232 private int lookForSelectablePosition(int position, boolean lookDown) { 1233 final ListAdapter adapter = getAdapter(); 1234 if (adapter == null || isInTouchMode()) { 1235 return INVALID_POSITION; 1236 } 1237 1238 final int count = adapter.getCount(); 1239 if (!getAdapter().areAllItemsEnabled()) { 1240 if (lookDown) { 1241 position = Math.max(0, position); 1242 while (position < count && !adapter.isEnabled(position)) { 1243 position++; 1244 } 1245 } else { 1246 position = Math.min(position, count - 1); 1247 while (position >= 0 && !adapter.isEnabled(position)) { 1248 position--; 1249 } 1250 } 1251 1252 if (position < 0 || position >= count) { 1253 return INVALID_POSITION; 1254 } 1255 return position; 1256 } else { 1257 if (position < 0 || position >= count) { 1258 return INVALID_POSITION; 1259 } 1260 return position; 1261 } 1262 } 1263 1264 @Override 1265 public boolean isInTouchMode() { 1266 // WARNING: Please read the comment where mListSelectionHidden is declared 1267 return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode(); 1268 } 1269 1270 /** 1271 * <p>Returns the focus state in the drop down.</p> 1272 * 1273 * @return true always if hijacking focus 1274 */ 1275 @Override 1276 public boolean hasWindowFocus() { 1277 return mHijackFocus || super.hasWindowFocus(); 1278 } 1279 1280 /** 1281 * <p>Returns the focus state in the drop down.</p> 1282 * 1283 * @return true always if hijacking focus 1284 */ 1285 @Override 1286 public boolean isFocused() { 1287 return mHijackFocus || super.isFocused(); 1288 } 1289 1290 /** 1291 * <p>Returns the focus state in the drop down.</p> 1292 * 1293 * @return true always if hijacking focus 1294 */ 1295 @Override 1296 public boolean hasFocus() { 1297 return mHijackFocus || super.hasFocus(); 1298 } 1299 1300 /** 1301 * Measures the height of the given range of children (inclusive) and returns the height 1302 * with this ListView's padding and divider heights included. If maxHeight is provided, the 1303 * measuring will stop when the current height reaches maxHeight. 1304 * 1305 * @param widthMeasureSpec The width measure spec to be given to a child's 1306 * {@link View#measure(int, int)}. 1307 * @param startPosition The position of the first child to be shown. 1308 * @param endPosition The (inclusive) position of the last child to be 1309 * shown. Specify {@link #NO_POSITION} if the last child 1310 * should be the last available child from the adapter. 1311 * @param maxHeight The maximum height that will be returned (if all the 1312 * children don't fit in this value, this value will be 1313 * returned). 1314 * @param disallowPartialChildPosition In general, whether the returned height should only 1315 * contain entire children. This is more powerful--it is 1316 * the first inclusive position at which partial 1317 * children will not be allowed. Example: it looks nice 1318 * to have at least 3 completely visible children, and 1319 * in portrait this will most likely fit; but in 1320 * landscape there could be times when even 2 children 1321 * can not be completely shown, so a value of 2 1322 * (remember, inclusive) would be good (assuming 1323 * startPosition is 0). 1324 * @return The height of this ListView with the given children. 1325 */ 1326 final int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition, 1327 int endPosition, final int maxHeight, 1328 int disallowPartialChildPosition) { 1329 1330 final int paddingTop = getListPaddingTop(); 1331 final int paddingBottom = getListPaddingBottom(); 1332 final int paddingLeft = getListPaddingLeft(); 1333 final int paddingRight = getListPaddingRight(); 1334 final int reportedDividerHeight = getDividerHeight(); 1335 final Drawable divider = getDivider(); 1336 1337 final ListAdapter adapter = getAdapter(); 1338 1339 if (adapter == null) { 1340 return paddingTop + paddingBottom; 1341 } 1342 1343 // Include the padding of the list 1344 int returnedHeight = paddingTop + paddingBottom; 1345 final int dividerHeight = ((reportedDividerHeight > 0) && divider != null) 1346 ? reportedDividerHeight : 0; 1347 1348 // The previous height value that was less than maxHeight and contained 1349 // no partial children 1350 int prevHeightWithoutPartialChild = 0; 1351 1352 View child = null; 1353 int viewType = 0; 1354 int count = adapter.getCount(); 1355 for (int i = 0; i < count; i++) { 1356 int newType = adapter.getItemViewType(i); 1357 if (newType != viewType) { 1358 child = null; 1359 viewType = newType; 1360 } 1361 child = adapter.getView(i, child, this); 1362 ; 1363 1364 // Compute child height spec 1365 int heightMeasureSpec; 1366 int childHeight = child.getLayoutParams().height; 1367 if (childHeight > 0) { 1368 heightMeasureSpec = MeasureSpec 1369 .makeMeasureSpec(childHeight, MeasureSpec.EXACTLY); 1370 } else { 1371 heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1372 } 1373 child.measure(widthMeasureSpec, heightMeasureSpec); 1374 1375 if (i > 0) { 1376 // Count the divider for all but one child 1377 returnedHeight += dividerHeight; 1378 } 1379 1380 returnedHeight += child.getMeasuredHeight(); 1381 1382 if (returnedHeight >= maxHeight) { 1383 // We went over, figure out which height to return. If returnedHeight > 1384 // maxHeight, then the i'th position did not fit completely. 1385 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) 1386 && (i > disallowPartialChildPosition) // We've past the min pos 1387 && (prevHeightWithoutPartialChild > 0) // We have a prev height 1388 && (returnedHeight != maxHeight) // i'th child did not fit completely 1389 ? prevHeightWithoutPartialChild 1390 : maxHeight; 1391 } 1392 1393 if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { 1394 prevHeightWithoutPartialChild = returnedHeight; 1395 } 1396 } 1397 1398 // At this point, we went through the range of children, and they each 1399 // completely fit, so return the returnedHeight 1400 return returnedHeight; 1401 } 1402 1403 } 1404 1405 private class PopupDataSetObserver extends DataSetObserver { 1406 1407 @Override 1408 public void onChanged() { 1409 if (isShowing()) { 1410 // Resize the popup to fit new content 1411 show(); 1412 } 1413 } 1414 1415 @Override 1416 public void onInvalidated() { 1417 dismiss(); 1418 } 1419 } 1420 1421 private class ListSelectorHider implements Runnable { 1422 1423 public void run() { 1424 clearListSelection(); 1425 } 1426 } 1427 1428 private class ResizePopupRunnable implements Runnable { 1429 1430 public void run() { 1431 if (mDropDownList != null && mDropDownList.getCount() > mDropDownList.getChildCount() && 1432 mDropDownList.getChildCount() <= mListItemExpandMaximum) { 1433 mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 1434 show(); 1435 } 1436 } 1437 } 1438 1439 private class PopupTouchInterceptor implements OnTouchListener { 1440 1441 public boolean onTouch(View v, MotionEvent event) { 1442 final int action = event.getAction(); 1443 final int x = (int) event.getX(); 1444 final int y = (int) event.getY(); 1445 1446 if (action == MotionEvent.ACTION_DOWN && 1447 mPopup != null && mPopup.isShowing() && 1448 (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) { 1449 mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT); 1450 } else if (action == MotionEvent.ACTION_UP) { 1451 mHandler.removeCallbacks(mResizePopupRunnable); 1452 } 1453 return false; 1454 } 1455 } 1456 1457 private class PopupScrollListener implements ListView.OnScrollListener { 1458 1459 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 1460 int totalItemCount) { 1461 1462 } 1463 1464 public void onScrollStateChanged(AbsListView view, int scrollState) { 1465 if (scrollState == SCROLL_STATE_TOUCH_SCROLL && 1466 !isInputMethodNotNeeded() && mPopup.getContentView() != null) { 1467 mHandler.removeCallbacks(mResizePopupRunnable); 1468 mResizePopupRunnable.run(); 1469 } 1470 } 1471 } 1472}