1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import com.android.internal.R; 20 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.view.KeyEvent; 24 import android.view.MotionEvent; 25 import android.view.View; 26 import android.view.WindowManager; 27 import android.view.Gravity; 28 import android.view.ViewGroup; 29 import android.view.ViewTreeObserver; 30 import android.view.ViewTreeObserver.OnScrollChangedListener; 31 import android.view.View.OnTouchListener; 32 import android.graphics.PixelFormat; 33 import android.graphics.Rect; 34 import android.graphics.drawable.Drawable; 35 import android.graphics.drawable.StateListDrawable; 36 import android.os.IBinder; 37 import android.util.AttributeSet; 38 39 import 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 */ 49 public 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 updateAboveAnchor(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#MATCH_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#MATCH_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#MATCH_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 733 updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff)); 734 735 if (mHeightMode < 0) p.height = mLastHeight = mHeightMode; 736 if (mWidthMode < 0) p.width = mLastWidth = mWidthMode; 737 738 p.windowAnimations = computeAnimationResource(); 739 740 invokePopup(p); 741 } 742 743 private void updateAboveAnchor(boolean aboveAnchor) { 744 if (aboveAnchor != mAboveAnchor) { 745 mAboveAnchor = aboveAnchor; 746 747 if (mBackground != null) { 748 // If the background drawable provided was a StateListDrawable with above-anchor 749 // and below-anchor states, use those. Otherwise rely on refreshDrawableState to 750 // do the job. 751 if (mAboveAnchorBackgroundDrawable != null) { 752 if (mAboveAnchor) { 753 mPopupView.setBackgroundDrawable(mAboveAnchorBackgroundDrawable); 754 } else { 755 mPopupView.setBackgroundDrawable(mBelowAnchorBackgroundDrawable); 756 } 757 } else { 758 mPopupView.refreshDrawableState(); 759 } 760 } 761 } 762 } 763 764 /** 765 * Indicates whether the popup is showing above (the y coordinate of the popup's bottom 766 * is less than the y coordinate of the anchor) or below the anchor view (the y coordinate 767 * of the popup is greater than y coordinate of the anchor's bottom). 768 * 769 * The value returned 770 * by this method is meaningful only after {@link #showAsDropDown(android.view.View)} 771 * or {@link #showAsDropDown(android.view.View, int, int)} was invoked. 772 * 773 * @return True if this popup is showing above the anchor view, false otherwise. 774 */ 775 public boolean isAboveAnchor() { 776 return mAboveAnchor; 777 } 778 779 /** 780 * <p>Prepare the popup by embedding in into a new ViewGroup if the 781 * background drawable is not null. If embedding is required, the layout 782 * parameters' height is mnodified to take into account the background's 783 * padding.</p> 784 * 785 * @param p the layout parameters of the popup's content view 786 */ 787 private void preparePopup(WindowManager.LayoutParams p) { 788 if (mContentView == null || mContext == null || mWindowManager == null) { 789 throw new IllegalStateException("You must specify a valid content view by " 790 + "calling setContentView() before attempting to show the popup."); 791 } 792 793 if (mBackground != null) { 794 final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); 795 int height = ViewGroup.LayoutParams.MATCH_PARENT; 796 if (layoutParams != null && 797 layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { 798 height = ViewGroup.LayoutParams.WRAP_CONTENT; 799 } 800 801 // when a background is available, we embed the content view 802 // within another view that owns the background drawable 803 PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); 804 PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( 805 ViewGroup.LayoutParams.MATCH_PARENT, height 806 ); 807 popupViewContainer.setBackgroundDrawable(mBackground); 808 popupViewContainer.addView(mContentView, listParams); 809 810 mPopupView = popupViewContainer; 811 } else { 812 mPopupView = mContentView; 813 } 814 mPopupWidth = p.width; 815 mPopupHeight = p.height; 816 } 817 818 /** 819 * <p>Invoke the popup window by adding the content view to the window 820 * manager.</p> 821 * 822 * <p>The content view must be non-null when this method is invoked.</p> 823 * 824 * @param p the layout parameters of the popup's content view 825 */ 826 private void invokePopup(WindowManager.LayoutParams p) { 827 p.packageName = mContext.getPackageName(); 828 mWindowManager.addView(mPopupView, p); 829 } 830 831 /** 832 * <p>Generate the layout parameters for the popup window.</p> 833 * 834 * @param token the window token used to bind the popup's window 835 * 836 * @return the layout parameters to pass to the window manager 837 */ 838 private WindowManager.LayoutParams createPopupLayout(IBinder token) { 839 // generates the layout parameters for the drop down 840 // we want a fixed size view located at the bottom left of the anchor 841 WindowManager.LayoutParams p = new WindowManager.LayoutParams(); 842 // these gravity settings put the view at the top left corner of the 843 // screen. The view is then positioned to the appropriate location 844 // by setting the x and y offsets to match the anchor's bottom 845 // left corner 846 p.gravity = Gravity.LEFT | Gravity.TOP; 847 p.width = mLastWidth = mWidth; 848 p.height = mLastHeight = mHeight; 849 if (mBackground != null) { 850 p.format = mBackground.getOpacity(); 851 } else { 852 p.format = PixelFormat.TRANSLUCENT; 853 } 854 p.flags = computeFlags(p.flags); 855 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 856 p.token = token; 857 p.softInputMode = mSoftInputMode; 858 p.setTitle("PopupWindow:" + Integer.toHexString(hashCode())); 859 860 return p; 861 } 862 863 private int computeFlags(int curFlags) { 864 curFlags &= ~( 865 WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | 866 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 867 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 868 WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | 869 WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | 870 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 871 if(mIgnoreCheekPress) { 872 curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; 873 } 874 if (!mFocusable) { 875 curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 876 if (mInputMethodMode == INPUT_METHOD_NEEDED) { 877 curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 878 } 879 } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) { 880 curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 881 } 882 if (!mTouchable) { 883 curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 884 } 885 if (mOutsideTouchable) { 886 curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; 887 } 888 if (!mClippingEnabled) { 889 curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 890 } 891 return curFlags; 892 } 893 894 private int computeAnimationResource() { 895 if (mAnimationStyle == -1) { 896 if (mIsDropdown) { 897 return mAboveAnchor 898 ? com.android.internal.R.style.Animation_DropDownUp 899 : com.android.internal.R.style.Animation_DropDownDown; 900 } 901 return 0; 902 } 903 return mAnimationStyle; 904 } 905 906 /** 907 * <p>Positions the popup window on screen. When the popup window is too 908 * tall to fit under the anchor, a parent scroll view is seeked and scrolled 909 * up to reclaim space. If scrolling is not possible or not enough, the 910 * popup window gets moved on top of the anchor.</p> 911 * 912 * <p>The height must have been set on the layout parameters prior to 913 * calling this method.</p> 914 * 915 * @param anchor the view on which the popup window must be anchored 916 * @param p the layout parameters used to display the drop down 917 * 918 * @return true if the popup is translated upwards to fit on screen 919 */ 920 private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams p, 921 int xoff, int yoff) { 922 923 anchor.getLocationInWindow(mDrawingLocation); 924 p.x = mDrawingLocation[0] + xoff; 925 p.y = mDrawingLocation[1] + anchor.getHeight() + yoff; 926 927 boolean onTop = false; 928 929 p.gravity = Gravity.LEFT | Gravity.TOP; 930 931 anchor.getLocationOnScreen(mScreenLocation); 932 final Rect displayFrame = new Rect(); 933 anchor.getWindowVisibleDisplayFrame(displayFrame); 934 935 final View root = anchor.getRootView(); 936 if (p.y + mPopupHeight > displayFrame.bottom || p.x + mPopupWidth - root.getWidth() > 0) { 937 // if the drop down disappears at the bottom of the screen. we try to 938 // scroll a parent scrollview or move the drop down back up on top of 939 // the edit box 940 int scrollX = anchor.getScrollX(); 941 int scrollY = anchor.getScrollY(); 942 Rect r = new Rect(scrollX, scrollY, scrollX + mPopupWidth + xoff, 943 scrollY + mPopupHeight + anchor.getHeight() + yoff); 944 anchor.requestRectangleOnScreen(r, true); 945 946 // now we re-evaluate the space available, and decide from that 947 // whether the pop-up will go above or below the anchor. 948 anchor.getLocationInWindow(mDrawingLocation); 949 p.x = mDrawingLocation[0] + xoff; 950 p.y = mDrawingLocation[1] + anchor.getHeight() + yoff; 951 952 // determine whether there is more space above or below the anchor 953 anchor.getLocationOnScreen(mScreenLocation); 954 955 onTop = (displayFrame.bottom - mScreenLocation[1] - anchor.getHeight() - yoff) < 956 (mScreenLocation[1] - yoff - displayFrame.top); 957 if (onTop) { 958 p.gravity = Gravity.LEFT | Gravity.BOTTOM; 959 p.y = root.getHeight() - mDrawingLocation[1] + yoff; 960 } else { 961 p.y = mDrawingLocation[1] + anchor.getHeight() + yoff; 962 } 963 } 964 965 p.gravity |= Gravity.DISPLAY_CLIP_VERTICAL; 966 967 return onTop; 968 } 969 970 /** 971 * Returns the maximum height that is available for the popup to be 972 * completely shown. It is recommended that this height be the maximum for 973 * the popup's height, otherwise it is possible that the popup will be 974 * clipped. 975 * 976 * @param anchor The view on which the popup window must be anchored. 977 * @return The maximum available height for the popup to be completely 978 * shown. 979 */ 980 public int getMaxAvailableHeight(View anchor) { 981 return getMaxAvailableHeight(anchor, 0); 982 } 983 984 /** 985 * Returns the maximum height that is available for the popup to be 986 * completely shown. It is recommended that this height be the maximum for 987 * the popup's height, otherwise it is possible that the popup will be 988 * clipped. 989 * 990 * @param anchor The view on which the popup window must be anchored. 991 * @param yOffset y offset from the view's bottom edge 992 * @return The maximum available height for the popup to be completely 993 * shown. 994 */ 995 public int getMaxAvailableHeight(View anchor, int yOffset) { 996 return getMaxAvailableHeight(anchor, yOffset, false); 997 } 998 999 /** 1000 * Returns the maximum height that is available for the popup to be 1001 * completely shown, optionally ignoring any bottom decorations such as 1002 * the input method. It is recommended that this height be the maximum for 1003 * the popup's height, otherwise it is possible that the popup will be 1004 * clipped. 1005 * 1006 * @param anchor The view on which the popup window must be anchored. 1007 * @param yOffset y offset from the view's bottom edge 1008 * @param ignoreBottomDecorations if true, the height returned will be 1009 * all the way to the bottom of the display, ignoring any 1010 * bottom decorations 1011 * @return The maximum available height for the popup to be completely 1012 * shown. 1013 * 1014 * @hide Pending API council approval. 1015 */ 1016 public int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) { 1017 final Rect displayFrame = new Rect(); 1018 anchor.getWindowVisibleDisplayFrame(displayFrame); 1019 1020 final int[] anchorPos = mDrawingLocation; 1021 anchor.getLocationOnScreen(anchorPos); 1022 1023 int bottomEdge = displayFrame.bottom; 1024 if (ignoreBottomDecorations) { 1025 bottomEdge = anchor.getContext().getResources().getDisplayMetrics().heightPixels; 1026 } 1027 final int distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset; 1028 final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset; 1029 1030 // anchorPos[1] is distance from anchor to top of screen 1031 int returnedHeight = Math.max(distanceToBottom, distanceToTop); 1032 if (mBackground != null) { 1033 mBackground.getPadding(mTempRect); 1034 returnedHeight -= mTempRect.top + mTempRect.bottom; 1035 } 1036 1037 return returnedHeight; 1038 } 1039 1040 /** 1041 * <p>Dispose of the popup window. This method can be invoked only after 1042 * {@link #showAsDropDown(android.view.View)} has been executed. Failing that, calling 1043 * this method will have no effect.</p> 1044 * 1045 * @see #showAsDropDown(android.view.View) 1046 */ 1047 public void dismiss() { 1048 if (isShowing() && mPopupView != null) { 1049 unregisterForScrollChanged(); 1050 1051 try { 1052 mWindowManager.removeView(mPopupView); 1053 } finally { 1054 if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { 1055 ((ViewGroup) mPopupView).removeView(mContentView); 1056 } 1057 mPopupView = null; 1058 mIsShowing = false; 1059 1060 if (mOnDismissListener != null) { 1061 mOnDismissListener.onDismiss(); 1062 } 1063 } 1064 } 1065 } 1066 1067 /** 1068 * Sets the listener to be called when the window is dismissed. 1069 * 1070 * @param onDismissListener The listener. 1071 */ 1072 public void setOnDismissListener(OnDismissListener onDismissListener) { 1073 mOnDismissListener = onDismissListener; 1074 } 1075 1076 /** 1077 * Updates the state of the popup window, if it is currently being displayed, 1078 * from the currently set state. This include: 1079 * {@link #setClippingEnabled(boolean)}, {@link #setFocusable(boolean)}, 1080 * {@link #setIgnoreCheekPress()}, {@link #setInputMethodMode(int)}, 1081 * {@link #setTouchable(boolean)}, and {@link #setAnimationStyle(int)}. 1082 */ 1083 public void update() { 1084 if (!isShowing() || mContentView == null) { 1085 return; 1086 } 1087 1088 WindowManager.LayoutParams p = (WindowManager.LayoutParams) 1089 mPopupView.getLayoutParams(); 1090 1091 boolean update = false; 1092 1093 final int newAnim = computeAnimationResource(); 1094 if (newAnim != p.windowAnimations) { 1095 p.windowAnimations = newAnim; 1096 update = true; 1097 } 1098 1099 final int newFlags = computeFlags(p.flags); 1100 if (newFlags != p.flags) { 1101 p.flags = newFlags; 1102 update = true; 1103 } 1104 1105 if (update) { 1106 mWindowManager.updateViewLayout(mPopupView, p); 1107 } 1108 } 1109 1110 /** 1111 * <p>Updates the dimension of the popup window. Calling this function 1112 * also updates the window with the current popup state as described 1113 * for {@link #update()}.</p> 1114 * 1115 * @param width the new width 1116 * @param height the new height 1117 */ 1118 public void update(int width, int height) { 1119 WindowManager.LayoutParams p = (WindowManager.LayoutParams) 1120 mPopupView.getLayoutParams(); 1121 update(p.x, p.y, width, height, false); 1122 } 1123 1124 /** 1125 * <p>Updates the position and the dimension of the popup window. Width and 1126 * height can be set to -1 to update location only. Calling this function 1127 * also updates the window with the current popup state as 1128 * described for {@link #update()}.</p> 1129 * 1130 * @param x the new x location 1131 * @param y the new y location 1132 * @param width the new width, can be -1 to ignore 1133 * @param height the new height, can be -1 to ignore 1134 */ 1135 public void update(int x, int y, int width, int height) { 1136 update(x, y, width, height, false); 1137 } 1138 1139 /** 1140 * <p>Updates the position and the dimension of the popup window. Width and 1141 * height can be set to -1 to update location only. Calling this function 1142 * also updates the window with the current popup state as 1143 * described for {@link #update()}.</p> 1144 * 1145 * @param x the new x location 1146 * @param y the new y location 1147 * @param width the new width, can be -1 to ignore 1148 * @param height the new height, can be -1 to ignore 1149 * @param force reposition the window even if the specified position 1150 * already seems to correspond to the LayoutParams 1151 */ 1152 public void update(int x, int y, int width, int height, boolean force) { 1153 if (width != -1) { 1154 mLastWidth = width; 1155 setWidth(width); 1156 } 1157 1158 if (height != -1) { 1159 mLastHeight = height; 1160 setHeight(height); 1161 } 1162 1163 if (!isShowing() || mContentView == null) { 1164 return; 1165 } 1166 1167 WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); 1168 1169 boolean update = force; 1170 1171 final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth; 1172 if (width != -1 && p.width != finalWidth) { 1173 p.width = mLastWidth = finalWidth; 1174 update = true; 1175 } 1176 1177 final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight; 1178 if (height != -1 && p.height != finalHeight) { 1179 p.height = mLastHeight = finalHeight; 1180 update = true; 1181 } 1182 1183 if (p.x != x) { 1184 p.x = x; 1185 update = true; 1186 } 1187 1188 if (p.y != y) { 1189 p.y = y; 1190 update = true; 1191 } 1192 1193 final int newAnim = computeAnimationResource(); 1194 if (newAnim != p.windowAnimations) { 1195 p.windowAnimations = newAnim; 1196 update = true; 1197 } 1198 1199 final int newFlags = computeFlags(p.flags); 1200 if (newFlags != p.flags) { 1201 p.flags = newFlags; 1202 update = true; 1203 } 1204 1205 if (update) { 1206 mWindowManager.updateViewLayout(mPopupView, p); 1207 } 1208 } 1209 1210 /** 1211 * <p>Updates the position and the dimension of the popup window. Calling this 1212 * function also updates the window with the current popup state as described 1213 * for {@link #update()}.</p> 1214 * 1215 * @param anchor the popup's anchor view 1216 * @param width the new width, can be -1 to ignore 1217 * @param height the new height, can be -1 to ignore 1218 */ 1219 public void update(View anchor, int width, int height) { 1220 update(anchor, false, 0, 0, true, width, height); 1221 } 1222 1223 /** 1224 * <p>Updates the position and the dimension of the popup window. Width and 1225 * height can be set to -1 to update location only. Calling this function 1226 * also updates the window with the current popup state as 1227 * described for {@link #update()}.</p> 1228 * <p>If the view later scrolls to move <code>anchor</code> to a different 1229 * location, the popup will be moved correspondingly.</p> 1230 * 1231 * @param anchor the popup's anchor view 1232 * @param xoff x offset from the view's left edge 1233 * @param yoff y offset from the view's bottom edge 1234 * @param width the new width, can be -1 to ignore 1235 * @param height the new height, can be -1 to ignore 1236 */ 1237 public void update(View anchor, int xoff, int yoff, int width, int height) { 1238 update(anchor, true, xoff, yoff, true, width, height); 1239 } 1240 1241 private void update(View anchor, boolean updateLocation, int xoff, int yoff, 1242 boolean updateDimension, int width, int height) { 1243 1244 if (!isShowing() || mContentView == null) { 1245 return; 1246 } 1247 1248 WeakReference<View> oldAnchor = mAnchor; 1249 if (oldAnchor == null || oldAnchor.get() != anchor || 1250 (updateLocation && (mAnchorXoff != xoff || mAnchorYoff != yoff))) { 1251 registerForScrollChanged(anchor, xoff, yoff); 1252 } 1253 1254 WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); 1255 1256 if (updateDimension) { 1257 if (width == -1) { 1258 width = mPopupWidth; 1259 } else { 1260 mPopupWidth = width; 1261 } 1262 if (height == -1) { 1263 height = mPopupHeight; 1264 } else { 1265 mPopupHeight = height; 1266 } 1267 } 1268 1269 int x = p.x; 1270 int y = p.y; 1271 1272 if (updateLocation) { 1273 updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff)); 1274 } else { 1275 updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff)); 1276 } 1277 1278 update(p.x, p.y, width, height, x != p.x || y != p.y); 1279 } 1280 1281 /** 1282 * Listener that is called when this popup window is dismissed. 1283 */ 1284 public interface OnDismissListener { 1285 /** 1286 * Called when this popup window is dismissed. 1287 */ 1288 public void onDismiss(); 1289 } 1290 1291 private void unregisterForScrollChanged() { 1292 WeakReference<View> anchorRef = mAnchor; 1293 View anchor = null; 1294 if (anchorRef != null) { 1295 anchor = anchorRef.get(); 1296 } 1297 if (anchor != null) { 1298 ViewTreeObserver vto = anchor.getViewTreeObserver(); 1299 vto.removeOnScrollChangedListener(mOnScrollChangedListener); 1300 } 1301 mAnchor = null; 1302 } 1303 1304 private void registerForScrollChanged(View anchor, int xoff, int yoff) { 1305 unregisterForScrollChanged(); 1306 1307 mAnchor = new WeakReference<View>(anchor); 1308 ViewTreeObserver vto = anchor.getViewTreeObserver(); 1309 if (vto != null) { 1310 vto.addOnScrollChangedListener(mOnScrollChangedListener); 1311 } 1312 1313 mAnchorXoff = xoff; 1314 mAnchorYoff = yoff; 1315 } 1316 1317 private class PopupViewContainer extends FrameLayout { 1318 1319 public PopupViewContainer(Context context) { 1320 super(context); 1321 } 1322 1323 @Override 1324 protected int[] onCreateDrawableState(int extraSpace) { 1325 if (mAboveAnchor) { 1326 // 1 more needed for the above anchor state 1327 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 1328 View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET); 1329 return drawableState; 1330 } else { 1331 return super.onCreateDrawableState(extraSpace); 1332 } 1333 } 1334 1335 @Override 1336 public boolean dispatchKeyEvent(KeyEvent event) { 1337 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { 1338 if (event.getAction() == KeyEvent.ACTION_DOWN 1339 && event.getRepeatCount() == 0) { 1340 getKeyDispatcherState().startTracking(event, this); 1341 return true; 1342 } else if (event.getAction() == KeyEvent.ACTION_UP 1343 && getKeyDispatcherState().isTracking(event) && !event.isCanceled()) { 1344 dismiss(); 1345 return true; 1346 } 1347 return super.dispatchKeyEvent(event); 1348 } else { 1349 return super.dispatchKeyEvent(event); 1350 } 1351 } 1352 1353 @Override 1354 public boolean dispatchTouchEvent(MotionEvent ev) { 1355 if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) { 1356 return true; 1357 } 1358 return super.dispatchTouchEvent(ev); 1359 } 1360 1361 @Override 1362 public boolean onTouchEvent(MotionEvent event) { 1363 final int x = (int) event.getX(); 1364 final int y = (int) event.getY(); 1365 1366 if ((event.getAction() == MotionEvent.ACTION_DOWN) 1367 && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) { 1368 dismiss(); 1369 return true; 1370 } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { 1371 dismiss(); 1372 return true; 1373 } else { 1374 return super.onTouchEvent(event); 1375 } 1376 } 1377 1378 @Override 1379 public void sendAccessibilityEvent(int eventType) { 1380 // clinets are interested in the content not the container, make it event source 1381 if (mContentView != null) { 1382 mContentView.sendAccessibilityEvent(eventType); 1383 } else { 1384 super.sendAccessibilityEvent(eventType); 1385 } 1386 } 1387 } 1388 1389 } 1390