PopupWindow.java revision 92be82e6a85bfe6f86400eb46d7c5e8ec5787b3c
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.view.KeyEvent;
24import android.view.MotionEvent;
25import android.view.View;
26import android.view.WindowManager;
27import android.view.Gravity;
28import android.view.ViewGroup;
29import android.view.ViewTreeObserver;
30import android.view.ViewTreeObserver.OnScrollChangedListener;
31import android.view.View.OnTouchListener;
32import android.graphics.PixelFormat;
33import android.graphics.Rect;
34import android.graphics.drawable.Drawable;
35import android.graphics.drawable.StateListDrawable;
36import android.os.IBinder;
37import android.util.AttributeSet;
38
39import java.lang.ref.WeakReference;
40
41/**
42 * <p>A popup window that can be used to display an arbitrary view. The popup
43 * windows is a floating container that appears on top of the current
44 * activity.</p>
45 *
46 * @see android.widget.AutoCompleteTextView
47 * @see android.widget.Spinner
48 */
49public class PopupWindow {
50    /**
51     * Mode for {@link #setInputMethodMode(int)}: the requirements for the
52     * input method should be based on the focusability of the popup.  That is
53     * if it is focusable than it needs to work with the input method, else
54     * it doesn't.
55     */
56    public static final int INPUT_METHOD_FROM_FOCUSABLE = 0;
57
58    /**
59     * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
60     * work with an input method, regardless of whether it is focusable.  This
61     * means that it will always be displayed so that the user can also operate
62     * the input method while it is shown.
63     */
64    public static final int INPUT_METHOD_NEEDED = 1;
65
66    /**
67     * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
68     * work with an input method, regardless of whether it is focusable.  This
69     * means that it will always be displayed to use as much space on the
70     * screen as needed, regardless of whether this covers the input method.
71     */
72    public static final int INPUT_METHOD_NOT_NEEDED = 2;
73
74    private Context mContext;
75    private WindowManager mWindowManager;
76
77    private boolean mIsShowing;
78    private boolean mIsDropdown;
79
80    private View mContentView;
81    private View mPopupView;
82    private boolean mFocusable;
83    private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE;
84    private int mSoftInputMode;
85    private boolean mTouchable = true;
86    private boolean mOutsideTouchable = false;
87    private boolean mClippingEnabled = true;
88
89    private OnTouchListener mTouchInterceptor;
90
91    private int mWidthMode;
92    private int mWidth;
93    private int mLastWidth;
94    private int mHeightMode;
95    private int mHeight;
96    private int mLastHeight;
97
98    private int mPopupWidth;
99    private int mPopupHeight;
100
101    private int[] mDrawingLocation = new int[2];
102    private int[] mScreenLocation = new int[2];
103    private Rect mTempRect = new Rect();
104
105    private Drawable mBackground;
106    private Drawable mAboveAnchorBackgroundDrawable;
107    private Drawable mBelowAnchorBackgroundDrawable;
108
109    private boolean mAboveAnchor;
110
111    private OnDismissListener mOnDismissListener;
112    private boolean mIgnoreCheekPress = false;
113
114    private int mAnimationStyle = -1;
115
116    private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] {
117        com.android.internal.R.attr.state_above_anchor
118    };
119
120    private WeakReference<View> mAnchor;
121    private OnScrollChangedListener mOnScrollChangedListener =
122        new OnScrollChangedListener() {
123            public void onScrollChanged() {
124                View anchor = mAnchor.get();
125                if (anchor != null && mPopupView != null) {
126                    WindowManager.LayoutParams p = (WindowManager.LayoutParams)
127                            mPopupView.getLayoutParams();
128
129                    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