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