Merge "Add transition support to PopupWindow"
diff --git a/api/current.txt b/api/current.txt
index dd2549d..47fa5d1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -38453,6 +38453,8 @@
method public void setClippingEnabled(boolean);
method public void setContentView(android.view.View);
method public void setElevation(float);
+ method public void setEnterTransition(android.transition.Transition);
+ method public void setExitTransition(android.transition.Transition);
method public void setFocusable(boolean);
method public void setHeight(int);
method public void setIgnoreCheekPress();
diff --git a/api/system-current.txt b/api/system-current.txt
index 8d503d6..4eb00f4 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -40948,6 +40948,8 @@
method public void setClippingEnabled(boolean);
method public void setContentView(android.view.View);
method public void setElevation(float);
+ method public void setEnterTransition(android.transition.Transition);
+ method public void setExitTransition(android.transition.Transition);
method public void setFocusable(boolean);
method public void setHeight(int);
method public void setIgnoreCheekPress();
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 259367e..19c9271 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -14885,10 +14885,9 @@
void setDisplayListProperties(RenderNode renderNode) {
if (renderNode != null) {
renderNode.setHasOverlappingRendering(hasOverlappingRendering());
- if (mParent instanceof ViewGroup) {
- renderNode.setClipToBounds(
- (((ViewGroup) mParent).mGroupFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0);
- }
+ renderNode.setClipToBounds(mParent instanceof ViewGroup
+ && ((ViewGroup) mParent).getClipChildren());
+
float alpha = 1;
if (mParent instanceof ViewGroup && (((ViewGroup) mParent).mGroupFlags &
ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 151ff83..15e7060 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -472,8 +472,10 @@
// Compute surface insets required to draw at specified Z value.
// TODO: Use real shadow insets for a constant max Z.
- final int surfaceInset = (int) Math.ceil(view.getZ() * 2);
- attrs.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset);
+ if (!attrs.hasManualSurfaceInsets) {
+ final int surfaceInset = (int) Math.ceil(view.getZ() * 2);
+ attrs.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset);
+ }
CompatibilityInfo compatibilityInfo = mDisplayAdjustments.getCompatibilityInfo();
mTranslator = compatibilityInfo.getTranslator();
@@ -760,6 +762,7 @@
final int oldInsetRight = mWindowAttributes.surfaceInsets.right;
final int oldInsetBottom = mWindowAttributes.surfaceInsets.bottom;
final int oldSoftInputMode = mWindowAttributes.softInputMode;
+ final boolean oldHasManualSurfaceInsets = mWindowAttributes.hasManualSurfaceInsets;
// Keep track of the actual window flags supplied by the client.
mClientWindowLayoutFlags = attrs.flags;
@@ -786,6 +789,7 @@
// Restore old surface insets.
mWindowAttributes.surfaceInsets.set(
oldInsetLeft, oldInsetTop, oldInsetRight, oldInsetBottom);
+ mWindowAttributes.hasManualSurfaceInsets = oldHasManualSurfaceInsets;
applyKeepScreenOnFlag(mWindowAttributes);
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 12b310f..740cb5d 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1325,6 +1325,16 @@
* @hide
*/
public final Rect surfaceInsets = new Rect();
+
+ /**
+ * Whether the surface insets have been manually set. When set to
+ * {@code false}, the view root will automatically determine the
+ * appropriate surface insets.
+ *
+ * @see #surfaceInsets
+ * @hide
+ */
+ public boolean hasManualSurfaceInsets;
/**
* The desired bitmap format. May be one of the constants in
@@ -1621,6 +1631,7 @@
out.writeInt(surfaceInsets.top);
out.writeInt(surfaceInsets.right);
out.writeInt(surfaceInsets.bottom);
+ out.writeInt(hasManualSurfaceInsets ? 1 : 0);
out.writeInt(needsMenuKey);
}
@@ -1669,6 +1680,7 @@
surfaceInsets.top = in.readInt();
surfaceInsets.right = in.readInt();
surfaceInsets.bottom = in.readInt();
+ hasManualSurfaceInsets = in.readInt() != 0;
needsMenuKey = in.readInt();
}
@@ -1851,6 +1863,11 @@
changes |= SURFACE_INSETS_CHANGED;
}
+ if (hasManualSurfaceInsets != o.hasManualSurfaceInsets) {
+ hasManualSurfaceInsets = o.hasManualSurfaceInsets;
+ changes |= SURFACE_INSETS_CHANGED;
+ }
+
if (needsMenuKey != o.needsMenuKey) {
needsMenuKey = o.needsMenuKey;
changes |= NEEDS_MENU_KEY_CHANGED;
@@ -1959,8 +1976,11 @@
if (userActivityTimeout >= 0) {
sb.append(" userActivityTimeout=").append(userActivityTimeout);
}
- if (!surfaceInsets.equals(Insets.NONE)) {
+ if (!surfaceInsets.equals(Insets.NONE) || hasManualSurfaceInsets) {
sb.append(" surfaceInsets=").append(surfaceInsets);
+ if (hasManualSurfaceInsets) {
+ sb.append(" (manual)");
+ }
}
if (needsMenuKey != NEEDS_MENU_UNSET) {
sb.append(" needsMenuKey=");
diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java
index f5cd915..7cf3eed 100644
--- a/core/java/android/widget/PopupWindow.java
+++ b/core/java/android/widget/PopupWindow.java
@@ -27,6 +27,11 @@
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.TransitionInflater;
+import android.transition.TransitionManager;
+import android.transition.TransitionSet;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -39,12 +44,13 @@
import android.view.WindowManager;
import java.lang.ref.WeakReference;
+import java.util.List;
/**
* <p>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>
- *
+ *
* @see android.widget.AutoCompleteTextView
* @see android.widget.Spinner
*/
@@ -56,7 +62,7 @@
* 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
@@ -64,7 +70,7 @@
* 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
@@ -75,14 +81,32 @@
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[] mDrawingLocation = new int[2];
+ private final int[] mScreenLocation = new int[2];
+ private final Rect mTempRect = new Rect();
+ private final Rect mAnchorBounds = new Rect();
+
private Context mContext;
private WindowManager mWindowManager;
-
+
private boolean mIsShowing;
private boolean mIsDropdown;
+ /** View that handles event dispatch and content transitions. */
+ private PopupDecorView mDecorView;
+
+ /** View that holds the popup background. May be the content view. */
+ private View mBackgroundView;
+
+ /** The contents of the popup. */
private View mContentView;
- private View mPopupView;
+
private boolean mFocusable;
private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE;
private int mSoftInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
@@ -112,49 +136,52 @@
private float mElevation;
- private int[] mDrawingLocation = new int[2];
- private int[] mScreenLocation = new int[2];
- private Rect mTempRect = new Rect();
-
private Drawable mBackground;
private Drawable mAboveAnchorBackgroundDrawable;
private Drawable mBelowAnchorBackgroundDrawable;
- // Temporary animation centers. Should be moved into window params?
- private int mAnchorRelativeX;
- private int mAnchorRelativeY;
+ private Transition mEnterTransition;
+ private Transition mExitTransition;
private boolean mAboveAnchor;
private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
-
+
private OnDismissListener mOnDismissListener;
private boolean mIgnoreCheekPress = false;
- private int mAnimationStyle = -1;
-
+ private int mAnimationStyle = ANIMATION_STYLE_DEFAULT;
+
private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] {
com.android.internal.R.attr.state_above_anchor
};
private WeakReference<View> mAnchor;
- private final OnScrollChangedListener mOnScrollChangedListener =
- new OnScrollChangedListener() {
- @Override
- public void onScrollChanged() {
- final View anchor = mAnchor != null ? mAnchor.get() : null;
- if (anchor != null && mPopupView != null) {
- final WindowManager.LayoutParams p = (WindowManager.LayoutParams)
- mPopupView.getLayoutParams();
+ private final EpicenterCallback mEpicenterCallback = new EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return mAnchorBounds;
+ }
+ };
- updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
- mAnchoredGravity));
- update(p.x, p.y, -1, -1, true);
- }
+ private final OnScrollChangedListener mOnScrollChangedListener = new OnScrollChangedListener() {
+ @Override
+ public void onScrollChanged() {
+ final View anchor = mAnchor != null ? mAnchor.get() : null;
+ if (anchor != null && mDecorView != null) {
+ final WindowManager.LayoutParams p = (WindowManager.LayoutParams)
+ mDecorView.getLayoutParams();
+
+ updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
+ mAnchoredGravity));
+ update(p.x, p.y, -1, -1, true);
}
- };
+ }
+ };
- private int mAnchorXoff, mAnchorYoff, mAnchoredGravity;
+ private int mAnchorXoff;
+ private int mAnchorYoff;
+ private int mAnchoredGravity;
private boolean mOverlapAnchor;
private boolean mPopupViewInitialLayoutDirectionInherited;
@@ -185,10 +212,10 @@
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) {
@@ -201,11 +228,34 @@
mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0);
mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false);
- final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, -1);
- mAnimationStyle = animStyle == R.style.Animation_PopupWindow ? -1 : animStyle;
+ // 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);
}
@@ -286,6 +336,37 @@
setFocusable(focusable);
}
+ public void setEnterTransition(Transition enterTransition) {
+ mEnterTransition = enterTransition;
+
+ if (mEnterTransition != null) {
+ mEnterTransition.setEpicenterCallback(mEpicenterCallback);
+ }
+ }
+
+ public void setExitTransition(Transition exitTransition) {
+ mExitTransition = exitTransition;
+
+ if (mExitTransition != null) {
+ mExitTransition.setEpicenterCallback(mEpicenterCallback);
+ }
+ }
+
+ 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.
*
@@ -379,7 +460,7 @@
* 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>
@@ -389,7 +470,7 @@
public void setIgnoreCheekPress() {
mIgnoreCheekPress = true;
}
-
+
/**
* <p>Change the animation style resource for this popup.</p>
@@ -401,13 +482,13 @@
* @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>
*
@@ -491,7 +572,7 @@
* @param focusable true if the popup should grab focus, false otherwise.
*
* @see #isFocusable()
- * @see #isShowing()
+ * @see #isShowing()
* @see #update()
*/
public void setFocusable(boolean focusable) {
@@ -500,23 +581,23 @@
/**
* 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()
*/
@@ -547,12 +628,12 @@
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() {
@@ -571,7 +652,7 @@
* @param touchable true if the popup should receive touch events, false otherwise
*
* @see #isTouchable()
- * @see #isShowing()
+ * @see #isShowing()
* @see #update()
*/
public void setTouchable(boolean touchable) {
@@ -581,9 +662,9 @@
/**
* <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() {
@@ -604,7 +685,7 @@
* touch events, false otherwise
*
* @see #isOutsideTouchable()
- * @see #isShowing()
+ * @see #isShowing()
* @see #update()
*/
public void setOutsideTouchable(boolean touchable) {
@@ -613,9 +694,9 @@
/**
* <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() {
@@ -626,13 +707,13 @@
* <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 #isShowing()
* @see #isClippingEnabled()
* @see #update()
*/
@@ -660,12 +741,12 @@
void setAllowScrollingAnchorParent(boolean enabled) {
mAllowScrollingAnchorParent = enabled;
}
-
+
/**
* <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() {
@@ -794,7 +875,7 @@
* 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#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>
@@ -815,7 +896,7 @@
mWidthMode = widthSpec;
mHeightMode = heightSpec;
}
-
+
/**
* <p>Return this popup's height MeasureSpec</p>
*
@@ -836,7 +917,7 @@
* @param height the height MeasureSpec of the popup
*
* @see #getHeight()
- * @see #isShowing()
+ * @see #isShowing()
*/
public void setHeight(int height) {
mHeight = height;
@@ -847,7 +928,7 @@
*
* @return the width MeasureSpec of the popup
*
- * @see #setWidth(int)
+ * @see #setWidth(int)
*/
public int getWidth() {
return mWidth;
@@ -913,7 +994,7 @@
* 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
@@ -946,7 +1027,7 @@
WindowManager.LayoutParams p = createPopupLayout(token);
p.windowAnimations = computeAnimationResource();
-
+
preparePopup(p);
if (gravity == Gravity.NO_GRAVITY) {
gravity = Gravity.TOP | Gravity.START;
@@ -1049,12 +1130,12 @@
// do the job.
if (mAboveAnchorBackgroundDrawable != null) {
if (mAboveAnchor) {
- mPopupView.setBackground(mAboveAnchorBackgroundDrawable);
+ mDecorView.setBackground(mAboveAnchorBackgroundDrawable);
} else {
- mPopupView.setBackground(mBelowAnchorBackgroundDrawable);
+ mDecorView.setBackground(mBelowAnchorBackgroundDrawable);
}
} else {
- mPopupView.refreshDrawableState();
+ mDecorView.refreshDrawableState();
}
}
}
@@ -1089,36 +1170,79 @@
+ "calling setContentView() before attempting to show the popup.");
}
+ // When a background is available, we embed the content view within
+ // another view that owns the background drawable.
if (mBackground != null) {
- final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
- int height = ViewGroup.LayoutParams.MATCH_PARENT;
- if (layoutParams != null &&
- layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
- height = ViewGroup.LayoutParams.WRAP_CONTENT;
- }
-
- // 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.MATCH_PARENT, height
- );
- popupViewContainer.setBackground(mBackground);
- popupViewContainer.addView(mContentView, listParams);
-
- mPopupView = popupViewContainer;
+ mBackgroundView = createBackgroundView(mContentView);
+ mBackgroundView.setBackground(mBackground);
} else {
- mPopupView = mContentView;
+ mBackgroundView = mContentView;
}
- mPopupView.setElevation(mElevation);
+ mDecorView = createDecorView(mBackgroundView);
+
+ // 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.
+ final int surfaceInset = (int) Math.ceil(mBackgroundView.getZ() * 2);
+ p.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset);
+ p.hasManualSurfaceInsets = true;
+
mPopupViewInitialLayoutDirectionInherited =
- (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
+ (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
mPopupWidth = p.width;
mPopupHeight = p.height;
}
/**
+ * 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 == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ } else {
+ height = ViewGroup.LayoutParams.MATCH_PARENT;
+ }
+
+ final PopupBackgroundView backgroundView = new PopupBackgroundView(mContext);
+ final PopupBackgroundView.LayoutParams listParams = new PopupBackgroundView.LayoutParams(
+ ViewGroup.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 == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ } else {
+ height = ViewGroup.LayoutParams.MATCH_PARENT;
+ }
+
+ final PopupDecorView decorView = new PopupDecorView(mContext);
+ decorView.addView(contentView, ViewGroup.LayoutParams.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>
*
@@ -1130,16 +1254,34 @@
if (mContext != null) {
p.packageName = mContext.getPackageName();
}
- mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
+
+ final View rootView = mContentView.getRootView();
+ rootView.setFitsSystemWindows(mLayoutInsetDecor);
setLayoutDirectionFromAnchor();
- mWindowManager.addView(mPopupView, p);
+
+ mWindowManager.addView(rootView, p);
+
+ // Postpone enter transition until the scene root has been laid out.
+ if (mEnterTransition != null) {
+ mEnterTransition.addTarget(mBackgroundView);
+ mEnterTransition.addListener(new Transition.TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ transition.removeTarget(mBackgroundView);
+ }
+ });
+
+ mDecorView.getViewTreeObserver().addOnGlobalLayoutListener(
+ new PostLayoutTransitionListener(mDecorView, mEnterTransition));
+ }
}
private void setLayoutDirectionFromAnchor() {
if (mAnchor != null) {
View anchor = mAnchor.get();
if (anchor != null && mPopupViewInitialLayoutDirectionInherited) {
- mPopupView.setLayoutDirection(anchor.getLayoutDirection());
+ mDecorView.setLayoutDirection(anchor.getLayoutDirection());
}
}
}
@@ -1224,7 +1366,7 @@
}
private int computeAnimationResource() {
- if (mAnimationStyle == -1) {
+ if (mAnimationStyle == ANIMATION_STYLE_DEFAULT) {
if (mIsDropdown) {
return mAboveAnchor
? com.android.internal.R.style.Animation_DropDownUp
@@ -1243,7 +1385,7 @@
* <p>
* The height must have been set on the layout parameters prior to calling
* this method.
- *
+ *
* @param anchor the view on which the popup window must be anchored
* @param p the layout parameters used to display the drop down
* @param xoff horizontal offset used to adjust for background padding
@@ -1342,18 +1484,18 @@
p.gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
// Compute the position of the anchor relative to the popup.
- mAnchorRelativeX = mDrawingLocation[0] - p.x + anchorHeight / 2;
- mAnchorRelativeY = mDrawingLocation[1] - p.y + anchorWidth / 2;
+ mAnchorBounds.set(0, 0, anchorWidth, anchorHeight);
+ mAnchorBounds.offset(mDrawingLocation[0] - p.x, mDrawingLocation[1] - p.y);
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.
@@ -1376,14 +1518,14 @@
public int getMaxAvailableHeight(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
@@ -1391,7 +1533,7 @@
* bottom decorations
* @return The maximum available height for the popup to be completely
* shown.
- *
+ *
* @hide Pending API council approval.
*/
public int getMaxAvailableHeight(View anchor, int yOffset, boolean ignoreBottomDecorations) {
@@ -1400,7 +1542,7 @@
final int[] anchorPos = mDrawingLocation;
anchor.getLocationOnScreen(anchorPos);
-
+
int bottomEdge = displayFrame.bottom;
if (ignoreBottomDecorations) {
Resources res = anchor.getContext().getResources();
@@ -1413,49 +1555,78 @@
int returnedHeight = Math.max(distanceToBottom, distanceToTop);
if (mBackground != null) {
mBackground.getPadding(mTempRect);
- returnedHeight -= mTempRect.top + mTempRect.bottom;
+ 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)
+ * @see #showAsDropDown(android.view.View)
*/
public void dismiss() {
- if (isShowing() && mPopupView != null) {
+ if (isShowing() && mDecorView != null) {
mIsShowing = false;
unregisterForScrollChanged();
- try {
- mWindowManager.removeViewImmediate(mPopupView);
- } finally {
- if (mPopupView != mContentView && mPopupView instanceof ViewGroup) {
- ((ViewGroup) mPopupView).removeView(mContentView);
- }
- mPopupView = null;
+ if (mExitTransition != null) {
+ mExitTransition.addTarget(mBackgroundView);
+ mExitTransition.addListener(new Transition.TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ transition.removeListener(this);
+ transition.removeTarget(mBackgroundView);
- if (mOnDismissListener != null) {
- mOnDismissListener.onDismiss();
- }
+ dismissImmediate();
+ }
+ });
+
+ TransitionManager.beginDelayedTransition(mDecorView, mExitTransition);
+
+ // Transition to invisible.
+ mBackgroundView.setVisibility(View.INVISIBLE);
+ } else {
+ dismissImmediate();
+ }
+ }
+ }
+
+ /**
+ * Removes the popup from the window manager and tears down the supporting
+ * view hierarchy, if necessary.
+ */
+ private void dismissImmediate() {
+ try {
+ mWindowManager.removeViewImmediate(mDecorView);
+ } finally {
+ mDecorView.removeView(mBackgroundView);
+ mDecorView = null;
+
+ if (mBackgroundView != mContentView) {
+ ((ViewGroup) mBackgroundView).removeView(mContentView);
+ }
+ mBackgroundView = null;
+
+ 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;
}
-
+
/**
* Updates the state of the popup window, if it is currently being displayed,
* from the currently set state. This includes:
@@ -1467,12 +1638,12 @@
if (!isShowing() || mContentView == null) {
return;
}
-
- WindowManager.LayoutParams p = (WindowManager.LayoutParams)
- mPopupView.getLayoutParams();
-
+
+ final WindowManager.LayoutParams p =
+ (WindowManager.LayoutParams) mDecorView.getLayoutParams();
+
boolean update = false;
-
+
final int newAnim = computeAnimationResource();
if (newAnim != p.windowAnimations) {
p.windowAnimations = newAnim;
@@ -1487,7 +1658,7 @@
if (update) {
setLayoutDirectionFromAnchor();
- mWindowManager.updateViewLayout(mPopupView, p);
+ mWindowManager.updateViewLayout(mDecorView, p);
}
}
@@ -1500,11 +1671,11 @@
* @param height the new height
*/
public void update(int width, int height) {
- WindowManager.LayoutParams p = (WindowManager.LayoutParams)
- mPopupView.getLayoutParams();
+ final WindowManager.LayoutParams p =
+ (WindowManager.LayoutParams) mDecorView.getLayoutParams();
update(p.x, p.y, width, height, false);
}
-
+
/**
* <p>Updates the position and the dimension of the popup window. Width and
* height can be set to -1 to update location only. Calling this function
@@ -1548,7 +1719,8 @@
return;
}
- WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams();
+ final WindowManager.LayoutParams p =
+ (WindowManager.LayoutParams) mDecorView.getLayoutParams();
boolean update = force;
@@ -1588,7 +1760,7 @@
if (update) {
setLayoutDirectionFromAnchor();
- mWindowManager.updateViewLayout(mPopupView, p);
+ mWindowManager.updateViewLayout(mDecorView, p);
}
}
@@ -1655,7 +1827,7 @@
}
final WindowManager.LayoutParams p =
- (WindowManager.LayoutParams) mPopupView.getLayoutParams();
+ (WindowManager.LayoutParams) mDecorView.getLayoutParams();
final int x = p.x;
final int y = p.y;
if (updateLocation) {
@@ -1694,7 +1866,7 @@
private void registerForScrollChanged(View anchor, int xoff, int yoff, int gravity) {
unregisterForScrollChanged();
- mAnchor = new WeakReference<View>(anchor);
+ mAnchor = new WeakReference<>(anchor);
ViewTreeObserver vto = anchor.getViewTreeObserver();
if (vto != null) {
vto.addOnScrollChangedListener(mOnScrollChangedListener);
@@ -1705,23 +1877,49 @@
mAnchoredGravity = gravity;
}
- private class PopupViewContainer extends FrameLayout {
- private static final String TAG = "PopupWindow.PopupViewContainer";
+ /**
+ * Layout listener used to run a transition immediately after a view is
+ * laid out. Forces the view to transition from invisible to visible.
+ */
+ private static class PostLayoutTransitionListener implements
+ ViewTreeObserver.OnGlobalLayoutListener {
+ private final ViewGroup mSceneRoot;
+ private final Transition mTransition;
- public PopupViewContainer(Context context) {
- super(context);
+ public PostLayoutTransitionListener(ViewGroup sceneRoot, Transition transition) {
+ mSceneRoot = sceneRoot;
+ mTransition = transition;
}
@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);
+ public void onGlobalLayout() {
+ final ViewTreeObserver observer = mSceneRoot.getViewTreeObserver();
+ if (observer == null) {
+ // View has been detached.
+ return;
}
+
+ observer.removeOnGlobalLayoutListener(this);
+
+ // Set all targets to be initially invisible.
+ final List<View> targets = mTransition.getTargets();
+ final int N = targets.size();
+ for (int i = 0; i < N; i++) {
+ targets.get(i).setVisibility(View.INVISIBLE);
+ }
+
+ TransitionManager.beginDelayedTransition(mSceneRoot, mTransition);
+
+ // Transition targets to visible.
+ for (int i = 0; i < N; i++) {
+ targets.get(i).setVisibility(View.VISIBLE);
+ }
+ }
+ }
+
+ private class PopupDecorView extends FrameLayout {
+ public PopupDecorView(Context context) {
+ super(context);
}
@Override
@@ -1731,15 +1929,14 @@
return super.dispatchKeyEvent(event);
}
- if (event.getAction() == KeyEvent.ACTION_DOWN
- && event.getRepeatCount() == 0) {
- KeyEvent.DispatcherState state = getKeyDispatcherState();
+ 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) {
- KeyEvent.DispatcherState state = getKeyDispatcherState();
+ final KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null && state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
@@ -1763,7 +1960,7 @@
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();
@@ -1775,17 +1972,22 @@
return super.onTouchEvent(event);
}
}
+ }
- /** @hide */
+ private class PopupBackgroundView extends FrameLayout {
+ public PopupBackgroundView(Context context) {
+ super(context);
+ }
+
@Override
- public void sendAccessibilityEventInternal(int eventType) {
- // clinets are interested in the content not the container, make it event source
- if (mContentView != null) {
- mContentView.sendAccessibilityEvent(eventType);
+ protected int[] onCreateDrawableState(int extraSpace) {
+ if (mAboveAnchor) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
+ return drawableState;
} else {
- super.sendAccessibilityEventInternal(eventType);
+ return super.onCreateDrawableState(extraSpace);
}
}
}
-
}
diff --git a/core/java/com/android/internal/transition/EpicenterClipReveal.java b/core/java/com/android/internal/transition/EpicenterClipReveal.java
new file mode 100644
index 0000000..d8a7f16
--- /dev/null
+++ b/core/java/com/android/internal/transition/EpicenterClipReveal.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2015 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 com.android.internal.transition;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.RectEvaluator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.transition.TransitionValues;
+import android.transition.Visibility;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * EpicenterClipReveal captures the {@link View#getClipBounds()} before and
+ * after the scene change and animates between those and the epicenter bounds
+ * during a visibility transition.
+ */
+public class EpicenterClipReveal extends Visibility {
+ private static final String PROPNAME_CLIP = "android:epicenterReveal:clip";
+ private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds";
+
+ public EpicenterClipReveal() {}
+
+ public EpicenterClipReveal(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void captureStartValues(TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ captureValues(transitionValues);
+ }
+
+ @Override
+ public void captureEndValues(TransitionValues transitionValues) {
+ super.captureEndValues(transitionValues);
+ captureValues(transitionValues);
+ }
+
+ private void captureValues(TransitionValues values) {
+ final View view = values.view;
+ if (view.getVisibility() == View.GONE) {
+ return;
+ }
+
+ final Rect clip = view.getClipBounds();
+ values.values.put(PROPNAME_CLIP, clip);
+
+ if (clip == null) {
+ final Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight());
+ values.values.put(PROPNAME_BOUNDS, bounds);
+ }
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (endValues == null) {
+ return null;
+ }
+
+ final Rect start = getEpicenter();
+ final Rect end = getBestRect(endValues);
+
+ // Prepare the view.
+ view.setClipBounds(start);
+
+ return createRectAnimator(view, start, end);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (startValues == null) {
+ return null;
+ }
+
+ final Rect start = getBestRect(startValues);
+ final Rect end = getEpicenter();
+
+ // Prepare the view.
+ view.setClipBounds(start);
+
+ return createRectAnimator(view, start, end);
+ }
+
+ private Rect getBestRect(TransitionValues values) {
+ final Rect clipRect = (Rect) values.values.get(PROPNAME_CLIP);
+ if (clipRect == null) {
+ return (Rect) values.values.get(PROPNAME_BOUNDS);
+ }
+ return clipRect;
+ }
+
+ private Animator createRectAnimator(View view, Rect start, Rect end) {
+ final RectEvaluator evaluator = new RectEvaluator(new Rect());
+ return ObjectAnimator.ofObject(view, "clipBounds", evaluator, start, end);
+ }
+}
diff --git a/core/res/res/transition/popup_window_enter.xml b/core/res/res/transition/popup_window_enter.xml
new file mode 100644
index 0000000..92d4c1e
--- /dev/null
+++ b/core/res/res/transition/popup_window_enter.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together">
+ <transition class="com.android.internal.transition.EpicenterClipReveal"
+ android:interpolator="@android:interpolator/accelerate_cubic"
+ android:startDelay="50"
+ android:duration="300" />
+ <fade
+ android:interpolator="@android:interpolator/linear"
+ android:duration="100" />
+</transitionSet>
diff --git a/core/res/res/transition/popup_window_exit.xml b/core/res/res/transition/popup_window_exit.xml
new file mode 100644
index 0000000..5cb9f0b
--- /dev/null
+++ b/core/res/res/transition/popup_window_exit.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<fade xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/linear" />
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 39c42ee..208c8ba 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -4338,6 +4338,10 @@
<attr name="popupAnimationStyle" format="reference" />
<!-- Whether the popup window should overlap its anchor view. -->
<attr name="overlapAnchor" format="boolean" />
+ <!-- Transition used to move views into the popup window. -->
+ <attr name="popupEnterTransition" format="reference" />
+ <!-- Transition used to move views out of the popup window. -->
+ <attr name="popupExitTransition" format="reference" />
</declare-styleable>
<declare-styleable name="ListPopupWindow">
<!-- Amount of pixels by which the drop down should be offset vertically. -->
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index 4329809..48645ed 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -770,6 +770,9 @@
<item name="dropDownSelector">?attr/listChoiceBackgroundIndicator</item>
<item name="popupBackground">@drawable/popup_background_material</item>
<item name="popupElevation">@dimen/floating_window_z</item>
+ <item name="popupAnimationStyle">@empty</item>
+ <item name="popupEnterTransition">@transition/popup_window_enter</item>
+ <item name="popupExitTransition">@transition/popup_window_exit</item>
<item name="dropDownVerticalOffset">0dip</item>
<item name="dropDownHorizontalOffset">0dip</item>
<item name="overlapAnchor">true</item>
@@ -780,11 +783,7 @@
</style>
<style name="Widget.Material.Spinner.DropDown"/>
-
- <style name="Widget.Material.Spinner.DropDown.ActionBar">
- <item name="background">@drawable/spinner_background_material</item>
- <item name="overlapAnchor">true</item>
- </style>
+ <style name="Widget.Material.Spinner.DropDown.ActionBar" />
<style name="Widget.Material.Spinner.Underlined">
<item name="background">@drawable/spinner_textfield_background_material</item>
@@ -847,7 +846,9 @@
<item name="dropDownSelector">?attr/listChoiceBackgroundIndicator</item>
<item name="popupBackground">@drawable/popup_background_material</item>
<item name="popupElevation">@dimen/floating_window_z</item>
- <item name="popupAnimationStyle">@style/Animation.Material.Popup</item>
+ <item name="popupAnimationStyle">@empty</item>
+ <item name="popupEnterTransition">@transition/popup_window_enter</item>
+ <item name="popupExitTransition">@transition/popup_window_exit</item>
<item name="dropDownVerticalOffset">0dip</item>
<item name="dropDownHorizontalOffset">0dip</item>
<item name="dropDownWidth">wrap_content</item>