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