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