Java tutorial
/* * 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 static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.os.Build; import android.os.IBinder; import android.transition.Transition; import android.transition.Transition.EpicenterCallback; import android.transition.Transition.TransitionListener; import android.transition.TransitionInflater; import android.transition.TransitionListenerAdapter; import android.transition.TransitionManager; import android.transition.TransitionSet; import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; import android.view.MotionEvent; import android.view.View; import android.view.View.OnAttachStateChangeListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.ViewTreeObserver.OnScrollChangedListener; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import android.view.WindowManagerGlobal; import com.android.internal.R; import java.lang.ref.WeakReference; import java.util.List; /** * <p> * This class represents a popup window that can be used to display an * arbitrary view. The popup window is a floating container that appears on top * of the current activity. * </p> * <a name="Animation"></a> * <h3>Animation</h3> * <p> * On all versions of Android, popup window enter and exit animations may be * specified by calling {@link #setAnimationStyle(int)} and passing the * resource ID for an animation style that defines {@code windowEnterAnimation} * and {@code windowExitAnimation}. For example, passing * {@link android.R.style#Animation_Dialog} will give a scale and alpha * animation. * </br> * A window animation style may also be specified in the popup window's style * XML via the {@link android.R.styleable#PopupWindow_popupAnimationStyle popupAnimationStyle} * attribute. * </p> * <p> * Starting with API 23, more complex popup window enter and exit transitions * may be specified by calling either {@link #setEnterTransition(Transition)} * or {@link #setExitTransition(Transition)} and passing a {@link Transition}. * </br> * Popup enter and exit transitions may also be specified in the popup window's * style XML via the {@link android.R.styleable#PopupWindow_popupEnterTransition popupEnterTransition} * and {@link android.R.styleable#PopupWindow_popupExitTransition popupExitTransition} * attributes, respectively. * </p> * * @attr ref android.R.styleable#PopupWindow_overlapAnchor * @attr ref android.R.styleable#PopupWindow_popupAnimationStyle * @attr ref android.R.styleable#PopupWindow_popupBackground * @attr ref android.R.styleable#PopupWindow_popupElevation * @attr ref android.R.styleable#PopupWindow_popupEnterTransition * @attr ref android.R.styleable#PopupWindow_popupExitTransition * * @see android.widget.AutoCompleteTextView * @see android.widget.Spinner */ public class PopupWindow { /** * Mode for {@link #setInputMethodMode(int)}: the requirements for the * input method should be based on the focusability of the popup. That is * if it is focusable than it needs to work with the input method, else * it doesn't. */ public static final int INPUT_METHOD_FROM_FOCUSABLE = 0; /** * Mode for {@link #setInputMethodMode(int)}: this popup always needs to * work with an input method, regardless of whether it is focusable. This * means that it will always be displayed so that the user can also operate * the input method while it is shown. */ public static final int INPUT_METHOD_NEEDED = 1; /** * Mode for {@link #setInputMethodMode(int)}: this popup never needs to * work with an input method, regardless of whether it is focusable. This * means that it will always be displayed to use as much space on the * screen as needed, regardless of whether this covers the input method. */ public static final int INPUT_METHOD_NOT_NEEDED = 2; private static final int DEFAULT_ANCHORED_GRAVITY = Gravity.TOP | Gravity.START; /** * Default animation style indicating that separate animations should be * used for top/bottom anchoring states. */ private static final int ANIMATION_STYLE_DEFAULT = -1; private final int[] mTmpDrawingLocation = new int[2]; private final int[] mTmpScreenLocation = new int[2]; private final int[] mTmpAppLocation = new int[2]; private final Rect mTempRect = new Rect(); @UnsupportedAppUsage private Context mContext; @UnsupportedAppUsage private WindowManager mWindowManager; /** * Keeps track of popup's parent's decor view. This is needed to dispatch * requestKeyboardShortcuts to the owning Activity. */ private WeakReference<View> mParentRootView; @UnsupportedAppUsage private boolean mIsShowing; private boolean mIsTransitioningToDismiss; @UnsupportedAppUsage private boolean mIsDropdown; /** View that handles event dispatch and content transitions. */ @UnsupportedAppUsage private PopupDecorView mDecorView; /** View that holds the background and may animate during a transition. */ @UnsupportedAppUsage private View mBackgroundView; /** The contents of the popup. May be identical to the background view. */ @UnsupportedAppUsage private View mContentView; private boolean mFocusable; private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE; @SoftInputModeFlags private int mSoftInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; private boolean mTouchable = true; private boolean mOutsideTouchable = false; private boolean mClippingEnabled = true; private int mSplitTouchEnabled = -1; @UnsupportedAppUsage private boolean mLayoutInScreen; private boolean mClipToScreen; private boolean mAllowScrollingAnchorParent = true; private boolean mLayoutInsetDecor = false; @UnsupportedAppUsage private boolean mNotTouchModal; private boolean mAttachedInDecor = true; private boolean mAttachedInDecorSet = false; @UnsupportedAppUsage private OnTouchListener mTouchInterceptor; @UnsupportedAppUsage private int mWidthMode; private int mWidth = LayoutParams.WRAP_CONTENT; @UnsupportedAppUsage private int mLastWidth; @UnsupportedAppUsage private int mHeightMode; private int mHeight = LayoutParams.WRAP_CONTENT; @UnsupportedAppUsage private int mLastHeight; private float mElevation; private Drawable mBackground; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Drawable mAboveAnchorBackgroundDrawable; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private Drawable mBelowAnchorBackgroundDrawable; private Transition mEnterTransition; private Transition mExitTransition; private Rect mEpicenterBounds; @UnsupportedAppUsage private boolean mAboveAnchor; @UnsupportedAppUsage private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; @UnsupportedAppUsage private OnDismissListener mOnDismissListener; private boolean mIgnoreCheekPress = false; @UnsupportedAppUsage private int mAnimationStyle = ANIMATION_STYLE_DEFAULT; private int mGravity = Gravity.NO_GRAVITY; private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] { com.android.internal.R.attr.state_above_anchor }; private final OnAttachStateChangeListener mOnAnchorDetachedListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { // Anchor might have been reattached in a different position. alignToAnchor(); } @Override public void onViewDetachedFromWindow(View v) { // Leave the popup in its current position. // The anchor might become attached again. } }; private final OnAttachStateChangeListener mOnAnchorRootDetachedListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { mIsAnchorRootAttached = false; } }; @UnsupportedAppUsage private WeakReference<View> mAnchor; private WeakReference<View> mAnchorRoot; private boolean mIsAnchorRootAttached; @UnsupportedAppUsage private final OnScrollChangedListener mOnScrollChangedListener = this::alignToAnchor; private final View.OnLayoutChangeListener mOnLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> alignToAnchor(); private int mAnchorXoff; private int mAnchorYoff; private int mAnchoredGravity; @UnsupportedAppUsage private boolean mOverlapAnchor; private boolean mPopupViewInitialLayoutDirectionInherited; /** * <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 defStyleAttr) { this(context, attrs, defStyleAttr, 0); } /** * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p> * * <p>The popup does not provide a background.</p> */ public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { mContext = context; mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes); final Drawable bg = a.getDrawable(R.styleable.PopupWindow_popupBackground); mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0); mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false); // Preserve default behavior from Gingerbread. If the animation is // undefined or explicitly specifies the Gingerbread animation style, // use a sentinel value. if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupAnimationStyle)) { final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, 0); if (animStyle == R.style.Animation_PopupWindow) { mAnimationStyle = ANIMATION_STYLE_DEFAULT; } else { mAnimationStyle = animStyle; } } else { mAnimationStyle = ANIMATION_STYLE_DEFAULT; } final Transition enterTransition = getTransition( a.getResourceId(R.styleable.PopupWindow_popupEnterTransition, 0)); final Transition exitTransition; if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupExitTransition)) { exitTransition = getTransition(a.getResourceId(R.styleable.PopupWindow_popupExitTransition, 0)); } else { exitTransition = enterTransition == null ? null : enterTransition.clone(); } a.recycle(); setEnterTransition(enterTransition); setExitTransition(exitTransition); setBackgroundDrawable(bg); } /** * <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) { if (contentView != null) { mContext = contentView.getContext(); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } setContentView(contentView); setWidth(width); setHeight(height); setFocusable(focusable); } /** * Sets the enter transition to be used when the popup window is shown. * * @param enterTransition the enter transition, or {@code null} to clear * @see #getEnterTransition() * @attr ref android.R.styleable#PopupWindow_popupEnterTransition */ public void setEnterTransition(@Nullable Transition enterTransition) { mEnterTransition = enterTransition; } /** * Returns the enter transition to be used when the popup window is shown. * * @return the enter transition, or {@code null} if not set * @see #setEnterTransition(Transition) * @attr ref android.R.styleable#PopupWindow_popupEnterTransition */ @Nullable public Transition getEnterTransition() { return mEnterTransition; } /** * Sets the exit transition to be used when the popup window is dismissed. * * @param exitTransition the exit transition, or {@code null} to clear * @see #getExitTransition() * @attr ref android.R.styleable#PopupWindow_popupExitTransition */ public void setExitTransition(@Nullable Transition exitTransition) { mExitTransition = exitTransition; } /** * Returns the exit transition to be used when the popup window is * dismissed. * * @return the exit transition, or {@code null} if not set * @see #setExitTransition(Transition) * @attr ref android.R.styleable#PopupWindow_popupExitTransition */ @Nullable public Transition getExitTransition() { return mExitTransition; } /** * Sets the bounds used as the epicenter of the enter and exit transitions. * <p> * Transitions use a point or Rect, referred to as the epicenter, to orient * the direction of travel. For popup windows, the anchor view bounds are * used as the default epicenter. * <p> * See {@link Transition#setEpicenterCallback(EpicenterCallback)} for more * information about how transition epicenters. * * @param bounds the epicenter bounds relative to the anchor view, or * {@code null} to use the default epicenter * @see #getTransitionEpicenter() * @hide */ @UnsupportedAppUsage public void setEpicenterBounds(Rect bounds) { mEpicenterBounds = bounds; } private Transition getTransition(int resId) { if (resId != 0 && resId != R.transition.no_transition) { final TransitionInflater inflater = TransitionInflater.from(mContext); final Transition transition = inflater.inflateTransition(resId); if (transition != null) { final boolean isEmpty = transition instanceof TransitionSet && ((TransitionSet) transition).getTransitionCount() == 0; if (!isEmpty) { return transition; } } } return null; } /** * Return the drawable used as the popup window's background. * * @return the background drawable or {@code null} if not set * @see #setBackgroundDrawable(Drawable) * @attr ref android.R.styleable#PopupWindow_popupBackground */ public Drawable getBackground() { return mBackground; } /** * Specifies the background drawable for this popup window. The background * can be set to {@code null}. * * @param background the popup's background * @see #getBackground() * @attr ref android.R.styleable#PopupWindow_popupBackground */ public void setBackgroundDrawable(Drawable background) { mBackground = background; // If this is a StateListDrawable, try to find and store the drawable to be // used when the drop-down is placed above its anchor view, and the one to be // used when the drop-down is placed below its anchor view. We extract // the drawables ourselves to work around a problem with using refreshDrawableState // that it will take into account the padding of all drawables specified in a // StateListDrawable, thus adding superfluous padding to drop-down views. // // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and // at least one other drawable, intended for the 'below-anchor state'. if (mBackground instanceof StateListDrawable) { StateListDrawable stateList = (StateListDrawable) mBackground; // Find the above-anchor view - this one's easy, it should be labeled as such. int aboveAnchorStateIndex = stateList.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET); // Now, for the below-anchor view, look for any other drawable specified in the // StateListDrawable which is not for the above-anchor state and use that. int count = stateList.getStateCount(); int belowAnchorStateIndex = -1; for (int i = 0; i < count; i++) { if (i != aboveAnchorStateIndex) { belowAnchorStateIndex = i; break; } } // Store the drawables we found, if we found them. Otherwise, set them both // to null so that we'll just use refreshDrawableState. if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) { mAboveAnchorBackgroundDrawable = stateList.getStateDrawable(aboveAnchorStateIndex); mBelowAnchorBackgroundDrawable = stateList.getStateDrawable(belowAnchorStateIndex); } else { mBelowAnchorBackgroundDrawable = null; mAboveAnchorBackgroundDrawable = null; } } } /** * @return the elevation for this popup window in pixels * @see #setElevation(float) * @attr ref android.R.styleable#PopupWindow_popupElevation */ public float getElevation() { return mElevation; } /** * Specifies the elevation for this popup window. * * @param elevation the popup's elevation in pixels * @see #getElevation() * @attr ref android.R.styleable#PopupWindow_popupElevation */ public void setElevation(float elevation) { mElevation = elevation; } /** * <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; by default this flag * is set to false * which means the popup will not ignore cheek press dispatch events. * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> * * @see #update() */ public void setIgnoreCheekPress() { mIgnoreCheekPress = true; } /** * <p>Change the animation style resource for this popup.</p> * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> * * @param animationStyle animation style to use when the popup appears * and disappears. Set to -1 for the default animation, 0 for no * animation, or a resource identifier for an explicit animation. * * @see #update() */ 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; if (mContext == null && mContentView != null) { mContext = mContentView.getContext(); } if (mWindowManager == null && mContentView != null) { mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } // Setting the default for attachedInDecor based on SDK version here // instead of in the constructor since we might not have the context // object in the constructor. We only want to set default here if the // app hasn't already set the attachedInDecor. if (mContext != null && !mAttachedInDecorSet) { // Attach popup window in decor frame of parent window by default for // {@link Build.VERSION_CODES.LOLLIPOP_MR1} or greater. Keep current // behavior of not attaching to decor frame for older SDKs. setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP_MR1); } } /** * Set a callback for all touch events being dispatched to the popup * window. */ public void setTouchInterceptor(OnTouchListener l) { mTouchInterceptor = l; } /** * <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}. By default a popup * window is not focusable.</p> * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> * * @param focusable true if the popup should grab focus, false otherwise. * * @see #isFocusable() * @see #isShowing() * @see #update() */ public void setFocusable(boolean focusable) { mFocusable = focusable; } /** * Return the current value in {@link #setInputMethodMode(int)}. * * @see #setInputMethodMode(int) */ public int getInputMethodMode() { return mInputMethodMode; } /** * Control how the popup operates with an input method: one of * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED}, * or {@link #INPUT_METHOD_NOT_NEEDED}. * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> * * @see #getInputMethodMode() * @see #update() */ public void setInputMethodMode(int mode) { mInputMethodMode = mode; } /** * Sets the operating mode for the soft input area. * * @param mode The desired mode, see * {@link android.view.WindowManager.LayoutParams#softInputMode} * for the full list * * @see android.view.WindowManager.LayoutParams#softInputMode * @see #getSoftInputMode() */ public void setSoftInputMode(@SoftInputModeFlags int mode) { mSoftInputMode = mode; } /** * Returns the current value in {@link #setSoftInputMode(int)}. * * @see #setSoftInputMode(int) * @see android.view.WindowManager.LayoutParams#softInputMode */ @SoftInputModeFlags public int getSoftInputMode() { return mSoftInputMode; } /** * <p>Indicates whether the popup window receives touch events.</p> * * @return true if the popup is touchable, false otherwise * * @see #setTouchable(boolean) */ public boolean isTouchable() { return mTouchable; } /** * <p>Changes the touchability of the popup window. When touchable, the * window will receive touch events, otherwise touch events will go to the * window below it. By default the window is touchable.</p> * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> * * @param touchable true if the popup should receive touch events, false otherwise * * @see #isTouchable() * @see #isShowing() * @see #update() */ public void setTouchable(boolean touchable) { mTouchable = touchable; } /** * <p>Indicates whether the popup window will be informed of touch events * outside of its window.</p> * * @return true if the popup is outside touchable, false otherwise * * @see #setOutsideTouchable(boolean) */ public boolean isOutsideTouchable() { return mOutsideTouchable; } /** * <p>Controls whether the pop-up will be informed of touch events outside * of its window. This only makes sense for pop-ups that are touchable * but not focusable, which means touches outside of the window will * be delivered to the window behind. The default is false.</p> * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> * * @param touchable true if the popup should receive outside * touch events, false otherwise * * @see #isOutsideTouchable() * @see #isShowing() * @see #update() */ public void setOutsideTouchable(boolean touchable) { mOutsideTouchable = touchable; } /** * <p>Indicates whether clipping of the popup window is enabled.</p> * * @return true if the clipping is enabled, false otherwise * * @see #setClippingEnabled(boolean) */ public boolean isClippingEnabled() { return mClippingEnabled; } /** * <p>Allows the popup window to extend beyond the bounds of the screen. By default the * window is clipped to the screen boundaries. Setting this to false will allow windows to be * accurately positioned.</p> * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown or through a manual call to one of * the {@link #update()} methods.</p> * * @param enabled false if the window should be allowed to extend outside of the screen * @see #isShowing() * @see #isClippingEnabled() * @see #update() */ public void setClippingEnabled(boolean enabled) { mClippingEnabled = enabled; } /** * Clip this popup window to the screen, but not to the containing window. * * @param enabled True to clip to the screen. * @hide */ @UnsupportedAppUsage public void setClipToScreenEnabled(boolean enabled) { mClipToScreen = enabled; } /** * Allow PopupWindow to scroll the anchor's parent to provide more room * for the popup. Enabled by default. * * @param enabled True to scroll the anchor's parent when more room is desired by the popup. */ @UnsupportedAppUsage void setAllowScrollingAnchorParent(boolean enabled) { mAllowScrollingAnchorParent = enabled; } /** @hide */ protected final boolean getAllowScrollingAnchorParent() { return mAllowScrollingAnchorParent; } /** * <p>Indicates whether the popup window supports splitting touches.</p> * * @return true if the touch splitting is enabled, false otherwise * * @see #setSplitTouchEnabled(boolean) */ public boolean isSplitTouchEnabled() { if (mSplitTouchEnabled < 0 && mContext != null) { return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB; } return mSplitTouchEnabled == 1; } /** * <p>Allows the popup window to split touches across other windows that also * support split touch. When this flag is false, the first pointer * that goes down determines the window to which all subsequent touches * go until all pointers go up. When this flag is true, each pointer * (not necessarily the first) that goes down determines the window * to which all subsequent touches of that pointer will go until that * pointer goes up thereby enabling touches with multiple pointers * to be split across multiple windows.</p> * * @param enabled true if the split touches should be enabled, false otherwise * @see #isSplitTouchEnabled() */ public void setSplitTouchEnabled(boolean enabled) { mSplitTouchEnabled = enabled ? 1 : 0; } /** * <p>Indicates whether the popup window will be forced into using absolute screen coordinates * for positioning.</p> * * @return true if the window will always be positioned in screen coordinates. * @hide */ public boolean isLayoutInScreenEnabled() { return mLayoutInScreen; } /** * <p>Allows the popup window to force the flag * {@link WindowManager.LayoutParams#FLAG_LAYOUT_IN_SCREEN}, overriding default behavior. * This will cause the popup to be positioned in absolute screen coordinates.</p> * * @param enabled true if the popup should always be positioned in screen coordinates * @hide */ @UnsupportedAppUsage public void setLayoutInScreenEnabled(boolean enabled) { mLayoutInScreen = enabled; } /** * <p>Indicates whether the popup window will be attached in the decor frame of its parent * window. * * @return true if the window will be attached to the decor frame of its parent window. * * @see #setAttachedInDecor(boolean) * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR */ public boolean isAttachedInDecor() { return mAttachedInDecor; } /** * <p>This will attach the popup window to the decor frame of the parent window to avoid * overlaping with screen decorations like the navigation bar. Overrides the default behavior of * the flag {@link WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR}. * * <p>By default the flag is set on SDK version {@link Build.VERSION_CODES#LOLLIPOP_MR1} or * greater and cleared on lesser SDK versions. * * @param enabled true if the popup should be attached to the decor frame of its parent window. * * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR */ public void setAttachedInDecor(boolean enabled) { mAttachedInDecor = enabled; mAttachedInDecorSet = true; } /** * Allows the popup window to force the flag * {@link WindowManager.LayoutParams#FLAG_LAYOUT_INSET_DECOR}, overriding default behavior. * This will cause the popup to inset its content to account for system windows overlaying * the screen, such as the status bar. * * <p>This will often be combined with {@link #setLayoutInScreenEnabled(boolean)}. * * @param enabled true if the popup's views should inset content to account for system windows, * the way that decor views behave for full-screen windows. * @hide */ @UnsupportedAppUsage public void setLayoutInsetDecor(boolean enabled) { mLayoutInsetDecor = enabled; } /** @hide */ protected final boolean isLayoutInsetDecor() { return mLayoutInsetDecor; } /** * Set the layout type for this window. * <p> * See {@link WindowManager.LayoutParams#type} for possible values. * * @param layoutType Layout type for this window. * * @see WindowManager.LayoutParams#type */ public void setWindowLayoutType(int layoutType) { mWindowLayoutType = layoutType; } /** * Returns the layout type for this window. * * @see #setWindowLayoutType(int) */ public int getWindowLayoutType() { return mWindowLayoutType; } /** * Set whether this window is touch modal or if outside touches will be sent to * other windows behind it. * @hide */ @UnsupportedAppUsage public void setTouchModal(boolean touchModal) { mNotTouchModal = !touchModal; } /** * <p>Change the width and height measure specs that are given to the * window manager by the popup. By default these are 0, meaning that * the current width or height is requested as an explicit size from * the window manager. You can supply * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or * {@link ViewGroup.LayoutParams#MATCH_PARENT} to have that measure * spec supplied instead, replacing the absolute width and height that * has been set in the popup.</p> * * <p>If the popup is showing, calling this method will take effect only * the next time the popup is shown.</p> * * @param widthSpec an explicit width measure spec mode, either * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute * width. * @param heightSpec an explicit height measure spec mode, either * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute * height. * * @deprecated Use {@link #setWidth(int)} and {@link #setHeight(int)}. */ @Deprecated public void setWindowLayoutMode(int widthSpec, int heightSpec) { mWidthMode = widthSpec; mHeightMode = heightSpec; } /** * Returns the popup's requested height. May be a layout constant such as * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}. * <p> * The actual size of the popup may depend on other factors such as * clipping and window layout. * * @return the popup height in pixels or a layout constant * @see #setHeight(int) */ public int getHeight() { return mHeight; } /** * Sets the popup's requested height. May be a layout constant such as * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}. * <p> * The actual size of the popup may depend on other factors such as * clipping and window layout. * <p> * If the popup is showing, calling this method will take effect the next * time the popup is shown. * * @param height the popup height in pixels or a layout constant * @see #getHeight() * @see #isShowing() */ public void setHeight(int height) { mHeight = height; } /** * Returns the popup's requested width. May be a layout constant such as * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}. * <p> * The actual size of the popup may depend on other factors such as * clipping and window layout. * * @return the popup width in pixels or a layout constant * @see #setWidth(int) */ public int getWidth() { return mWidth; } /** * Sets the popup's requested width. May be a layout constant such as * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}. * <p> * The actual size of the popup may depend on other factors such as * clipping and window layout. * <p> * If the popup is showing, calling this method will take effect the next * time the popup is shown. * * @param width the popup width in pixels or a layout constant * @see #getWidth() * @see #isShowing() */ public void setWidth(int width) { mWidth = width; } /** * Sets whether the popup window should overlap its anchor view when * displayed as a drop-down. * <p> * If the popup is showing, calling this method will take effect only * the next time the popup is shown. * * @param overlapAnchor Whether the popup should overlap its anchor. * * @see #getOverlapAnchor() * @see #isShowing() */ public void setOverlapAnchor(boolean overlapAnchor) { mOverlapAnchor = overlapAnchor; } /** * Returns whether the popup window should overlap its anchor view when * displayed as a drop-down. * * @return Whether the popup should overlap its anchor. * * @see #setOverlapAnchor(boolean) */ public boolean getOverlapAnchor() { return mOverlapAnchor; } /** * <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; } /** @hide */ protected final void setShowing(boolean isShowing) { mIsShowing = isShowing; } /** @hide */ protected final void setDropDown(boolean isDropDown) { mIsDropdown = isDropDown; } /** @hide */ protected final void setTransitioningToDismiss(boolean transitioningToDismiss) { mIsTransitioningToDismiss = transitioningToDismiss; } /** @hide */ protected final boolean isTransitioningToDismiss() { return mIsTransitioningToDismiss; } /** * <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) { mParentRootView = new WeakReference<>(parent.getRootView()); showAtLocation(parent.getWindowToken(), gravity, x, y); } /** * Display the content view in a popup window at the specified location. * * @param token Window token to use for creating the new window * @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 * * @hide Internal use only. Applications should use * {@link #showAtLocation(View, int, int, int)} instead. */ @UnsupportedAppUsage public void showAtLocation(IBinder token, int gravity, int x, int y) { if (isShowing() || mContentView == null) { return; } TransitionManager.endTransitions(mDecorView); detachFromAnchor(); mIsShowing = true; mIsDropdown = false; mGravity = gravity; final WindowManager.LayoutParams p = createPopupLayoutParams(token); preparePopup(p); p.x = x; p.y = y; invokePopup(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. * * @param anchor the view on which to pin the popup window * * @see #dismiss() */ public void showAsDropDown(View anchor) { showAsDropDown(anchor, 0, 0); } /** * 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> * If the view later scrolls to move <code>anchor</code> to a different * location, the popup will be moved correspondingly. * * @param anchor the view on which to pin the popup window * @param xoff A horizontal offset from the anchor in pixels * @param yoff A vertical offset from the anchor in pixels * * @see #dismiss() */ public void showAsDropDown(View anchor, int xoff, int yoff) { showAsDropDown(anchor, xoff, yoff, DEFAULT_ANCHORED_GRAVITY); } /** * Displays the content view in a popup window anchored to the corner of * another view. The window is positioned according to the specified * gravity and offset by the specified x and y coordinates. * <p> * 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 * view can be scrolled, the specified vertical gravity will be ignored and * the popup will anchor itself such that it is visible. * <p> * If the view later scrolls to move <code>anchor</code> to a different * location, the popup will be moved correspondingly. * * @param anchor the view on which to pin the popup window * @param xoff A horizontal offset from the anchor in pixels * @param yoff A vertical offset from the anchor in pixels * @param gravity Alignment of the popup relative to the anchor * * @see #dismiss() */ public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { if (isShowing() || !hasContentView()) { return; } TransitionManager.endTransitions(mDecorView); attachToAnchor(anchor, xoff, yoff, gravity); mIsShowing = true; mIsDropdown = true; final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getApplicationWindowToken()); preparePopup(p); final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, p.width, p.height, gravity, mAllowScrollingAnchorParent); updateAboveAnchor(aboveAnchor); p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1; invokePopup(p); } /** @hide */ @UnsupportedAppUsage protected final void updateAboveAnchor(boolean aboveAnchor) { if (aboveAnchor != mAboveAnchor) { mAboveAnchor = aboveAnchor; if (mBackground != null && mBackgroundView != null) { // If the background drawable provided was a StateListDrawable // with above-anchor and below-anchor states, use those. // Otherwise, rely on refreshDrawableState to do the job. if (mAboveAnchorBackgroundDrawable != null) { if (mAboveAnchor) { mBackgroundView.setBackground(mAboveAnchorBackgroundDrawable); } else { mBackgroundView.setBackground(mBelowAnchorBackgroundDrawable); } } else { mBackgroundView.refreshDrawableState(); } } } } /** * Indicates whether the popup is showing above (the y coordinate of the popup's bottom * is less than the y coordinate of the anchor) or below the anchor view (the y coordinate * of the popup is greater than y coordinate of the anchor's bottom). * * The value returned * by this method is meaningful only after {@link #showAsDropDown(android.view.View)} * or {@link #showAsDropDown(android.view.View, int, int)} was invoked. * * @return True if this popup is showing above the anchor view, false otherwise. */ public boolean isAboveAnchor() { return mAboveAnchor; } /** * Prepare the popup by embedding it into a new ViewGroup if the background * drawable is not null. If embedding is required, the layout parameters' * height is modified to take into account the background's padding. * * @param p the layout parameters of the popup's content view */ @UnsupportedAppUsage private void preparePopup(WindowManager.LayoutParams p) { if (mContentView == null || mContext == null || mWindowManager == null) { throw new IllegalStateException("You must specify a valid content view by " + "calling setContentView() before attempting to show the popup."); } if (p.accessibilityTitle == null) { p.accessibilityTitle = mContext.getString(R.string.popup_window_default_title); } // The old decor view may be transitioning out. Make sure it finishes // and cleans up before we try to create another one. if (mDecorView != null) { mDecorView.cancelTransitions(); } // When a background is available, we embed the content view within // another view that owns the background drawable. if (mBackground != null) { mBackgroundView = createBackgroundView(mContentView); mBackgroundView.setBackground(mBackground); } else { mBackgroundView = mContentView; } mDecorView = createDecorView(mBackgroundView); mDecorView.setIsRootNamespace(true); // The background owner should be elevated so that it casts a shadow. mBackgroundView.setElevation(mElevation); // We may wrap that in another view, so we'll need to manually specify // the surface insets. p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/); mPopupViewInitialLayoutDirectionInherited = (mContentView .getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT); } /** * Wraps a content view in a PopupViewContainer. * * @param contentView the content view to wrap * @return a PopupViewContainer that wraps the content view */ private PopupBackgroundView createBackgroundView(View contentView) { final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); final int height; if (layoutParams != null && layoutParams.height == WRAP_CONTENT) { height = WRAP_CONTENT; } else { height = MATCH_PARENT; } final PopupBackgroundView backgroundView = new PopupBackgroundView(mContext); final PopupBackgroundView.LayoutParams listParams = new PopupBackgroundView.LayoutParams(MATCH_PARENT, height); backgroundView.addView(contentView, listParams); return backgroundView; } /** * Wraps a content view in a FrameLayout. * * @param contentView the content view to wrap * @return a FrameLayout that wraps the content view */ private PopupDecorView createDecorView(View contentView) { final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); final int height; if (layoutParams != null && layoutParams.height == WRAP_CONTENT) { height = WRAP_CONTENT; } else { height = MATCH_PARENT; } final PopupDecorView decorView = new PopupDecorView(mContext); decorView.addView(contentView, MATCH_PARENT, height); decorView.setClipChildren(false); decorView.setClipToPadding(false); return decorView; } /** * <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 */ @UnsupportedAppUsage private void invokePopup(WindowManager.LayoutParams p) { if (mContext != null) { p.packageName = mContext.getPackageName(); } final PopupDecorView decorView = mDecorView; decorView.setFitsSystemWindows(mLayoutInsetDecor); setLayoutDirectionFromAnchor(); mWindowManager.addView(decorView, p); if (mEnterTransition != null) { decorView.requestEnterTransition(mEnterTransition); } } private void setLayoutDirectionFromAnchor() { if (mAnchor != null) { View anchor = mAnchor.get(); if (anchor != null && mPopupViewInitialLayoutDirectionInherited) { mDecorView.setLayoutDirection(anchor.getLayoutDirection()); } } } private int computeGravity() { int gravity = mGravity == Gravity.NO_GRAVITY ? Gravity.START | Gravity.TOP : mGravity; if (mIsDropdown && (mClipToScreen || mClippingEnabled)) { gravity |= Gravity.DISPLAY_CLIP_VERTICAL; } return gravity; } /** * <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 * * @hide */ @UnsupportedAppUsage protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) { final 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 = computeGravity(); p.flags = computeFlags(p.flags); p.type = mWindowLayoutType; p.token = token; p.softInputMode = mSoftInputMode; p.windowAnimations = computeAnimationResource(); if (mBackground != null) { p.format = mBackground.getOpacity(); } else { p.format = PixelFormat.TRANSLUCENT; } if (mHeightMode < 0) { p.height = mLastHeight = mHeightMode; } else { p.height = mLastHeight = mHeight; } if (mWidthMode < 0) { p.width = mLastWidth = mWidthMode; } else { p.width = mLastWidth = mWidth; } p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME; // Used for debugging. p.setTitle("PopupWindow:" + Integer.toHexString(hashCode())); return p; } private int computeFlags(int curFlags) { curFlags &= ~(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH); if (mIgnoreCheekPress) { curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; } if (!mFocusable) { curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; if (mInputMethodMode == INPUT_METHOD_NEEDED) { curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) { curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; } if (!mTouchable) { curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; } if (mOutsideTouchable) { curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; } if (!mClippingEnabled || mClipToScreen) { curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; } if (isSplitTouchEnabled()) { curFlags |= WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; } if (mLayoutInScreen) { curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; } if (mLayoutInsetDecor) { curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; } if (mNotTouchModal) { curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; } if (mAttachedInDecor) { curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR; } return curFlags; } @UnsupportedAppUsage private int computeAnimationResource() { if (mAnimationStyle == ANIMATION_STYLE_DEFAULT) { if (mIsDropdown) { return mAboveAnchor ? com.android.internal.R.style.Animation_DropDownUp : com.android.internal.R.style.Animation_DropDownDown; } return 0; } return mAnimationStyle; } /** * 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> * The results of positioning are placed in {@code outParams}. * * @param anchor the view on which the popup window must be anchored * @param outParams the layout parameters used to display the drop down * @param xOffset absolute horizontal offset from the left of the anchor * @param yOffset absolute vertical offset from the top of the anchor * @param gravity horizontal gravity specifying popup alignment * @param allowScroll whether the anchor view's parent may be scrolled * when the popup window doesn't fit on screen * @return true if the popup is translated upwards to fit on screen * * @hide */ protected boolean findDropDownPosition(View anchor, WindowManager.LayoutParams outParams, int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) { final int anchorHeight = anchor.getHeight(); final int anchorWidth = anchor.getWidth(); if (mOverlapAnchor) { yOffset -= anchorHeight; } // Initially, align to the bottom-left corner of the anchor plus offsets. final int[] appScreenLocation = mTmpAppLocation; final View appRootView = getAppRootView(anchor); appRootView.getLocationOnScreen(appScreenLocation); final int[] screenLocation = mTmpScreenLocation; anchor.getLocationOnScreen(screenLocation); final int[] drawingLocation = mTmpDrawingLocation; drawingLocation[0] = screenLocation[0] - appScreenLocation[0]; drawingLocation[1] = screenLocation[1] - appScreenLocation[1]; outParams.x = drawingLocation[0] + xOffset; outParams.y = drawingLocation[1] + anchorHeight + yOffset; final Rect displayFrame = new Rect(); appRootView.getWindowVisibleDisplayFrame(displayFrame); if (width == MATCH_PARENT) { width = displayFrame.right - displayFrame.left; } if (height == MATCH_PARENT) { height = displayFrame.bottom - displayFrame.top; } // Let the window manager know to align the top to y. outParams.gravity = computeGravity(); outParams.width = width; outParams.height = height; // If we need to adjust for gravity RIGHT, align to the bottom-right // corner of the anchor (still accounting for offsets). final int hgrav = Gravity.getAbsoluteGravity(gravity, anchor.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; if (hgrav == Gravity.RIGHT) { outParams.x -= width - anchorWidth; } // First, attempt to fit the popup vertically without resizing. final boolean fitsVertical = tryFitVertical(outParams, yOffset, height, anchorHeight, drawingLocation[1], screenLocation[1], displayFrame.top, displayFrame.bottom, false); // Next, attempt to fit the popup horizontally without resizing. final boolean fitsHorizontal = tryFitHorizontal(outParams, xOffset, width, anchorWidth, drawingLocation[0], screenLocation[0], displayFrame.left, displayFrame.right, false); // If the popup still doesn't fit, attempt to scroll the parent. if (!fitsVertical || !fitsHorizontal) { final int scrollX = anchor.getScrollX(); final int scrollY = anchor.getScrollY(); final Rect r = new Rect(scrollX, scrollY, scrollX + width + xOffset, scrollY + height + anchorHeight + yOffset); if (allowScroll && anchor.requestRectangleOnScreen(r, true)) { // Reset for the new anchor position. anchor.getLocationOnScreen(screenLocation); drawingLocation[0] = screenLocation[0] - appScreenLocation[0]; drawingLocation[1] = screenLocation[1] - appScreenLocation[1]; outParams.x = drawingLocation[0] + xOffset; outParams.y = drawingLocation[1] + anchorHeight + yOffset; // Preserve the gravity adjustment. if (hgrav == Gravity.RIGHT) { outParams.x -= width - anchorWidth; } } // Try to fit the popup again and allowing resizing. tryFitVertical(outParams, yOffset, height, anchorHeight, drawingLocation[1], screenLocation[1], displayFrame.top, displayFrame.bottom, mClipToScreen); tryFitHorizontal(outParams, xOffset, width, anchorWidth, drawingLocation[0], screenLocation[0], displayFrame.left, displayFrame.right, mClipToScreen); } // Return whether the popup's top edge is above the anchor's top edge. return outParams.y < drawingLocation[1]; } private boolean tryFitVertical(@NonNull LayoutParams outParams, int yOffset, int height, int anchorHeight, int drawingLocationY, int screenLocationY, int displayFrameTop, int displayFrameBottom, boolean allowResize) { final int winOffsetY = screenLocationY - drawingLocationY; final int anchorTopInScreen = outParams.y + winOffsetY; final int spaceBelow = displayFrameBottom - anchorTopInScreen; if (anchorTopInScreen >= 0 && height <= spaceBelow) { return true; } final int spaceAbove = anchorTopInScreen - anchorHeight - displayFrameTop; if (height <= spaceAbove) { // Move everything up. if (mOverlapAnchor) { yOffset += anchorHeight; } outParams.y = drawingLocationY - height + yOffset; return true; } if (positionInDisplayVertical(outParams, height, drawingLocationY, screenLocationY, displayFrameTop, displayFrameBottom, allowResize)) { return true; } return false; } private boolean positionInDisplayVertical(@NonNull LayoutParams outParams, int height, int drawingLocationY, int screenLocationY, int displayFrameTop, int displayFrameBottom, boolean canResize) { boolean fitsInDisplay = true; final int winOffsetY = screenLocationY - drawingLocationY; outParams.y += winOffsetY; outParams.height = height; final int bottom = outParams.y + height; if (bottom > displayFrameBottom) { // The popup is too far down, move it back in. outParams.y -= bottom - displayFrameBottom; } if (outParams.y < displayFrameTop) { // The popup is too far up, move it back in and clip if // it's still too large. outParams.y = displayFrameTop; final int displayFrameHeight = displayFrameBottom - displayFrameTop; if (canResize && height > displayFrameHeight) { outParams.height = displayFrameHeight; } else { fitsInDisplay = false; } } outParams.y -= winOffsetY; return fitsInDisplay; } private boolean tryFitHorizontal(@NonNull LayoutParams outParams, int xOffset, int width, int anchorWidth, int drawingLocationX, int screenLocationX, int displayFrameLeft, int displayFrameRight, boolean allowResize) { final int winOffsetX = screenLocationX - drawingLocationX; final int anchorLeftInScreen = outParams.x + winOffsetX; final int spaceRight = displayFrameRight - anchorLeftInScreen; if (anchorLeftInScreen >= 0 && width <= spaceRight) { return true; } if (positionInDisplayHorizontal(outParams, width, drawingLocationX, screenLocationX, displayFrameLeft, displayFrameRight, allowResize)) { return true; } return false; } private boolean positionInDisplayHorizontal(@NonNull LayoutParams outParams, int width, int drawingLocationX, int screenLocationX, int displayFrameLeft, int displayFrameRight, boolean canResize) { boolean fitsInDisplay = true; // Use screen coordinates for comparison against display frame. final int winOffsetX = screenLocationX - drawingLocationX; outParams.x += winOffsetX; final int right = outParams.x + width; if (right > displayFrameRight) { // The popup is too far right, move it back in. outParams.x -= right - displayFrameRight; } if (outParams.x < displayFrameLeft) { // The popup is too far left, move it back in and clip if it's // still too large. outParams.x = displayFrameLeft; final int displayFrameWidth = displayFrameRight - displayFrameLeft; if (canResize && width > displayFrameWidth) { outParams.width = displayFrameWidth; } else { fitsInDisplay = false; } } outParams.x -= winOffsetX; return fitsInDisplay; } /** * 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(@NonNull View anchor) { return getMaxAvailableHeight(anchor, 0); } /** * 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. * @param yOffset y offset from the view's bottom edge * @return The maximum available height for the popup to be completely * shown. */ public int getMaxAvailableHeight(@NonNull View anchor, int yOffset) { return getMaxAvailableHeight(anchor, yOffset, false); } /** * Returns the maximum height that is available for the popup to be * completely shown, optionally ignoring any bottom decorations such as * the input method. 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. * @param yOffset y offset from the view's bottom edge * @param ignoreBottomDecorations if true, the height returned will be * all the way to the bottom of the display, ignoring any * bottom decorations * @return The maximum available height for the popup to be completely * shown. */ public int getMaxAvailableHeight(@NonNull View anchor, int yOffset, boolean ignoreBottomDecorations) { Rect displayFrame = null; final Rect visibleDisplayFrame = new Rect(); final View appView = getAppRootView(anchor); appView.getWindowVisibleDisplayFrame(visibleDisplayFrame); if (ignoreBottomDecorations) { // In the ignore bottom decorations case we want to // still respect all other decorations so we use the inset visible // frame on the top right and left and take the bottom // value from the full frame. displayFrame = new Rect(); anchor.getWindowDisplayFrame(displayFrame); displayFrame.top = visibleDisplayFrame.top; displayFrame.right = visibleDisplayFrame.right; displayFrame.left = visibleDisplayFrame.left; } else { displayFrame = visibleDisplayFrame; } final int[] anchorPos = mTmpDrawingLocation; anchor.getLocationOnScreen(anchorPos); final int bottomEdge = displayFrame.bottom; final int distanceToBottom; if (mOverlapAnchor) { distanceToBottom = bottomEdge - anchorPos[1] - yOffset; } else { distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset; } final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset; // anchorPos[1] is distance from anchor to top of screen int returnedHeight = Math.max(distanceToBottom, distanceToTop); if (mBackground != null) { mBackground.getPadding(mTempRect); returnedHeight -= mTempRect.top + mTempRect.bottom; } return returnedHeight; } /** * Disposes 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. * * @see #showAsDropDown(android.view.View) */ public void dismiss() { if (!isShowing() || isTransitioningToDismiss()) { return; } final PopupDecorView decorView = mDecorView; final View contentView = mContentView; final ViewGroup contentHolder; final ViewParent contentParent = contentView.getParent(); if (contentParent instanceof ViewGroup) { contentHolder = ((ViewGroup) contentParent); } else { contentHolder = null; } // Ensure any ongoing or pending transitions are canceled. decorView.cancelTransitions(); mIsShowing = false; mIsTransitioningToDismiss = true; // This method may be called as part of window detachment, in which // case the anchor view (and its root) will still return true from // isAttachedToWindow() during execution of this method; however, we // can expect the OnAttachStateChangeListener to have been called prior // to executing this method, so we can rely on that instead. final Transition exitTransition = mExitTransition; if (exitTransition != null && decorView.isLaidOut() && (mIsAnchorRootAttached || mAnchorRoot == null)) { // The decor view is non-interactive and non-IME-focusable during exit transitions. final LayoutParams p = (LayoutParams) decorView.getLayoutParams(); p.flags |= LayoutParams.FLAG_NOT_TOUCHABLE; p.flags |= LayoutParams.FLAG_NOT_FOCUSABLE; p.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM; mWindowManager.updateViewLayout(decorView, p); final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null; final Rect epicenter = getTransitionEpicenter(); // Once we start dismissing the decor view, all state (including // the anchor root) needs to be moved to the decor view since we // may open another popup while it's busy exiting. decorView.startExitTransition(exitTransition, anchorRoot, epicenter, new TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { dismissImmediate(decorView, contentHolder, contentView); } }); } else { dismissImmediate(decorView, contentHolder, contentView); } // Clears the anchor view. detachFromAnchor(); if (mOnDismissListener != null) { mOnDismissListener.onDismiss(); } } /** * Returns the window-relative epicenter bounds to be used by enter and * exit transitions. * <p> * <strong>Note:</strong> This is distinct from the rect passed to * {@link #setEpicenterBounds(Rect)}, which is anchor-relative. * * @return the window-relative epicenter bounds to be used by enter and * exit transitions * * @hide */ protected final Rect getTransitionEpicenter() { final View anchor = mAnchor != null ? mAnchor.get() : null; final View decor = mDecorView; if (anchor == null || decor == null) { return null; } final int[] anchorLocation = anchor.getLocationOnScreen(); final int[] popupLocation = mDecorView.getLocationOnScreen(); // Compute the position of the anchor relative to the popup. final Rect bounds = new Rect(0, 0, anchor.getWidth(), anchor.getHeight()); bounds.offset(anchorLocation[0] - popupLocation[0], anchorLocation[1] - popupLocation[1]); // Use anchor-relative epicenter, if specified. if (mEpicenterBounds != null) { final int offsetX = bounds.left; final int offsetY = bounds.top; bounds.set(mEpicenterBounds); bounds.offset(offsetX, offsetY); } return bounds; } /** * Removes the popup from the window manager and tears down the supporting * view hierarchy, if necessary. */ private void dismissImmediate(View decorView, ViewGroup contentHolder, View contentView) { // If this method gets called and the decor view doesn't have a parent, // then it was either never added or was already removed. That should // never happen, but it's worth checking to avoid potential crashes. if (decorView.getParent() != null) { mWindowManager.removeViewImmediate(decorView); } if (contentHolder != null) { contentHolder.removeView(contentView); } // This needs to stay until after all transitions have ended since we // need the reference to cancel transitions in preparePopup(). mDecorView = null; mBackgroundView = null; mIsTransitioningToDismiss = false; } /** * Sets the listener to be called when the window is dismissed. * * @param onDismissListener The listener. */ public void setOnDismissListener(OnDismissListener onDismissListener) { mOnDismissListener = onDismissListener; } /** @hide */ protected final OnDismissListener getOnDismissListener() { return mOnDismissListener; } /** * Updates the state of the popup window, if it is currently being displayed, * from the currently set state. * <p> * This includes: * <ul> * <li>{@link #setClippingEnabled(boolean)}</li> * <li>{@link #setFocusable(boolean)}</li> * <li>{@link #setIgnoreCheekPress()}</li> * <li>{@link #setInputMethodMode(int)}</li> * <li>{@link #setTouchable(boolean)}</li> * <li>{@link #setAnimationStyle(int)}</li> * </ul> */ public void update() { if (!isShowing() || !hasContentView()) { return; } final WindowManager.LayoutParams p = getDecorViewLayoutParams(); boolean update = false; final int newAnim = computeAnimationResource(); if (newAnim != p.windowAnimations) { p.windowAnimations = newAnim; update = true; } final int newFlags = computeFlags(p.flags); if (newFlags != p.flags) { p.flags = newFlags; update = true; } final int newGravity = computeGravity(); if (newGravity != p.gravity) { p.gravity = newGravity; update = true; } if (update) { update(mAnchor != null ? mAnchor.get() : null, p); } } /** @hide */ protected void update(View anchor, WindowManager.LayoutParams params) { setLayoutDirectionFromAnchor(); mWindowManager.updateViewLayout(mDecorView, params); } /** * Updates the dimension of the popup window. * <p> * Calling this function also updates the window with the current popup * state as described for {@link #update()}. * * @param width the new width in pixels, must be >= 0 or -1 to ignore * @param height the new height in pixels, must be >= 0 or -1 to ignore */ public void update(int width, int height) { final WindowManager.LayoutParams p = getDecorViewLayoutParams(); update(p.x, p.y, width, height, false); } /** * Updates the position and the dimension of the popup window. * <p> * Width and height can be set to -1 to update location only. Calling this * function also updates the window with the current popup state as * described for {@link #update()}. * * @param x the new x location * @param y the new y location * @param width the new width in pixels, must be >= 0 or -1 to ignore * @param height the new height in pixels, must be >= 0 or -1 to ignore */ public void update(int x, int y, int width, int height) { update(x, y, width, height, false); } /** * Updates the position and the dimension of the popup window. * <p> * Width and height can be set to -1 to update location only. Calling this * function also updates the window with the current popup state as * described for {@link #update()}. * * @param x the new x location * @param y the new y location * @param width the new width in pixels, must be >= 0 or -1 to ignore * @param height the new height in pixels, must be >= 0 or -1 to ignore * @param force {@code true} to reposition the window even if the specified * position already seems to correspond to the LayoutParams, * {@code false} to only reposition if needed */ public void update(int x, int y, int width, int height, boolean force) { if (width >= 0) { mLastWidth = width; setWidth(width); } if (height >= 0) { mLastHeight = height; setHeight(height); } if (!isShowing() || !hasContentView()) { return; } final WindowManager.LayoutParams p = getDecorViewLayoutParams(); boolean update = force; final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth; if (width != -1 && p.width != finalWidth) { p.width = mLastWidth = finalWidth; update = true; } final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight; if (height != -1 && p.height != finalHeight) { p.height = mLastHeight = finalHeight; update = true; } if (p.x != x) { p.x = x; update = true; } if (p.y != y) { p.y = y; update = true; } final int newAnim = computeAnimationResource(); if (newAnim != p.windowAnimations) { p.windowAnimations = newAnim; update = true; } final int newFlags = computeFlags(p.flags); if (newFlags != p.flags) { p.flags = newFlags; update = true; } final int newGravity = computeGravity(); if (newGravity != p.gravity) { p.gravity = newGravity; update = true; } View anchor = null; int newAccessibilityIdOfAnchor = -1; if (mAnchor != null && mAnchor.get() != null) { anchor = mAnchor.get(); newAccessibilityIdOfAnchor = anchor.getAccessibilityViewId(); } if (newAccessibilityIdOfAnchor != p.accessibilityIdOfAnchor) { p.accessibilityIdOfAnchor = newAccessibilityIdOfAnchor; update = true; } if (update) { update(anchor, p); } } /** @hide */ protected boolean hasContentView() { return mContentView != null; } /** @hide */ protected boolean hasDecorView() { return mDecorView != null; } /** @hide */ protected WindowManager.LayoutParams getDecorViewLayoutParams() { return (WindowManager.LayoutParams) mDecorView.getLayoutParams(); } /** * Updates the position and the dimension of the popup window. * <p> * Calling this function also updates the window with the current popup * state as described for {@link #update()}. * * @param anchor the popup's anchor view * @param width the new width in pixels, must be >= 0 or -1 to ignore * @param height the new height in pixels, must be >= 0 or -1 to ignore */ public void update(View anchor, int width, int height) { update(anchor, false, 0, 0, width, height); } /** * Updates the position and the dimension of the popup window. * <p> * Width and height can be set to -1 to update location only. Calling this * function also updates the window with the current popup state as * described for {@link #update()}. * <p> * If the view later scrolls to move {@code anchor} to a different * location, the popup will be moved correspondingly. * * @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 in pixels, must be >= 0 or -1 to ignore * @param height the new height in pixels, must be >= 0 or -1 to ignore */ public void update(View anchor, int xoff, int yoff, int width, int height) { update(anchor, true, xoff, yoff, width, height); } private void update(View anchor, boolean updateLocation, int xoff, int yoff, int width, int height) { if (!isShowing() || !hasContentView()) { return; } final WeakReference<View> oldAnchor = mAnchor; final int gravity = mAnchoredGravity; final boolean needsUpdate = updateLocation && (mAnchorXoff != xoff || mAnchorYoff != yoff); if (oldAnchor == null || oldAnchor.get() != anchor || (needsUpdate && !mIsDropdown)) { attachToAnchor(anchor, xoff, yoff, gravity); } else if (needsUpdate) { // No need to register again if this is a DropDown, showAsDropDown already did. mAnchorXoff = xoff; mAnchorYoff = yoff; } final WindowManager.LayoutParams p = getDecorViewLayoutParams(); final int oldGravity = p.gravity; final int oldWidth = p.width; final int oldHeight = p.height; final int oldX = p.x; final int oldY = p.y; // If an explicit width/height has not specified, use the most recent // explicitly specified value (either from setWidth/Height or update). if (width < 0) { width = mWidth; } if (height < 0) { height = mHeight; } final boolean aboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff, width, height, gravity, mAllowScrollingAnchorParent); updateAboveAnchor(aboveAnchor); final boolean paramsChanged = oldGravity != p.gravity || oldX != p.x || oldY != p.y || oldWidth != p.width || oldHeight != p.height; // If width and mWidth were both < 0 then we have a MATCH_PARENT or // WRAP_CONTENT case. findDropDownPosition will have resolved this to // absolute values, but we don't want to update mWidth/mHeight to these // absolute values. final int newWidth = width < 0 ? width : p.width; final int newHeight = height < 0 ? height : p.height; update(p.x, p.y, newWidth, newHeight, paramsChanged); } /** * Listener that is called when this popup window is dismissed. */ public interface OnDismissListener { /** * Called when this popup window is dismissed. */ public void onDismiss(); } /** @hide */ protected void detachFromAnchor() { final View anchor = getAnchor(); if (anchor != null) { final ViewTreeObserver vto = anchor.getViewTreeObserver(); vto.removeOnScrollChangedListener(mOnScrollChangedListener); anchor.removeOnAttachStateChangeListener(mOnAnchorDetachedListener); } final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null; if (anchorRoot != null) { anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener); anchorRoot.removeOnLayoutChangeListener(mOnLayoutChangeListener); } mAnchor = null; mAnchorRoot = null; mIsAnchorRootAttached = false; } /** @hide */ protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) { detachFromAnchor(); final ViewTreeObserver vto = anchor.getViewTreeObserver(); if (vto != null) { vto.addOnScrollChangedListener(mOnScrollChangedListener); } anchor.addOnAttachStateChangeListener(mOnAnchorDetachedListener); final View anchorRoot = anchor.getRootView(); anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener); anchorRoot.addOnLayoutChangeListener(mOnLayoutChangeListener); mAnchor = new WeakReference<>(anchor); mAnchorRoot = new WeakReference<>(anchorRoot); mIsAnchorRootAttached = anchorRoot.isAttachedToWindow(); mParentRootView = mAnchorRoot; mAnchorXoff = xoff; mAnchorYoff = yoff; mAnchoredGravity = gravity; } /** @hide */ protected @Nullable View getAnchor() { return mAnchor != null ? mAnchor.get() : null; } private void alignToAnchor() { final View anchor = mAnchor != null ? mAnchor.get() : null; if (anchor != null && anchor.isAttachedToWindow() && hasDecorView()) { final WindowManager.LayoutParams p = getDecorViewLayoutParams(); updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff, p.width, p.height, mAnchoredGravity, false)); update(p.x, p.y, -1, -1, true); } } private View getAppRootView(View anchor) { final View appWindowView = WindowManagerGlobal.getInstance() .getWindowView(anchor.getApplicationWindowToken()); if (appWindowView != null) { return appWindowView; } return anchor.getRootView(); } private class PopupDecorView extends FrameLayout { /** Runnable used to clean up listeners after exit transition. */ private Runnable mCleanupAfterExit; public PopupDecorView(Context context) { super(context); } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (getKeyDispatcherState() == null) { return super.dispatchKeyEvent(event); } if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { final KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null) { state.startTracking(event, this); } return true; } else if (event.getAction() == KeyEvent.ACTION_UP) { final KeyEvent.DispatcherState state = getKeyDispatcherState(); if (state != null && state.isTracking(event) && !event.isCanceled()) { dismiss(); return true; } } return super.dispatchKeyEvent(event); } else { return super.dispatchKeyEvent(event); } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) { return true; } return super.dispatchTouchEvent(ev); } @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 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { dismiss(); return true; } else { return super.onTouchEvent(event); } } /** * Requests that an enter transition run after the next layout pass. */ public void requestEnterTransition(Transition transition) { final ViewTreeObserver observer = getViewTreeObserver(); if (observer != null && transition != null) { final Transition enterTransition = transition.clone(); // Postpone the enter transition after the first layout pass. observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { final ViewTreeObserver observer = getViewTreeObserver(); if (observer != null) { observer.removeOnGlobalLayoutListener(this); } final Rect epicenter = getTransitionEpicenter(); enterTransition.setEpicenterCallback(new EpicenterCallback() { @Override public Rect onGetEpicenter(Transition transition) { return epicenter; } }); startEnterTransition(enterTransition); } }); } } /** * Starts the pending enter transition, if one is set. */ private void startEnterTransition(Transition enterTransition) { final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); enterTransition.addTarget(child); child.setTransitionVisibility(View.INVISIBLE); } TransitionManager.beginDelayedTransition(this, enterTransition); for (int i = 0; i < count; i++) { final View child = getChildAt(i); child.setTransitionVisibility(View.VISIBLE); } } /** * Starts an exit transition immediately. * <p> * <strong>Note:</strong> The transition listener is guaranteed to have * its {@code onTransitionEnd} method called even if the transition * never starts. */ public void startExitTransition(@NonNull Transition transition, @Nullable final View anchorRoot, @Nullable final Rect epicenter, @NonNull final TransitionListener listener) { if (transition == null) { return; } // The anchor view's window may go away while we're executing our // transition, in which case we need to end the transition // immediately and execute the listener to remove the popup. if (anchorRoot != null) { anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener); } // The cleanup runnable MUST be called even if the transition is // canceled before it starts (and thus can't call onTransitionEnd). mCleanupAfterExit = () -> { listener.onTransitionEnd(transition); if (anchorRoot != null) { anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener); } // The listener was called. Our job here is done. mCleanupAfterExit = null; }; final Transition exitTransition = transition.clone(); exitTransition.addListener(new TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition t) { t.removeListener(this); // This null check shouldn't be necessary, but it's easier // to check here than it is to test every possible case. if (mCleanupAfterExit != null) { mCleanupAfterExit.run(); } } }); exitTransition.setEpicenterCallback(new EpicenterCallback() { @Override public Rect onGetEpicenter(Transition transition) { return epicenter; } }); final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); exitTransition.addTarget(child); } TransitionManager.beginDelayedTransition(this, exitTransition); for (int i = 0; i < count; i++) { final View child = getChildAt(i); child.setVisibility(View.INVISIBLE); } } /** * Cancels all pending or current transitions. */ public void cancelTransitions() { TransitionManager.endTransitions(this); // If the cleanup runnable is still around, that means the // transition never started. We should run it now to clean up. if (mCleanupAfterExit != null) { mCleanupAfterExit.run(); } } private final OnAttachStateChangeListener mOnAnchorRootDetachedListener = new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { v.removeOnAttachStateChangeListener(this); if (isAttachedToWindow()) { TransitionManager.endTransitions(PopupDecorView.this); } } }; @Override public void requestKeyboardShortcuts(List<KeyboardShortcutGroup> list, int deviceId) { if (mParentRootView != null) { View parentRoot = mParentRootView.get(); if (parentRoot != null) { parentRoot.requestKeyboardShortcuts(list, deviceId); } } } } private class PopupBackgroundView extends FrameLayout { public PopupBackgroundView(Context context) { super(context); } @Override protected int[] onCreateDrawableState(int extraSpace) { if (mAboveAnchor) { final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET); return drawableState; } else { return super.onCreateDrawableState(extraSpace); } } } }