diff options
-rw-r--r-- | api/current.txt | 2 | ||||
-rw-r--r-- | api/system-current.txt | 2 | ||||
-rw-r--r-- | core/java/android/view/View.java | 7 | ||||
-rw-r--r-- | core/java/android/view/ViewRootImpl.java | 8 | ||||
-rw-r--r-- | core/java/android/view/WindowManager.java | 22 | ||||
-rw-r--r-- | core/java/android/widget/PopupWindow.java | 510 | ||||
-rw-r--r-- | core/java/com/android/internal/transition/EpicenterClipReveal.java | 115 | ||||
-rw-r--r-- | core/res/res/transition/popup_window_enter.xml | 25 | ||||
-rw-r--r-- | core/res/res/transition/popup_window_exit.xml | 17 | ||||
-rw-r--r-- | core/res/res/values/attrs.xml | 4 | ||||
-rw-r--r-- | core/res/res/values/styles_material.xml | 13 |
11 files changed, 558 insertions, 167 deletions
diff --git a/api/current.txt b/api/current.txt index 120025d..6d94dcb 100644 --- a/api/current.txt +++ b/api/current.txt @@ -38390,6 +38390,8 @@ package android.widget { 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 0d51f5a..d73556a 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -40885,6 +40885,8 @@ package android.widget { 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 f55892d..c3f679f 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -14889,10 +14889,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 87d9a58..f233c44 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -472,8 +472,10 @@ public final class ViewRootImpl implements ViewParent, // 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 @@ public final class ViewRootImpl implements ViewParent, 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 @@ public final class ViewRootImpl implements ViewParent, // 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 094a8a1..8cfe80c 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1332,6 +1332,16 @@ public interface WindowManager extends ViewManager { * @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 @@ -1628,6 +1638,7 @@ public interface WindowManager extends ViewManager { out.writeInt(surfaceInsets.top); out.writeInt(surfaceInsets.right); out.writeInt(surfaceInsets.bottom); + out.writeInt(hasManualSurfaceInsets ? 1 : 0); out.writeInt(needsMenuKey); } @@ -1676,6 +1687,7 @@ public interface WindowManager extends ViewManager { surfaceInsets.top = in.readInt(); surfaceInsets.right = in.readInt(); surfaceInsets.bottom = in.readInt(); + hasManualSurfaceInsets = in.readInt() != 0; needsMenuKey = in.readInt(); } @@ -1858,6 +1870,11 @@ public interface WindowManager extends ViewManager { 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; @@ -1966,8 +1983,11 @@ public interface WindowManager extends ViewManager { 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.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.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.ViewTreeObserver.OnScrollChangedListener; 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 @@ public class PopupWindow { * 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 @@ public class PopupWindow { * 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 @@ public class PopupWindow { 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 @@ public class PopupWindow { 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(); - - updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff, - mAnchoredGravity)); - update(p.x, p.y, -1, -1, true); - } + private final EpicenterCallback mEpicenterCallback = new EpicenterCallback() { + @Override + public Rect onGetEpicenter(Transition transition) { + return mAnchorBounds; + } + }; + + 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 class PopupWindow { 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 @@ public class PopupWindow { 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 @@ public class PopupWindow { 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 @@ public class PopupWindow { * 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 class PopupWindow { public void setIgnoreCheekPress() { mIgnoreCheekPress = true; } - + /** * <p>Change the animation style resource for this popup.</p> @@ -401,13 +482,13 @@ public class PopupWindow { * @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 @@ public class PopupWindow { * @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 @@ public class PopupWindow { /** * 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 class PopupWindow { 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 @@ public class PopupWindow { * @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 @@ public class PopupWindow { /** * <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 @@ public class PopupWindow { * touch events, false otherwise * * @see #isOutsideTouchable() - * @see #isShowing() + * @see #isShowing() * @see #update() */ public void setOutsideTouchable(boolean touchable) { @@ -613,9 +694,9 @@ public class PopupWindow { /** * <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 @@ public class PopupWindow { * <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 @@ public class PopupWindow { 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 @@ public class PopupWindow { * 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 @@ public class PopupWindow { mWidthMode = widthSpec; mHeightMode = heightSpec; } - + /** * <p>Return this popup's height MeasureSpec</p> * @@ -836,7 +917,7 @@ public class PopupWindow { * @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 @@ public class PopupWindow { * * @return the width MeasureSpec of the popup * - * @see #setWidth(int) + * @see #setWidth(int) */ public int getWidth() { return mWidth; @@ -913,7 +994,7 @@ public class PopupWindow { * 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 @@ public class PopupWindow { WindowManager.LayoutParams p = createPopupLayout(token); p.windowAnimations = computeAnimationResource(); - + preparePopup(p); if (gravity == Gravity.NO_GRAVITY) { gravity = Gravity.TOP | Gravity.START; @@ -1049,12 +1130,12 @@ public class PopupWindow { // 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 @@ public class PopupWindow { + "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 @@ public class PopupWindow { 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 @@ public class PopupWindow { } 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 @@ public class PopupWindow { * <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 @@ public class PopupWindow { 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 class PopupWindow { 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 @@ public class PopupWindow { * 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 @@ public class PopupWindow { final int[] anchorPos = mDrawingLocation; anchor.getLocationOnScreen(anchorPos); - + int bottomEdge = displayFrame.bottom; if (ignoreBottomDecorations) { Resources res = anchor.getContext().getResources(); @@ -1413,49 +1555,78 @@ public class PopupWindow { 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 @@ public class PopupWindow { 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 @@ public class PopupWindow { if (update) { setLayoutDirectionFromAnchor(); - mWindowManager.updateViewLayout(mPopupView, p); + mWindowManager.updateViewLayout(mDecorView, p); } } @@ -1500,11 +1671,11 @@ public class PopupWindow { * @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 @@ public class PopupWindow { return; } - WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); + final WindowManager.LayoutParams p = + (WindowManager.LayoutParams) mDecorView.getLayoutParams(); boolean update = force; @@ -1588,7 +1760,7 @@ public class PopupWindow { if (update) { setLayoutDirectionFromAnchor(); - mWindowManager.updateViewLayout(mPopupView, p); + mWindowManager.updateViewLayout(mDecorView, p); } } @@ -1655,7 +1827,7 @@ public class PopupWindow { } 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 @@ public class PopupWindow { 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,24 +1877,50 @@ public class PopupWindow { 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 public boolean dispatchKeyEvent(KeyEvent event) { @@ -1731,15 +1929,14 @@ public class PopupWindow { 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 class PopupWindow { 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 @@ public class PopupWindow { return super.onTouchEvent(event); } } + } + + private class PopupBackgroundView extends FrameLayout { + public PopupBackgroundView(Context context) { + super(context); + } - /** @hide */ @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 @@ please see styles_device_defaults.xml. <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 @@ please see styles_device_defaults.xml. </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 @@ please see styles_device_defaults.xml. <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> |