PopupWindow.java revision d034856ab901a8be1877174da33458630faeb382
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.widget; 18 19import com.android.internal.R; 20 21import android.content.Context; 22import android.content.res.TypedArray; 23import android.view.KeyEvent; 24import android.view.MotionEvent; 25import android.view.View; 26import android.view.WindowManager; 27import android.view.Gravity; 28import android.view.ViewGroup; 29import android.view.ViewTreeObserver; 30import android.view.ViewTreeObserver.OnScrollChangedListener; 31import android.view.View.OnTouchListener; 32import android.graphics.PixelFormat; 33import android.graphics.Rect; 34import android.graphics.drawable.Drawable; 35import android.graphics.drawable.StateListDrawable; 36import android.os.IBinder; 37import android.util.AttributeSet; 38 39import java.lang.ref.WeakReference; 40 41/** 42 * <p>A popup window that can be used to display an arbitrary view. The popup 43 * windows is a floating container that appears on top of the current 44 * activity.</p> 45 * 46 * @see android.widget.AutoCompleteTextView 47 * @see android.widget.Spinner 48 */ 49public class PopupWindow { 50 /** 51 * Mode for {@link #setInputMethodMode(int)}: the requirements for the 52 * input method should be based on the focusability of the popup. That is 53 * if it is focusable than it needs to work with the input method, else 54 * it doesn't. 55 */ 56 public static final int INPUT_METHOD_FROM_FOCUSABLE = 0; 57 58 /** 59 * Mode for {@link #setInputMethodMode(int)}: this popup always needs to 60 * work with an input method, regardless of whether it is focusable. This 61 * means that it will always be displayed so that the user can also operate 62 * the input method while it is shown. 63 */ 64 public static final int INPUT_METHOD_NEEDED = 1; 65 66 /** 67 * Mode for {@link #setInputMethodMode(int)}: this popup never needs to 68 * work with an input method, regardless of whether it is focusable. This 69 * means that it will always be displayed to use as much space on the 70 * screen as needed, regardless of whether this covers the input method. 71 */ 72 public static final int INPUT_METHOD_NOT_NEEDED = 2; 73 74 private Context mContext; 75 private WindowManager mWindowManager; 76 77 private boolean mIsShowing; 78 private boolean mIsDropdown; 79 80 private View mContentView; 81 private View mPopupView; 82 private boolean mFocusable; 83 private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE; 84 private int mSoftInputMode; 85 private boolean mTouchable = true; 86 private boolean mOutsideTouchable = false; 87 private boolean mClippingEnabled = true; 88 89 private OnTouchListener mTouchInterceptor; 90 91 private int mWidthMode; 92 private int mWidth; 93 private int mLastWidth; 94 private int mHeightMode; 95 private int mHeight; 96 private int mLastHeight; 97 98 private int mPopupWidth; 99 private int mPopupHeight; 100 101 private int[] mDrawingLocation = new int[2]; 102 private int[] mScreenLocation = new int[2]; 103 private Rect mTempRect = new Rect(); 104 105 private Drawable mBackground; 106 private Drawable mAboveAnchorBackgroundDrawable; 107 private Drawable mBelowAnchorBackgroundDrawable; 108 109 private boolean mAboveAnchor; 110 111 private OnDismissListener mOnDismissListener; 112 private boolean mIgnoreCheekPress = false; 113 114 private int mAnimationStyle = -1; 115 116 private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] { 117 com.android.internal.R.attr.state_above_anchor 118 }; 119 120 private WeakReference<View> mAnchor; 121 private OnScrollChangedListener mOnScrollChangedListener = 122 new OnScrollChangedListener() { 123 public void onScrollChanged() { 124 View anchor = mAnchor.get(); 125 if (anchor != null && mPopupView != null) { 126 WindowManager.LayoutParams p = (WindowManager.LayoutParams) 127 mPopupView.getLayoutParams(); 128 129 mAboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff); 130 update(p.x, p.y, -1, -1, true); 131 } 132 } 133 }; 134 private int mAnchorXoff, mAnchorYoff; 135 136 /** 137 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> 138 * 139 * <p>The popup does provide a background.</p> 140 */ 141 public PopupWindow(Context context) { 142 this(context, null); 143 } 144 145 /** 146 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> 147 * 148 * <p>The popup does provide a background.</p> 149 */ 150 public PopupWindow(Context context, AttributeSet attrs) { 151 this(context, attrs, com.android.internal.R.attr.popupWindowStyle); 152 } 153 154 /** 155 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> 156 * 157 * <p>The popup does provide a background.</p> 158 */ 159 public PopupWindow(Context context, AttributeSet attrs, int defStyle) { 160 mContext = context; 161 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 162 163 TypedArray a = 164 context.obtainStyledAttributes( 165 attrs, com.android.internal.R.styleable.PopupWindow, defStyle, 0); 166 167 mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground); 168 169 // If this is a StateListDrawable, try to find and store the drawable to be 170 // used when the drop-down is placed above its anchor view, and the one to be 171 // used when the drop-down is placed below its anchor view. We extract 172 // the drawables ourselves to work around a problem with using refreshDrawableState 173 // that it will take into account the padding of all drawables specified in a 174 // StateListDrawable, thus adding superfluous padding to drop-down views. 175 // 176 // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and 177 // at least one other drawable, intended for the 'below-anchor state'. 178 if (mBackground instanceof StateListDrawable) { 179 StateListDrawable background = (StateListDrawable) mBackground; 180 181 // Find the above-anchor view - this one's easy, it should be labeled as such. 182 int aboveAnchorStateIndex = background.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET); 183 184 // Now, for the below-anchor view, look for any other drawable specified in the 185 // StateListDrawable which is not for the above-anchor state and use that. 186 int count = background.getStateCount(); 187 int belowAnchorStateIndex = -1; 188 for (int i = 0; i < count; i++) { 189 if (i != aboveAnchorStateIndex) { 190 belowAnchorStateIndex = i; 191 break; 192 } 193 } 194 195 // Store the drawables we found, if we found them. Otherwise, set them both 196 // to null so that we'll just use refreshDrawableState. 197 if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) { 198 mAboveAnchorBackgroundDrawable = background.getStateDrawable(aboveAnchorStateIndex); 199 mBelowAnchorBackgroundDrawable = background.getStateDrawable(belowAnchorStateIndex); 200 } else { 201 mBelowAnchorBackgroundDrawable = null; 202 mAboveAnchorBackgroundDrawable = null; 203 } 204 } 205 206 a.recycle(); 207 } 208 209 /** 210 * <p>Create a new empty, non focusable popup window of dimension (0,0).</p> 211 * 212 * <p>The popup does not provide any background. This should be handled 213 * by the content view.</p> 214 */ 215 public PopupWindow() { 216 this(null, 0, 0); 217 } 218 219 /** 220 * <p>Create a new non focusable popup window which can display the 221 * <tt>contentView</tt>. The dimension of the window are (0,0).</p> 222 * 223 * <p>The popup does not provide any background. This should be handled 224 * by the content view.</p> 225 * 226 * @param contentView the popup's content 227 */ 228 public PopupWindow(View contentView) { 229 this(contentView, 0, 0); 230 } 231 232 /** 233 * <p>Create a new empty, non focusable popup window. The dimension of the 234 * window must be passed to this constructor.</p> 235 * 236 * <p>The popup does not provide any background. This should be handled 237 * by the content view.</p> 238 * 239 * @param width the popup's width 240 * @param height the popup's height 241 */ 242 public PopupWindow(int width, int height) { 243 this(null, width, height); 244 } 245 246 /** 247 * <p>Create a new non focusable popup window which can display the 248 * <tt>contentView</tt>. The dimension of the window must be passed to 249 * this constructor.</p> 250 * 251 * <p>The popup does not provide any background. This should be handled 252 * by the content view.</p> 253 * 254 * @param contentView the popup's content 255 * @param width the popup's width 256 * @param height the popup's height 257 */ 258 public PopupWindow(View contentView, int width, int height) { 259 this(contentView, width, height, false); 260 } 261 262 /** 263 * <p>Create a new popup window which can display the <tt>contentView</tt>. 264 * The dimension of the window must be passed to this constructor.</p> 265 * 266 * <p>The popup does not provide any background. This should be handled 267 * by the content view.</p> 268 * 269 * @param contentView the popup's content 270 * @param width the popup's width 271 * @param height the popup's height 272 * @param focusable true if the popup can be focused, false otherwise 273 */ 274 public PopupWindow(View contentView, int width, int height, boolean focusable) { 275 if (contentView != null) { 276 mContext = contentView.getContext(); 277 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 278 } 279 setContentView(contentView); 280 setWidth(width); 281 setHeight(height); 282 setFocusable(focusable); 283 } 284 285 /** 286 * <p>Return the drawable used as the popup window's background.</p> 287 * 288 * @return the background drawable or null 289 */ 290 public Drawable getBackground() { 291 return mBackground; 292 } 293 294 /** 295 * <p>Change the background drawable for this popup window. The background 296 * can be set to null.</p> 297 * 298 * @param background the popup's background 299 */ 300 public void setBackgroundDrawable(Drawable background) { 301 mBackground = background; 302 } 303 304 /** 305 * <p>Return the animation style to use the popup appears and disappears</p> 306 * 307 * @return the animation style to use the popup appears and disappears 308 */ 309 public int getAnimationStyle() { 310 return mAnimationStyle; 311 } 312 313 /** 314 * Set the flag on popup to ignore cheek press eventt; by default this flag 315 * is set to false 316 * which means the pop wont ignore cheek press dispatch events. 317 * 318 * <p>If the popup is showing, calling this method will take effect only 319 * the next time the popup is shown or through a manual call to one of 320 * the {@link #update()} methods.</p> 321 * 322 * @see #update() 323 */ 324 public void setIgnoreCheekPress() { 325 mIgnoreCheekPress = true; 326 } 327 328 329 /** 330 * <p>Change the animation style resource for this popup.</p> 331 * 332 * <p>If the popup is showing, calling this method will take effect only 333 * the next time the popup is shown or through a manual call to one of 334 * the {@link #update()} methods.</p> 335 * 336 * @param animationStyle animation style to use when the popup appears 337 * and disappears. Set to -1 for the default animation, 0 for no 338 * animation, or a resource identifier for an explicit animation. 339 * 340 * @see #update() 341 */ 342 public void setAnimationStyle(int animationStyle) { 343 mAnimationStyle = animationStyle; 344 } 345 346 /** 347 * <p>Return the view used as the content of the popup window.</p> 348 * 349 * @return a {@link android.view.View} representing the popup's content 350 * 351 * @see #setContentView(android.view.View) 352 */ 353 public View getContentView() { 354 return mContentView; 355 } 356 357 /** 358 * <p>Change the popup's content. The content is represented by an instance 359 * of {@link android.view.View}.</p> 360 * 361 * <p>This method has no effect if called when the popup is showing. To 362 * apply it while a popup is showing, call </p> 363 * 364 * @param contentView the new content for the popup 365 * 366 * @see #getContentView() 367 * @see #isShowing() 368 */ 369 public void setContentView(View contentView) { 370 if (isShowing()) { 371 return; 372 } 373 374 mContentView = contentView; 375 376 if (mContext == null) { 377 mContext = mContentView.getContext(); 378 } 379 380 if (mWindowManager == null) { 381 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 382 } 383 } 384 385 /** 386 * Set a callback for all touch events being dispatched to the popup 387 * window. 388 */ 389 public void setTouchInterceptor(OnTouchListener l) { 390 mTouchInterceptor = l; 391 } 392 393 /** 394 * <p>Indicate whether the popup window can grab the focus.</p> 395 * 396 * @return true if the popup is focusable, false otherwise 397 * 398 * @see #setFocusable(boolean) 399 */ 400 public boolean isFocusable() { 401 return mFocusable; 402 } 403 404 /** 405 * <p>Changes the focusability of the popup window. When focusable, the 406 * window will grab the focus from the current focused widget if the popup 407 * contains a focusable {@link android.view.View}. By default a popup 408 * window is not focusable.</p> 409 * 410 * <p>If the popup is showing, calling this method will take effect only 411 * the next time the popup is shown or through a manual call to one of 412 * the {@link #update()} methods.</p> 413 * 414 * @param focusable true if the popup should grab focus, false otherwise. 415 * 416 * @see #isFocusable() 417 * @see #isShowing() 418 * @see #update() 419 */ 420 public void setFocusable(boolean focusable) { 421 mFocusable = focusable; 422 } 423 424 /** 425 * Return the current value in {@link #setInputMethodMode(int)}. 426 * 427 * @see #setInputMethodMode(int) 428 */ 429 public int getInputMethodMode() { 430 return mInputMethodMode; 431 432 } 433 434 /** 435 * Control how the popup operates with an input method: one of 436 * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, 437 * or {@link #INPUT_METHOD_NOT_NEEDED}. 438 * 439 * <p>If the popup is showing, calling this method will take effect only 440 * the next time the popup is shown or through a manual call to one of 441 * the {@link #update()} methods.</p> 442 * 443 * @see #getInputMethodMode() 444 * @see #update() 445 */ 446 public void setInputMethodMode(int mode) { 447 mInputMethodMode = mode; 448 } 449 450 /** 451 * Sets the operating mode for the soft input area. 452 * 453 * @param mode The desired mode, see 454 * {@link android.view.WindowManager.LayoutParams#softInputMode} 455 * for the full list 456 * 457 * @see android.view.WindowManager.LayoutParams#softInputMode 458 * @see #getSoftInputMode() 459 */ 460 public void setSoftInputMode(int mode) { 461 mSoftInputMode = mode; 462 } 463 464 /** 465 * Returns the current value in {@link #setSoftInputMode(int)}. 466 * 467 * @see #setSoftInputMode(int) 468 * @see android.view.WindowManager.LayoutParams#softInputMode 469 */ 470 public int getSoftInputMode() { 471 return mSoftInputMode; 472 } 473 474 /** 475 * <p>Indicates whether the popup window receives touch events.</p> 476 * 477 * @return true if the popup is touchable, false otherwise 478 * 479 * @see #setTouchable(boolean) 480 */ 481 public boolean isTouchable() { 482 return mTouchable; 483 } 484 485 /** 486 * <p>Changes the touchability of the popup window. When touchable, the 487 * window will receive touch events, otherwise touch events will go to the 488 * window below it. By default the window is touchable.</p> 489 * 490 * <p>If the popup is showing, calling this method will take effect only 491 * the next time the popup is shown or through a manual call to one of 492 * the {@link #update()} methods.</p> 493 * 494 * @param touchable true if the popup should receive touch events, false otherwise 495 * 496 * @see #isTouchable() 497 * @see #isShowing() 498 * @see #update() 499 */ 500 public void setTouchable(boolean touchable) { 501 mTouchable = touchable; 502 } 503 504 /** 505 * <p>Indicates whether the popup window will be informed of touch events 506 * outside of its window.</p> 507 * 508 * @return true if the popup is outside touchable, false otherwise 509 * 510 * @see #setOutsideTouchable(boolean) 511 */ 512 public boolean isOutsideTouchable() { 513 return mOutsideTouchable; 514 } 515 516 /** 517 * <p>Controls whether the pop-up will be informed of touch events outside 518 * of its window. This only makes sense for pop-ups that are touchable 519 * but not focusable, which means touches outside of the window will 520 * be delivered to the window behind. The default is false.</p> 521 * 522 * <p>If the popup is showing, calling this method will take effect only 523 * the next time the popup is shown or through a manual call to one of 524 * the {@link #update()} methods.</p> 525 * 526 * @param touchable true if the popup should receive outside 527 * touch events, false otherwise 528 * 529 * @see #isOutsideTouchable() 530 * @see #isShowing() 531 * @see #update() 532 */ 533 public void setOutsideTouchable(boolean touchable) { 534 mOutsideTouchable = touchable; 535 } 536 537 /** 538 * <p>Indicates whether clipping of the popup window is enabled.</p> 539 * 540 * @return true if the clipping is enabled, false otherwise 541 * 542 * @see #setClippingEnabled(boolean) 543 */ 544 public boolean isClippingEnabled() { 545 return mClippingEnabled; 546 } 547 548 /** 549 * <p>Allows the popup window to extend beyond the bounds of the screen. By default the 550 * window is clipped to the screen boundaries. Setting this to false will allow windows to be 551 * accurately positioned.</p> 552 * 553 * <p>If the popup is showing, calling this method will take effect only 554 * the next time the popup is shown or through a manual call to one of 555 * the {@link #update()} methods.</p> 556 * 557 * @param enabled false if the window should be allowed to extend outside of the screen 558 * @see #isShowing() 559 * @see #isClippingEnabled() 560 * @see #update() 561 */ 562 public void setClippingEnabled(boolean enabled) { 563 mClippingEnabled = enabled; 564 } 565 566 /** 567 * <p>Change the width and height measure specs that are given to the 568 * window manager by the popup. By default these are 0, meaning that 569 * the current width or height is requested as an explicit size from 570 * the window manager. You can supply 571 * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or 572 * {@link ViewGroup.LayoutParams#FILL_PARENT} to have that measure 573 * spec supplied instead, replacing the absolute width and height that 574 * has been set in the popup.</p> 575 * 576 * <p>If the popup is showing, calling this method will take effect only 577 * the next time the popup is shown.</p> 578 * 579 * @param widthSpec an explicit width measure spec mode, either 580 * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, 581 * {@link ViewGroup.LayoutParams#FILL_PARENT}, or 0 to use the absolute 582 * width. 583 * @param heightSpec an explicit height measure spec mode, either 584 * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, 585 * {@link ViewGroup.LayoutParams#FILL_PARENT}, or 0 to use the absolute 586 * height. 587 */ 588 public void setWindowLayoutMode(int widthSpec, int heightSpec) { 589 mWidthMode = widthSpec; 590 mHeightMode = heightSpec; 591 } 592 593 /** 594 * <p>Return this popup's height MeasureSpec</p> 595 * 596 * @return the height MeasureSpec of the popup 597 * 598 * @see #setHeight(int) 599 */ 600 public int getHeight() { 601 return mHeight; 602 } 603 604 /** 605 * <p>Change the popup's height MeasureSpec</p> 606 * 607 * <p>If the popup is showing, calling this method will take effect only 608 * the next time the popup is shown.</p> 609 * 610 * @param height the height MeasureSpec of the popup 611 * 612 * @see #getHeight() 613 * @see #isShowing() 614 */ 615 public void setHeight(int height) { 616 mHeight = height; 617 } 618 619 /** 620 * <p>Return this popup's width MeasureSpec</p> 621 * 622 * @return the width MeasureSpec of the popup 623 * 624 * @see #setWidth(int) 625 */ 626 public int getWidth() { 627 return mWidth; 628 } 629 630 /** 631 * <p>Change the popup's width MeasureSpec</p> 632 * 633 * <p>If the popup is showing, calling this method will take effect only 634 * the next time the popup is shown.</p> 635 * 636 * @param width the width MeasureSpec of the popup 637 * 638 * @see #getWidth() 639 * @see #isShowing() 640 */ 641 public void setWidth(int width) { 642 mWidth = width; 643 } 644 645 /** 646 * <p>Indicate whether this popup window is showing on screen.</p> 647 * 648 * @return true if the popup is showing, false otherwise 649 */ 650 public boolean isShowing() { 651 return mIsShowing; 652 } 653 654 /** 655 * <p> 656 * Display the content view in a popup window at the specified location. If the popup window 657 * cannot fit on screen, it will be clipped. See {@link android.view.WindowManager.LayoutParams} 658 * for more information on how gravity and the x and y parameters are related. Specifying 659 * a gravity of {@link android.view.Gravity#NO_GRAVITY} is similar to specifying 660 * <code>Gravity.LEFT | Gravity.TOP</code>. 661 * </p> 662 * 663 * @param parent a parent view to get the {@link android.view.View#getWindowToken()} token from 664 * @param gravity the gravity which controls the placement of the popup window 665 * @param x the popup's x location offset 666 * @param y the popup's y location offset 667 */ 668 public void showAtLocation(View parent, int gravity, int x, int y) { 669 if (isShowing() || mContentView == null) { 670 return; 671 } 672 673 unregisterForScrollChanged(); 674 675 mIsShowing = true; 676 mIsDropdown = false; 677 678 WindowManager.LayoutParams p = createPopupLayout(parent.getWindowToken()); 679 p.windowAnimations = computeAnimationResource(); 680 681 preparePopup(p); 682 if (gravity == Gravity.NO_GRAVITY) { 683 gravity = Gravity.TOP | Gravity.LEFT; 684 } 685 p.gravity = gravity; 686 p.x = x; 687 p.y = y; 688 invokePopup(p); 689 } 690 691 /** 692 * <p>Display the content view in a popup window anchored to the bottom-left 693 * corner of the anchor view. If there is not enough room on screen to show 694 * the popup in its entirety, this method tries to find a parent scroll 695 * view to scroll. If no parent scroll view can be scrolled, the bottom-left 696 * corner of the popup is pinned at the top left corner of the anchor view.</p> 697 * 698 * @param anchor the view on which to pin the popup window 699 * 700 * @see #dismiss() 701 */ 702 public void showAsDropDown(View anchor) { 703 showAsDropDown(anchor, 0, 0); 704 } 705 706 /** 707 * <p>Display the content view in a popup window anchored to the bottom-left 708 * corner of the anchor view offset by the specified x and y coordinates. 709 * If there is not enough room on screen to show 710 * the popup in its entirety, this method tries to find a parent scroll 711 * view to scroll. If no parent scroll view can be scrolled, the bottom-left 712 * corner of the popup is pinned at the top left corner of the anchor view.</p> 713 * <p>If the view later scrolls to move <code>anchor</code> to a different 714 * location, the popup will be moved correspondingly.</p> 715 * 716 * @param anchor the view on which to pin the popup window 717 * 718 * @see #dismiss() 719 */ 720 public void showAsDropDown(View anchor, int xoff, int yoff) { 721 if (isShowing() || mContentView == null) { 722 return; 723 } 724 725 registerForScrollChanged(anchor, xoff, yoff); 726 727 mIsShowing = true; 728 mIsDropdown = true; 729 730 WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken()); 731 preparePopup(p); 732 mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); 733 734 if (mBackground != null) { 735 // If the background drawable provided was a StateListDrawable with above-anchor 736 // and below-anchor states, use those. Otherwise rely on refreshDrawableState to 737 // do the job. 738 if (mAboveAnchorBackgroundDrawable != null) { 739 if (mAboveAnchor) { 740 mPopupView.setBackgroundDrawable(mAboveAnchorBackgroundDrawable); 741 } else { 742 mPopupView.setBackgroundDrawable(mBelowAnchorBackgroundDrawable); 743 } 744 } else { 745 mPopupView.refreshDrawableState(); 746 } 747 } 748 749 if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; 750 if (mWidthMode < 0) p.width = mLastWidth = mWidthMode; 751 752 p.windowAnimations = computeAnimationResource(); 753 754 invokePopup(p); 755 } 756 757 /** 758 * Indicates whether the popup is showing above (the y coordinate of the popup's bottom 759 * is less than the y coordinate of the anchor) or below the anchor view (the y coordinate 760 * of the popup is greater than y coordinate of the anchor's bottom). 761 * 762 * The value returned 763 * by this method is meaningful only after {@link #showAsDropDown(android.view.View)} 764 * or {@link #showAsDropDown(android.view.View, int, int)} was invoked. 765 * 766 * @return True if this popup is showing above the anchor view, false otherwise. 767 */ 768 public boolean isAboveAnchor() { 769 return mAboveAnchor; 770 } 771 772 /** 773 * <p>Prepare the popup by embedding in into a new ViewGroup if the 774 * background drawable is not null. If embedding is required, the layout 775 * parameters' height is mnodified to take into account the background's 776 * padding.</p> 777 * 778 * @param p the layout parameters of the popup's content view 779 */ 780 private void preparePopup(WindowManager.LayoutParams p) { 781 if (mContentView == null || mContext == null || mWindowManager == null) { 782 throw new IllegalStateException("You must specify a valid content view by " 783 + "calling setContentView() before attempting to show the popup."); 784 } 785 786 if (mBackground != null) { 787 final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); 788 int height = ViewGroup.LayoutParams.FILL_PARENT; 789 if (layoutParams != null && 790 layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { 791 height = ViewGroup.LayoutParams.WRAP_CONTENT; 792 } 793 794 // when a background is available, we embed the content view 795 // within another view that owns the background drawable 796 PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); 797 PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( 798 ViewGroup.LayoutParams.FILL_PARENT, height 799 ); 800 popupViewContainer.setBackgroundDrawable(mBackground); 801 popupViewContainer.addView(mContentView, listParams); 802 803 mPopupView = popupViewContainer; 804 } else { 805 mPopupView = mContentView; 806 } 807 mPopupWidth = p.width; 808 mPopupHeight = p.height; 809 } 810 811 /** 812 * <p>Invoke the popup window by adding the content view to the window 813 * manager.</p> 814 * 815 * <p>The content view must be non-null when this method is invoked.</p> 816 * 817 * @param p the layout parameters of the popup's content view 818 */ 819 private void invokePopup(WindowManager.LayoutParams p) { 820 p.packageName = mContext.getPackageName(); 821 mWindowManager.addView(mPopupView, p); 822 } 823 824 /** 825 * <p>Generate the layout parameters for the popup window.</p> 826 * 827 * @param token the window token used to bind the popup's window 828 * 829 * @return the layout parameters to pass to the window manager 830 */ 831 private WindowManager.LayoutParams createPopupLayout(IBinder token) { 832 // generates the layout parameters for the drop down 833 // we want a fixed size view located at the bottom left of the anchor 834 WindowManager.LayoutParams p = new WindowManager.LayoutParams(); 835 // these gravity settings put the view at the top left corner of the 836 // screen. The view is then positioned to the appropriate location 837 // by setting the x and y offsets to match the anchor's bottom 838 // left corner 839 p.gravity = Gravity.LEFT | Gravity.TOP; 840 p.width = mLastWidth = mWidth; 841 p.height = mLastHeight = mHeight; 842 if (mBackground != null) { 843 p.format = mBackground.getOpacity(); 844 } else { 845 p.format = PixelFormat.TRANSLUCENT; 846 } 847 p.flags = computeFlags(p.flags); 848 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 849 p.token = token; 850 p.softInputMode = mSoftInputMode; 851 p.setTitle("PopupWindow:" + Integer.toHexString(hashCode())); 852 853 return p; 854 } 855 856 private int computeFlags(int curFlags) { 857 curFlags &= ~( 858 WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | 859 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 860 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 861 WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | 862 WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | 863 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 864 if(mIgnoreCheekPress) { 865 curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; 866 } 867 if (!mFocusable) { 868 curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 869 if (mInputMethodMode == INPUT_METHOD_NEEDED) { 870 curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 871 } 872 } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) { 873 curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 874 } 875 if (!mTouchable) { 876 curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 877 } 878 if (mOutsideTouchable) { 879 curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; 880 } 881 if (!mClippingEnabled) { 882 curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 883 } 884 return curFlags; 885 } 886 887 private int computeAnimationResource() { 888 if (mAnimationStyle == -1) { 889 if (mIsDropdown) { 890 return mAboveAnchor 891 ? com.android.internal.R.style.Animation_DropDownUp 892 : com.android.internal.R.style.Animation_DropDownDown; 893 } 894 return 0; 895 } 896 return mAnimationStyle; 897 } 898 899 /** 900 * <p>Positions the popup window on screen. When the popup window is too 901 * tall to fit under the anchor, a parent scroll view is seeked and scrolled 902 * up to reclaim space. If scrolling is not possible or not enough, the 903 * popup window gets moved on top of the anchor.</p> 904 * 905 * <p>The height must have been set on the layout parameters prior to 906 * calling this method.</p> 907 * 908 * @param anchor the view on which the popup window must be anchored 909 * @param p the layout parameters used to display the drop down 910 * 911 * @return true if the popup is translated upwards to fit on screen 912 */ 913 private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams p, 914 int xoff, int yoff) { 915 916 anchor.getLocationInWindow(mDrawingLocation); 917 p.x = mDrawingLocation[0] + xoff; 918 p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; 919 920 boolean onTop = false; 921 922 p.gravity = Gravity.LEFT | Gravity.TOP; 923 924 anchor.getLocationOnScreen(mScreenLocation); 925 final Rect displayFrame = new Rect(); 926 anchor.getWindowVisibleDisplayFrame(displayFrame); 927 928 final View root = anchor.getRootView(); 929 if (p.y + mPopupHeight > displayFrame.bottom || p.x + mPopupWidth - root.getWidth() > 0) { 930 // if the drop down disappears at the bottom of the screen. we try to 931 // scroll a parent scrollview or move the drop down back up on top of 932 // the edit box 933 int scrollX = anchor.getScrollX(); 934 int scrollY = anchor.getScrollY(); 935 Rect r = new Rect(scrollX, scrollY, scrollX + mPopupWidth, 936 scrollY + mPopupHeight + anchor.getMeasuredHeight()); 937 anchor.requestRectangleOnScreen(r, true); 938 939 // now we re-evaluate the space available, and decide from that 940 // whether the pop-up will go above or below the anchor. 941 anchor.getLocationInWindow(mDrawingLocation); 942 p.x = mDrawingLocation[0] + xoff; 943 p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; 944 945 // determine whether there is more space above or below the anchor 946 anchor.getLocationOnScreen(mScreenLocation); 947 948 onTop = (displayFrame.bottom - mScreenLocation[1] - anchor.getMeasuredHeight() - yoff) < 949 (mScreenLocation[1] - yoff - displayFrame.top); 950 if (onTop) { 951 p.gravity = Gravity.LEFT | Gravity.BOTTOM; 952 p.y = root.getHeight() - mDrawingLocation[1] + yoff; 953 } else { 954 p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff; 955 } 956 } 957 958 p.gravity |= Gravity.DISPLAY_CLIP_VERTICAL; 959 960 return onTop; 961 } 962 963 /** 964 * Returns the maximum height that is available for the popup to be 965 * completely shown. It is recommended that this height be the maximum for 966 * the popup's height, otherwise it is possible that the popup will be 967 * clipped. 968 * 969 * @param anchor The view on which the popup window must be anchored. 970 * @return The maximum available height for the popup to be completely 971 * shown. 972 */ 973 public int getMaxAvailableHeight(View anchor) { 974 return getMaxAvailableHeight(anchor, 0); 975 } 976 977 /** 978 * Returns the maximum height that is available for the popup to be 979 * completely shown. It is recommended that this height be the maximum for 980 * the popup's height, otherwise it is possible that the popup will be 981 * clipped. 982 * 983 * @param anchor The view on which the popup window must be anchored. 984 * @param yOffset y offset from the view's bottom edge 985 * @return The maximum available height for the popup to be completely 986 * shown. 987 */ 988 public int getMaxAvailableHeight(View anchor, int yOffset) { 989 return getMaxAvailableHeight(anchor, yOffset, false); 990 } 991 992 /** 993 * Returns the maximum height that is available for the popup to be 994 * completely shown, optionally ignoring any bottom decorations such as 995 * the input method. It is recommended that this height be the maximum for 996 * the popup's height, otherwise it is possible that the popup will be 997 * clipped. 998 * 999 * @param anchor The view on which the popup window must be anchored. 1000 * @param yOffset y offset from the view's bottom edge 1001 * @param ignoreBottomDecorations if true, the height returned will be 1002 * all the way to the bottom of the display, ignoring any 1003 * bottom decorations 1004 * @return The maximum available height for the popup to be completely 1005 * shown. 1006 * 1007 * @hide Pending API council approval. 1008 */ 1009 public int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) { 1010 final Rect displayFrame = new Rect(); 1011 anchor.getWindowVisibleDisplayFrame(displayFrame); 1012 1013 final int[] anchorPos = mDrawingLocation; 1014 anchor.getLocationOnScreen(anchorPos); 1015 1016 int bottomEdge = displayFrame.bottom; 1017 if (ignoreBottomDecorations) { 1018 bottomEdge = anchor.getContext().getResources().getDisplayMetrics().heightPixels; 1019 } 1020 final int distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset; 1021 final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset; 1022 1023 // anchorPos[1] is distance from anchor to top of screen 1024 int returnedHeight = Math.max(distanceToBottom, distanceToTop); 1025 if (mBackground != null) { 1026 mBackground.getPadding(mTempRect); 1027 returnedHeight -= mTempRect.top + mTempRect.bottom; 1028 } 1029 1030 return returnedHeight; 1031 } 1032 1033 /** 1034 * <p>Dispose of the popup window. This method can be invoked only after 1035 * {@link #showAsDropDown(android.view.View)} has been executed. Failing that, calling 1036 * this method will have no effect.</p> 1037 * 1038 * @see #showAsDropDown(android.view.View) 1039 */ 1040 public void dismiss() { 1041 if (isShowing() && mPopupView != null) { 1042 unregisterForScrollChanged(); 1043 1044 mWindowManager.removeView(mPopupView); 1045 1046 if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { 1047 ((ViewGroup) mPopupView).removeView(mContentView); 1048 } 1049 mPopupView = null; 1050 mIsShowing = false; 1051 1052 if (mOnDismissListener != null) { 1053 mOnDismissListener.onDismiss(); 1054 } 1055 } 1056 } 1057 1058 /** 1059 * Sets the listener to be called when the window is dismissed. 1060 * 1061 * @param onDismissListener The listener. 1062 */ 1063 public void setOnDismissListener(OnDismissListener onDismissListener) { 1064 mOnDismissListener = onDismissListener; 1065 } 1066 1067 /** 1068 * Updates the state of the popup window, if it is currently being displayed, 1069 * from the currently set state. This include: 1070 * {@link #setClippingEnabled(boolean)}, {@link #setFocusable(boolean)}, 1071 * {@link #setIgnoreCheekPress()}, {@link #setInputMethodMode(int)}, 1072 * {@link #setTouchable(boolean)}, and {@link #setAnimationStyle(int)}. 1073 */ 1074 public void update() { 1075 if (!isShowing() || mContentView == null) { 1076 return; 1077 } 1078 1079 WindowManager.LayoutParams p = (WindowManager.LayoutParams) 1080 mPopupView.getLayoutParams(); 1081 1082 boolean update = false; 1083 1084 final int newAnim = computeAnimationResource(); 1085 if (newAnim != p.windowAnimations) { 1086 p.windowAnimations = newAnim; 1087 update = true; 1088 } 1089 1090 final int newFlags = computeFlags(p.flags); 1091 if (newFlags != p.flags) { 1092 p.flags = newFlags; 1093 update = true; 1094 } 1095 1096 if (update) { 1097 mWindowManager.updateViewLayout(mPopupView, p); 1098 } 1099 } 1100 1101 /** 1102 * <p>Updates the dimension of the popup window. Calling this function 1103 * also updates the window with the current popup state as described 1104 * for {@link #update()}.</p> 1105 * 1106 * @param width the new width 1107 * @param height the new height 1108 */ 1109 public void update(int width, int height) { 1110 WindowManager.LayoutParams p = (WindowManager.LayoutParams) 1111 mPopupView.getLayoutParams(); 1112 update(p.x, p.y, width, height, false); 1113 } 1114 1115 /** 1116 * <p>Updates the position and the dimension of the popup window. Width and 1117 * height can be set to -1 to update location only. Calling this function 1118 * also updates the window with the current popup state as 1119 * described for {@link #update()}.</p> 1120 * 1121 * @param x the new x location 1122 * @param y the new y location 1123 * @param width the new width, can be -1 to ignore 1124 * @param height the new height, can be -1 to ignore 1125 */ 1126 public void update(int x, int y, int width, int height) { 1127 update(x, y, width, height, false); 1128 } 1129 1130 /** 1131 * <p>Updates the position and the dimension of the popup window. Width and 1132 * height can be set to -1 to update location only. Calling this function 1133 * also updates the window with the current popup state as 1134 * described for {@link #update()}.</p> 1135 * 1136 * @param x the new x location 1137 * @param y the new y location 1138 * @param width the new width, can be -1 to ignore 1139 * @param height the new height, can be -1 to ignore 1140 * @param force reposition the window even if the specified position 1141 * already seems to correspond to the LayoutParams 1142 */ 1143 public void update(int x, int y, int width, int height, boolean force) { 1144 if (width != -1) { 1145 mLastWidth = width; 1146 setWidth(width); 1147 } 1148 1149 if (height != -1) { 1150 mLastHeight = height; 1151 setHeight(height); 1152 } 1153 1154 if (!isShowing() || mContentView == null) { 1155 return; 1156 } 1157 1158 WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); 1159 1160 boolean update = force; 1161 1162 final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth; 1163 if (width != -1 && p.width != finalWidth) { 1164 p.width = mLastWidth = finalWidth; 1165 update = true; 1166 } 1167 1168 final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight; 1169 if (height != -1 && p.height != finalHeight) { 1170 p.height = mLastHeight = finalHeight; 1171 update = true; 1172 } 1173 1174 if (p.x != x) { 1175 p.x = x; 1176 update = true; 1177 } 1178 1179 if (p.y != y) { 1180 p.y = y; 1181 update = true; 1182 } 1183 1184 final int newAnim = computeAnimationResource(); 1185 if (newAnim != p.windowAnimations) { 1186 p.windowAnimations = newAnim; 1187 update = true; 1188 } 1189 1190 final int newFlags = computeFlags(p.flags); 1191 if (newFlags != p.flags) { 1192 p.flags = newFlags; 1193 update = true; 1194 } 1195 1196 if (update) { 1197 mWindowManager.updateViewLayout(mPopupView, p); 1198 } 1199 } 1200 1201 /** 1202 * <p>Updates the position and the dimension of the popup window. Calling this 1203 * function also updates the window with the current popup state as described 1204 * for {@link #update()}.</p> 1205 * 1206 * @param anchor the popup's anchor view 1207 * @param width the new width, can be -1 to ignore 1208 * @param height the new height, can be -1 to ignore 1209 */ 1210 public void update(View anchor, int width, int height) { 1211 update(anchor, false, 0, 0, true, width, height); 1212 } 1213 1214 /** 1215 * <p>Updates the position and the dimension of the popup window. Width and 1216 * height can be set to -1 to update location only. Calling this function 1217 * also updates the window with the current popup state as 1218 * described for {@link #update()}.</p> 1219 * <p>If the view later scrolls to move <code>anchor</code> to a different 1220 * location, the popup will be moved correspondingly.</p> 1221 * 1222 * @param anchor the popup's anchor view 1223 * @param xoff x offset from the view's left edge 1224 * @param yoff y offset from the view's bottom edge 1225 * @param width the new width, can be -1 to ignore 1226 * @param height the new height, can be -1 to ignore 1227 */ 1228 public void update(View anchor, int xoff, int yoff, int width, int height) { 1229 update(anchor, true, xoff, yoff, true, width, height); 1230 } 1231 1232 private void update(View anchor, boolean updateLocation, int xoff, int yoff, 1233 boolean updateDimension, int width, int height) { 1234 1235 if (!isShowing() || mContentView == null) { 1236 return; 1237 } 1238 1239 WeakReference<View> oldAnchor = mAnchor; 1240 if (oldAnchor == null || oldAnchor.get() != anchor || 1241 (updateLocation && (mAnchorXoff != xoff || mAnchorYoff != yoff))) { 1242 registerForScrollChanged(anchor, xoff, yoff); 1243 } 1244 1245 WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); 1246 1247 if (updateDimension) { 1248 if (width == -1) { 1249 width = mPopupWidth; 1250 } else { 1251 mPopupWidth = width; 1252 } 1253 if (height == -1) { 1254 height = mPopupHeight; 1255 } else { 1256 mPopupHeight = height; 1257 } 1258 } 1259 1260 if (updateLocation) { 1261 mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff); 1262 } else { 1263 mAboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff); 1264 } 1265 1266 update(p.x, p.y, width, height); 1267 } 1268 1269 /** 1270 * Listener that is called when this popup window is dismissed. 1271 */ 1272 public interface OnDismissListener { 1273 /** 1274 * Called when this popup window is dismissed. 1275 */ 1276 public void onDismiss(); 1277 } 1278 1279 private void unregisterForScrollChanged() { 1280 WeakReference<View> anchorRef = mAnchor; 1281 View anchor = null; 1282 if (anchorRef != null) { 1283 anchor = anchorRef.get(); 1284 } 1285 if (anchor != null) { 1286 ViewTreeObserver vto = anchor.getViewTreeObserver(); 1287 vto.removeOnScrollChangedListener(mOnScrollChangedListener); 1288 } 1289 mAnchor = null; 1290 } 1291 1292 private void registerForScrollChanged(View anchor, int xoff, int yoff) { 1293 unregisterForScrollChanged(); 1294 1295 mAnchor = new WeakReference<View>(anchor); 1296 ViewTreeObserver vto = anchor.getViewTreeObserver(); 1297 if (vto != null) { 1298 vto.addOnScrollChangedListener(mOnScrollChangedListener); 1299 } 1300 1301 mAnchorXoff = xoff; 1302 mAnchorYoff = yoff; 1303 } 1304 1305 private class PopupViewContainer extends FrameLayout { 1306 1307 public PopupViewContainer(Context context) { 1308 super(context); 1309 } 1310 1311 @Override 1312 protected int[] onCreateDrawableState(int extraSpace) { 1313 if (mAboveAnchor) { 1314 // 1 more needed for the above anchor state 1315 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 1316 View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET); 1317 return drawableState; 1318 } else { 1319 return super.onCreateDrawableState(extraSpace); 1320 } 1321 } 1322 1323 @Override 1324 public boolean dispatchKeyEvent(KeyEvent event) { 1325 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 1326 dismiss(); 1327 return true; 1328 } else { 1329 return super.dispatchKeyEvent(event); 1330 } 1331 } 1332 1333 @Override 1334 public boolean dispatchTouchEvent(MotionEvent ev) { 1335 if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) { 1336 return true; 1337 } 1338 return super.dispatchTouchEvent(ev); 1339 } 1340 1341 @Override 1342 public boolean onTouchEvent(MotionEvent event) { 1343 final int x = (int) event.getX(); 1344 final int y = (int) event.getY(); 1345 1346 if ((event.getAction() == MotionEvent.ACTION_DOWN) 1347 && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) { 1348 dismiss(); 1349 return true; 1350 } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 1351 dismiss(); 1352 return true; 1353 } else { 1354 return super.onTouchEvent(event); 1355 } 1356 } 1357 1358 @Override 1359 public void sendAccessibilityEvent(int eventType) { 1360 // clinets are interested in the content not the container, make it event source 1361 if (mContentView != null) { 1362 mContentView.sendAccessibilityEvent(eventType); 1363 } else { 1364 super.sendAccessibilityEvent(eventType); 1365 } 1366 } 1367 } 1368 1369} 1370