diff options
| author | George Mount <mount@google.com> | 2014-03-10 16:51:16 -0700 |
|---|---|---|
| committer | George Mount <mount@google.com> | 2014-04-07 09:14:09 -0700 |
| commit | d6107a3170df61d9e776fcd5666acfc9135c6f16 (patch) | |
| tree | 8eebc42794fe87d3974a3e9bd21ab47b0244ae23 /core/java/android | |
| parent | cb4b7d999e7bcba608726188421772e313e67163 (diff) | |
| download | frameworks_base-d6107a3170df61d9e776fcd5666acfc9135c6f16.zip frameworks_base-d6107a3170df61d9e776fcd5666acfc9135c6f16.tar.gz frameworks_base-d6107a3170df61d9e776fcd5666acfc9135c6f16.tar.bz2 | |
Add Transitions useful for Activity transitions.
Slide: transition in and out of the edge of the scene.
Explode: transition to the scene borders
Moved capability from Fade to Visibility.
Change-Id: Ibeb0d8f751c990edc467570d9665fbe251af2703
Diffstat (limited to 'core/java/android')
| -rw-r--r-- | core/java/android/app/ActivityOptions.java | 27 | ||||
| -rw-r--r-- | core/java/android/transition/CircularPropagation.java | 103 | ||||
| -rw-r--r-- | core/java/android/transition/Explode.java | 228 | ||||
| -rw-r--r-- | core/java/android/transition/Fade.java | 258 | ||||
| -rw-r--r-- | core/java/android/transition/MatrixClippedDrawable.java | 300 | ||||
| -rw-r--r-- | core/java/android/transition/MoveImage.java | 326 | ||||
| -rw-r--r-- | core/java/android/transition/SidePropagation.java | 165 | ||||
| -rw-r--r-- | core/java/android/transition/Slide.java | 243 | ||||
| -rw-r--r-- | core/java/android/transition/Transition.java | 162 | ||||
| -rw-r--r-- | core/java/android/transition/TransitionInflater.java | 18 | ||||
| -rw-r--r-- | core/java/android/transition/TransitionPropagation.java | 88 | ||||
| -rw-r--r-- | core/java/android/transition/TransitionSet.java | 29 | ||||
| -rw-r--r-- | core/java/android/transition/Visibility.java | 200 | ||||
| -rw-r--r-- | core/java/android/transition/VisibilityPropagation.java | 112 |
14 files changed, 1991 insertions, 268 deletions
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 8839ee6..4384580 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -722,8 +722,6 @@ public class ActivityOptions { private static class ExitTransitionListener extends ResultReceiver implements Transition.TransitionListener { private boolean mSharedElementNotified; - private Transition mExitTransition; - private Transition mSharedElementTransition; private IRemoteCallback mTransitionCompleteCallback; private boolean mExitComplete; private boolean mSharedElementComplete; @@ -733,10 +731,15 @@ public class ActivityOptions { SharedElementSource sharedElementSource) { super(null); mSharedElementSource = sharedElementSource; - mExitTransition = exitTransition; - mExitTransition.addListener(this); - mSharedElementTransition = sharedElementTransition; - mSharedElementTransition.addListener(this); + exitTransition.addListener(this); + sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + mSharedElementComplete = true; + notifySharedElement(); + transition.removeListener(this); + } + }); } @Override @@ -769,15 +772,9 @@ public class ActivityOptions { @Override public void onTransitionEnd(Transition transition) { - if (transition == mExitTransition) { - mExitComplete = true; - notifyExit(); - mExitTransition.removeListener(this); - } else { - mSharedElementComplete = true; - notifySharedElement(); - mSharedElementTransition.removeListener(this); - } + mExitComplete = true; + notifyExit(); + transition.removeListener(this); } @Override diff --git a/core/java/android/transition/CircularPropagation.java b/core/java/android/transition/CircularPropagation.java new file mode 100644 index 0000000..18a3d22 --- /dev/null +++ b/core/java/android/transition/CircularPropagation.java @@ -0,0 +1,103 @@ +/* + * 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. + */ +package android.transition; + +import android.graphics.Rect; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * A propagation that varies with the distance to the epicenter of the Transition + * or center of the scene if no epicenter exists. When a View is visible in the + * start of the transition, Views farther from the epicenter will transition + * sooner than Views closer to the epicenter. When a View is not in the start + * of the transition or is not visible at the start of the transition, it will + * transition sooner when closer to the epicenter and later when farther from + * the epicenter. This is the default TransitionPropagation used with + * {@link android.transition.Explode}. + */ +public class CircularPropagation extends VisibilityPropagation { + private static final String TAG = "CircularPropagation"; + + private float mPropagationSpeed = 4.0f; + + /** + * Sets the speed at which transition propagation happens, relative to the duration of the + * Transition. A <code>propagationSpeed</code> of 1 means that a View centered farthest from + * the epicenter and View centered at the epicenter will have a difference + * in start delay of approximately the duration of the Transition. A speed of 2 means the + * start delay difference will be approximately half of the duration of the transition. A + * value of 0 is illegal, but negative values will invert the propagation. + * + * @param propagationSpeed The speed at which propagation occurs, relative to the duration + * of the transition. A speed of 4 means it works 4 times as fast + * as the duration of the transition. May not be 0. + */ + public void setPropagationSpeed(float propagationSpeed) { + if (propagationSpeed == 0) { + throw new IllegalArgumentException("propagationSpeed may not be 0"); + } + mPropagationSpeed = propagationSpeed; + } + + @Override + public long getStartDelay(ViewGroup sceneRoot, Transition transition, + TransitionValues startValues, TransitionValues endValues) { + if (startValues == null && endValues == null) { + return 0; + } + int directionMultiplier = 1; + TransitionValues positionValues; + if (endValues == null || getViewVisibility(startValues) == View.VISIBLE) { + positionValues = startValues; + directionMultiplier = -1; + } else { + positionValues = endValues; + } + + int viewCenterX = getViewX(positionValues); + int viewCenterY = getViewY(positionValues); + + Rect epicenter = transition.getEpicenter(); + int epicenterX; + int epicenterY; + if (epicenter != null) { + epicenterX = epicenter.centerX(); + epicenterY = epicenter.centerY(); + } else { + int[] loc = new int[2]; + sceneRoot.getLocationOnScreen(loc); + epicenterX = Math.round(loc[0] + (sceneRoot.getWidth() / 2) + + sceneRoot.getTranslationX()); + epicenterY = Math.round(loc[1] + (sceneRoot.getHeight() / 2) + + sceneRoot.getTranslationY()); + } + float distance = distance(viewCenterX, viewCenterY, epicenterX, epicenterY); + float maxDistance = distance(0, 0, sceneRoot.getWidth(), sceneRoot.getHeight()); + float distanceFraction = distance/maxDistance; + + return Math.round(transition.getDuration() * directionMultiplier / mPropagationSpeed + * distanceFraction); + } + + private static float distance(float x1, float y1, float x2, float y2) { + float x = x2 - x1; + float y = y2 - y1; + return FloatMath.sqrt((x * x) + (y * y)); + } +} diff --git a/core/java/android/transition/Explode.java b/core/java/android/transition/Explode.java new file mode 100644 index 0000000..fae527c --- /dev/null +++ b/core/java/android/transition/Explode.java @@ -0,0 +1,228 @@ +/* + * 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. + */ +package android.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.graphics.Path; +import android.graphics.Rect; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +/** + * This transition tracks changes to the visibility of target views in the + * start and end scenes and moves views in or out from the edges of the + * scene. Visibility is determined by both the + * {@link View#setVisibility(int)} state of the view as well as whether it + * is parented in the current view hierarchy. Disappearing Views are + * limited as described in {@link Visibility#onDisappear(android.view.ViewGroup, + * TransitionValues, int, TransitionValues, int)}. + * <p>Views move away from the focal View or the center of the Scene if + * no epicenter was provided.</p> + */ +public class Explode extends Visibility { + private static final TimeInterpolator sDecelerate = new DecelerateInterpolator(); + private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); + private static final String TAG = "Explode"; + + private static final String PROPNAME_SCREEN_BOUNDS = "android:out:screenBounds"; + + private int[] mTempLoc = new int[2]; + + public Explode() { + setPropagation(new CircularPropagation()); + } + + private void captureValues(TransitionValues transitionValues) { + View view = transitionValues.view; + view.getLocationOnScreen(mTempLoc); + int left = mTempLoc[0] + Math.round(view.getTranslationX()); + int top = mTempLoc[1] + Math.round(view.getTranslationY()); + int right = left + view.getWidth(); + int bottom = top + view.getHeight(); + transitionValues.values.put(PROPNAME_SCREEN_BOUNDS, new Rect(left, top, right, bottom)); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + captureValues(transitionValues); + } + + private Animator createAnimation(final View view, float startX, float startY, float endX, + float endY, float terminalX, float terminalY, TimeInterpolator interpolator) { + view.setTranslationX(startX); + view.setTranslationY(startY); + if (startY == endY && startX == endX) { + return null; + } + Path path = new Path(); + path.moveTo(startX, startY); + path.lineTo(endX, endY); + ObjectAnimator pathAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, + View.TRANSLATION_Y, path); + pathAnimator.setInterpolator(interpolator); + OutAnimatorListener listener = new OutAnimatorListener(view, terminalX, terminalY, + endX, endY); + pathAnimator.addListener(listener); + pathAnimator.addPauseListener(listener); + + return pathAnimator; + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (endValues == null) { + return null; + } + Rect bounds = (Rect) endValues.values.get(PROPNAME_SCREEN_BOUNDS); + calculateOut(sceneRoot, bounds, mTempLoc); + + final float endX = view.getTranslationX(); + final float startX = endX + mTempLoc[0]; + final float endY = view.getTranslationY(); + final float startY = endY + mTempLoc[1]; + + return createAnimation(view, startX, startY, endX, endY, endX, endY, sDecelerate); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + Rect bounds = (Rect) startValues.values.get(PROPNAME_SCREEN_BOUNDS); + calculateOut(sceneRoot, bounds, mTempLoc); + + final float startX = view.getTranslationX(); + final float endX = startX + mTempLoc[0]; + final float startY = view.getTranslationY(); + final float endY = startY + mTempLoc[1]; + + return createAnimation(view, startX, startY, endX, endY, startX, startY, + sAccelerate); + } + + private void calculateOut(View sceneRoot, Rect bounds, int[] outVector) { + sceneRoot.getLocationOnScreen(mTempLoc); + int sceneRootX = mTempLoc[0]; + int sceneRootY = mTempLoc[1]; + int focalX; + int focalY; + + Rect epicenter = getEpicenter(); + if (epicenter == null) { + focalX = sceneRootX + (sceneRoot.getWidth() / 2) + + Math.round(sceneRoot.getTranslationX()); + focalY = sceneRootY + (sceneRoot.getHeight() / 2) + + Math.round(sceneRoot.getTranslationY()); + } else { + focalX = epicenter.centerX(); + focalY = epicenter.centerY(); + } + + int centerX = bounds.centerX(); + int centerY = bounds.centerY(); + float xVector = centerX - focalX; + float yVector = centerY - focalY; + + if (xVector == 0 && yVector == 0) { + // Random direction when View is centered on focal View. + xVector = (float)(Math.random() * 2) - 1; + yVector = (float)(Math.random() * 2) - 1; + } + float vectorSize = calculateDistance(xVector, yVector); + xVector /= vectorSize; + yVector /= vectorSize; + + float maxDistance = + calculateMaxDistance(sceneRoot, focalX - sceneRootX, focalY - sceneRootY); + + outVector[0] = Math.round(maxDistance * xVector); + outVector[1] = Math.round(maxDistance * yVector); + } + + private static float calculateMaxDistance(View sceneRoot, int focalX, int focalY) { + int maxX = Math.max(focalX, sceneRoot.getWidth() - focalX); + int maxY = Math.max(focalY, sceneRoot.getHeight() - focalY); + return calculateDistance(maxX, maxY); + } + + private static float calculateDistance(float x, float y) { + return FloatMath.sqrt((x * x) + (y * y)); + } + + private static class OutAnimatorListener extends AnimatorListenerAdapter { + private final View mView; + private boolean mCanceled = false; + private float mPausedX; + private float mPausedY; + private final float mTerminalX; + private final float mTerminalY; + private final float mEndX; + private final float mEndY; + + public OutAnimatorListener(View view, float terminalX, float terminalY, + float endX, float endY) { + mView = view; + mTerminalX = terminalX; + mTerminalY = terminalY; + mEndX = endX; + mEndY = endY; + } + + @Override + public void onAnimationCancel(Animator animator) { + mView.setTranslationX(mTerminalX); + mView.setTranslationY(mTerminalY); + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + if (!mCanceled) { + mView.setTranslationX(mTerminalX); + mView.setTranslationY(mTerminalY); + } + } + + @Override + public void onAnimationPause(Animator animator) { + mPausedX = mView.getTranslationX(); + mPausedY = mView.getTranslationY(); + mView.setTranslationY(mEndX); + mView.setTranslationY(mEndY); + } + + @Override + public void onAnimationResume(Animator animator) { + mView.setTranslationX(mPausedX); + mView.setTranslationY(mPausedY); + } + } +} diff --git a/core/java/android/transition/Fade.java b/core/java/android/transition/Fade.java index 8edb1ff..08e27d3 100644 --- a/core/java/android/transition/Fade.java +++ b/core/java/android/transition/Fade.java @@ -59,8 +59,6 @@ public class Fade extends Visibility { private static boolean DBG = Transition.DBG && false; private static final String LOG_TAG = "Fade"; - private static final String PROPNAME_SCREEN_X = "android:fade:screenX"; - private static final String PROPNAME_SCREEN_Y = "android:fade:screenY"; /** * Fading mode used in {@link #Fade(int)} to make the transition @@ -98,245 +96,81 @@ public class Fade extends Visibility { /** * Utility method to handle creating and running the Animator. */ - private Animator createAnimation(View view, float startAlpha, float endAlpha, - AnimatorListenerAdapter listener) { + private Animator createAnimation(View view, float startAlpha, float endAlpha) { if (startAlpha == endAlpha) { - // run listener if we're noop'ing the animation, to get the end-state results now - if (listener != null) { - listener.onAnimationEnd(null); - } return null; } - final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "transitionAlpha", startAlpha, - endAlpha); + view.setTransitionAlpha(startAlpha); + final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "transitionAlpha", endAlpha); if (DBG) { Log.d(LOG_TAG, "Created animator " + anim); } - if (listener != null) { - anim.addListener(listener); - anim.addPauseListener(listener); - } + FadeAnimatorListener listener = new FadeAnimatorListener(view, endAlpha); + anim.addListener(listener); + anim.addPauseListener(listener); return anim; } - private void captureValues(TransitionValues transitionValues) { - int[] loc = new int[2]; - transitionValues.view.getLocationOnScreen(loc); - transitionValues.values.put(PROPNAME_SCREEN_X, loc[0]); - transitionValues.values.put(PROPNAME_SCREEN_Y, loc[1]); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { - super.captureStartValues(transitionValues); - captureValues(transitionValues); - } - @Override - public Animator onAppear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, + TransitionValues endValues) { if ((mFadingMode & IN) != IN || endValues == null) { return null; } - final View endView = endValues.view; if (DBG) { View startView = (startValues != null) ? startValues.view : null; Log.d(LOG_TAG, "Fade.onAppear: startView, startVis, endView, endVis = " + - startView + ", " + startVisibility + ", " + endView + ", " + endVisibility); + startView + ", " + view); } - endView.setTransitionAlpha(0); - TransitionListener transitionListener = new TransitionListenerAdapter() { - boolean mCanceled = false; - float mPausedAlpha; - - @Override - public void onTransitionCancel(Transition transition) { - endView.setTransitionAlpha(1); - mCanceled = true; - } - - @Override - public void onTransitionEnd(Transition transition) { - if (!mCanceled) { - endView.setTransitionAlpha(1); - } - } - - @Override - public void onTransitionPause(Transition transition) { - mPausedAlpha = endView.getTransitionAlpha(); - endView.setTransitionAlpha(1); - } - - @Override - public void onTransitionResume(Transition transition) { - endView.setTransitionAlpha(mPausedAlpha); - } - }; - addListener(transitionListener); - return createAnimation(endView, 0, 1, null); + return createAnimation(view, 0, 1); } @Override - public Animator onDisappear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { + public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, + TransitionValues endValues) { if ((mFadingMode & OUT) != OUT) { return null; } - View view = null; - View startView = (startValues != null) ? startValues.view : null; - View endView = (endValues != null) ? endValues.view : null; - if (DBG) { - Log.d(LOG_TAG, "Fade.onDisappear: startView, startVis, endView, endVis = " + - startView + ", " + startVisibility + ", " + endView + ", " + endVisibility); - } - View overlayView = null; - View viewToKeep = null; - if (endView == null || endView.getParent() == null) { - if (endView != null) { - // endView was removed from its parent - add it to the overlay - view = overlayView = endView; - } else if (startView != null) { - // endView does not exist. Use startView only under certain - // conditions, because placing a view in an overlay necessitates - // it being removed from its current parent - if (startView.getParent() == null) { - // no parent - safe to use - view = overlayView = startView; - } else if (startView.getParent() instanceof View && - startView.getParent().getParent() == null) { - View startParent = (View) startView.getParent(); - int id = startParent.getId(); - if (id != View.NO_ID && sceneRoot.findViewById(id) != null && mCanRemoveViews) { - // no parent, but its parent is unparented but the parent - // hierarchy has been replaced by a new hierarchy with the same id - // and it is safe to un-parent startView - view = overlayView = startView; - } - } - } - } else { - // visibility change - if (endVisibility == View.INVISIBLE) { - view = endView; - viewToKeep = view; - } else { - // Becoming GONE - if (startView == endView) { - view = endView; - viewToKeep = view; - } else { - view = startView; - overlayView = view; - } - } - } - final int finalVisibility = endVisibility; - // TODO: add automatic facility to Visibility superclass for keeping views around - if (overlayView != null) { - // TODO: Need to do this for general case of adding to overlay - int screenX = (Integer) startValues.values.get(PROPNAME_SCREEN_X); - int screenY = (Integer) startValues.values.get(PROPNAME_SCREEN_Y); - int[] loc = new int[2]; - sceneRoot.getLocationOnScreen(loc); - overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); - overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); - sceneRoot.getOverlay().add(overlayView); - // TODO: add automatic facility to Visibility superclass for keeping views around - final float startAlpha = 1; - float endAlpha = 0; - final View finalView = view; - final View finalOverlayView = overlayView; - final View finalViewToKeep = viewToKeep; - final ViewGroup finalSceneRoot = sceneRoot; - final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finalView.setTransitionAlpha(startAlpha); - // TODO: restore view offset from overlay repositioning - if (finalViewToKeep != null) { - finalViewToKeep.setVisibility(finalVisibility); - } - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().remove(finalOverlayView); - } - } - @Override - public void onAnimationPause(Animator animation) { - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().remove(finalOverlayView); - } - } + return createAnimation(view, 1, 0); + } + + private static class FadeAnimatorListener extends AnimatorListenerAdapter { + private final View mView; + private final float mEndAlpha; + private boolean mCanceled = false; + private float mPausedAlpha; - @Override - public void onAnimationResume(Animator animation) { - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().add(finalOverlayView); - } - } - }; - return createAnimation(view, startAlpha, endAlpha, endListener); + public FadeAnimatorListener(View view, float endAlpha) { + mView = view; + mEndAlpha = endAlpha; } - if (viewToKeep != null) { - // TODO: find a different way to do this, like just changing the view to be - // VISIBLE for the duration of the transition - viewToKeep.setVisibility((View.VISIBLE)); - // TODO: add automatic facility to Visibility superclass for keeping views around - final float startAlpha = 1; - float endAlpha = 0; - final View finalView = view; - final View finalOverlayView = overlayView; - final View finalViewToKeep = viewToKeep; - final ViewGroup finalSceneRoot = sceneRoot; - final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { - boolean mCanceled = false; - float mPausedAlpha = -1; - @Override - public void onAnimationPause(Animator animation) { - if (finalViewToKeep != null && !mCanceled) { - finalViewToKeep.setVisibility(finalVisibility); - } - mPausedAlpha = finalView.getTransitionAlpha(); - finalView.setTransitionAlpha(startAlpha); - } + @Override + public void onAnimationCancel(Animator animator) { + mCanceled = true; + if (mPausedAlpha >= 0) { + mView.setTransitionAlpha(mPausedAlpha); + } + } - @Override - public void onAnimationResume(Animator animation) { - if (finalViewToKeep != null && !mCanceled) { - finalViewToKeep.setVisibility(View.VISIBLE); - } - finalView.setTransitionAlpha(mPausedAlpha); - } + @Override + public void onAnimationEnd(Animator animator) { + if (!mCanceled) { + mView.setTransitionAlpha(mEndAlpha); + } + } - @Override - public void onAnimationCancel(Animator animation) { - mCanceled = true; - if (mPausedAlpha >= 0) { - finalView.setTransitionAlpha(mPausedAlpha); - } - } + @Override + public void onAnimationPause(Animator animator) { + mPausedAlpha = mView.getTransitionAlpha(); + mView.setTransitionAlpha(mEndAlpha); + } - @Override - public void onAnimationEnd(Animator animation) { - if (!mCanceled) { - finalView.setTransitionAlpha(startAlpha); - } - // TODO: restore view offset from overlay repositioning - if (finalViewToKeep != null && !mCanceled) { - finalViewToKeep.setVisibility(finalVisibility); - } - if (finalOverlayView != null) { - finalSceneRoot.getOverlay().remove(finalOverlayView); - } - } - }; - return createAnimation(view, startAlpha, endAlpha, endListener); + @Override + public void onAnimationResume(Animator animator) { + mView.setTransitionAlpha(mPausedAlpha); } - return null; } - -}
\ No newline at end of file +} diff --git a/core/java/android/transition/MatrixClippedDrawable.java b/core/java/android/transition/MatrixClippedDrawable.java new file mode 100644 index 0000000..ebaad59 --- /dev/null +++ b/core/java/android/transition/MatrixClippedDrawable.java @@ -0,0 +1,300 @@ +/* + * 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. + */ +package android.transition; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.Property; + +/** + * Used in MoveImage to mock an ImageView as a Drawable to be scaled in the scene root Overlay. + * @hide + */ +class MatrixClippedDrawable extends Drawable implements Drawable.Callback { + private static final String TAG = "MatrixClippedDrawable"; + + private ClippedMatrixState mClippedMatrixState; + + public static final Property<MatrixClippedDrawable, Rect> CLIP_PROPERTY + = new Property<MatrixClippedDrawable, Rect>(Rect.class, "clipRect") { + + @Override + public Rect get(MatrixClippedDrawable object) { + return object.getClipRect(); + } + + @Override + public void set(MatrixClippedDrawable object, Rect value) { + object.setClipRect(value); + } + }; + + public static final Property<MatrixClippedDrawable, Matrix> MATRIX_PROPERTY + = new Property<MatrixClippedDrawable, Matrix>(Matrix.class, "matrix") { + @Override + public void set(MatrixClippedDrawable object, Matrix value) { + object.setMatrix(value); + } + + @Override + public Matrix get(MatrixClippedDrawable object) { + return object.getMatrix(); + } + }; + + public MatrixClippedDrawable(Drawable drawable) { + this(null, null); + + mClippedMatrixState.mDrawable = drawable; + + if (drawable != null) { + drawable.setCallback(this); + } + } + + public void setMatrix(Matrix matrix) { + if (matrix == null) { + mClippedMatrixState.mMatrix = null; + } else { + if (mClippedMatrixState.mMatrix == null) { + mClippedMatrixState.mMatrix = new Matrix(); + } + mClippedMatrixState.mMatrix.set(matrix); + } + invalidateSelf(); + } + + public Matrix getMatrix() { + return mClippedMatrixState.mMatrix; + } + + public Rect getClipRect() { + return mClippedMatrixState.mClipRect; + } + + public void setClipRect(Rect clipRect) { + if (clipRect == null) { + if (mClippedMatrixState.mClipRect != null) { + mClippedMatrixState.mClipRect = null; + invalidateSelf(); + } + } else { + if (mClippedMatrixState.mClipRect == null) { + mClippedMatrixState.mClipRect = new Rect(clipRect); + } else { + mClippedMatrixState.mClipRect.set(clipRect); + } + invalidateSelf(); + } + } + + // overrides from Drawable.Callback + + public void invalidateDrawable(Drawable who) { + final Drawable.Callback callback = getCallback(); + if (callback != null) { + callback.invalidateDrawable(this); + } + } + + public void scheduleDrawable(Drawable who, Runnable what, long when) { + final Drawable.Callback callback = getCallback(); + if (callback != null) { + callback.scheduleDrawable(this, what, when); + } + } + + public void unscheduleDrawable(Drawable who, Runnable what) { + final Drawable.Callback callback = getCallback(); + if (callback != null) { + callback.unscheduleDrawable(this, what); + } + } + + // overrides from Drawable + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() + | mClippedMatrixState.mChangingConfigurations + | mClippedMatrixState.mDrawable.getChangingConfigurations(); + } + + @Override + public boolean getPadding(Rect padding) { + // XXX need to adjust padding! + return mClippedMatrixState.mDrawable.getPadding(padding); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + mClippedMatrixState.mDrawable.setVisible(visible, restart); + return super.setVisible(visible, restart); + } + + @Override + public void setAlpha(int alpha) { + mClippedMatrixState.mDrawable.setAlpha(alpha); + } + + @Override + public int getAlpha() { + return mClippedMatrixState.mDrawable.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mClippedMatrixState.mDrawable.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return mClippedMatrixState.mDrawable.getOpacity(); + } + + @Override + public boolean isStateful() { + return mClippedMatrixState.mDrawable.isStateful(); + } + + @Override + protected boolean onStateChange(int[] state) { + return mClippedMatrixState.mDrawable.setState(state); + } + + @Override + protected boolean onLevelChange(int level) { + mClippedMatrixState.mDrawable.setLevel(level); + invalidateSelf(); + return true; + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.setBounds(bounds); + if (mClippedMatrixState.mMatrix == null) { + mClippedMatrixState.mDrawable.setBounds(bounds); + } else { + int drawableWidth = mClippedMatrixState.mDrawable.getIntrinsicWidth(); + int drawableHeight = mClippedMatrixState.mDrawable.getIntrinsicHeight(); + mClippedMatrixState.mDrawable.setBounds(bounds.left, bounds.top, + drawableWidth + bounds.left, drawableHeight + bounds.top); + } + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + int left = bounds.left; + int top = bounds.top; + int saveCount = canvas.getSaveCount(); + canvas.save(); + if (mClippedMatrixState.mClipRect != null) { + canvas.clipRect(mClippedMatrixState.mClipRect); + } else { + canvas.clipRect(bounds); + } + + if (mClippedMatrixState != null && !mClippedMatrixState.mMatrix.isIdentity()) { + canvas.translate(left, top); + canvas.concat(mClippedMatrixState.mMatrix); + canvas.translate(-left, -top); + } + mClippedMatrixState.mDrawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + + @Override + public int getIntrinsicWidth() { + return mClippedMatrixState.mDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mClippedMatrixState.mDrawable.getIntrinsicHeight(); + } + + @Override + public Drawable.ConstantState getConstantState() { + if (mClippedMatrixState.canConstantState()) { + mClippedMatrixState.mChangingConfigurations = getChangingConfigurations(); + return mClippedMatrixState; + } + return null; + } + + final static class ClippedMatrixState extends Drawable.ConstantState { + Drawable mDrawable; + Matrix mMatrix; + Rect mClipRect; + + private boolean mCheckedConstantState; + private boolean mCanConstantState; + int mChangingConfigurations; + + ClippedMatrixState(ClippedMatrixState orig, MatrixClippedDrawable owner, Resources res) { + if (orig != null) { + if (res != null) { + mDrawable = orig.mDrawable.getConstantState().newDrawable(res); + } else { + mDrawable = orig.mDrawable.getConstantState().newDrawable(); + } + mDrawable.setCallback(owner); + mCheckedConstantState = mCanConstantState = true; + if (orig.mMatrix != null) { + mMatrix = new Matrix(orig.mMatrix); + } + if (orig.mClipRect != null) { + mClipRect = new Rect(orig.mClipRect); + } + } + } + + @Override + public Drawable newDrawable() { + return new MatrixClippedDrawable(this, null); + } + + @Override + public Drawable newDrawable(Resources res) { + return new MatrixClippedDrawable(this, res); + } + + @Override + public int getChangingConfigurations() { + return mChangingConfigurations; + } + + boolean canConstantState() { + if (!mCheckedConstantState) { + mCanConstantState = mDrawable.getConstantState() != null; + mCheckedConstantState = true; + } + + return mCanConstantState; + } + } + + private MatrixClippedDrawable(ClippedMatrixState state, Resources res) { + mClippedMatrixState = new ClippedMatrixState(state, this, res); + } + +} diff --git a/core/java/android/transition/MoveImage.java b/core/java/android/transition/MoveImage.java new file mode 100644 index 0000000..d68e971 --- /dev/null +++ b/core/java/android/transition/MoveImage.java @@ -0,0 +1,326 @@ +/* + * 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. + */ +package android.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroupOverlay; +import android.view.ViewParent; +import android.widget.ImageView; + +import java.util.ArrayList; +import java.util.Map; + +/** + * Transitions ImageViews, including size, scaleType, and matrix. The ImageView drawable + * must remain the same between both start and end states, but the + * {@link ImageView#setScaleType(android.widget.ImageView.ScaleType)} may + * differ. + */ +public class MoveImage extends Transition { + private static final String TAG = "MoveImage"; + private static final String PROPNAME_MATRIX = "android:moveImage:matrix"; + private static final String PROPNAME_BOUNDS = "android:moveImage:bounds"; + private static final String PROPNAME_CLIP = "android:moveImage:clip"; + private static final String PROPNAME_DRAWABLE = "android:moveImage:drawable"; + + private int[] mTempLoc = new int[2]; + + private static final String[] sTransitionProperties = { + PROPNAME_MATRIX, + PROPNAME_BOUNDS, + PROPNAME_CLIP, + PROPNAME_DRAWABLE, + }; + + private void captureValues(TransitionValues transitionValues) { + View view = transitionValues.view; + if (!(view instanceof ImageView) || view.getVisibility() != View.VISIBLE) { + return; + } + Map<String, Object> values = transitionValues.values; + + ViewGroup parent = (ViewGroup) view.getParent(); + parent.getLocationInWindow(mTempLoc); + int paddingLeft = view.getPaddingLeft(); + int paddingTop = view.getPaddingTop(); + int paddingRight = view.getPaddingRight(); + int paddingBottom = view.getPaddingBottom(); + int left = mTempLoc[0] + paddingLeft + view.getLeft() + Math.round(view.getTranslationX()); + int top = mTempLoc[1] + paddingTop + view.getTop() + Math.round(view.getTranslationY()); + int right = left + view.getWidth() - paddingRight - paddingLeft; + int bottom = top + view.getHeight() - paddingTop - paddingBottom; + + Rect bounds = new Rect(left, top, right, bottom); + values.put(PROPNAME_BOUNDS, bounds); + ImageView imageView = (ImageView) view; + Matrix matrix = getMatrix(imageView); + values.put(PROPNAME_MATRIX, matrix); + values.put(PROPNAME_CLIP, findClip(imageView)); + values.put(PROPNAME_DRAWABLE, imageView.getDrawable()); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + captureValues(transitionValues); + } + + @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + + /** + * Creates an Animator for ImageViews moving, changing dimensions, and/or changing + * {@link android.widget.ImageView.ScaleType}. + * @param sceneRoot The root of the transition hierarchy. + * @param startValues The values for a specific target in the start scene. + * @param endValues The values for the target in the end scene. + * @return An Animator to move an ImageView or null if the View is not an ImageView, + * the Drawable changed, the View is not VISIBLE, or there was no change. + */ + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null + || startValues.values.get(PROPNAME_BOUNDS) == null + || endValues.values.get(PROPNAME_BOUNDS) == null + || startValues.values.get(PROPNAME_DRAWABLE) + != endValues.values.get(PROPNAME_DRAWABLE)) { + return null; + } + ArrayList<PropertyValuesHolder> changes = new ArrayList<PropertyValuesHolder>(); + + Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX); + Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX); + + if (!startMatrix.equals(endMatrix)) { + changes.add(PropertyValuesHolder.ofObject(MatrixClippedDrawable.MATRIX_PROPERTY, + new MatrixEvaluator(), startMatrix, endMatrix)); + } + + sceneRoot.getLocationInWindow(mTempLoc); + int rootX = mTempLoc[0]; + int rootY = mTempLoc[1]; + final ImageView imageView = (ImageView) endValues.view; + + Drawable drawable = imageView.getDrawable(); + + Rect startBounds = new Rect((Rect) startValues.values.get(PROPNAME_BOUNDS)); + Rect endBounds = new Rect((Rect) endValues.values.get(PROPNAME_BOUNDS)); + startBounds.offset(-rootX, -rootY); + endBounds.offset(-rootX, -rootY); + + if (!startBounds.equals(endBounds)) { + changes.add(PropertyValuesHolder.ofObject("bounds", new RectEvaluator(new Rect()), + startBounds, endBounds)); + } + + Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP); + Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP); + if (startClip != null || endClip != null) { + startClip = nonNullClip(startClip, sceneRoot, rootX, rootY); + endClip = nonNullClip(endClip, sceneRoot, rootX, rootY); + + expandClip(startBounds, startMatrix, startClip, endClip); + expandClip(endBounds, endMatrix, endClip, startClip); + boolean clipped = !startClip.contains(startBounds) || !endClip.contains(endBounds); + if (!clipped) { + startClip = null; + } else if (!startClip.equals(endClip)) { + changes.add(PropertyValuesHolder.ofObject(MatrixClippedDrawable.CLIP_PROPERTY, + new RectEvaluator(), startClip, endClip)); + } + } + + if (changes.isEmpty()) { + return null; + } + + drawable = drawable.getConstantState().newDrawable(); + final MatrixClippedDrawable matrixClippedDrawable = new MatrixClippedDrawable(drawable); + matrixClippedDrawable.setMatrix(startMatrix); + matrixClippedDrawable.setBounds(startBounds); + matrixClippedDrawable.setClipRect(startClip); + + imageView.setVisibility(View.INVISIBLE); + final ViewGroupOverlay overlay = sceneRoot.getOverlay(); + overlay.add(matrixClippedDrawable); + ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(matrixClippedDrawable, + changes.toArray(new PropertyValuesHolder[changes.size()])); + + AnimatorListenerAdapter listener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + imageView.setVisibility(View.VISIBLE); + overlay.remove(matrixClippedDrawable); + } + + @Override + public void onAnimationPause(Animator animation) { + imageView.setVisibility(View.VISIBLE); + overlay.remove(matrixClippedDrawable); + } + + @Override + public void onAnimationResume(Animator animation) { + imageView.setVisibility(View.INVISIBLE); + overlay.add(matrixClippedDrawable); + } + }; + + animator.addListener(listener); + animator.addPauseListener(listener); + + return animator; + } + + private static Rect nonNullClip(Rect clip, ViewGroup sceneRoot, int rootX, int rootY) { + if (clip != null) { + clip = new Rect(clip); + clip.offset(-rootX, -rootY); + } else { + clip = new Rect(0, 0, sceneRoot.getWidth(), sceneRoot.getHeight()); + } + return clip; + } + + private static void expandClip(Rect bounds, Matrix matrix, Rect clip, Rect otherClip) { + RectF boundsF = new RectF(bounds); + matrix.mapRect(boundsF); + clip.left = expandMinDimension(boundsF.left, clip.left, otherClip.left); + clip.top = expandMinDimension(boundsF.top, clip.top, otherClip.top); + clip.right = expandMaxDimension(boundsF.right, clip.right, otherClip.right); + clip.bottom = expandMaxDimension(boundsF.bottom, clip.bottom, otherClip.bottom); + } + + private static int expandMinDimension(float boundsDimension, int clipDimension, + int otherClipDimension) { + if (clipDimension > boundsDimension) { + // Already clipped in that dimension, return the clipped value + return clipDimension; + } + return Math.min(clipDimension, otherClipDimension); + } + + private static int expandMaxDimension(float boundsDimension, int clipDimension, + int otherClipDimension) { + return -expandMinDimension(-boundsDimension, -clipDimension, -otherClipDimension); + } + + private static Matrix getMatrix(ImageView imageView) { + Drawable drawable = imageView.getDrawable(); + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + ImageView.ScaleType scaleType = imageView.getScaleType(); + if (drawableWidth <= 0 || drawableHeight <= 0 || scaleType == ImageView.ScaleType.FIT_XY) { + return null; + } + return new Matrix(imageView.getImageMatrix()); + } + + private Rect findClip(ImageView imageView) { + if (imageView.getCropToPadding()) { + Rect clip = getClip(imageView); + clip.left += imageView.getPaddingLeft(); + clip.right -= imageView.getPaddingRight(); + clip.top += imageView.getPaddingTop(); + clip.bottom -= imageView.getPaddingBottom(); + return clip; + } else { + View view = imageView; + ViewParent viewParent; + while ((viewParent = view.getParent()) instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) viewParent; + if (viewGroup.getClipChildren()) { + Rect clip = getClip(view); + return clip; + } + view = viewGroup; + } + } + return null; + } + + private Rect getClip(View clipView) { + Rect clipBounds = clipView.getClipBounds(); + if (clipBounds == null) { + clipBounds = new Rect(clipView.getLeft(), clipView.getTop(), + clipView.getRight(), clipView.getBottom()); + } + + ViewParent parent = clipView.getParent(); + if (parent instanceof ViewGroup) { + ViewGroup parentViewGroup = (ViewGroup) parent; + parentViewGroup.getLocationInWindow(mTempLoc); + clipBounds.offset(mTempLoc[0], mTempLoc[1]); + } + + return clipBounds; + } + + @Override + public Transition clone() { + MoveImage clone = (MoveImage) super.clone(); + clone.mTempLoc = new int[2]; + return clone; + } + + private static class MatrixEvaluator implements TypeEvaluator<Matrix> { + static final Matrix sIdentity = new Matrix(); + float[] mTempStartValues = new float[9]; + float[] mTempEndValues = new float[9]; + Matrix mTempMatrix = new Matrix(); + + @Override + public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) { + if (startValue == null && endValue == null) { + return null; + } + if (startValue == null) { + startValue = sIdentity; + } else if (endValue == null) { + endValue = sIdentity; + } + startValue.getValues(mTempStartValues); + endValue.getValues(mTempEndValues); + for (int i = 0; i < 9; i++) { + float diff = mTempEndValues[i] - mTempStartValues[i]; + mTempEndValues[i] = mTempStartValues[i] + (fraction * diff); + } + mTempMatrix.setValues(mTempEndValues); + return mTempMatrix; + } + } +} diff --git a/core/java/android/transition/SidePropagation.java b/core/java/android/transition/SidePropagation.java new file mode 100644 index 0000000..c331945 --- /dev/null +++ b/core/java/android/transition/SidePropagation.java @@ -0,0 +1,165 @@ +/* + * 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. + */ +package android.transition; + +import android.graphics.Rect; +import android.util.FloatMath; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * A <code>TransitionPropagation</code> that propagates based on the distance to the side + * and, orthogonally, the distance to epicenter. If the transitioning View is visible in + * the start of the transition, then it will transition sooner when closer to the side and + * later when farther. If the view is not visible in the start of the transition, then + * it will transition later when closer to the side and sooner when farther from the edge. + * This is the default TransitionPropagation used with {@link android.transition.Slide}. + */ +public class SidePropagation extends VisibilityPropagation { + private static final String TAG = "SlidePropagation"; + + /** + * Transition propagates relative to the distance of the left side of the scene. + */ + public static final int LEFT = Slide.LEFT; + + /** + * Transition propagates relative to the distance of the top of the scene. + */ + public static final int TOP = Slide.TOP; + + /** + * Transition propagates relative to the distance of the right side of the scene. + */ + public static final int RIGHT = Slide.RIGHT; + + /** + * Transition propagates relative to the distance of the bottom of the scene. + */ + public static final int BOTTOM = Slide.BOTTOM; + + private float mPropagationSpeed = 4.0f; + private int mSide = BOTTOM; + + /** + * Sets the side that is used to calculate the transition propagation. If the transitioning + * View is visible in the start of the transition, then it will transition sooner when + * closer to the side and later when farther. If the view is not visible in the start of + * the transition, then it will transition later when closer to the side and sooner when + * farther from the edge. The default is {@link #BOTTOM}. + * + * @param side The side that is used to calculate the transition propagation. Must be one of + * {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, or {@link #BOTTOM}. + */ + public void setSide(int side) { + mSide = side; + } + + /** + * Sets the speed at which transition propagation happens, relative to the duration of the + * Transition. A <code>propagationSpeed</code> of 1 means that a View centered at the side + * set in {@link #setSide(int)} and View centered at the opposite edge will have a difference + * in start delay of approximately the duration of the Transition. A speed of 2 means the + * start delay difference will be approximately half of the duration of the transition. A + * value of 0 is illegal, but negative values will invert the propagation. + * + * @param propagationSpeed The speed at which propagation occurs, relative to the duration + * of the transition. A speed of 4 means it works 4 times as fast + * as the duration of the transition. May not be 0. + */ + public void setPropagationSpeed(float propagationSpeed) { + if (propagationSpeed == 0) { + throw new IllegalArgumentException("propagationSpeed may not be 0"); + } + mPropagationSpeed = propagationSpeed; + } + + @Override + public long getStartDelay(ViewGroup sceneRoot, Transition transition, + TransitionValues startValues, TransitionValues endValues) { + if (startValues == null && endValues == null) { + return 0; + } + int directionMultiplier = 1; + Rect epicenter = transition.getEpicenter(); + TransitionValues positionValues; + if (endValues == null || getViewVisibility(startValues) == View.VISIBLE) { + positionValues = startValues; + directionMultiplier = -1; + } else { + positionValues = endValues; + } + + int viewCenterX = getViewX(positionValues); + int viewCenterY = getViewY(positionValues); + + int[] loc = new int[2]; + sceneRoot.getLocationOnScreen(loc); + int left = loc[0] + Math.round(sceneRoot.getTranslationX()); + int top = loc[1] + Math.round(sceneRoot.getTranslationY()); + int right = left + sceneRoot.getWidth(); + int bottom = top + sceneRoot.getHeight(); + + int epicenterX; + int epicenterY; + if (epicenter != null) { + epicenterX = epicenter.centerX(); + epicenterY = epicenter.centerY(); + } else { + epicenterX = (left + right) / 2; + epicenterY = (top + bottom) / 2; + } + + float distance = distance(viewCenterX, viewCenterY, epicenterX, epicenterY, + left, top, right, bottom); + float maxDistance = getMaxDistance(sceneRoot); + float distanceFraction = distance/maxDistance; + + return Math.round(transition.getDuration() * directionMultiplier / mPropagationSpeed + * distanceFraction); + } + + private int distance(int viewX, int viewY, int epicenterX, int epicenterY, + int left, int top, int right, int bottom) { + int distance = 0; + switch (mSide) { + case LEFT: + distance = right - viewX + Math.abs(epicenterY - viewY); + break; + case TOP: + distance = bottom - viewY + Math.abs(epicenterX - viewX); + break; + case RIGHT: + distance = viewX - left + Math.abs(epicenterY - viewY); + break; + case BOTTOM: + distance = viewY - top + Math.abs(epicenterX - viewX); + break; + } + return distance; + } + + private int getMaxDistance(ViewGroup sceneRoot) { + switch (mSide) { + case LEFT: + case RIGHT: + return sceneRoot.getWidth(); + default: + return sceneRoot.getHeight(); + } + } +} diff --git a/core/java/android/transition/Slide.java b/core/java/android/transition/Slide.java index b38973c..0ff8ddd 100644 --- a/core/java/android/transition/Slide.java +++ b/core/java/android/transition/Slide.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * 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. @@ -13,53 +13,240 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.transition; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.graphics.Rect; +import android.util.Log; +import android.util.Property; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; /** - * This transition captures the visibility of target objects before and - * after a scene change and animates any changes by sliding the target - * objects into or out of place. - * - * @hide + * This transition tracks changes to the visibility of target views in the + * start and end scenes and moves views in or out from one of the edges of the + * scene. Visibility is determined by both the + * {@link View#setVisibility(int)} state of the view as well as whether it + * is parented in the current view hierarchy. Disappearing Views are + * limited as described in {@link Visibility#onDisappear(android.view.ViewGroup, + * TransitionValues, int, TransitionValues, int)}. */ public class Slide extends Visibility { + private static final String TAG = "Slide"; - // TODO: Add parameter for sliding factor - it's hard-coded below + /** + * Move Views in or out of the left edge of the scene. + * @see #setSlideEdge(int) + */ + public static final int LEFT = 0; - private static final TimeInterpolator sAccelerator = new AccelerateInterpolator(); - private static final TimeInterpolator sDecelerator = new DecelerateInterpolator(); + /** + * Move Views in or out of the top edge of the scene. + * @see #setSlideEdge(int) + */ + public static final int TOP = 1; - @Override - public Animator onAppear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { - View endView = (endValues != null) ? endValues.view : null; - endView.setTranslationY(-2 * endView.getHeight()); - ObjectAnimator anim = ObjectAnimator.ofFloat(endView, View.TRANSLATION_Y, - -2 * endView.getHeight(), 0); - anim.setInterpolator(sDecelerator); + /** + * Move Views in or out of the right edge of the scene. + * @see #setSlideEdge(int) + */ + public static final int RIGHT = 2; + + /** + * Move Views in or out of the bottom edge of the scene. This is the + * default slide direction. + * @see #setSlideEdge(int) + */ + public static final int BOTTOM = 3; + + private static final TimeInterpolator sDecelerate = new DecelerateInterpolator(); + private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); + + private int[] mTempLoc = new int[2]; + private CalculateSlide mSlideCalculator = sCalculateBottom; + + private interface CalculateSlide { + /** Returns the translation value for view when it out of the scene */ + float getGone(ViewGroup sceneRoot, View view); + + /** Returns the translation value for view when it is in the scene */ + float getHere(View view); + + /** Returns the property to animate translation */ + Property<View, Float> getProperty(); + } + + private static abstract class CalculateSlideHorizontal implements CalculateSlide { + @Override + public float getHere(View view) { + return view.getTranslationX(); + } + + @Override + public Property<View, Float> getProperty() { + return View.TRANSLATION_X; + } + } + + private static abstract class CalculateSlideVertical implements CalculateSlide { + @Override + public float getHere(View view) { + return view.getTranslationY(); + } + + @Override + public Property<View, Float> getProperty() { + return View.TRANSLATION_Y; + } + } + + private static final CalculateSlide sCalculateLeft = new CalculateSlideHorizontal() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationX() - sceneRoot.getWidth(); + } + }; + + private static final CalculateSlide sCalculateTop = new CalculateSlideVertical() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationY() - sceneRoot.getHeight(); + } + }; + + private static final CalculateSlide sCalculateRight = new CalculateSlideHorizontal() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationX() + sceneRoot.getWidth(); + } + }; + + private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() { + @Override + public float getGone(ViewGroup sceneRoot, View view) { + return view.getTranslationY() + sceneRoot.getHeight(); + } + }; + + /** + * Constructor using the default {@link android.transition.Slide#BOTTOM} + * slide edge direction. + */ + public Slide() { + setSlideEdge(BOTTOM); + } + + /** + * Constructor using the provided slide edge direction. + */ + public Slide(int slideEdge) { + setSlideEdge(slideEdge); + } + + /** + * Change the edge that Views appear and disappear from. + * @param slideEdge The edge of the scene to use for Views appearing and disappearing. + */ + public void setSlideEdge(int slideEdge) { + switch (slideEdge) { + case LEFT: + mSlideCalculator = sCalculateLeft; + break; + case TOP: + mSlideCalculator = sCalculateTop; + break; + case RIGHT: + mSlideCalculator = sCalculateRight; + break; + case BOTTOM: + mSlideCalculator = sCalculateBottom; + break; + default: + throw new IllegalArgumentException("Invalid slide direction"); + } + SidePropagation propagation = new SidePropagation(); + propagation.setSide(slideEdge); + setPropagation(propagation); + } + + private Animator createAnimation(final View view, Property<View, Float> property, + float start, float end, float terminalValue, TimeInterpolator interpolator) { + view.setTranslationY(start); + if (start == end) { + return null; + } + final ObjectAnimator anim = ObjectAnimator.ofFloat(view, property, start, end); + + SlideAnimatorListener listener = new SlideAnimatorListener(view, terminalValue, end); + anim.addListener(listener); + anim.addPauseListener(listener); + anim.setInterpolator(interpolator); return anim; } @Override - public Animator onDisappear(ViewGroup sceneRoot, - TransitionValues startValues, int startVisibility, - TransitionValues endValues, int endVisibility) { - View startView = (startValues != null) ? startValues.view : null; - startView.setTranslationY(0); - ObjectAnimator anim = ObjectAnimator.ofFloat(startView, View.TRANSLATION_Y, 0, - -2 * startView.getHeight()); - anim.setInterpolator(sAccelerator); - return anim; + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (endValues == null) { + return null; + } + float end = mSlideCalculator.getHere(view); + float start = mSlideCalculator.getGone(sceneRoot, view); + return createAnimation(view, mSlideCalculator.getProperty(), start, end, end, sDecelerate); } + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + float start = mSlideCalculator.getHere(view); + float end = mSlideCalculator.getGone(sceneRoot, view); + + return createAnimation(view, mSlideCalculator.getProperty(), start, end, start, + sAccelerate); + } + + private static class SlideAnimatorListener extends AnimatorListenerAdapter { + private boolean mCanceled = false; + private float mPausedY; + private final View mView; + private final float mEndY; + private final float mTerminalY; + + public SlideAnimatorListener(View view, float terminalY, float endY) { + mView = view; + mTerminalY = terminalY; + mEndY = endY; + } + + @Override + public void onAnimationCancel(Animator animator) { + mView.setTranslationY(mTerminalY); + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animator) { + if (!mCanceled) { + mView.setTranslationY(mTerminalY); + } + } + + @Override + public void onAnimationPause(Animator animator) { + mPausedY = mView.getTranslationY(); + mView.setTranslationY(mEndY); + } + + @Override + public void onAnimationResume(Animator animator) { + mView.setTranslationY(mPausedY); + } + } } diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index c88b4c0..b7ae31e 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -19,10 +19,12 @@ package android.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; +import android.graphics.Rect; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; +import android.util.SparseLongArray; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; @@ -60,10 +62,18 @@ import java.util.List; * <p>Transitions can be declared in XML resource files inside the <code>res/transition</code> * directory. Transition resources consist of a tag name for one of the Transition * subclasses along with attributes to define some of the attributes of that transition. - * For example, here is a minimal resource file that declares a {@link ChangeBounds} transition:</p> + * For example, here is a minimal resource file that declares a {@link ChangeBounds} transition: * * {@sample development/samples/ApiDemos/res/transition/changebounds.xml ChangeBounds} * + * <p>{@link android.transition.Explode} transition:</p> + * + * {@sample development/samples/ApiDemos/res/transition/explode.xml Explode} + * + * <p>{@link android.transition.MoveImage} transition:</p> + * + * {@sample development/samples/ApiDemos/res/transition/move_image.xml MoveImage} + * * <p>Note that attributes for the transition are not required, just as they are * optional when declared in code; Transitions created from XML resources will use * the same defaults as their code-created equivalents. Here is a slightly more @@ -87,7 +97,8 @@ import java.util.List; * * Further information on XML resource descriptions for transitions can be found for * {@link android.R.styleable#Transition}, {@link android.R.styleable#TransitionSet}, - * {@link android.R.styleable#TransitionTarget}, and {@link android.R.styleable#Fade}. + * {@link android.R.styleable#TransitionTarget}, {@link android.R.styleable#Fade}, and + * {@link android.R.styleable#Slide}. * */ public abstract class Transition implements Cloneable { @@ -149,6 +160,13 @@ public abstract class Transition implements Cloneable { // to be run in runAnimators() ArrayList<Animator> mAnimators = new ArrayList<Animator>(); + // The function for calculating the Animation start delay. + TransitionPropagation mPropagation; + + // The rectangular region for Transitions like Explode and TransitionPropagations + // like CircularPropagation + EpicenterCallback mEpicenterCallback; + /** * Constructs a Transition object with no target objects. A transition with * no targets defaults to running on all target objects in the scene hierarchy @@ -435,6 +453,9 @@ public abstract class Transition implements Cloneable { endValuesList.add(end); } ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); + long minStartDelay = Long.MAX_VALUE; + int minAnimator = mAnimators.size(); + SparseLongArray startDelays = new SparseLongArray(); for (int i = 0; i < startValuesList.size(); ++i) { TransitionValues start = startValuesList.get(i); TransitionValues end = endValuesList.get(i); @@ -497,6 +518,12 @@ public abstract class Transition implements Cloneable { view = (start != null) ? start.view : null; } if (animator != null) { + if (mPropagation != null) { + long delay = mPropagation + .getStartDelay(sceneRoot, this, start, end); + startDelays.put(mAnimators.size(), delay); + minStartDelay = Math.min(delay, minStartDelay); + } AnimationInfo info = new AnimationInfo(view, getName(), sceneRoot.getWindowId(), infoValues); runningAnimators.put(animator, info); @@ -506,6 +533,14 @@ public abstract class Transition implements Cloneable { } } } + if (minStartDelay != 0) { + for (int i = 0; i < startDelays.size(); i++) { + int index = startDelays.keyAt(i); + Animator animator = mAnimators.get(index); + long delay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay(); + animator.setStartDelay(delay); + } + } } /** @@ -565,7 +600,7 @@ public abstract class Transition implements Cloneable { /** * This is called internally once all animations have been set up by the - * transition hierarchy. \ + * transition hierarchy. * * @hide */ @@ -1010,6 +1045,7 @@ public abstract class Transition implements Cloneable { } else { captureEndValues(values); } + capturePropagationValues(values); if (start) { mStartValues.viewValues.put(view, values); if (id >= 0) { @@ -1035,6 +1071,7 @@ public abstract class Transition implements Cloneable { } else { captureEndValues(values); } + capturePropagationValues(values); if (start) { mStartValues.viewValues.put(view, values); } else { @@ -1122,6 +1159,7 @@ public abstract class Transition implements Cloneable { } else { captureEndValues(values); } + capturePropagationValues(values); if (start) { if (!isListViewItem) { mStartValues.viewValues.put(view, values); @@ -1340,7 +1378,7 @@ public abstract class Transition implements Cloneable { animator.setDuration(getDuration()); } if (getStartDelay() >= 0) { - animator.setStartDelay(getStartDelay()); + animator.setStartDelay(getStartDelay() + animator.getStartDelay()); } if (getInterpolator() != null) { animator.setInterpolator(getInterpolator()); @@ -1473,6 +1511,98 @@ public abstract class Transition implements Cloneable { return this; } + /** + * Sets the callback to use to find the epicenter of a Transition. A null value indicates + * that there is no epicenter in the Transition and getEpicenter() will return null. + * Transitions like {@link android.transition.Explode} use a point or Rect to orient + * the direction of travel. This is called the epicenter of the Transition and is + * typically centered on a touched View. The + * {@link android.transition.Transition.EpicenterCallback} allows a Transition to + * dynamically retrieve the epicenter during a Transition. + * @param epicenterCallback The callback to use to find the epicenter of the Transition. + */ + public void setEpicenterCallback(EpicenterCallback epicenterCallback) { + mEpicenterCallback = epicenterCallback; + } + + /** + * Returns the callback used to find the epicenter of the Transition. + * Transitions like {@link android.transition.Explode} use a point or Rect to orient + * the direction of travel. This is called the epicenter of the Transition and is + * typically centered on a touched View. The + * {@link android.transition.Transition.EpicenterCallback} allows a Transition to + * dynamically retrieve the epicenter during a Transition. + * @return the callback used to find the epicenter of the Transition. + */ + public EpicenterCallback getEpicenterCallback() { + return mEpicenterCallback; + } + + /** + * Returns the epicenter as specified by the + * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists. + * @return the epicenter as specified by the + * {@link android.transition.Transition.EpicenterCallback} or null if no callback exists. + * @see #setEpicenterCallback(android.transition.Transition.EpicenterCallback) + */ + public Rect getEpicenter() { + if (mEpicenterCallback == null) { + return null; + } + return mEpicenterCallback.getEpicenter(this); + } + + /** + * Sets the method for determining Animator start delays. + * When a Transition affects several Views like {@link android.transition.Explode} or + * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect + * such that the Animator start delay depends on position of the View. The + * TransitionPropagation specifies how the start delays are calculated. + * @param transitionPropagation The class used to determine the start delay of + * Animators created by this Transition. A null value + * indicates that no delay should be used. + */ + public void setPropagation(TransitionPropagation transitionPropagation) { + mPropagation = transitionPropagation; + } + + /** + * Returns the {@link android.transition.TransitionPropagation} used to calculate Animator start + * delays. + * When a Transition affects several Views like {@link android.transition.Explode} or + * {@link android.transition.Slide}, there may be a desire to have a "wave-front" effect + * such that the Animator start delay depends on position of the View. The + * TransitionPropagation specifies how the start delays are calculated. + * @return the {@link android.transition.TransitionPropagation} used to calculate Animator start + * delays. This is null by default. + */ + public TransitionPropagation getPropagation() { + return mPropagation; + } + + /** + * Captures TransitionPropagation values for the given view and the + * hierarchy underneath it. + */ + void capturePropagationValues(TransitionValues transitionValues) { + if (mPropagation != null) { + String[] propertyNames = mPropagation.getPropagationProperties(); + if (propertyNames == null) { + return; + } + boolean containsAll = true; + for (int i = 0; i < propertyNames.length; i++) { + if (!transitionValues.values.containsKey(propertyNames[i])) { + containsAll = false; + break; + } + } + if (!containsAll) { + mPropagation.captureValues(transitionValues); + } + } + } + Transition setSceneRoot(ViewGroup sceneRoot) { mSceneRoot = sceneRoot; return this; @@ -1710,4 +1840,28 @@ public abstract class Transition implements Cloneable { } } + /** + * Class to get the epicenter of Transition. Use + * {@link #setEpicenterCallback(android.transition.Transition.EpicenterCallback)} to + * set the callback used to calculate the epicenter of the Transition. Override + * {@link #getEpicenter()} to return the rectangular region in screen coordinates of + * the epicenter of the transition. + * @see #setEpicenterCallback(android.transition.Transition.EpicenterCallback) + */ + public static abstract class EpicenterCallback { + + /** + * Implementers must override to return the epicenter of the Transition in screen + * coordinates. Transitions like {@link android.transition.Explode} depend upon + * an epicenter for the Transition. In Explode, Views move toward or away from the + * center of the epicenter Rect along the vector between the epicenter and the center + * of the View appearing and disappearing. Some Transitions, such as + * {@link android.transition.Fade} pay no attention to the epicenter. + * + * @param transition The transition for which the epicenter applies. + * @return The Rect region of the epicenter of <code>transition</code> or null if + * there is no epicenter. + */ + public abstract Rect getEpicenter(Transition transition); + } } diff --git a/core/java/android/transition/TransitionInflater.java b/core/java/android/transition/TransitionInflater.java index 912f2ed..f675c6a 100644 --- a/core/java/android/transition/TransitionInflater.java +++ b/core/java/android/transition/TransitionInflater.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; -import android.text.TextUtils; import android.util.AttributeSet; import android.util.Xml; import android.view.InflateException; @@ -146,7 +145,13 @@ public class TransitionInflater { transition = new ChangeBounds(); newTransition = true; } else if ("slide".equals(name)) { - transition = new Slide(); + transition = createSlideTransition(attrs); + newTransition = true; + } else if ("explode".equals(name)) { + transition = new Explode(); + newTransition = true; + } else if ("moveImage".equals(name)) { + transition = new MoveImage(); newTransition = true; } else if ("autoTransition".equals(name)) { transition = new AutoTransition(); @@ -189,6 +194,15 @@ public class TransitionInflater { return transition; } + private Slide createSlideTransition(AttributeSet attrs) { + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Slide); + int edge = a.getInt(com.android.internal.R.styleable.Slide_slideEdge, Slide.BOTTOM); + Slide slide = new Slide(edge); + a.recycle(); + return slide; + } + private void getTargetIds(XmlPullParser parser, AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException { diff --git a/core/java/android/transition/TransitionPropagation.java b/core/java/android/transition/TransitionPropagation.java new file mode 100644 index 0000000..9a481c2 --- /dev/null +++ b/core/java/android/transition/TransitionPropagation.java @@ -0,0 +1,88 @@ +/* + * 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. + */ +package android.transition; + +import android.graphics.Rect; +import android.view.ViewGroup; + +/** + * Extend <code>TransitionPropagation</code> to customize start delays for Animators created + * in {@link android.transition.Transition#createAnimator(ViewGroup, + * TransitionValues, TransitionValues)}. A Transition such as {@link android.transition.Explode} + * defaults to using {@link android.transition.CircularPropagation} and Views closer to the + * epicenter will move out of the scene later and into the scene sooner than Views farther + * from the epicenter, giving the appearance of inertia. With no TransitionPropagation, all + * Views will react simultaneously to the start of the transition. + * + * @see Transition#setPropagation(TransitionPropagation) + * @see Transition#getEpicenter() + */ +public abstract class TransitionPropagation { + /** + * Called by Transition to alter the Animator start delay. All start delays will be adjusted + * such that the minimum becomes zero. + * @param sceneRoot The root of the View hierarchy running the transition. + * @param transition The transition that created the Animator + * @param startValues The values for a specific target in the start scene. + * @param endValues The values for the target in the end scene. + * @return A start delay to use with the Animator created by <code>transition</code>. The + * delay will be offset by the minimum delay of all <code>TransitionPropagation</code>s + * used in the Transition so that the smallest delay will be 0. Returned values may be + * negative. + */ + public abstract long getStartDelay(ViewGroup sceneRoot, Transition transition, + TransitionValues startValues, TransitionValues endValues); + + /** + * Captures the values in the start or end scene for the properties that this + * transition propagation monitors. These values are then passed as the startValues + * or endValues structure in a later call to + * {@link #getStartDelay(ViewGroup, Transition, TransitionValues, TransitionValues)}. + * The main concern for an implementation is what the + * properties are that the transition cares about and what the values are + * for all of those properties. The start and end values will be compared + * later during the + * {@link #getStartDelay(ViewGroup, Transition, TransitionValues, TransitionValues)}. + * method to determine the start delay. + * + * <p>Subclasses must implement this method. The method should only be called by the + * transition system; it is not intended to be called from external classes.</p> + * + * @param transitionValues The holder for any values that the Transition + * wishes to store. Values are stored in the <code>values</code> field + * of this TransitionValues object and are keyed from + * a String value. For example, to store a view's rotation value, + * a transition might call + * <code>transitionValues.values.put("appname:transitionname:rotation", + * view.getRotation())</code>. The target view will already be stored in + * the transitionValues structure when this method is called. + */ + public abstract void captureValues(TransitionValues transitionValues); + + /** + * Returns the set of property names stored in the {@link TransitionValues} + * object passed into {@link #captureValues(TransitionValues)} that + * this transition propagation cares about for the purposes of preventing + * duplicate capturing of property values. + + * <p>A <code>TransitionPropagation</code> must override this method to prevent + * duplicate capturing of values and must contain at least one </p> + * + * @return An array of property names as described in the class documentation for + * {@link TransitionValues}. + */ + public abstract String[] getPropagationProperties() ; +} diff --git a/core/java/android/transition/TransitionSet.java b/core/java/android/transition/TransitionSet.java index 19d6b3d..966b24d 100644 --- a/core/java/android/transition/TransitionSet.java +++ b/core/java/android/transition/TransitionSet.java @@ -17,6 +17,7 @@ package android.transition; import android.animation.TimeInterpolator; +import android.graphics.Rect; import android.util.AndroidRuntimeException; import android.view.View; import android.view.ViewGroup; @@ -315,6 +316,15 @@ public class TransitionSet extends Transition { } } + @Override + void capturePropagationValues(TransitionValues transitionValues) { + super.capturePropagationValues(transitionValues); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).capturePropagationValues(transitionValues); + } + } + /** @hide */ @Override public void pause(View sceneRoot) { @@ -365,6 +375,24 @@ public class TransitionSet extends Transition { } @Override + public void setPropagation(TransitionPropagation propagation) { + super.setPropagation(propagation); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).setPropagation(propagation); + } + } + + @Override + public void setEpicenterCallback(EpicenterCallback epicenterCallback) { + super.setEpicenterCallback(epicenterCallback); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).setEpicenterCallback(epicenterCallback); + } + } + + @Override String toString(String indent) { String result = super.toString(indent); for (int i = 0; i < mTransitions.size(); ++i) { @@ -383,5 +411,4 @@ public class TransitionSet extends Transition { } return clone; } - } diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java index 44f92cd..7783b6f 100644 --- a/core/java/android/transition/Visibility.java +++ b/core/java/android/transition/Visibility.java @@ -17,6 +17,7 @@ package android.transition; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.view.View; import android.view.ViewGroup; @@ -29,15 +30,20 @@ import android.view.ViewGroup; * information to determine the specific animations to run when visibility * changes occur. Subclasses should implement one or both of the methods * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}, - * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}, + * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or + * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}, + * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. */ public abstract class Visibility extends Transition { private static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; private static final String PROPNAME_PARENT = "android:visibility:parent"; + private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation"; + private static final String[] sTransitionProperties = { PROPNAME_VISIBILITY, PROPNAME_PARENT, + PROPNAME_SCREEN_LOCATION, }; private static class VisibilityInfo { @@ -58,6 +64,9 @@ public abstract class Visibility extends Transition { int visibility = transitionValues.view.getVisibility(); transitionValues.values.put(PROPNAME_VISIBILITY, visibility); transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent()); + int[] loc = new int[2]; + transitionValues.view.getLocationOnScreen(loc); + transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc); } @Override @@ -179,8 +188,11 @@ public abstract class Visibility extends Transition { } /** - * The default implementation of this method does nothing. Subclasses - * should override if they need to create an Animator when targets appear. + * The default implementation of this method calls + * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. + * Subclasses should override this method or + * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}. + * if they need to create an Animator when targets appear. * The method should only be called by the Visibility class; it is * not intended to be called from external classes. * @@ -196,15 +208,53 @@ public abstract class Visibility extends Transition { public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility) { + return onAppear(sceneRoot, endValues.view, startValues, endValues); + } + + /** + * The default implementation of this method returns a null Animator. Subclasses should + * override this method to make targets appear with the desired transition. The + * method should only be called from + * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}. + * + * @param sceneRoot The root of the transition hierarchy + * @param view The View to make appear. This will be in the target scene's View hierarchy and + * will be VISIBLE. + * @param startValues The target values in the start scene + * @param endValues The target values in the end scene + * @return An Animator to be started at the appropriate time in the + * overall transition for this scene change. A null value means no animation + * should be run. + */ + public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { return null; } /** - * The default implementation of this method does nothing. Subclasses - * should override if they need to create an Animator when targets disappear. + * Subclasses should override this method or + * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)} + * if they need to create an Animator when targets disappear. * The method should only be called by the Visibility class; it is * not intended to be called from external classes. - * + * <p> + * The default implementation of this method attempts to find a View to use to call + * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}, + * based on the situation of the View in the View hierarchy. For example, + * if a View was simply removed from its parent, then the View will be added + * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code> + * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. + * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE}, + * then it can be used as the <code>view</code> and the visibility will be changed + * to {@link View#VISIBLE} for the duration of the animation. However, if a View + * is in a hierarchy which is also altering its visibility, the situation can be + * more complicated. In general, if a view that is no longer in the hierarchy in + * the end scene still has a parent (so its parent hierarchy was removed, but it + * was not removed from its parent), then it will be left alone to avoid side-effects from + * improperly removing it from its parent. The only exception to this is if + * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int, + * android.content.Context) created from a layout resource file}, then it is considered + * safe to un-parent the starting scene view in order to make it disappear.</p> * * @param sceneRoot The root of the transition hierarchy * @param startValues The target values in the start scene @@ -218,6 +268,144 @@ public abstract class Visibility extends Transition { public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility) { + View startView = (startValues != null) ? startValues.view : null; + View endView = (endValues != null) ? endValues.view : null; + View overlayView = null; + View viewToKeep = null; + if (endView == null || endView.getParent() == null) { + if (endView != null) { + // endView was removed from its parent - add it to the overlay + overlayView = endView; + } else if (startView != null) { + // endView does not exist. Use startView only under certain + // conditions, because placing a view in an overlay necessitates + // it being removed from its current parent + if (startView.getParent() == null) { + // no parent - safe to use + overlayView = startView; + } else if (startView.getParent() instanceof View && + startView.getParent().getParent() == null) { + View startParent = (View) startView.getParent(); + int id = startParent.getId(); + if (id != View.NO_ID && sceneRoot.findViewById(id) != null && mCanRemoveViews) { + // no parent, but its parent is unparented but the parent + // hierarchy has been replaced by a new hierarchy with the same id + // and it is safe to un-parent startView + overlayView = startView; + } + } + } + } else { + // visibility change + if (endVisibility == View.INVISIBLE) { + viewToKeep = endView; + } else { + // Becoming GONE + if (startView == endView) { + viewToKeep = endView; + } else { + overlayView = startView; + } + } + } + final int finalVisibility = endVisibility; + final ViewGroup finalSceneRoot = sceneRoot; + + if (overlayView != null) { + // TODO: Need to do this for general case of adding to overlay + int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION); + int screenX = screenLoc[0]; + int screenY = screenLoc[1]; + int[] loc = new int[2]; + sceneRoot.getLocationOnScreen(loc); + overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); + overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); + sceneRoot.getOverlay().add(overlayView); + Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues); + if (animator == null) { + sceneRoot.getOverlay().remove(overlayView); + } else { + final View finalOverlayView = overlayView; + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finalSceneRoot.getOverlay().remove(finalOverlayView); + } + + @Override + public void onAnimationPause(Animator animation) { + finalSceneRoot.getOverlay().remove(finalOverlayView); + } + + @Override + public void onAnimationResume(Animator animation) { + finalSceneRoot.getOverlay().add(finalOverlayView); + } + }); + } + return animator; + } + + if (viewToKeep != null) { + viewToKeep.setVisibility(View.VISIBLE); + Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues); + if (animator == null) { + viewToKeep.setVisibility(finalVisibility); + } else { + final View finalViewToKeep = viewToKeep; + animator.addListener(new AnimatorListenerAdapter() { + boolean mCanceled = false; + + @Override + public void onAnimationPause(Animator animation) { + if (!mCanceled) { + finalViewToKeep.setVisibility(finalVisibility); + } + } + + @Override + public void onAnimationResume(Animator animation) { + if (!mCanceled) { + finalViewToKeep.setVisibility(View.VISIBLE); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCanceled) { + finalViewToKeep.setVisibility(finalVisibility); + } + } + }); + } + return animator; + } + return null; + } + + /** + * The default implementation of this method returns a null Animator. Subclasses should + * override this method to make targets disappear with the desired transition. The + * method should only be called from + * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}. + * + * @param sceneRoot The root of the transition hierarchy + * @param view The View to make disappear. This will be in the target scene's View + * hierarchy or in an {@link android.view.ViewGroupOverlay} and will be + * VISIBLE. + * @param startValues The target values in the start scene + * @param endValues The target values in the end scene + * @return An Animator to be started at the appropriate time in the + * overall transition for this scene change. A null value means no animation + * should be run. + */ + public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, + TransitionValues endValues) { return null; } } diff --git a/core/java/android/transition/VisibilityPropagation.java b/core/java/android/transition/VisibilityPropagation.java new file mode 100644 index 0000000..0326d47 --- /dev/null +++ b/core/java/android/transition/VisibilityPropagation.java @@ -0,0 +1,112 @@ +/* + * 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. + */ +package android.transition; + +import android.view.View; + +/** + * Base class for <code>TransitionPropagation</code>s that care about + * View Visibility and the center position of the View. + */ +public abstract class VisibilityPropagation extends TransitionPropagation { + + /** + * The property key used for {@link android.view.View#getVisibility()}. + */ + private static final String PROPNAME_VISIBILITY = "android:visibilityPropagation:visibility"; + + /** + * The property key used for the center of the View in screen coordinates. This is an + * int[2] with the index 0 taking the x coordinate and index 1 taking the y coordinate. + */ + private static final String PROPNAME_VIEW_CENTER = "android:visibilityPropagation:center"; + + private static final String[] VISIBILITY_PROPAGATION_VALUES = { + PROPNAME_VISIBILITY, + PROPNAME_VIEW_CENTER, + }; + + @Override + public void captureValues(TransitionValues values) { + View view = values.view; + values.values.put(PROPNAME_VISIBILITY, view.getVisibility()); + int[] loc = new int[2]; + view.getLocationOnScreen(loc); + loc[0] += Math.round(view.getTranslationX()); + loc[0] += view.getWidth() / 2; + loc[1] += Math.round(view.getTranslationY()); + loc[1] += view.getHeight() / 2; + values.values.put(PROPNAME_VIEW_CENTER, loc); + } + + @Override + public String[] getPropagationProperties() { + return VISIBILITY_PROPAGATION_VALUES; + } + + /** + * Returns {@link android.view.View#getVisibility()} for the View at the time the values + * were captured. + * @param values The TransitionValues captured at the start or end of the Transition. + * @return {@link android.view.View#getVisibility()} for the View at the time the values + * were captured. + */ + public int getViewVisibility(TransitionValues values) { + if (values == null) { + return View.GONE; + } + Integer visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); + if (visibility == null) { + return View.GONE; + } + return visibility; + } + + /** + * Returns the View's center x coordinate, relative to the screen, at the time the values + * were captured. + * @param values The TransitionValues captured at the start or end of the Transition. + * @return the View's center x coordinate, relative to the screen, at the time the values + * were captured. + */ + public int getViewX(TransitionValues values) { + return getViewCoordinate(values, 0); + } + + /** + * Returns the View's center y coordinate, relative to the screen, at the time the values + * were captured. + * @param values The TransitionValues captured at the start or end of the Transition. + * @return the View's center y coordinate, relative to the screen, at the time the values + * were captured. + */ + public int getViewY(TransitionValues values) { + return getViewCoordinate(values, 1); + } + + private static int getViewCoordinate(TransitionValues values, int coordinateIndex) { + if (values == null) { + return -1; + } + + int[] coordinates = (int[]) values.values.get(PROPNAME_VIEW_CENTER); + if (coordinates == null) { + return -1; + } + + return coordinates[coordinateIndex]; + } +} |
