method protected int getBottomPaddingOffset();
method public float getCameraDistance();
method public android.graphics.Rect getClipBounds();
+ method public boolean getClipBounds(android.graphics.Rect);
method public final boolean getClipToOutline();
method public java.lang.CharSequence getContentDescription();
method public final android.content.Context getContext();
method protected int getBottomPaddingOffset();
method public float getCameraDistance();
method public android.graphics.Rect getClipBounds();
+ method public boolean getClipBounds(android.graphics.Rect);
method public final boolean getClipToOutline();
method public java.lang.CharSequence getContentDescription();
method public final android.content.Context getContext();
return (mClipBounds != null) ? new Rect(mClipBounds) : null;
}
+
+ /**
+ * Populates an output rectangle with the clip bounds of the view,
+ * returning {@code true} if successful or {@code false} if the view's
+ * clip bounds are {@code null}.
+ *
+ * @param outRect rectangle in which to place the clip bounds of the view
+ * @return {@code true} if successful or {@code false} if the view's
+ * clip bounds are {@code null}
+ */
+ public boolean getClipBounds(Rect outRect) {
+ if (mClipBounds != null) {
+ outRect.set(mClipBounds);
+ return true;
+ }
+ return false;
+ }
+
/**
* Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
* case of an active Animation being run on the view.
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 WeakReference<View> mAnchor;
- private final EpicenterCallback mEpicenterCallback = new EpicenterCallback() {
- @Override
- public Rect onGetEpicenter(Transition transition) {
- final View anchor = mAnchor != null ? mAnchor.get() : null;
- final View decor = mDecorView;
- if (anchor == null || decor == null) {
- return null;
- }
-
- final Rect anchorBounds = mAnchorBounds;
- final int[] anchorLocation = anchor.getLocationOnScreen();
- final int[] popupLocation = mDecorView.getLocationOnScreen();
-
- // Compute the position of the anchor relative to the popup.
- anchorBounds.set(0, 0, anchor.getWidth(), anchor.getHeight());
- anchorBounds.offset(anchorLocation[0] - popupLocation[0],
- anchorLocation[1] - popupLocation[1]);
-
- return anchorBounds;
- }
- };
-
private final OnScrollChangedListener mOnScrollChangedListener = new OnScrollChangedListener() {
@Override
public void onScrollChanged() {
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) {
final PopupDecorView decorView = mDecorView;
decorView.setFitsSystemWindows(mLayoutInsetDecor);
- decorView.requestEnterTransition(mEnterTransition);
setLayoutDirectionFromAnchor();
mWindowManager.addView(decorView, p);
+
+ if (mEnterTransition != null) {
+ decorView.requestEnterTransition(mEnterTransition);
+ }
}
private void setLayoutDirectionFromAnchor() {
// Ensure any ongoing or pending transitions are canceled.
decorView.cancelTransitions();
- unregisterForScrollChanged();
-
mIsShowing = false;
mIsTransitioningToDismiss = true;
- if (mExitTransition != null && decorView.isLaidOut()) {
- decorView.startExitTransition(mExitTransition, new TransitionListenerAdapter() {
+ final Transition exitTransition = mExitTransition;
+ if (exitTransition != null && decorView.isLaidOut()) {
+ // The decor view is non-interactive during exit transitions.
+ final LayoutParams p = (LayoutParams) decorView.getLayoutParams();
+ p.flags |= LayoutParams.FLAG_NOT_TOUCHABLE;
+ p.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
+ mWindowManager.updateViewLayout(decorView, p);
+
+ final Rect epicenter = getRelativeAnchorBounds();
+ exitTransition.setEpicenterCallback(new EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return epicenter;
+ }
+ });
+ decorView.startExitTransition(exitTransition, new TransitionListenerAdapter() {
@Override
public void onTransitionEnd(Transition transition) {
dismissImmediate(decorView, contentHolder, contentView);
dismissImmediate(decorView, contentHolder, contentView);
}
+ // Clears the anchor view.
+ unregisterForScrollChanged();
+
if (mOnDismissListener != null) {
mOnDismissListener.onDismiss();
}
}
+ private Rect getRelativeAnchorBounds() {
+ 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]);
+ return bounds;
+ }
+
/**
* Removes the popup from the window manager and tears down the supporting
* view hierarchy, if necessary.
observer.removeOnGlobalLayoutListener(this);
}
+ final Rect epicenter = getRelativeAnchorBounds();
+ enterTransition.setEpicenterCallback(new EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return epicenter;
+ }
+ });
startEnterTransition(enterTransition);
}
});
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.RectEvaluator;
+import android.animation.TimeInterpolator;
import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.Rect;
import android.transition.TransitionValues;
import android.transition.Visibility;
import android.util.AttributeSet;
+import android.util.Property;
import android.view.View;
import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.PathInterpolator;
+
+import com.android.internal.R;
/**
* EpicenterClipReveal captures the {@link View#getClipBounds()} before and
private static final String PROPNAME_CLIP = "android:epicenterReveal:clip";
private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds";
- public EpicenterClipReveal() {}
+ private final TimeInterpolator mInterpolatorX;
+ private final TimeInterpolator mInterpolatorY;
+ private final boolean mCenterClipBounds;
+
+ public EpicenterClipReveal() {
+ mInterpolatorX = null;
+ mInterpolatorY = null;
+ mCenterClipBounds = false;
+ }
public EpicenterClipReveal(Context context, AttributeSet attrs) {
super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.EpicenterClipReveal, 0, 0);
+
+ mCenterClipBounds = a.getBoolean(R.styleable.EpicenterClipReveal_centerClipBounds, false);
+
+ final int interpolatorX = a.getResourceId(R.styleable.EpicenterClipReveal_interpolatorX, 0);
+ if (interpolatorX != 0) {
+ mInterpolatorX = AnimationUtils.loadInterpolator(context, interpolatorX);
+ } else {
+ mInterpolatorX = TransitionConstants.LINEAR_OUT_SLOW_IN;
+ }
+
+ final int interpolatorY = a.getResourceId(R.styleable.EpicenterClipReveal_interpolatorY, 0);
+ if (interpolatorY != 0) {
+ mInterpolatorY = AnimationUtils.loadInterpolator(context, interpolatorY);
+ } else {
+ mInterpolatorY = TransitionConstants.FAST_OUT_SLOW_IN;
+ }
+
+ a.recycle();
}
@Override
// Prepare the view.
view.setClipBounds(start);
- return createRectAnimator(view, start, end, endValues);
+ return createRectAnimator(view, start, end, endValues, mInterpolatorX, mInterpolatorY);
}
@Override
// Prepare the view.
view.setClipBounds(start);
- return createRectAnimator(view, start, end, endValues);
+ return createRectAnimator(view, start, end, endValues, mInterpolatorX, mInterpolatorY);
}
private Rect getEpicenterOrCenter(Rect bestRect) {
final Rect epicenter = getEpicenter();
if (epicenter != null) {
+ // Translate the clip bounds to be centered within the target bounds.
+ if (mCenterClipBounds) {
+ final int offsetX = bestRect.centerX() - epicenter.centerX();
+ final int offsetY = bestRect.centerY() - epicenter.centerY();
+ epicenter.offset(offsetX, offsetY);
+ }
return epicenter;
}
- int centerX = bestRect.centerX();
- int centerY = bestRect.centerY();
+ final int centerX = bestRect.centerX();
+ final int centerY = bestRect.centerY();
return new Rect(centerX, centerY, centerX, centerY);
}
return clipRect;
}
- private Animator createRectAnimator(final View view, Rect start, Rect end,
- TransitionValues endValues) {
- final Rect terminalClip = (Rect) endValues.values.get(PROPNAME_CLIP);
+ private static Animator createRectAnimator(final View view, Rect start, Rect end,
+ TransitionValues endValues, TimeInterpolator interpolatorX,
+ TimeInterpolator interpolatorY) {
final RectEvaluator evaluator = new RectEvaluator(new Rect());
- ObjectAnimator anim = ObjectAnimator.ofObject(view, "clipBounds", evaluator, start, end);
- anim.addListener(new AnimatorListenerAdapter() {
+ final Rect terminalClip = (Rect) endValues.values.get(PROPNAME_CLIP);
+
+ final ClipDimenProperty propX = new ClipDimenProperty(ClipDimenProperty.TARGET_X);
+ final ObjectAnimator animX = ObjectAnimator.ofObject(view, propX, evaluator, start, end);
+ if (interpolatorX != null) {
+ animX.setInterpolator(interpolatorX);
+ }
+
+ final ClipDimenProperty propY = new ClipDimenProperty(ClipDimenProperty.TARGET_Y);
+ final ObjectAnimator animY = ObjectAnimator.ofObject(view, propY, evaluator, start, end);
+ if (interpolatorY != null) {
+ animY.setInterpolator(interpolatorY);
+ }
+
+ final AnimatorSet animSet = new AnimatorSet();
+ animSet.playTogether(animX, animY);
+ animSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setClipBounds(terminalClip);
}
});
- return anim;
+ return animSet;
+ }
+
+ private static class ClipDimenProperty extends Property<View, Rect> {
+ public static final char TARGET_X = 'x';
+ public static final char TARGET_Y = 'y';
+
+ private final Rect mTempRect = new Rect();
+
+ private final int mTargetDimension;
+
+ public ClipDimenProperty(char targetDimension) {
+ super(Rect.class, "clip_bounds_" + targetDimension);
+
+ mTargetDimension = targetDimension;
+ }
+
+ @Override
+ public Rect get(View object) {
+ final Rect tempRect = mTempRect;
+ if (!object.getClipBounds(tempRect)) {
+ tempRect.setEmpty();
+ }
+ return tempRect;
+ }
+
+ @Override
+ public void set(View object, Rect value) {
+ final Rect tempRect = mTempRect;
+ if (object.getClipBounds(tempRect)) {
+ if (mTargetDimension == TARGET_X) {
+ tempRect.left = value.left;
+ tempRect.right = value.right;
+ } else {
+ tempRect.top = value.top;
+ tempRect.bottom = value.bottom;
+ }
+ object.setClipBounds(tempRect);
+ }
+ }
}
}
--- /dev/null
+/*
+ * 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 com.android.internal.R;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.transition.TransitionValues;
+import android.transition.Visibility;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+
+/**
+ * EpicenterTranslate captures the {@link View#getTranslationX()} and
+ * {@link View#getTranslationY()} before and after the scene change and
+ * animates between those and the epicenter's center during a visibility
+ * transition.
+ */
+public class EpicenterTranslate extends Visibility {
+ private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds";
+ private static final String PROPNAME_TRANSLATE_X = "android:epicenterReveal:translateX";
+ private static final String PROPNAME_TRANSLATE_Y = "android:epicenterReveal:translateY";
+ private static final String PROPNAME_TRANSLATE_Z = "android:epicenterReveal:translateZ";
+ private static final String PROPNAME_Z = "android:epicenterReveal:z";
+
+ private final TimeInterpolator mInterpolatorX;
+ private final TimeInterpolator mInterpolatorY;
+ private final TimeInterpolator mInterpolatorZ;
+
+ public EpicenterTranslate() {
+ mInterpolatorX = null;
+ mInterpolatorY = null;
+ mInterpolatorZ = null;
+ }
+
+ public EpicenterTranslate(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.EpicenterTranslate, 0, 0);
+
+ final int interpolatorX = a.getResourceId(R.styleable.EpicenterTranslate_interpolatorX, 0);
+ if (interpolatorX != 0) {
+ mInterpolatorX = AnimationUtils.loadInterpolator(context, interpolatorX);
+ } else {
+ mInterpolatorX = TransitionConstants.FAST_OUT_SLOW_IN;
+ }
+
+ final int interpolatorY = a.getResourceId(R.styleable.EpicenterTranslate_interpolatorY, 0);
+ if (interpolatorY != 0) {
+ mInterpolatorY = AnimationUtils.loadInterpolator(context, interpolatorY);
+ } else {
+ mInterpolatorY = TransitionConstants.FAST_OUT_SLOW_IN;
+ }
+
+ final int interpolatorZ = a.getResourceId(R.styleable.EpicenterTranslate_interpolatorZ, 0);
+ if (interpolatorZ != 0) {
+ mInterpolatorZ = AnimationUtils.loadInterpolator(context, interpolatorZ);
+ } else {
+ mInterpolatorZ = TransitionConstants.FAST_OUT_SLOW_IN;
+ }
+
+ a.recycle();
+ }
+
+ @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 bounds = new Rect(0, 0, view.getWidth(), view.getHeight());
+ values.values.put(PROPNAME_BOUNDS, bounds);
+ values.values.put(PROPNAME_TRANSLATE_X, view.getTranslationX());
+ values.values.put(PROPNAME_TRANSLATE_Y, view.getTranslationY());
+ values.values.put(PROPNAME_TRANSLATE_Z, view.getTranslationZ());
+ values.values.put(PROPNAME_Z, view.getZ());
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (endValues == null) {
+ return null;
+ }
+
+ final Rect end = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+ final Rect start = getEpicenterOrCenter(end);
+ final float startX = start.centerX() - end.centerX();
+ final float startY = start.centerY() - end.centerY();
+ final float startZ = 0 - (float) endValues.values.get(PROPNAME_Z);
+
+ // Translate the view to be centered on the epicenter.
+ view.setTranslationX(startX);
+ view.setTranslationY(startY);
+ view.setTranslationZ(startZ);
+
+ final float endX = (float) endValues.values.get(PROPNAME_TRANSLATE_X);
+ final float endY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y);
+ final float endZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z);
+ return createAnimator(view, startX, startY, startZ, endX, endY, endZ,
+ mInterpolatorX, mInterpolatorY, mInterpolatorZ);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot, View view,
+ TransitionValues startValues, TransitionValues endValues) {
+ if (startValues == null) {
+ return null;
+ }
+
+ final Rect start = (Rect) endValues.values.get(PROPNAME_BOUNDS);
+ final Rect end = getEpicenterOrCenter(start);
+ final float endX = end.centerX() - start.centerX();
+ final float endY = end.centerY() - start.centerY();
+ final float endZ = 0 - (float) startValues.values.get(PROPNAME_Z);
+
+ final float startX = (float) endValues.values.get(PROPNAME_TRANSLATE_X);
+ final float startY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y);
+ final float startZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z);
+ return createAnimator(view, startX, startY, startZ, endX, endY, endZ,
+ mInterpolatorX, mInterpolatorY, mInterpolatorZ);
+ }
+
+ private Rect getEpicenterOrCenter(Rect bestRect) {
+ final Rect epicenter = getEpicenter();
+ if (epicenter != null) {
+ return epicenter;
+ }
+
+ final int centerX = bestRect.centerX();
+ final int centerY = bestRect.centerY();
+ return new Rect(centerX, centerY, centerX, centerY);
+ }
+
+ private static Animator createAnimator(final View view, float startX, float startY,
+ float startZ, float endX, float endY, float endZ, TimeInterpolator interpolatorX,
+ TimeInterpolator interpolatorY, TimeInterpolator interpolatorZ) {
+ final ObjectAnimator animX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, startX, endX);
+ if (interpolatorX != null) {
+ animX.setInterpolator(interpolatorX);
+ }
+
+ final ObjectAnimator animY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY);
+ if (interpolatorY != null) {
+ animY.setInterpolator(interpolatorY);
+ }
+
+ final ObjectAnimator animZ = ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, startZ, endZ);
+ if (interpolatorZ != null) {
+ animZ.setInterpolator(interpolatorZ);
+ }
+
+ final AnimatorSet animSet = new AnimatorSet();
+ animSet.playTogether(animX, animY, animZ);
+ return animSet;
+ }
+}
--- /dev/null
+/*
+ * 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.TimeInterpolator;
+import android.view.animation.PathInterpolator;
+
+class TransitionConstants {
+ static final TimeInterpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0, 0, 0.2f, 1);
+ static final TimeInterpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0, 0.2f, 1);
+}
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" />
+ <!-- Start from location of epicenter, move to popup location. -->
+ <transition
+ class="com.android.internal.transition.EpicenterTranslate"
+ android:duration="300" />
+
+ <!-- Start from size of epicenter, expand to full width/height. -->
+ <transition
+ class="com.android.internal.transition.EpicenterClipReveal"
+ android:centerClipBounds="true"
+ android:duration="300" />
+
+ <!-- Quickly fade in. -->
+ <fade android:duration="100" />
</transitionSet>
See the License for the specific language governing permissions and
limitations under the License.
-->
+
+<!-- Fade out moderately fast. -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
- android:interpolator="@android:interpolator/linear" />
+ android:duration="300" />
<attr name="matchOrder" format="string" />
</declare-styleable>
+ <!-- @hide For internal use only. Use only as directed. -->
+ <declare-styleable name="EpicenterClipReveal">
+ <attr name="centerClipBounds" format="boolean" />
+ <attr name="interpolatorX" format="reference" />
+ <attr name="interpolatorY" format="reference" />
+ </declare-styleable>
+
+ <!-- @hide For internal use only. Use only as directed. -->
+ <declare-styleable name="EpicenterTranslate">
+ <attr name="interpolatorX" />
+ <attr name="interpolatorY" />
+ <attr name="interpolatorZ" format="reference" />
+ </declare-styleable>
+
<!-- Use <code>fade</code>as the root tag of the XML resource that
describes a {@link android.transition.Fade Fade} transition.
The attributes of the {@link android.R.styleable#Transition Transition}