blob: 6a7b1fb2d1bcb76d59117c5e58331fa31f0c1744 [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.widget;
import com.android.internal.R;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManagerImpl;
import android.view.Gravity;
import android.view.ViewGroup;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.IBinder;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
/**
* <p>A popup window that can be used to display an arbitrary view. The popup
* windows is a floating container that appears on top of the current
* activity.</p>
*
* @see android.widget.AutoCompleteTextView
* @see android.widget.Spinner
*/
public class PopupWindow {
/**
* The height of the status bar so we know how much of the screen we can
* actually be displayed in.
* <p>
* TODO: This IS NOT the right way to do this.
* Instead of knowing how much of the screen is available, a popup that
* wants anchor and maximize space shouldn't be setting a height, instead
* the PopupViewContainer should have its layout height as fill_parent and
* properly position the popup.
*/
private static final int STATUS_BAR_HEIGHT = 30;
private boolean mIsShowing;
private View mContentView;
private View mPopupView;
private boolean mFocusable;
private int mWidth;
private int mHeight;
private int[] mDrawingLocation = new int[2];
private int[] mRootLocation = new int[2];
private Rect mTempRect = new Rect();
private Context mContext;
private Drawable mBackground;
private boolean mAboveAnchor;
private OnDismissListener mOnDismissListener;
private boolean mIgnoreCheekPress = false;
private int mAnimationStyle = -1;
private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] {
com.android.internal.R.attr.state_above_anchor
};
/**
* <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
*
* <p>The popup does provide a background.</p>
*/
public PopupWindow(Context context) {
this(context, null);
}
/**
* <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
*
* <p>The popup does provide a background.</p>
*/
public PopupWindow(Context context, AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.popupWindowStyle);
}
/**
* <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
*
* <p>The popup does provide a background.</p>
*/
public PopupWindow(Context context, AttributeSet attrs, int defStyle) {
mContext = context;
TypedArray a =
context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.PopupWindow, defStyle, 0);
mBackground = a.getDrawable(R.styleable.PopupWindow_popupBackground);
a.recycle();
}
/**
* <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
*
* <p>The popup does not provide any background. This should be handled
* by the content view.</p>
*/
public PopupWindow() {
this(null, 0, 0);
}
/**
* <p>Create a new non focusable popup window which can display the
* <tt>contentView</tt>. The dimension of the window are (0,0).</p>
*
* <p>The popup does not provide any background. This should be handled
* by the content view.</p>
*
* @param contentView the popup's content
*/
public PopupWindow(View contentView) {
this(contentView, 0, 0);
}
/**
* <p>Create a new empty, non focusable popup window. The dimension of the
* window must be passed to this constructor.</p>
*
* <p>The popup does not provide any background. This should be handled
* by the content view.</p>
*
* @param width the popup's width
* @param height the popup's height
*/
public PopupWindow(int width, int height) {
this(null, width, height);
}
/**
* <p>Create a new non focusable popup window which can display the
* <tt>contentView</tt>. The dimension of the window must be passed to
* this constructor.</p>
*
* <p>The popup does not provide any background. This should be handled
* by the content view.</p>
*
* @param contentView the popup's content
* @param width the popup's width
* @param height the popup's height
*/
public PopupWindow(View contentView, int width, int height) {
this(contentView, width, height, false);
}
/**
* <p>Create a new popup window which can display the <tt>contentView</tt>.
* The dimension of the window must be passed to this constructor.</p>
*
* <p>The popup does not provide any background. This should be handled
* by the content view.</p>
*
* @param contentView the popup's content
* @param width the popup's width
* @param height the popup's height
* @param focusable true if the popup can be focused, false otherwise
*/
public PopupWindow(View contentView, int width, int height,
boolean focusable) {
setContentView(contentView);
setWidth(width);
setHeight(height);
setFocusable(focusable);
}
/**
* <p>Return the drawable used as the popup window's background.</p>
*
* @return the background drawable or null
*/
public Drawable getBackground() {
return mBackground;
}
/**
* <p>Change the background drawable for this popup window. The background
* can be set to null.</p>
*
* @param background the popup's background
*/
public void setBackgroundDrawable(Drawable background) {
mBackground = background;
}
/**
* <p>Return the animation style to use the popup appears and disappears</p>
*
* @return the animation style to use the popup appears and disappears
*/
public int getAnimationStyle() {
return mAnimationStyle;
}
/**
* set the flag on popup to ignore cheek press events
* This method has to be invoked before displaying the content view
* of the popup for the window flags to take effect and will be ignored
* if the pop up is already displayed. By default this flag is set to false
* which means the pop wont ignore cheek press dispatch events.
*/
public void setIgnoreCheekPress() {
mIgnoreCheekPress = true;
}
/**
* <p>Change the animation style for this popup.</p>
*
* @param animationStyle animation style to use when the popup appears and disappears
*/
public void setAnimationStyle(int animationStyle) {
mAnimationStyle = animationStyle;
}
/**
* <p>Return the view used as the content of the popup window.</p>
*
* @return a {@link android.view.View} representing the popup's content
*
* @see #setContentView(android.view.View)
*/
public View getContentView() {
return mContentView;
}
/**
* <p>Change the popup's content. The content is represented by an instance
* of {@link android.view.View}.</p>
*
* <p>This method has no effect if called when the popup is showing.</p>
*
* @param contentView the new content for the popup
*
* @see #getContentView()
* @see #isShowing()
*/
public void setContentView(View contentView) {
if (isShowing()) {
return;
}
mContentView = contentView;
}
/**
* <p>Indicate whether the popup window can grab the focus.</p>
*
* @return true if the popup is focusable, false otherwise
*
* @see #setFocusable(boolean)
*/
public boolean isFocusable() {
return mFocusable;
}
/**
* <p>Changes the focusability of the popup window. When focusable, the
* window will grab the focus from the current focused widget if the popup
* contains a focusable {@link android.view.View}.</p>
*
* <p>If the popup is showing, calling this method will take effect only
* the next time the popup is shown.</p>
*
* @param focusable true if the popup should grab focus, false otherwise
*
* @see #isFocusable()
* @see #isShowing()
*/
public void setFocusable(boolean focusable) {
mFocusable = focusable;
}
/**
* <p>Return this popup's height MeasureSpec</p>
*
* @return the height MeasureSpec of the popup
*
* @see #setHeight(int)
*/
public int getHeight() {
return mHeight;
}
/**
* <p>Change the popup's height MeasureSpec</p>
*
* <p>If the popup is showing, calling this method will take effect only
* the next time the popup is shown.</p>
*
* @param height the height MeasureSpec of the popup
*
* @see #getHeight()
* @see #isShowing()
*/
public void setHeight(int height) {
mHeight = height;
}
/**
* <p>Return this popup's width MeasureSpec</p>
*
* @return the width MeasureSpec of the popup
*
* @see #setWidth(int)
*/
public int getWidth() {
return mWidth;
}
/**
* <p>Change the popup's width MeasureSpec</p>
*
* <p>If the popup is showing, calling this method will take effect only
* the next time the popup is shown.</p>
*
* @param width the width MeasureSpec of the popup
*
* @see #getWidth()
* @see #isShowing()
*/
public void setWidth(int width) {
mWidth = width;
}
/**
* <p>Indicate whether this popup window is showing on screen.</p>
*
* @return true if the popup is showing, false otherwise
*/
public boolean isShowing() {
return mIsShowing;
}
/**
* <p>
* Display the content view in a popup window at the specified location. If the popup window
* cannot fit on screen, it will be clipped. See {@link android.view.WindowManager.LayoutParams}
* for more information on how gravity and the x and y parameters are related. Specifying
* a gravity of {@link android.view.Gravity#NO_GRAVITY} is similar to specifying
* <code>Gravity.LEFT | Gravity.TOP</code>.
* </p>
*
* @param parent a parent view to get the {@link android.view.View#getWindowToken()} token from
* @param gravity the gravity which controls the placement of the popup window
* @param x the popup's x location offset
* @param y the popup's y location offset
*/
public void showAtLocation(View parent, int gravity, int x, int y) {
if (isShowing() || mContentView == null) {
return;
}
mIsShowing = true;
WindowManager.LayoutParams p = createPopupLayout(parent.getWindowToken());
if (mAnimationStyle != -1) {
p.windowAnimations = mAnimationStyle;
}
preparePopup(p);
if (gravity == Gravity.NO_GRAVITY) {
gravity = Gravity.TOP | Gravity.LEFT;
}
p.gravity = gravity;
p.x = x;
p.y = y;
invokePopup(p);
}
/**
* <p>Display the content view in a popup window anchored to the bottom-left
* corner of the anchor view. If there is not enough room on screen to show
* the popup in its entirety, this method tries to find a parent scroll
* view to scroll. If no parent scroll view can be scrolled, the bottom-left
* corner of the popup is pinned at the top left corner of the anchor view.</p>
*
* @param anchor the view on which to pin the popup window
*
* @see #dismiss()
*/
public void showAsDropDown(View anchor) {
showAsDropDown(anchor, 0, 0);
}
/**
* <p>Display the content view in a popup window anchored to the bottom-left
* corner of the anchor view offset by the specified x and y coordinates.
* If there is not enough room on screen to show
* the popup in its entirety, this method tries to find a parent scroll
* view to scroll. If no parent scroll view can be scrolled, the bottom-left
* corner of the popup is pinned at the top left corner of the anchor view.</p>
*
* @param anchor the view on which to pin the popup window
*
* @see #dismiss()
*/
public void showAsDropDown(View anchor, int xoff, int yoff) {
if (isShowing() || mContentView == null) {
return;
}
mIsShowing = true;
WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
preparePopup(p);
if (mBackground != null) {
mPopupView.refreshDrawableState();
}
mAboveAnchor = findDropDownPosition(anchor, p, xoff, yoff);
if (mAnimationStyle == -1) {
p.windowAnimations = mAboveAnchor
? com.android.internal.R.style.Animation_DropDownUp
: com.android.internal.R.style.Animation_DropDownDown;
} else {
p.windowAnimations = mAnimationStyle;
}
invokePopup(p);
}
/**
* <p>Prepare the popup by embedding in into a new ViewGroup if the
* background drawable is not null. If embedding is required, the layout
* parameters' height is mnodified to take into account the background's
* padding.</p>
*
* @param p the layout parameters of the popup's content view
*/
private void preparePopup(WindowManager.LayoutParams p) {
if (mBackground != null) {
// when a background is available, we embed the content view
// within another view that owns the background drawable
PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT
);
popupViewContainer.setBackgroundDrawable(mBackground);
popupViewContainer.addView(mContentView, listParams);
if (p.height >= 0) {
// accomodate the popup's height to take into account the
// background's padding
p.height += popupViewContainer.getPaddingTop() +
popupViewContainer.getPaddingBottom();
}
if (p.width >= 0) {
// accomodate the popup's width to take into account the
// background's padding
p.width += popupViewContainer.getPaddingLeft() +
popupViewContainer.getPaddingRight();
}
mPopupView = popupViewContainer;
} else {
mPopupView = mContentView;
}
}
/**
* <p>Invoke the popup window by adding the content view to the window
* manager.</p>
*
* <p>The content view must be non-null when this method is invoked.</p>
*
* @param p the layout parameters of the popup's content view
*/
private void invokePopup(WindowManager.LayoutParams p) {
WindowManagerImpl wm = WindowManagerImpl.getDefault();
wm.addView(mPopupView, p);
}
/**
* <p>Generate the layout parameters for the popup window.</p>
*
* @param token the window token used to bind the popup's window
*
* @return the layout parameters to pass to the window manager
*/
private WindowManager.LayoutParams createPopupLayout(IBinder token) {
// generates the layout parameters for the drop down
// we want a fixed size view located at the bottom left of the anchor
WindowManager.LayoutParams p = new WindowManager.LayoutParams();
// these gravity settings put the view at the top left corner of the
// screen. The view is then positioned to the appropriate location
// by setting the x and y offsets to match the anchor's bottom
// left corner
p.gravity = Gravity.LEFT | Gravity.TOP;
p.width = mWidth;
p.height = mHeight;
if (mBackground != null) {
p.format = mBackground.getOpacity();
} else {
p.format = PixelFormat.TRANSLUCENT;
}
if(mIgnoreCheekPress) {
p.flags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
}
if (!mFocusable) {
p.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
}
p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
p.token = token;
return p;
}
/**
* <p>Positions the popup window on screen. When the popup window is too
* tall to fit under the anchor, a parent scroll view is seeked and scrolled
* up to reclaim space. If scrolling is not possible or not enough, the
* popup window gets moved on top of the anchor.</p>
*
* <p>The height must have been set on the layout parameters prior to
* calling this method.</p>
*
* @param anchor the view on which the popup window must be anchored
* @param p the layout parameters used to display the drop down
*
* @return true if the popup is translated upwards to fit on screen
*/
private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams p, int xoff, int yoff) {
anchor.getLocationInWindow(mDrawingLocation);
p.x = mDrawingLocation[0] + xoff;
p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff;
boolean onTop = false;
if (p.y + p.height > WindowManagerImpl.getDefault().getDefaultDisplay().getHeight()) {
// if the drop down disappears at the bottom of the screen. we try to
// scroll a parent scrollview or move the drop down back up on top of
// the edit box
View root = anchor.getRootView();
root.getLocationInWindow(mRootLocation);
int delta = p.y + p.height - mRootLocation[1] - root.getHeight();
if (delta > 0 || p.x + p.width - mRootLocation[0] - root.getWidth() > 0) {
Rect r = new Rect(anchor.getScrollX(), anchor.getScrollY(),
p.width, p.height + anchor.getMeasuredHeight());
onTop = !anchor.requestRectangleOnScreen(r, true);
if (onTop) {
p.y -= anchor.getMeasuredHeight() + p.height;
} else {
anchor.getLocationOnScreen(mDrawingLocation);
p.x = mDrawingLocation[0] + xoff;
p.y = mDrawingLocation[1] + anchor.getMeasuredHeight() + yoff;
}
}
}
return onTop;
}
/**
* Returns the maximum height that is available for the popup to be
* completely shown. It is recommended that this height be the maximum for
* the popup's height, otherwise it is possible that the popup will be
* clipped.
*
* @param anchor The view on which the popup window must be anchored.
* @return The maximum available height for the popup to be completely
* shown.
*/
public int getMaxAvailableHeight(View anchor) {
// TODO: read comment on STATUS_BAR_HEIGHT
final int screenHeight = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight()
- STATUS_BAR_HEIGHT;
final int[] anchorPos = mDrawingLocation;
anchor.getLocationOnScreen(anchorPos);
anchorPos[1] -= STATUS_BAR_HEIGHT;
final int distanceFromAnchorToBottom = screenHeight - (anchorPos[1] + anchor.getHeight());
// anchorPos[1] is distance from anchor to top of screen
int returnedHeight = Math.max(anchorPos[1], distanceFromAnchorToBottom);
if (mBackground != null) {
mBackground.getPadding(mTempRect);
returnedHeight -= mTempRect.top + mTempRect.bottom;
}
return returnedHeight;
}
/**
* <p>Dispose of the popup window. This method can be invoked only after
* {@link #showAsDropDown(android.view.View)} has been executed. Failing that, calling
* this method will have no effect.</p>
*
* @see #showAsDropDown(android.view.View)
*/
public void dismiss() {
if (isShowing() && mPopupView != null) {
WindowManagerImpl wm = WindowManagerImpl.getDefault();
wm.removeView(mPopupView);
if (mPopupView != mContentView && mPopupView instanceof ViewGroup) {
((ViewGroup) mPopupView).removeView(mContentView);
}
mIsShowing = false;
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss();
}
}
}
/**
* Sets the listener to be called when the window is dismissed.
*
* @param onDismissListener The listener.
*/
public void setOnDismissListener(OnDismissListener onDismissListener) {
mOnDismissListener = onDismissListener;
}
/**
* <p>Updates the position and the dimension of the popup window. Width and
* height can be set to -1 to update location only.</p>
*
* @param x the new x location
* @param y the new y location
* @param width the new width, can be -1 to ignore
* @param height the new height, can be -1 to ignore
*/
public void update(int x, int y, int width, int height) {
if (width != -1) {
setWidth(width);
}
if (height != -1) {
setHeight(height);
}
if (!isShowing() || mContentView == null) {
return;
}
WindowManager.LayoutParams p = (WindowManager.LayoutParams)
mPopupView.getLayoutParams();
boolean update = false;
if (width != -1 && p.width != width) {
p.width = width;
update = true;
}
if (height != -1 && p.height != height) {
p.height = height;
update = true;
}
if (p.x != x) {
p.x = x;
update = true;
}
if (p.y != y) {
p.y = y;
update = true;
}
if (update) {
if (mPopupView != mContentView) {
final View popupViewContainer = mPopupView;
if (p.height >= 0) {
// accomodate the popup's height to take into account the
// background's padding
p.height += popupViewContainer.getPaddingTop() +
popupViewContainer.getPaddingBottom();
}
if (p.width >= 0) {
// accomodate the popup's width to take into account the
// background's padding
p.width += popupViewContainer.getPaddingLeft() +
popupViewContainer.getPaddingRight();
}
}
WindowManagerImpl wm = WindowManagerImpl.getDefault();
wm.updateViewLayout(mPopupView, p);
}
}
/**
* <p>Updates the position and the dimension of the popup window. Width and
* height can be set to -1 to update location only.</p>
*
* @param anchor the popup's anchor view
* @param width the new width, can be -1 to ignore
* @param height the new height, can be -1 to ignore
*/
public void update(View anchor, int width, int height) {
update(anchor, 0, 0, width, height);
}
/**
* <p>Updates the position and the dimension of the popup window. Width and
* height can be set to -1 to update location only.</p>
*
* @param anchor the popup's anchor view
* @param xoff x offset from the view's left edge
* @param yoff y offset from the view's bottom edge
* @param width the new width, can be -1 to ignore
* @param height the new height, can be -1 to ignore
*/
public void update(View anchor, int xoff, int yoff, int width, int height) {
if (!isShowing() || mContentView == null) {
return;
}
WindowManager.LayoutParams p = (WindowManager.LayoutParams)
mPopupView.getLayoutParams();
int x = p.x;
int y = p.y;
findDropDownPosition(anchor, p, xoff, yoff);
update(x, y, width, height);
}
/**
* Listener that is called when this popup window is dismissed.
*/
interface OnDismissListener {
/**
* Called when this popup window is dismissed.
*/
public void onDismiss();
}
private class PopupViewContainer extends FrameLayout {
public PopupViewContainer(Context context) {
super(context);
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
if (mAboveAnchor) {
// 1 more needed for the above anchor state
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
return drawableState;
} else {
return super.onCreateDrawableState(extraSpace);
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
dismiss();
return true;
} else {
return super.dispatchKeyEvent(event);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int x = (int) event.getX();
final int y = (int) event.getY();
if ((event.getAction() == MotionEvent.ACTION_DOWN)
&& ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
dismiss();
return true;
} else {
return super.onTouchEvent(event);
}
}
}
}