diff --git a/api/current.txt b/api/current.txt index 5b6e3a12be731..c1a02da6ca68e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -512,6 +512,7 @@ package android { field public static final int freezesText = 16843116; // 0x101016c field public static final int fromAlpha = 16843210; // 0x10101ca field public static final int fromDegrees = 16843187; // 0x10101b3 + field public static final int fromScene = 16843737; // 0x10103d9 field public static final int fromXDelta = 16843206; // 0x10101c6 field public static final int fromXScale = 16843202; // 0x10101c2 field public static final int fromYDelta = 16843208; // 0x10101c8 @@ -1012,6 +1013,7 @@ package android { field public static final int targetActivity = 16843266; // 0x1010202 field public static final int targetClass = 16842799; // 0x101002f field public static final int targetDescriptions = 16843680; // 0x10103a0 + field public static final int targetID = 16843736; // 0x10103d8 field public static final int targetPackage = 16842785; // 0x1010021 field public static final int targetSdkVersion = 16843376; // 0x1010270 field public static final int taskAffinity = 16842770; // 0x1010012 @@ -1100,6 +1102,7 @@ package android { field public static final int titleTextStyle = 16843512; // 0x10102f8 field public static final int toAlpha = 16843211; // 0x10101cb field public static final int toDegrees = 16843188; // 0x10101b4 + field public static final int toScene = 16843738; // 0x10103da field public static final int toXDelta = 16843207; // 0x10101c7 field public static final int toXScale = 16843203; // 0x10101c3 field public static final int toYDelta = 16843209; // 0x10101c9 @@ -1114,6 +1117,7 @@ package android { field public static final int transcriptMode = 16843008; // 0x1010100 field public static final int transformPivotX = 16843552; // 0x1010320 field public static final int transformPivotY = 16843553; // 0x1010321 + field public static final int transition = 16843739; // 0x10103db field public static final int translationX = 16843554; // 0x1010322 field public static final int translationY = 16843555; // 0x1010323 field public static final int type = 16843169; // 0x10101a1 @@ -25876,6 +25880,7 @@ package android.view { method public java.lang.CharSequence getContentDescription(); method public final android.content.Context getContext(); method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo(); + method public android.view.transition.Scene getCurrentScene(); method public static int getDefaultSize(int, int); method public android.view.Display getDisplay(); method public final int[] getDrawableState(); @@ -28023,6 +28028,146 @@ package android.view.textservice { } +package android.view.transition { + + public class AutoTransition extends android.view.transition.TransitionGroup { + ctor public AutoTransition(); + } + + public class Crossfade extends android.view.transition.Transition { + ctor public Crossfade(); + method protected void captureValues(android.view.transition.TransitionValues, boolean); + method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + } + + public class Fade extends android.view.transition.Visibility { + ctor public Fade(); + ctor public Fade(int); + field public static final int IN = 1; // 0x1 + field public static final int OUT = 2; // 0x2 + } + + public class Move extends android.view.transition.Transition { + ctor public Move(); + method protected void captureValues(android.view.transition.TransitionValues, boolean); + method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + method public void setReparent(boolean); + method public void setResizeClip(boolean); + } + + public class Recolor extends android.view.transition.Transition { + ctor public Recolor(); + method protected void captureValues(android.view.transition.TransitionValues, boolean); + method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + } + + public class Rotate extends android.view.transition.Transition { + ctor public Rotate(); + method protected void captureValues(android.view.transition.TransitionValues, boolean); + method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + } + + public final class Scene { + ctor public Scene(android.view.ViewGroup); + ctor public Scene(android.view.ViewGroup, int, android.content.Context); + ctor public Scene(android.view.ViewGroup, android.view.ViewGroup); + method public void enter(); + method public void exit(); + method public android.view.ViewGroup getSceneRoot(); + method public void setEnterAction(java.lang.Runnable); + method public void setExitAction(java.lang.Runnable); + } + + public class Slide extends android.view.transition.Visibility { + ctor public Slide(); + } + + public class TextChange extends android.view.transition.Transition { + ctor public TextChange(); + method protected void captureValues(android.view.transition.TransitionValues, boolean); + method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + } + + public abstract class Transition { + ctor public Transition(); + method public void addListener(android.view.transition.Transition.TransitionListener); + method protected void cancelTransition(); + method protected abstract void captureValues(android.view.transition.TransitionValues, boolean); + method public long getDuration(); + method public android.animation.TimeInterpolator getInterpolator(); + method public java.util.ArrayList getListeners(); + method public long getStartDelay(); + method public int[] getTargetIds(); + method public android.view.View[] getTargets(); + method protected void onTransitionCancel(); + method protected void onTransitionEnd(); + method protected void onTransitionStart(); + method protected abstract android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + method protected boolean prePlay(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + method public void removeListener(android.view.transition.Transition.TransitionListener); + method public android.view.transition.Transition setDuration(long); + method public void setInterpolator(android.animation.TimeInterpolator); + method public void setStartDelay(long); + method public android.view.transition.Transition setTargetIds(int...); + method public android.view.transition.Transition setTargets(android.view.View...); + } + + public static abstract interface Transition.TransitionListener { + method public abstract void onTransitionCancel(android.view.transition.Transition); + method public abstract void onTransitionEnd(android.view.transition.Transition); + method public abstract void onTransitionStart(android.view.transition.Transition); + } + + public class TransitionGroup extends android.view.transition.Transition { + ctor public TransitionGroup(); + ctor public TransitionGroup(int); + method public void addTransitions(android.view.transition.Transition...); + method protected void captureValues(android.view.transition.TransitionValues, boolean); + method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + method public void removeTransition(android.view.transition.Transition); + method public void setOrdering(int); + field public static final int SEQUENTIALLY = 1; // 0x1 + field public static final int TOGETHER = 0; // 0x0 + } + + public class TransitionInflater { + method public static android.view.transition.TransitionInflater from(android.content.Context); + method public android.view.transition.Scene inflateScene(int, android.view.ViewGroup); + method public android.view.transition.Transition inflateTransition(int); + method public android.view.transition.TransitionManager inflateTransitionManager(int, android.view.ViewGroup); + } + + public class TransitionManager { + ctor public TransitionManager(); + method public android.view.transition.Transition getDefaultTransition(); + method public static void go(android.view.transition.Scene); + method public static void go(android.view.transition.Scene, android.view.transition.Transition); + method public static void go(android.view.ViewGroup, java.lang.Runnable); + method public static void go(android.view.ViewGroup, java.lang.Runnable, android.view.transition.Transition); + method public void setDefaultTransition(android.view.transition.Transition); + method public void setTransition(android.view.transition.Scene, android.view.transition.Transition); + method public void setTransition(android.view.transition.Scene, android.view.transition.Scene, android.view.transition.Transition); + method public void transitionTo(android.view.transition.Scene); + } + + public class TransitionValues { + ctor public TransitionValues(); + field public final java.util.HashMap values; + field public android.view.View view; + } + + public abstract class Visibility extends android.view.transition.Transition { + ctor public Visibility(); + method protected android.animation.Animator appear(android.view.ViewGroup, android.view.View, int, android.view.View, int); + method protected void captureValues(android.view.transition.TransitionValues, boolean); + method protected android.animation.Animator disappear(android.view.ViewGroup, android.view.View, int, android.view.View, int); + method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); + method protected boolean preAppear(android.view.ViewGroup, android.view.View, int, android.view.View, int); + method protected boolean preDisappear(android.view.ViewGroup, android.view.View, int, android.view.View, int); + } + +} + package android.webkit { public class ConsoleMessage { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 2903b6f266bc1..7aa2cbffeb3cb 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -73,6 +73,7 @@ import android.view.animation.Transformation; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; +import android.view.transition.Scene; import android.widget.ScrollBarDrawable; import static android.os.Build.VERSION_CODES.*; @@ -1572,6 +1573,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ protected Object mTag; + private Scene mCurrentScene = null; + // for mPrivateFlags: /** {@hide} */ static final int PFLAG_WANTS_FOCUS = 0x00000001; @@ -12037,6 +12040,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mCurrentAnimation = null; + mCurrentScene = null; + resetAccessibilityStateChanged(); } @@ -17757,6 +17762,31 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return mAnimator; } + /** + * Set the current Scene that this view is in. The current scene is set only + * on the root view of a scene, not for every view in that hierarchy. This + * information is used by Scene to determine whether there is a previous + * scene which should be exited before the new scene is entered. + * + * @param scene The new scene being set on the view + * + * @hide + */ + public void setCurrentScene(Scene scene) { + mCurrentScene = scene; + } + + /** + * Gets the current {@link Scene} set on this view. A scene is set on a view + * only if that view is the scene root. + * + * @return The current Scene set on this view. A value of null indicates that + * no Scene is current set. + */ + public Scene getCurrentScene() { + return mCurrentScene; + } + /** * Interface definition for a callback to be invoked when a hardware key event is * dispatched to this view. The callback will be invoked before the key event is diff --git a/core/java/android/view/transition/AutoTransition.java b/core/java/android/view/transition/AutoTransition.java new file mode 100644 index 0000000000000..d94cf2c42438b --- /dev/null +++ b/core/java/android/view/transition/AutoTransition.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2013 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.view.transition; + +/** + * Utility class for creating a default transition that automatically fades, + * moves, and resizes views during a scene change. + */ +public class AutoTransition extends TransitionGroup { + + /** + * Constructs an AutoTransition object, which is a TransitionGroup which + * first fades out disappearing targets, then moves and resizes existing + * targets, and finally fades in appearing targets. + * + */ + public AutoTransition() { + setOrdering(SEQUENTIALLY); + addTransitions(new Fade(Fade.OUT), new Move(), new Fade(Fade.IN)); + } +} diff --git a/core/java/android/view/transition/Crossfade.java b/core/java/android/view/transition/Crossfade.java new file mode 100644 index 0000000000000..babf58f59a8f0 --- /dev/null +++ b/core/java/android/view/transition/Crossfade.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.RectEvaluator; +import android.animation.ValueAnimator; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; + +import java.util.HashMap; + +/** + * This transition captures bitmap representations of target views before and + * after the scene change and fades between them. + * + *

Note: This transition is not compatible with {@link TextureView} + * or {@link SurfaceView}.

+ */ +public class Crossfade extends Transition { + // TODO: Add a hook that lets a Transition call user code to query whether it should run on + // a given target view. This would save bitmap comparisons in this transition, for example. + + private static final String LOG_TAG = "Crossfade"; + + private static final String PROPNAME_BITMAP = "android:crossfade:bitmap"; + private static final String PROPNAME_DRAWABLE = "android:crossfade:drawable"; + private static final String PROPNAME_BOUNDS = "android:crossfade:bounds"; + + private static RectEvaluator sRectEvaluator = new RectEvaluator(); + + @Override + protected boolean prePlay(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return false; + } + final View view = startValues.view; + HashMap startVals = startValues.values; + HashMap endVals = endValues.values; + Bitmap startBitmap = (Bitmap) startVals.get(PROPNAME_BITMAP); + Bitmap endBitmap = (Bitmap) endVals.get(PROPNAME_BITMAP); + Drawable startDrawable = (Drawable) startVals.get(PROPNAME_DRAWABLE); + Drawable endDrawable = (Drawable) endVals.get(PROPNAME_DRAWABLE); + if (Transition.DBG) { + Log.d(LOG_TAG, "StartBitmap.sameAs(endBitmap) = " + startBitmap.sameAs(endBitmap) + + " for start, end: " + startBitmap + ", " + endBitmap); + } + if (startDrawable != null && endDrawable != null && !startBitmap.sameAs(endBitmap)) { + view.getOverlay().add(endDrawable); + view.getOverlay().add(startDrawable); + return true; + } else { + return false; + } + } + + @Override + protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + HashMap startVals = startValues.values; + HashMap endVals = endValues.values; + + final View view = endValues.view; + Rect startBounds = (Rect) startVals.get(PROPNAME_BOUNDS); + Rect endBounds = (Rect) endVals.get(PROPNAME_BOUNDS); + final BitmapDrawable startDrawable = (BitmapDrawable) startVals.get(PROPNAME_DRAWABLE); + final BitmapDrawable endDrawable = (BitmapDrawable) endVals.get(PROPNAME_DRAWABLE); + + // The transition works by placing the end drawable under the start drawable and + // gradually fading out the start drawable. So it's not really a cross-fade, but rather + // a reveal of the end scene over time. Also, animate the bounds of both drawables + // to mimic the change in the size of the view itself between scenes. + ObjectAnimator anim = ObjectAnimator.ofInt(startDrawable, "alpha", 0); + anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + // TODO: some way to auto-invalidate views based on drawable changes? callbacks? + view.invalidate(startDrawable.getBounds()); + } + }); + if (Transition.DBG) { + Log.d(LOG_TAG, "Crossfade: created anim " + anim + " for start, end values " + + startValues + ", " + endValues); + } + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.getOverlay().remove(startDrawable); + view.getOverlay().remove(endDrawable); + } + }); + AnimatorSet set = new AnimatorSet(); + set.playTogether(anim); + if (!startBounds.equals(endBounds)) { + if (Transition.DBG) { + Log.d(LOG_TAG, "animating from startBounds to endBounds: " + + startBounds + ", " + endBounds); + } + Animator anim2 = ObjectAnimator.ofObject(startDrawable, "bounds", + sRectEvaluator, startBounds, endBounds); + Animator anim3 = ObjectAnimator.ofObject(endDrawable, "bounds", + sRectEvaluator, startBounds, endBounds); + set.playTogether(anim2); + set.playTogether(anim3); + } + return set; + } + + @Override + protected void captureValues(TransitionValues values, boolean start) { + View view = values.view; + values.values.put(PROPNAME_BOUNDS, new Rect(0, 0, + view.getWidth(), view.getHeight())); + + if (Transition.DBG) { + Log.d(LOG_TAG, "Captured bounds " + values.values.get(PROPNAME_BOUNDS) + ": start = " + + start); + } + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), + Bitmap.Config.ARGB_8888); + if (view instanceof TextureView) { + bitmap = ((TextureView) view).getBitmap(); + } else { + Canvas c = new Canvas(bitmap); + view.draw(c); + } + values.values.put(PROPNAME_BITMAP, bitmap); + // TODO: I don't have resources, can't call the non-deprecated method? + BitmapDrawable drawable = new BitmapDrawable(bitmap); + // TODO: lrtb will be wrong if the view has transXY set + drawable.setBounds(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + values.values.put(PROPNAME_DRAWABLE, drawable); + } + +} diff --git a/core/java/android/view/transition/Fade.java b/core/java/android/view/transition/Fade.java new file mode 100644 index 0000000000000..8e4909dbfbe37 --- /dev/null +++ b/core/java/android/view/transition/Fade.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * This transition tracks changes to the visibility of target views in the + * start and end scenes and fades views in or out when they become visible + * or non-visible. 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. + */ +public class Fade extends Visibility { + + private static final String LOG_TAG = "Fade"; + + /** + * Fading mode used in {@link #Fade(int)} to make the transition + * operate on targets that are appearing. Maybe be combined with + * {@link #OUT} to fade both in and out. + */ + public static final int IN = 0x1; + /** + * Fading mode used in {@link #Fade(int)} to make the transition + * operate on targets that are disappearing. Maybe be combined with + * {@link #IN} to fade both in and out. + */ + public static final int OUT = 0x2; + + private int mFadingMode; + + /** + * Constructs a Fade transition that will fade targets in and out. + */ + public Fade() { + this(IN | OUT); + } + + /** + * Constructs a Fade transition that will fade targets in + * and/or out, according to the value of fadingMode. + * + * @param fadingMode The behavior of this transition, a combination of + * {@link #IN} and {@link #OUT}. + */ + public Fade(int fadingMode) { + mFadingMode = fadingMode; + } + + /** + * Utility method to handle creating and running the Animator. + */ + private Animator runAnimation(View view, float startAlpha, float endAlpha, + Animator.AnimatorListener listener) { + final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", startAlpha, endAlpha); + if (listener != null) { + anim.addListener(listener); + } + // TODO: Maybe extract a method into Transition to run an animation that handles the + // duration/startDelay stuff for all subclasses. + return anim; + } + + @Override + protected boolean preAppear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + if ((mFadingMode & IN) != IN) { + return false; + } + endView.setAlpha(0); + return true; + } + + @Override + protected Animator appear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + if ((mFadingMode & IN) != IN) { + return null; + } + // TODO: hack - retain original value from before preAppear + return runAnimation(endView, 0, 1, null); + // TODO: end listener to make sure we end at 1 no matter what + } + + @Override + protected boolean preDisappear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + if ((mFadingMode & OUT) != OUT) { + return false; + } + if (Transition.DBG) { + Log.d(LOG_TAG, "Fade.predisappear: startView, startVis, endView, endVis = " + + startView + ", " + startVisibility + ", " + endView + ", " + endVisibility); + } + View view; + View overlayView = null; + View viewToKeep = null; + if (endView == null) { + // view was removed: add the start view to the Overlay + view = startView; + overlayView = view; + } 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; + } + } + } + // 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 + sceneRoot.getOverlay().add(overlayView); + return true; + } + 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)); + return true; + } + return false; + } + + @Override + protected Animator disappear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + if ((mFadingMode & OUT) != OUT) { + return null; + } + if (Transition.DBG) { + Log.d(LOG_TAG, "Fade.disappear: startView, startVis, endView, endVis = " + + startView + ", " + startVisibility + ", " + endView + ", " + endVisibility); + } + View view; + View overlayView = null; + View viewToKeep = null; + final int finalVisibility = endVisibility; + if (endView == null) { + // view was removed: add the start view to the Overlay + view = startView; + overlayView = view; + } 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; + } + } + } + // TODO: add automatic facility to Visibility superclass for keeping views around + final float startAlpha = view.getAlpha(); + float endAlpha = 0; + final View finalView = view; + final View finalOverlayView = overlayView; + final View finalViewToKeep = viewToKeep; + final ViewGroup finalSceneRoot = sceneRoot; + final Animator.AnimatorListener endListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finalView.setAlpha(startAlpha); + // TODO: restore view offset from overlay repositioning + if (finalViewToKeep != null) { + finalViewToKeep.setVisibility(finalVisibility); + } + if (finalOverlayView != null) { + finalSceneRoot.getOverlay().remove(finalOverlayView); + } + } + }; + return runAnimation(view, startAlpha, endAlpha, endListener); + } + +} \ No newline at end of file diff --git a/core/java/android/view/transition/Move.java b/core/java/android/view/transition/Move.java new file mode 100644 index 0000000000000..3bd57bd0d624c --- /dev/null +++ b/core/java/android/view/transition/Move.java @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.animation.RectEvaluator; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.view.View; +import android.view.ViewGroup; + +import java.util.HashMap; + +/** + * This transition captures the layout bounds of target views before and after + * the scene change and animates those changes during the transition. + */ +public class Move extends Transition { + + private static final String PROPNAME_BOUNDS = "android:move:bounds"; + private static final String PROPNAME_PARENT = "android:move:parent"; + private static final String PROPNAME_WINDOW_X = "android:move:windowX"; + private static final String PROPNAME_WINDOW_Y = "android:move:windowY"; + int[] tempLocation = new int[2]; + boolean mResizeClip = false; + boolean mReparent = false; + + private static RectEvaluator sRectEvaluator = new RectEvaluator(); + + public void setResizeClip(boolean resizeClip) { + mResizeClip = resizeClip; + } + + /** + * Setting this flag tells Move to track the before/after parent + * of every view using this transition. The flag is not enabled by + * default because it requires the parent instances to be the same + * in the two scenes or else all parents must use ids to allow + * the transition to determine which parents are the same. + * + * @param reparent true if the transition should track the parent + * container of target views and animate parent changes. + */ + public void setReparent(boolean reparent) { + mReparent = reparent; + } + + @Override + protected void captureValues(TransitionValues values, boolean start) { + View view = values.view; + values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(), + view.getRight(), view.getBottom())); + values.values.put(PROPNAME_PARENT, values.view.getParent()); + values.view.getLocationInWindow(tempLocation); + values.values.put(PROPNAME_WINDOW_X, tempLocation[0]); + values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]); + } + + @Override + protected Animator play(final ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + final View view = endValues.view; + if (view.getParent() == null) { + // TODO: Might want to make it possible to Move an disappearing view. + // This workaround is here because if a parallel Fade is not running on the view + // Then it won't get added to the hierarchy and the animator below will not fire, + // causing the transition to not end + return null; + } + // TODO: need to handle non-VG case? + ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); + ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); + if (startParent == null || endParent == null) { + return null; + } + boolean parentsEqual = (startParent == endParent) || + (startParent.getId() == endParent.getId()); + if (!mReparent || parentsEqual) { + // Common case - view belongs to the same layout before/after. Just animate its bounds + Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); + Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); + int startLeft = startBounds.left; + int endLeft = endBounds.left; + int startTop = startBounds.top; + int endTop = endBounds.top; + int startRight = startBounds.right; + int endRight = endBounds.right; + int startBottom = startBounds.bottom; + int endBottom = endBounds.bottom; + int startWidth = startRight - startLeft; + int startHeight = startBottom - startTop; + int endWidth = endRight - endLeft; + int endHeight = endBottom - endTop; + int numChanges = 0; + if (startWidth != 0 && startHeight != 0 && endWidth != 0 && endHeight != 0) { + if (startLeft != endLeft) ++numChanges; + if (startTop != endTop) ++numChanges; + if (startRight != endRight) ++numChanges; + if (startBottom != endBottom) ++numChanges; + } + if (numChanges > 0) { + if (!mResizeClip) { + PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges]; + int pvhIndex = 0; + if (startLeft != endLeft) { + pvh[pvhIndex++] = PropertyValuesHolder.ofInt("left", startLeft, endLeft); + } + if (startTop != endTop) { + pvh[pvhIndex++] = PropertyValuesHolder.ofInt("top", startTop, endTop); + } + if (startRight != endRight) { + pvh[pvhIndex++] = PropertyValuesHolder.ofInt("right", + startRight, endRight); + } + if (startBottom != endBottom) { + pvh[pvhIndex++] = PropertyValuesHolder.ofInt("bottom", + startBottom, endBottom); + } + ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh); + if (view.getParent() instanceof ViewGroup) { + final ViewGroup parent = (ViewGroup) view.getParent(); + parent.suppressLayout(true); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + parent.suppressLayout(false); + } + }); + } + return anim; + } else { + // Animate location with translationX/Y and size with clip bounds + float transXDelta = endLeft - startLeft; + float transYDelta = endTop - startTop; + int widthDelta = endWidth - startWidth; + int heightDelta = endHeight - startHeight; + numChanges = 0; + if (transXDelta != 0) numChanges++; + if (transYDelta != 0) numChanges++; + if (widthDelta != 0 || heightDelta != 0) numChanges++; + PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges]; + int pvhIndex = 0; + if (transXDelta != 0) { + pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationX", + view.getTranslationX(), 0); + } + if (transYDelta != 0) { + pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationY", + view.getTranslationY(), 0); + } + if (widthDelta != 0 || heightDelta != 0) { + Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight); + Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight); + pvh[pvhIndex++] = PropertyValuesHolder.ofObject("clipBounds", + sRectEvaluator, tempStartBounds, tempEndBounds); + } + ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh); + if (view.getParent() instanceof ViewGroup) { + final ViewGroup parent = (ViewGroup) view.getParent(); + parent.suppressLayout(true); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + parent.suppressLayout(false); + } + }); + } + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setClipBounds(null); + } + }); + return anim; + } + } + } else { + return (ObjectAnimator) endValues.values.get("drawableAnim"); + } + return null; + } + + @Override + protected boolean prePlay(final ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return false; + } + HashMap startParentVals = startValues.values; + HashMap endParentVals = endValues.values; + ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT); + ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT); + if (startParent == null || endParent == null) { + return false; + } + final View view = endValues.view; + boolean parentsEqual = (startParent == endParent) || + (startParent.getId() == endParent.getId()); + // TODO: Might want reparenting to be separate/subclass transition, or at least + // triggered by a property on Move. Otherwise, we're forcing the requirement that + // all parents in layouts have IDs to avoid layout-inflation resulting in a side-effect + // of reparenting the views. + if (!mReparent || parentsEqual) { + Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS); + Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); + int startLeft = startBounds.left; + int endLeft = endBounds.left; + int startTop = startBounds.top; + int endTop = endBounds.top; + int startRight = startBounds.right; + int endRight = endBounds.right; + int startBottom = startBounds.bottom; + int endBottom = endBounds.bottom; + int startWidth = startRight - startLeft; + int startHeight = startBottom - startTop; + int endWidth = endRight - endLeft; + int endHeight = endBottom - endTop; + int numChanges = 0; + if (startWidth != 0 && startHeight != 0 && endWidth != 0 && endHeight != 0) { + if (startLeft != endLeft) ++numChanges; + if (startTop != endTop) ++numChanges; + if (startRight != endRight) ++numChanges; + if (startBottom != endBottom) ++numChanges; + } + if (numChanges > 0) { + if (!mResizeClip) { + if (startLeft != endLeft) view.setLeft(startLeft); + if (startTop != endTop) view.setTop(startTop); + if (startRight != endRight) view.setRight(startRight); + if (startBottom != endBottom) view.setBottom(startBottom); + } else { + if (startWidth != endWidth) view.setRight(endLeft + + Math.max(startWidth, endWidth)); + if (startHeight != endHeight) view.setBottom(endTop + + Math.max(startHeight, endHeight)); + // TODO: don't clobber TX/TY + if (startLeft != endLeft) view.setTranslationX(startLeft - endLeft); + if (startTop != endTop) view.setTranslationY(startTop - endTop); + } + return true; + } + } else { + int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X); + int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y); + int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X); + int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y); + // TODO: also handle size changes: check bounds and animate size changes + if (startX != endX || startY != endY) { + sceneRoot.getLocationInWindow(tempLocation); + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + final BitmapDrawable drawable = new BitmapDrawable(bitmap); + view.setVisibility(View.INVISIBLE); + sceneRoot.getOverlay().add(drawable); + Rect startBounds = new Rect(startX - tempLocation[0], startY - tempLocation[1], + startX - tempLocation[0] + view.getWidth(), + startY - tempLocation[1] + view.getHeight()); + Rect endBounds = new Rect(endX - tempLocation[0], endY - tempLocation[1], + endX - tempLocation[0] + view.getWidth(), + endY - tempLocation[1] + view.getHeight()); + ObjectAnimator anim = ObjectAnimator.ofObject(drawable, "bounds", + sRectEvaluator, startBounds, endBounds); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + sceneRoot.getOverlay().remove(drawable); + view.setVisibility(View.VISIBLE); + } + }); + endParentVals.put("drawableAnim", anim); + return true; + } + } + return false; + } +} diff --git a/core/java/android/view/transition/Recolor.java b/core/java/android/view/transition/Recolor.java new file mode 100644 index 0000000000000..7048be96094a6 --- /dev/null +++ b/core/java/android/view/transition/Recolor.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.HashMap; + +/** + * This transition tracks changes during scene changes to the + * {@link View#setBackground(android.graphics.drawable.Drawable) background} + * property of its target views (when the background is a + * {@link ColorDrawable}, as well as the + * {@link TextView#setTextColor(android.content.res.ColorStateList) + * color} of the text for target TextViews. If the color changes between + * scenes, the color change is animated. + */ +public class Recolor extends Transition { + + private static final String PROPNAME_BACKGROUND = "android:recolor:background"; + private static final String PROPNAME_TEXT_COLOR = "android:recolor:textColor"; + + @Override + protected void captureValues(TransitionValues values, boolean start) { + values.values.put(PROPNAME_BACKGROUND, values.view.getBackground()); + if (values.view instanceof TextView) { + values.values.put(PROPNAME_TEXT_COLOR, ((TextView)values.view).getCurrentTextColor()); + } + } + + @Override + protected boolean prePlay(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return false; + } + final View view = endValues.view; + Drawable startBackground = (Drawable) startValues.values.get(PROPNAME_BACKGROUND); + Drawable endBackground = (Drawable) endValues.values.get(PROPNAME_BACKGROUND); + boolean changed = false; + if (startBackground instanceof ColorDrawable && endBackground instanceof ColorDrawable) { + ColorDrawable startColor = (ColorDrawable) startBackground; + ColorDrawable endColor = (ColorDrawable) endBackground; + if (startColor.getColor() != endColor.getColor()) { + endColor.setColor(startColor.getColor()); + changed = true; + } + } + if (view instanceof TextView) { + TextView textView = (TextView) view; + int start = (Integer) startValues.values.get(PROPNAME_TEXT_COLOR); + int end = (Integer) endValues.values.get(PROPNAME_TEXT_COLOR); + if (start != end) { + textView.setTextColor(end); + changed = true; + } + } + return changed; + } + + @Override + protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + ObjectAnimator anim = null; + final View view = endValues.view; + HashMap startVals = startValues.values; + HashMap endVals = endValues.values; + Drawable startBackground = (Drawable) startVals.get(PROPNAME_BACKGROUND); + Drawable endBackground = (Drawable) endVals.get(PROPNAME_BACKGROUND); + if (startBackground instanceof ColorDrawable && endBackground instanceof ColorDrawable) { + ColorDrawable startColor = (ColorDrawable) startBackground; + ColorDrawable endColor = (ColorDrawable) endBackground; + if (startColor.getColor() != endColor.getColor()) { + anim = ObjectAnimator.ofObject(endBackground, "color", + new ArgbEvaluator(), startColor.getColor(), endColor.getColor()); + if (getStartDelay() > 0) { + endColor.setColor(startColor.getColor()); + } + } + } + if (view instanceof TextView) { + TextView textView = (TextView) view; + int start = (Integer) startValues.values.get(PROPNAME_TEXT_COLOR); + int end = (Integer) endValues.values.get(PROPNAME_TEXT_COLOR); + if (start != end) { + anim = ObjectAnimator.ofObject(textView, "textColor", + new ArgbEvaluator(), start, end); + if (getStartDelay() > 0) { + textView.setTextColor(end); + } + } + } + return anim; + } +} diff --git a/core/java/android/view/transition/Rotate.java b/core/java/android/view/transition/Rotate.java new file mode 100644 index 0000000000000..b42fbe524dd89 --- /dev/null +++ b/core/java/android/view/transition/Rotate.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.view.View; +import android.view.ViewGroup; + +/** + * This transition captures the rotation property of targets before and after + * the scene change and animates any changes. + */ +public class Rotate extends Transition { + + private static final String PROPNAME_ROTATION = "android:rotate:rotation"; + + @Override + protected void captureValues(TransitionValues values, boolean start) { + values.values.put(PROPNAME_ROTATION, values.view.getRotation()); + } + + @Override + protected boolean prePlay(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return false; + } + final View view = endValues.view; + float startRotation = (Float) startValues.values.get(PROPNAME_ROTATION); + float endRotation = (Float) endValues.values.get(PROPNAME_ROTATION); + if (startRotation != endRotation) { + view.setRotation(startRotation); + return true; + } + return false; + } + + @Override + protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + final View view = endValues.view; + float startRotation = (Float) startValues.values.get(PROPNAME_ROTATION); + float endRotation = (Float) endValues.values.get(PROPNAME_ROTATION); + if (startRotation != endRotation) { + return ObjectAnimator.ofFloat(view, View.ROTATION, + startRotation, endRotation); + } + return null; + } +} diff --git a/core/java/android/view/transition/Scene.java b/core/java/android/view/transition/Scene.java new file mode 100644 index 0000000000000..62cb9d378297a --- /dev/null +++ b/core/java/android/view/transition/Scene.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +/** + * A scene represents the collection of values that various properties in the + * View hierarchy will have when the scene is applied. A Scene can be + * configured to automatically run a Transition when it is applied, which will + * animate the various property changes that take place during the + * scene change. + */ +public final class Scene { + + private Context mContext; + private int mLayoutId = -1; + private ViewGroup mSceneRoot; + private ViewGroup mLayout; // alternative to layoutId + Runnable mEnterAction, mExitAction; + + /** + * Constructs a Scene with no information about how values will change + * when this scene is applied. This constructor might be used when + * a Scene is created with the intention of being dynamically configured, + * through setting {@link #setEnterAction(Runnable)} and possibly + * {@link #setExitAction(Runnable)}. + * + * @param sceneRoot The root of the hierarchy in which scene changes + * and transitions will take place. + */ + public Scene(ViewGroup sceneRoot) { + mSceneRoot = sceneRoot; + } + + /** + * Constructs a Scene which, when entered, will remove any + * children from the sceneRoot container and will inflate and add + * the hierarchy specified by the layoutId resource file. + * + * @param sceneRoot The root of the hierarchy in which scene changes + * and transitions will take place. + * @param layoutId The id of a resource file that defines the view + * hierarchy of this scene. + * @param context The context used in the process of inflating + * the layout resource. + */ + public Scene(ViewGroup sceneRoot, int layoutId, Context context) { + mContext = context; + mSceneRoot = sceneRoot; + mLayoutId = layoutId; + } + + /** + * Constructs a Scene which, when entered, will remove any + * children from the sceneRoot container and add the layout + * object as a new child of that container. + * + * @param sceneRoot The root of the hierarchy in which scene changes + * and transitions will take place. + * @param layout The view hiearrchy of this scene, added as a child + * of sceneRoot when this scene is entered. + */ + public Scene(ViewGroup sceneRoot, ViewGroup layout) { + mSceneRoot = sceneRoot; + mLayout = layout; + } + + /** + * Gets the root of the scene, which is the root of the view hierarchy + * affected by changes due to this scene, and which will be animated + * when this scene is entered. + * + * @return The root of the view hierarchy affected by this scene. + */ + public ViewGroup getSceneRoot() { + return mSceneRoot; + } + + /** + * Exits this scene, if it is the {@link ViewGroup#getCurrentScene() + * currentScene} on the scene's {@link #getSceneRoot() scene root}. + * Exiting a scene involves removing the layout added if the scene + * has either a layoutId or layout view group (set at construction + * time) and running the {@link #setExitAction(Runnable) exit action} + * if there is one. + */ + public void exit() { + if (mSceneRoot.getCurrentScene() == this) { + if (mLayoutId >= 0 || mLayout != null) { + // Undo layout change caused by entering this scene + getSceneRoot().removeAllViews(); + } + if (mExitAction != null) { + mExitAction.run(); + } + } + } + + /** + * Enters this scene, which entails changing all values that + * are specified by this scene. These may be values associated + * with a layout view group or layout resource file which will + * now be added to the scene root, or it may be values changed by + * an {@link #setEnterAction(Runnable)} enter action}, or a + * combination of the these. No transition will be run when the + * scene is entered. To get transition behavior in scene changes, + * use one of the methods in {@link TransitionManager} instead. + */ + public void enter() { + + // Apply layout change, if any + if (mLayoutId >= 0 || mLayout != null) { + // redundant with exit() action of previous scene, but must + // empty out that parent container before adding to it + getSceneRoot().removeAllViews(); + + if (mLayoutId >= 0) { + LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot); + } else { + mSceneRoot.addView(mLayout); + } + } + + // Notify next scene that it is entering. Subclasses may override to configure scene. + if (mEnterAction != null) { + mEnterAction.run(); + } + + mSceneRoot.setCurrentScene(this ); + } + + /** + * Scenes that are not defined with layout resources or + * hierarchies, or which need to perform additional steps + * after those hierarchies are changed to, should set an enter + * action, and possibly an exit action as well. An enter action + * will cause Scene to call back into application code to do + * anything else the application needs after transitions have + * captured pre-change values and after any other scene changes + * have been applied, such as the layout (if any) being added to + * the view hierarchy. After this method is called, Transitions will + * be played. + * + * @param action The runnable whose {@link Runnable#run() run()} method will + * be called when this scene is entered + * @see #setExitAction(Runnable) + * @see Scene#Scene(ViewGroup, int, Context) + * @see Scene#Scene(ViewGroup, ViewGroup) + */ + public void setEnterAction(Runnable action) { + mEnterAction = action; + } + + /** + * Scenes that are not defined with layout resources or + * hierarchies, or which need to perform additional steps + * after those hierarchies are changed to, should set an enter + * action, and possibly an exit action as well. An exit action + * will cause Scene to call back into application code to do + * anything the application needs to do after applicable transitions have + * captured pre-change values, but before any other scene changes + * have been applied, such as the new layout (if any) being added to + * the view hierarchy. After this method is called, the next scene + * will be entered, including a call to {@link #setEnterAction(Runnable)} + * if an enter action is set. + * + * @see #setEnterAction(Runnable) + * @see Scene#Scene(ViewGroup, int, Context) + * @see Scene#Scene(ViewGroup, ViewGroup) + */ + public void setExitAction(Runnable action) { + mExitAction = action; + } + +} \ No newline at end of file diff --git a/core/java/android/view/transition/Slide.java b/core/java/android/view/transition/Slide.java new file mode 100644 index 0000000000000..8630ee2dc6f6f --- /dev/null +++ b/core/java/android/view/transition/Slide.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +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. + */ +public class Slide extends Visibility { + + // TODO: Add parameter for sliding factor - it's hard-coded below + + private static final TimeInterpolator sAccelerator = new AccelerateInterpolator(); + private static final TimeInterpolator sDecelerator = new DecelerateInterpolator(); + + @Override + protected Animator appear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + ObjectAnimator anim = ObjectAnimator.ofFloat(endView, View.TRANSLATION_Y, + -2 * endView.getHeight(), 0); + anim.setInterpolator(sDecelerator); + return anim; + } + + @Override + protected boolean preAppear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + endView.setTranslationY(-2 * endView.getHeight()); + return true; + } + + @Override + protected boolean preDisappear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + startView.setTranslationY(0); + return true; + } + + @Override + protected Animator disappear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + ObjectAnimator anim = ObjectAnimator.ofFloat(startView, View.TRANSLATION_Y, 0, + -2 * startView.getHeight()); + anim.setInterpolator(sAccelerator); + return anim; + } + +} diff --git a/core/java/android/view/transition/TextChange.java b/core/java/android/view/transition/TextChange.java new file mode 100644 index 0000000000000..0ba2412dad912 --- /dev/null +++ b/core/java/android/view/transition/TextChange.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.HashMap; + +/** + * This transition tracks changes to the text in TextView targets. If the text + * changes between the start and end scenes, the transition ensures that the + * starting text stays until the transition ends, at which point it changes + * to the end text. This is useful in situations where you want to resize a + * text view to its new size before displaying the text that goes there. + */ +public class TextChange extends Transition { + private static final String PROPNAME_TEXT = "android:textchange:text"; + + // TODO: think about other options we could have here, like cross-fading the text, or fading + // it out/in. These could be parameters to supply to the constructors (and xml attributes). + + @Override + protected void captureValues(TransitionValues values, boolean start) { + if (values.view instanceof TextView) { + TextView textview = (TextView) values.view; + values.values.put(PROPNAME_TEXT, textview.getText()); + } + } + + @Override + protected boolean prePlay(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null || !(endValues.view instanceof TextView)) { + return false; + } + final TextView view = (TextView) endValues.view; + HashMap startVals = startValues.values; + HashMap endVals = endValues.values; + String startText = (String) startVals.get(PROPNAME_TEXT); + String endText = (String) endVals.get(PROPNAME_TEXT); + if (!startText.equals(endText)) { + view.setText(startText); + return true; + } + return false; + } + + @Override + protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null || !(endValues.view instanceof TextView)) { + return null; + } + final TextView view = (TextView) endValues.view; + HashMap startVals = startValues.values; + HashMap endVals = endValues.values; + final String startText = (String) startVals.get(PROPNAME_TEXT); + final String endText = (String) endVals.get(PROPNAME_TEXT); + if (!startText.equals(endText)) { + // This noop animation is just used to keep the text in its start state + // until the transition ends + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setText(endText); + } + }); + return anim; + } + return null; + } +} diff --git a/core/java/android/view/transition/Transition.java b/core/java/android/view/transition/Transition.java new file mode 100644 index 0000000000000..150c218c366b8 --- /dev/null +++ b/core/java/android/view/transition/Transition.java @@ -0,0 +1,911 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOverlay; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A Transition holds information about animations that will be run on its + * targets during a scene change. Subclasses of this abstract class may + * choreograph several child transitions ({@link TransitionGroup} or they may + * perform custom animations themselves. Any Transition has two main jobs: + * (1) capture property values, and (2) play animations based on changes to + * captured property values. A custom transition knows what property values + * on View objects are of interest to it, and also knows how to animate + * changes to those values. For example, the {@link Fade} transition tracks + * changes to visibility-related properties and is able to construct and run + * animations that fade items in or out based on changes to those properties. + * + *

Note: Transitions may not work correctly with either {@link SurfaceView} + * or {@link TextureView}, due to the way that these views are displayed + * on the screen. For SurfaceView, the problem is that the view is updated from + * a non-UI thread, so changes to the view due to transitions (such as moving + * and resizing the view) may be out of sync with the display inside those bounds. + * TextureView is more compatible with transitions in general, but some + * specific transitions (such as {@link Crossfade}) may not be compatible + * with TextureView because they rely on {@link ViewOverlay} functionality, + * which does not currently work with TextureView.

+ */ +public abstract class Transition { + + private static final String LOG_TAG = "Transition"; + static final boolean DBG = false; + + long mStartDelay = -1; + long mDuration = -1; + TimeInterpolator mInterpolator = null; + int[] mTargetIds; + View[] mTargets; + // TODO: sparse arrays instead of hashmaps? + private HashMap mStartValues = + new HashMap(); + private SparseArray mStartIdValues = new SparseArray(); + private LongSparseArray mStartItemIdValues = + new LongSparseArray(); + private HashMap mEndValues = + new HashMap(); + private SparseArray mEndIdValues = new SparseArray(); + private LongSparseArray mEndItemIdValues = + new LongSparseArray(); + + // Used to carry data between preplay() and play(), cleared before every scene transition + private ArrayList mPlayStartValuesList = new ArrayList(); + private ArrayList mPlayEndValuesList = new ArrayList(); + + // Number of per-target instances of this Transition currently running. This count is + // determined by calls to startTransition() and endTransition() + int mNumInstances = 0; + + + /** + * The set of listeners to be sent transition lifecycle events. + */ + ArrayList mListeners = null; + + /** + * Constructs a Transition object with no target objects. A transition with + * no targets defaults to running on all target objects in the scene hierarchy + * (if the transition is not contained in a TransitionGroup), or all target + * objects passed down from its parent (if it is in a TransitionGroup). + */ + public Transition() {} + + /** + * Sets the duration of this transition. By default, there is no duration + * (indicated by a negative number), which means that the Animator created by + * the transition will have its own specified duration. If the duration of a + * Transition is set, that duration will override the Animator duration. + * + * @param duration The length of the animation, in milliseconds. + * @return This transition object. + */ + public Transition setDuration(long duration) { + mDuration = duration; + return this; + } + + public long getDuration() { + return mDuration; + } + + /** + * Sets the startDelay of this transition. By default, there is no delay + * (indicated by a negative number), which means that the Animator created by + * the transition will have its own specified startDelay. If the delay of a + * Transition is set, that delay will override the Animator delay. + * + * @param startDelay The length of the delay, in milliseconds. + */ + public void setStartDelay(long startDelay) { + mStartDelay = startDelay; + } + + public long getStartDelay() { + return mStartDelay; + } + + /** + * Sets the interpolator of this transition. By default, the interpolator + * is null, which means that the Animator created by the transition + * will have its own specified interpolator. If the interpolator of a + * Transition is set, that interpolator will override the Animator interpolator. + * + * @param interpolator The time interpolator used by the transition + */ + public void setInterpolator(TimeInterpolator interpolator) { + mInterpolator = interpolator; + } + + public TimeInterpolator getInterpolator() { + return mInterpolator; + } + + /** + * This method is called by the transition's parent (all the way up to the + * topmost Transition in the hierarchy) with the sceneRoot and start/end + * values that the transition may need to run animations on its target + * views. The method is called for every applicable target object, which + * is stored in the {@link TransitionValues#view} field. When the method + * results in an animation needing to be run, the transition will construct + * the appropriate {@link Animator} object and return it. The transition + * mechanism will apply any applicable duration, startDelay, and interpolator + * to that animation and start it. Returning null from the method tells the + * transition engine that there is no animation to be played (TransitionGroup + * will return null because any applicable animations were started on its child + * transitions already and there is no animation to be run on the group itself). + * + * @param sceneRoot + * @param startValues + * @param endValues + * @return Animator The animation to run. + */ + protected abstract Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues); + + /** + * This method is called by the transition's parent (all the way up to the + * topmost Transition in the hierarchy) with the sceneRoot and start/end + * values that the transition may need to set things up at the start of a + * Transition. For example, if an overall Transition consists of several + * child transitions in sequence, then some of the child transitions may + * want to set initial values on target views prior to the overall + * Transition commencing, to put them in an appropriate scene for the + * delay between that start and the child Transition start time. For + * example, a transition that fades an item in may wish to set the starting + * alpha value to 0, to avoid it blinking in prior to the transition + * actually starting the animation. This is necessary because the scene + * change that triggers the Transition will automatically set the end-scene + * on all target views, so a Transition that wants to animate from a + * different value should set that value in the preplay() method. + * + *

Additionally, a Transition can perform logic to determine whether + * the transition needs to run on the given target and start/end values. + * For example, a transition that resizes objects on the screen may wish + * to avoid running for views which are not present in either the start + * or end scenes. A return value of false indicates that + * the transition should not run, and there will be no ensuing call to the + * {@link #play(ViewGroup, TransitionValues, TransitionValues)} method during + * this scene change. The default implementation returns true.

+ * + *

The method is called for every applicable target object, which is + * stored in the {@link TransitionValues#view} field.

+ * + * @param sceneRoot + * @param startValues + * @param endValues + * @return True if the Transition's {@link #play(ViewGroup, + * TransitionValues, TransitionValues) play()} method should be called + * during this scene change, false otherwise. + */ + protected boolean prePlay(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + return true; + } + + /** + * This version of prePlay() is called with the entire set of start/end + * values. The implementation in Transition iterates through these lists + * and calls {@link #prePlay(ViewGroup, TransitionValues, TransitionValues)} + * with each set of start/end values on this transition. The + * TransitionGroup subclass overrides this method and delegates it to + * each of its children in succession. The intention in splitting + * preplay() out from play() is to allow all Transitions in the tree to + * set up the appropriate start scene for their target objects prior to + * any calls to play(), which is necessary when there is a sequential + * Transition, where a child transition which is not the first may want to + * set up a target's scene prior to the overall Transition start. + * + * @hide + */ + protected void prePlay(ViewGroup sceneRoot, HashMap startValues, + SparseArray startIdValues, + LongSparseArray startItemIdValues, + HashMap endValues, + SparseArray endIdValues, + LongSparseArray endItemIdValues) { + mPlayStartValuesList.clear(); + mPlayEndValuesList.clear(); + HashMap endCopy = new HashMap(endValues); + SparseArray endIdCopy = + new SparseArray(endIdValues.size()); + for (int i = 0; i < endIdValues.size(); ++i) { + int id = endIdValues.keyAt(i); + endIdCopy.put(id, endIdValues.valueAt(i)); + } + LongSparseArray endItemIdCopy = + new LongSparseArray(endItemIdValues.size()); + for (int i = 0; i < endItemIdValues.size(); ++i) { + long id = endItemIdValues.keyAt(i); + endItemIdCopy.put(id, endItemIdValues.valueAt(i)); + } + // Walk through the start values, playing everything we find + // Remove from the end set as we go + ArrayList startValuesList = new ArrayList(); + ArrayList endValuesList = new ArrayList(); + for (View view : startValues.keySet()) { + TransitionValues start = null; + TransitionValues end = null; + boolean isInListView = false; + if (view.getParent() instanceof ListView) { + isInListView = true; + } + if (!isInListView) { + int id = view.getId(); + start = startValues.get(view) != null ? + startValues.get(view) : startIdValues.get(id); + if (endValues.get(view) != null) { + end = endValues.get(view); + endCopy.remove(view); + } else { + end = endIdValues.get(id); + View removeView = null; + for (View viewToRemove : endCopy.keySet()) { + if (viewToRemove.getId() == id) { + removeView = viewToRemove; + } + } + if (removeView != null) { + endCopy.remove(removeView); + } + } + endIdCopy.remove(id); + if (isValidTarget(view, id)) { + startValuesList.add(start); + endValuesList.add(end); + } + } else { + ListView parent = (ListView) view.getParent(); + if (parent.getAdapter().hasStableIds()) { + int position = parent.getPositionForView(view); + long itemId = parent.getItemIdAtPosition(position); + start = startItemIdValues.get(itemId); + endItemIdCopy.remove(itemId); + // TODO: deal with targetIDs for itemIDs for ListView items + startValuesList.add(start); + endValuesList.add(end); + } + } + } + int startItemIdCopySize = startItemIdValues.size(); + for (int i = 0; i < startItemIdCopySize; ++i) { + long id = startItemIdValues.keyAt(i); + if (isValidTarget(null, id)) { + TransitionValues start = startItemIdValues.get(id); + TransitionValues end = endItemIdValues.get(id); + endItemIdCopy.remove(id); + startValuesList.add(start); + endValuesList.add(end); + } + } + // Now walk through the remains of the end set + for (View view : endCopy.keySet()) { + int id = view.getId(); + if (isValidTarget(view, id)) { + TransitionValues start = startValues.get(view) != null ? + startValues.get(view) : startIdValues.get(id); + TransitionValues end = endCopy.get(view); + endIdCopy.remove(id); + startValuesList.add(start); + endValuesList.add(end); + } + } + int endIdCopySize = endIdCopy.size(); + for (int i = 0; i < endIdCopySize; ++i) { + int id = endIdCopy.keyAt(i); + if (isValidTarget(null, id)) { + TransitionValues start = startIdValues.get(id); + TransitionValues end = endIdCopy.get(id); + startValuesList.add(start); + endValuesList.add(end); + } + } + int endItemIdCopySize = endItemIdCopy.size(); + for (int i = 0; i < endItemIdCopySize; ++i) { + long id = endItemIdCopy.keyAt(i); + // TODO: Deal with targetIDs and itemIDs + TransitionValues start = startItemIdValues.get(id); + TransitionValues end = endItemIdCopy.get(id); + startValuesList.add(start); + endValuesList.add(end); + } + for (int i = 0; i < startValuesList.size(); ++i) { + TransitionValues start = startValuesList.get(i); + TransitionValues end = endValuesList.get(i); + // TODO: what to do about targetIds and itemIds? + if (prePlay(sceneRoot, start, end)) { + // Note: we've already done the check against targetIDs in these lists + mPlayStartValuesList.add(start); + mPlayEndValuesList.add(end); + } + } + } + + /** + * Internal utility method for checking whether a given view/id + * is valid for this transition, where "valid" means that either + * the Transition has no target/targetId list (the default, in which + * cause the transition should act on all views in the hiearchy), or + * the given view is in the target list or the view id is in the + * targetId list. If the target parameter is null, then the target list + * is not checked (this is in the case of ListView items, where the + * views are ignored and only the ids are used). + */ + boolean isValidTarget(View target, long targetId) { + if (mTargetIds == null && mTargets == null) { + return true; + } + if (mTargetIds != null) { + for (int i = 0; i < mTargetIds.length; ++i) { + if (mTargetIds[i] == targetId) { + return true; + } + } + } + if (target != null && mTargets != null) { + for (int i = 0; i < mTargets.length; ++i) { + if (mTargets[i] == target) { + return true; + } + } + } + return false; + } + + /** + * This version of play() is called with the entire set of start/end + * values. The implementation in Transition iterates through these lists + * and calls {@link #play(ViewGroup, TransitionValues, TransitionValues)} + * with each set of start/end values on this transition. The + * TransitionGroup subclass overrides this method and delegates it to + * each of its children in succession. + * + * @hide + */ + protected void play(ViewGroup sceneRoot, + final HashMap startValues, + final SparseArray startIdValues, + final LongSparseArray startItemIdValues, + final HashMap endValues, + final SparseArray endIdValues, + final LongSparseArray endItemIdValues) { + + startTransition(); + // Now walk the list of TransitionValues, calling play for each pair + for (int i = 0; i < mPlayStartValuesList.size(); ++i) { + TransitionValues start = mPlayStartValuesList.get(i); + TransitionValues end = mPlayEndValuesList.get(i); + startTransition(); + animate(play(sceneRoot, start, end)); + } + mPlayStartValuesList.clear(); + mPlayEndValuesList.clear(); + endTransition(); + } + + /** + * Captures the current scene of values for the properties that this + * transition monitors. These values can be either the start or end + * values used in a subsequent call to + * {@link #play(ViewGroup, TransitionValues, TransitionValues)}, as indicated by + * start. 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 preplay and play() methods to determine what, if any, + * animations, should be run. + * + * @param transitionValues The holder any values that the Transition + * wishes to store. Values are stored in the fields of this + * TransitionValues object, according to their type, and are keyed from + * a String value. For example, to start a view's rotation value, + * a Transition might call + * transitionValues.floatValues.put("rotation", view.getRotation()) + * . The target View will already be stored in + * the transitionValues structure when this method is called. The other + * fields in TransitionValues, e.g. floatValues, + * may need to be instantiated if they have not yet been created. + */ + protected abstract void captureValues(TransitionValues transitionValues, boolean start); + + /** + * Sets the ids of target views that this Transition is interested in + * animating. By default, there are no targetIds, and a Transition will + * listen for changes on every view in the hierarchy below the sceneRoot + * of the Scene being transitioned into. Setting targetIDs constrains + * the Transition to only listen for, and act on, views with these IDs. + * Views with different IDs, or no IDs whatsoever, will be ignored. + * + * @see View#getId() + * @param targetIds A list of IDs which specify the set of Views on which + * the Transition will act. + * @return Transition The Transition on which the targetIds have been set. + * Returning the same object makes it easier to chain calls during + * construction, such as + * transitionGroup.addTransitions(new Fade()).setTargetIds(someId); + */ + public Transition setTargetIds(int... targetIds) { + int numTargets = targetIds.length; + mTargetIds = new int[numTargets]; + System.arraycopy(targetIds, 0, mTargetIds, 0, numTargets); + return this; + } + + /** + * Sets the target view instances that this Transition is interested in + * animating. By default, there are no targets, and a Transition will + * listen for changes on every view in the hierarchy below the sceneRoot + * of the Scene being transitioned into. Setting targets constrains + * the Transition to only listen for, and act on, these views. + * All other views will be ignored. + * + *

The target list is like the {@link #setTargetIds(int...) targetId} + * list except this list specifies the actual View instances, not the ids + * of the views. This is an important distinction when scene changes involve + * view hierarchies which have been inflated separately; different views may + * share the same id but not actually be the same instance. If the transition + * should treat those views as the same, then seTargetIds() should be used + * instead of setTargets(). If, on the other hand, scene changes involve + * changes all within the same view hierarchy, among views which do not + * necessary have ids set on them, then the target list may be more + * convenient.

+ * + * @see #setTargetIds(int...) + * @param targets A list of Views on which the Transition will act. + * @return Transition The Transition on which the targets have been set. + * Returning the same object makes it easier to chain calls during + * construction, such as + * transitionGroup.addTransitions(new Fade()).setTargets(someView); + */ + public Transition setTargets(View... targets) { + int numTargets = targets.length; + mTargets = new View[numTargets]; + System.arraycopy(targets, 0, mTargets, 0, numTargets); + return this; + } + + /** + * Returns the array of target IDs that this transition limits itself to + * tracking and animating. If the array is null for both this method and + * {@link #getTargets()}, then this transition is + * not limited to specific views, and will handle changes to any views + * in the hierarchy of a scene change. + * + * @return the list of target IDs + */ + public int[] getTargetIds() { + return mTargetIds; + } + + /** + * Returns the array of target views that this transition limits itself to + * tracking and animating. If the array is null for both this method and + * {@link #getTargetIds()}, then this transition is + * not limited to specific views, and will handle changes to any views + * in the hierarchy of a scene change. + * + * @return the list of target views + */ + public View[] getTargets() { + return mTargets; + } + + /** + * Recursive method that captures values for the given view and the + * hierarchy underneath it. + * @param sceneRoot The root of the view hierarchy being captured + * @param start true if this capture is happening before the scene change, + * false otherwise + */ + void captureValues(ViewGroup sceneRoot, boolean start) { + if (mTargetIds != null && mTargetIds.length > 0 || + mTargets != null && mTargets.length > 0) { + if (mTargetIds != null) { + for (int i = 0; i < mTargetIds.length; ++i) { + int id = mTargetIds[i]; + View view = sceneRoot.findViewById(id); + if (view != null) { + TransitionValues values = new TransitionValues(); + values.view = view; + captureValues(values, start); + if (start) { + mStartValues.put(view, values); + mStartIdValues.put(id, values); + } else { + mEndValues.put(view, values); + mEndIdValues.put(id, values); + } + } + } + } + if (mTargets != null) { + for (int i = 0; i < mTargets.length; ++i) { + View view = mTargets[i]; + if (view != null) { + TransitionValues values = new TransitionValues(); + values.view = view; + captureValues(values, start); + if (start) { + mStartValues.put(view, values); + } else { + mEndValues.put(view, values); + } + } + } + } + } else { + captureHierarchy(sceneRoot, start); + } + } + + /** + * Recursive method which captures values for an entire view hierarchy, + * starting at some root view. Transitions without targetIDs will use this + * method to capture values for all possible views. + * + * @param view The view for which to capture values. Children of this View + * will also be captured, recursively down to the leaf nodes. + * @param start true if values are being captured in the start scene, false + * otherwise. + */ + private void captureHierarchy(View view, boolean start) { + if (view == null) { + return; + } + boolean isListViewItem = false; + if (view.getParent() instanceof ListView) { + isListViewItem = true; + } + if (isListViewItem && !((ListView) view.getParent()).getAdapter().hasStableIds()) { + // ignore listview children unless we can track them with stable IDs + return; + } + long id; + if (!isListViewItem) { + id = view.getId(); + } else { + ListView listview = (ListView) view.getParent(); + int position = listview.getPositionForView(view); + id = listview.getItemIdAtPosition(position); + view.setHasTransientState(true); + } + TransitionValues values = new TransitionValues(); + values.view = view; + captureValues(values, start); + if (start) { + if (!isListViewItem) { + mStartValues.put(view, values); + mStartIdValues.put((int) id, values); + } else { + mStartItemIdValues.put(id, values); + } + } else { + if (!isListViewItem) { + mEndValues.put(view, values); + mEndIdValues.put((int) id, values); + } else { + mEndItemIdValues.put(id, values); + } + } + if (view instanceof ViewGroup) { + ViewGroup parent = (ViewGroup) view; + for (int i = 0; i < parent.getChildCount(); ++i) { + captureHierarchy(parent.getChildAt(i), start); + } + } + } + + /** + * Called by TransitionManager to play the transition. This calls + * prePlay() and then play() with the full set of per-view + * transitionValues objects + */ + void play(ViewGroup sceneRoot) { + // prePlay() must be called on entire transition hierarchy and set of views + // before calling play() on anything; every transition needs a chance to set up + // target views appropriately before transitions begin running + prePlay(sceneRoot, mStartValues, mStartIdValues, mStartItemIdValues, + mEndValues, mEndIdValues, mEndItemIdValues); + play(sceneRoot, mStartValues, mStartIdValues, mStartItemIdValues, + mEndValues, mEndIdValues, mEndItemIdValues); + } + + /** + * This is a utility method used by subclasses to handle standard parts of + * setting up and running an Animator: it sets the {@link #getDuration() + * duration} and the {@link #getStartDelay() startDelay}, starts the + * animation, and, when the animator ends, calls {@link #endTransition()}. + * + * @param animator The Animator to be run during this transition. + * + * @hide + */ + protected void animate(Animator animator) { + // TODO: maybe pass auto-end as a boolean parameter? + if (animator == null) { + endTransition(); + } else { + if (getDuration() >= 0) { + animator.setDuration(getDuration()); + } + if (getStartDelay() >= 0) { + animator.setStartDelay(getStartDelay()); + } + if (getInterpolator() != null) { + animator.setInterpolator(getInterpolator()); + } + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + cancelTransition(); + } + + @Override + public void onAnimationEnd(Animator animation) { + endTransition(); + animation.removeListener(this); + } + }); + animator.start(); + } + } + + /** + * Subclasses may override to receive notice of when the transition starts. + * This is equivalent to listening for the + * {@link TransitionListener#onTransitionStart(Transition)} callback. + */ + protected void onTransitionStart() { + } + + /** + * Subclasses may override to receive notice of when the transition is + * canceled. This is equivalent to listening for the + * {@link TransitionListener#onTransitionCancel(Transition)} callback. + */ + protected void onTransitionCancel() { + } + + /** + * Subclasses may override to receive notice of when the transition ends. + * This is equivalent to listening for the + * {@link TransitionListener#onTransitionEnd(Transition)} callback. + */ + protected void onTransitionEnd() { + } + + /** + * This method is called automatically by the transition and + * TransitionGroup classes prior to a Transition subclass starting; + * subclasses should not need to call it directly. + * + * @hide + */ + protected void startTransition() { + if (mNumInstances == 0) { + onTransitionStart(); + if (mListeners != null && mListeners.size() > 0) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onTransitionStart(this); + } + } + } + mNumInstances++; + } + + /** + * This method is called automatically by the Transition and + * TransitionGroup classes when a transition finishes, either because + * a transition did nothing (returned a null Animator from + * {@link Transition#play(ViewGroup, TransitionValues, + * TransitionValues)}) or because the transition returned a valid + * Animator and endTransition() was called in the onAnimationEnd() + * callback of the AnimatorListener. + * + * @hide + */ + protected void endTransition() { + --mNumInstances; + if (mNumInstances == 0) { + onTransitionEnd(); + if (mListeners != null && mListeners.size() > 0) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onTransitionEnd(this); + } + } + for (int i = 0; i < mStartItemIdValues.size(); ++i) { + TransitionValues tv = mStartItemIdValues.valueAt(i); + View v = tv.view; + if (v.hasTransientState()) { + v.setHasTransientState(false); + } + } + for (int i = 0; i < mEndItemIdValues.size(); ++i) { + TransitionValues tv = mEndItemIdValues.valueAt(i); + View v = tv.view; + if (v.hasTransientState()) { + v.setHasTransientState(false); + } + } + mStartValues.clear(); + mStartIdValues.clear(); + mStartItemIdValues.clear(); + mEndValues.clear(); + mEndIdValues.clear(); + mEndItemIdValues.clear(); + } + } + + /** + * This method cancels a transition that is currently running. + * Implementation TBD. + */ + protected void cancelTransition() { + // TODO: how does this work with instances? + // TODO: this doesn't actually do *anything* yet + onTransitionCancel(); + if (mListeners != null && mListeners.size() > 0) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onTransitionCancel(this); + } + } + } + + /** + * Adds a listener to the set of listeners that are sent events through the + * life of an animation, such as start, repeat, and end. + * + * @param listener the listener to be added to the current set of listeners + * for this animation. + */ + public void addListener(TransitionListener listener) { + if (mListeners == null) { + mListeners = new ArrayList(); + } + mListeners.add(listener); + } + + /** + * Removes a listener from the set listening to this animation. + * + * @param listener the listener to be removed from the current set of + * listeners for this transition. + */ + public void removeListener(TransitionListener listener) { + if (mListeners == null) { + return; + } + mListeners.remove(listener); + if (mListeners.size() == 0) { + mListeners = null; + } + } + + /** + * Gets the set of {@link TransitionListener} objects that are currently + * listening for events on this Transition object. + * + * @return ArrayList The set of listeners. + */ + public ArrayList getListeners() { + return mListeners; + } + + @Override + public String toString() { + return toString(""); + } + + String toString(String indent) { + String result = indent + getClass().getSimpleName() + "@" + + Integer.toHexString(hashCode()) + ": "; + result += "dur(" + mDuration + ") "; + result += "dly(" + mStartDelay + ") "; + result += "interp(" + mInterpolator + ") "; + result += "tgts("; + if (mTargetIds != null) { + for (int i = 0; i < mTargetIds.length; ++i) { + if (i > 0) { + result += ", "; + } + result += mTargetIds[i]; + } + } + if (mTargets != null) { + for (int i = 0; i < mTargets.length; ++i) { + if (i > 0) { + result += ", "; + } + result += mTargets[i]; + } + } + result += ")"; + return result; + } + + /** + * A transition listener receives notifications from a transition. + * Notifications indicate transition lifecycle events: when the transition + * begins, ends, or is canceled. + */ + public static interface TransitionListener { + /** + * Notification about the start of the transition. + * + * @param transition The started transition. + */ + void onTransitionStart(Transition transition); + + /** + * Notification about the end of the transition. Canceled transitions + * will always notify listeners of both the cancellation and end + * events. That is, {@link #onTransitionEnd()} is always called, + * regardless of whether the transition was canceled or played + * through to completion. + * + * @param transition The transition which reached its end. + */ + void onTransitionEnd(Transition transition); + + /** + * Notification about the cancellation of the transition. + * + * @param transition The transition which was canceled. + */ + void onTransitionCancel(Transition transition); + } + + /** + * Utility adapter class to avoid having to override all three methods + * whenever someone just wants to listen for a single event. + * + * @hide + * */ + public static class TransitionListenerAdapter implements TransitionListener { + @Override + public void onTransitionStart(Transition transition) { + } + + @Override + public void onTransitionEnd(Transition transition) { + } + + @Override + public void onTransitionCancel(Transition transition) { + } + } + +} diff --git a/core/java/android/view/transition/TransitionGroup.java b/core/java/android/view/transition/TransitionGroup.java new file mode 100644 index 0000000000000..363872a1e0270 --- /dev/null +++ b/core/java/android/view/transition/TransitionGroup.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.util.AndroidRuntimeException; +import android.util.LongSparseArray; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A TransitionGroup is a parent of child transitions (including other + * TransitionGroups). Using TransitionGroups enables more complex + * choreography of transitions, where some groups play {@link #TOGETHER} and + * others play {@link #SEQUENTIALLY}. For example, {@link AutoTransition} + * uses a TransitionGroup to sequentially play a Fade(Fade.OUT), followed by + * a {@link Move}, followed by a Fade(Fade.OUT) transition. + */ +public class TransitionGroup extends Transition { + + ArrayList mTransitions = new ArrayList(); + private boolean mPlayTogether = true; + int mCurrentListeners; + boolean mStarted = false; + + /** + * A flag used to indicate that the child transitions of this group + * should all start at the same time. + */ + public static final int TOGETHER = 0; + /** + * A flag used to indicate that the child transitions of this group should + * play in sequence; when one child transition ends, the next child + * transition begins. Note that a transition does not end until all + * instances of it (which are playing on all applicable targets of the + * transition) end. + */ + public static final int SEQUENTIALLY = 1; + + /** + * Constructs an empty transition group. Add child transitions to the + * group by calling to {@link #addTransitions(Transition...)} )}. By default, + * child transitions will play {@link #TOGETHER}. + */ + public TransitionGroup() { + } + + /** + * Constructs an empty transition group with the specified ordering. + * + * @param ordering {@link #TOGETHER} to start this group's child + * transitions together, {@link #SEQUENTIALLY} to play the child + * transitions in sequence. + * @see #setOrdering(int) + */ + public TransitionGroup(int ordering) { + setOrdering(ordering); + } + + /** + * Sets the play order of this group's child transitions. + * + * @param ordering {@link #TOGETHER} to start this group's child + * transitions together, {@link #SEQUENTIALLY} to play the child + * transitions in sequence. + */ + public void setOrdering(int ordering) { + switch (ordering) { + case SEQUENTIALLY: + mPlayTogether = false; + break; + case TOGETHER: + mPlayTogether = true; + break; + default: + throw new AndroidRuntimeException("Invalid parameter for TransitionGroup " + + "ordering: " + ordering); + } + } + + /** + * Adds child transitions to this group. The order of the child transitions + * passed in determines the order in which they are started. + * + * @param transitions A list of child transition to be added to this group. + */ + public void addTransitions(Transition... transitions) { + if (transitions != null) { + int numTransitions = transitions.length; + for (int i = 0; i < numTransitions; ++i) { + mTransitions.add(transitions[i]); + } + } + } + + /** + * Removes the specified child transition from this group. + * + * @param transition The transition to be removed. + */ + public void removeTransition(Transition transition) { + mTransitions.remove(transition); + } + + /** + * Sets up listeners for each of the child transitions. This is used to + * determine when this transition group is finished (all child transitions + * must finish first). + */ + private void setupStartEndListeners() { + for (Transition childTransition : mTransitions) { + childTransition.addListener(mListener); + } + mCurrentListeners = mTransitions.size(); + } + + /** + * This listener is used to detect when all child transitions are done, at + * which point this transition group is also done. + */ + private TransitionListener mListener = new TransitionListenerAdapter() { + @Override + public void onTransitionStart(Transition transition) { + if (!mStarted) { + startTransition(); + mStarted = true; + } + } + + @Override + public void onTransitionEnd(Transition transition) { + --mCurrentListeners; + if (mCurrentListeners == 0) { + // All child trans + mStarted = false; + endTransition(); + } + transition.removeListener(this); + } + }; + + /** + * @hide + */ + @Override + protected void prePlay(ViewGroup sceneRoot, + HashMap startValues, + SparseArray startIdValues, + LongSparseArray startItemIdValues, + HashMap endValues, + SparseArray endIdValues, + LongSparseArray endItemIdValues) { + for (Transition childTransition : mTransitions) { + childTransition.prePlay(sceneRoot, startValues, startIdValues, startItemIdValues, + endValues, endIdValues, endItemIdValues); + } + } + + /** + * @hide + */ + @Override + protected void play(ViewGroup sceneRoot, + final HashMap startValues, + final SparseArray startIdValues, + final LongSparseArray startItemIdValues, + final HashMap endValues, + final SparseArray endIdValues, + final LongSparseArray endItemIdValues) { + setupStartEndListeners(); + final ViewGroup finalSceneRoot = sceneRoot; + if (!mPlayTogether) { + // Setup sequence with listeners + // TODO: Need to add listeners in such a way that we can remove them later if canceled + for (int i = 1; i < mTransitions.size(); ++i) { + Transition previousTransition = mTransitions.get(i - 1); + final Transition nextTransition = mTransitions.get(i); + previousTransition.addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + nextTransition.play(finalSceneRoot, + startValues, startIdValues, startItemIdValues, + endValues, endIdValues, endItemIdValues); + transition.removeListener(this); + } + }); + } + Transition firstTransition = mTransitions.get(0); + if (firstTransition != null) { + firstTransition.play(finalSceneRoot, startValues, startIdValues, startItemIdValues, + endValues, endIdValues, endItemIdValues); + } + } else { + for (Transition childTransition : mTransitions) { + childTransition.play(finalSceneRoot, startValues, startIdValues, startItemIdValues, + endValues, endIdValues, endItemIdValues); + } + } + } + + @Override + protected Animator play(ViewGroup sceneRoot, + TransitionValues startValues, TransitionValues endValues) { + final View view = (endValues != null) ? endValues.view : + (startValues != null) ? startValues.view : null; + final int targetId = (view != null) ? view.getId() : -1; + // TODO: not sure this is a valid check - what about auto-targets? No need for ids. + if (targetId < 0) { + return null; + } + setupStartEndListeners(); + if (!mPlayTogether) { + final ViewGroup finalSceneRoot = sceneRoot; + final TransitionValues finalStartValues = startValues; + final TransitionValues finalEndValues = endValues; + // Setup sequence with listeners + // TODO: Need to add listeners in such a way that we can remove them later if canceled + for (int i = 1; i < mTransitions.size(); ++i) { + Transition previousTransition = mTransitions.get(i - 1); + final Transition nextTransition = mTransitions.get(i); + previousTransition.addListener(new TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + nextTransition.startTransition(); + if (nextTransition.isValidTarget(view, targetId)) { + animate(nextTransition.play(finalSceneRoot, finalStartValues, + finalEndValues)); + } else { + nextTransition.endTransition(); + } + } + }); + } + Transition firstTransition = mTransitions.get(0); + if (firstTransition != null) { + firstTransition.startTransition(); + if (firstTransition.isValidTarget(view, targetId)) { + animate(firstTransition.play(finalSceneRoot, finalStartValues, finalEndValues)); + } else { + firstTransition.endTransition(); + } + } + } else { + for (Transition childTransition : mTransitions) { + childTransition.startTransition(); + if (childTransition.isValidTarget(view, targetId)) { + animate(childTransition.play(sceneRoot, startValues, endValues)); + } else { + childTransition.endTransition(); + } + } + } + return null; + } + + @Override + protected void captureValues(TransitionValues transitionValues, boolean start) { + int targetId = transitionValues.view.getId(); + for (Transition childTransition : mTransitions) { + if (childTransition.isValidTarget(transitionValues.view, targetId)) { + childTransition.captureValues(transitionValues, start); + } + } + } + + @Override + String toString(String indent) { + String result = super.toString(indent); + for (int i = 0; i < mTransitions.size(); ++i) { + result += "\n" + mTransitions.get(i).toString(indent + " "); + } + return result; + } + +} diff --git a/core/java/android/view/transition/TransitionInflater.java b/core/java/android/view/transition/TransitionInflater.java new file mode 100644 index 0000000000000..a5f5836578722 --- /dev/null +++ b/core/java/android/view/transition/TransitionInflater.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.Xml; +import android.view.InflateException; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This class inflates scenes and transitions from resource files. + */ +public class TransitionInflater { + + // We only need one inflater for any given context. Also, this allows us to associate + // ids with unique instances per-Context, used to avoid re-inflating + // already-inflated resources into new/different instances + private static final HashMap sInflaterMap = + new HashMap(); + + private Context mContext; + // TODO: do we need id maps for transitions and transitionMgrs as well? + SparseArray mScenes = new SparseArray(); + + private TransitionInflater(Context context) { + mContext = context; + } + + /** + * Obtains the TransitionInflater from the given context. + */ + public static TransitionInflater from(Context context) { + TransitionInflater inflater = sInflaterMap.get(context); + if (inflater != null) { + return inflater; + } + inflater = new TransitionInflater(context); + sInflaterMap.put(context, inflater); + return inflater; + } + + /** + * Loads a {@link Transition} object from a resource + * + * @param resource The resource id of the transition to load + * @return The loaded Transition object + * @throws android.content.res.Resources.NotFoundException when the + * transition cannot be loaded + */ + public Transition inflateTransition(int resource) { + XmlResourceParser parser = mContext.getResources().getXml(resource); + try { + return createTransitionFromXml(parser, Xml.asAttributeSet(parser), null); + } catch (XmlPullParserException e) { + InflateException ex = new InflateException(e.getMessage()); + ex.initCause(e); + throw ex; + } catch (IOException e) { + InflateException ex = new InflateException( + parser.getPositionDescription() + + ": " + e.getMessage()); + ex.initCause(e); + throw ex; + } finally { + parser.close(); + } + } + + /** + * Loads a {@link TransitionManager} object from a resource + * + * + * + * @param resource The resource id of the transition manager to load + * @return The loaded TransitionManager object + * @throws android.content.res.Resources.NotFoundException when the + * transition manager cannot be loaded + */ + public TransitionManager inflateTransitionManager(int resource, ViewGroup sceneRoot) { + XmlResourceParser parser = mContext.getResources().getXml(resource); + try { + return createTransitionManagerFromXml(parser, Xml.asAttributeSet(parser), sceneRoot); + } catch (XmlPullParserException e) { + InflateException ex = new InflateException(e.getMessage()); + ex.initCause(e); + throw ex; + } catch (IOException e) { + InflateException ex = new InflateException( + parser.getPositionDescription() + + ": " + e.getMessage()); + ex.initCause(e); + throw ex; + } finally { + parser.close(); + } + } + + /** + * Loads a {@link Scene} object from a resource + * + * @param resource The resource id of the scene to load + * @return The loaded Scene object + * @throws android.content.res.Resources.NotFoundException when the scene + * cannot be loaded + */ + public Scene inflateScene(int resource, ViewGroup parent) { + Scene scene = mScenes.get(resource); + if (scene != null) { + return scene; + } + XmlResourceParser parser = mContext.getResources().getXml(resource); + try { + scene = createSceneFromXml(parser, Xml.asAttributeSet(parser), parent); + mScenes.put(resource, scene); + return scene; + } catch (XmlPullParserException e) { + InflateException ex = new InflateException(e.getMessage()); + ex.initCause(e); + throw ex; + } catch (IOException e) { + InflateException ex = new InflateException( + parser.getPositionDescription() + + ": " + e.getMessage()); + ex.initCause(e); + throw ex; + } finally { + parser.close(); + } + } + + + // + // Transition loading + // + + private Transition createTransitionFromXml(XmlPullParser parser, + AttributeSet attrs, TransitionGroup transitionGroup) + throws XmlPullParserException, IOException { + + Transition transition = null; + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + + while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && type != XmlPullParser.END_DOCUMENT) { + + boolean newTransition = false; + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + if ("fade".equals(name)) { + transition = new Fade(); + newTransition = true; + } else if ("move".equals(name)) { + transition = new Move(); + newTransition = true; + } else if ("slide".equals(name)) { + transition = new Slide(); + newTransition = true; + } else if ("autoTransition".equals(name)) { + transition = new AutoTransition(); + newTransition = true; + } else if ("recolor".equals(name)) { + transition = new Recolor(); + newTransition = true; + } else if ("transitionGroup".equals(name)) { + transition = new TransitionGroup(); + createTransitionFromXml(parser, attrs, ((TransitionGroup) transition)); + newTransition = true; + } else if ("targets".equals(name)) { + if (parser.getDepth() - 1 > depth && transition != null) { + // We're inside the child tag - add targets to the child + getTargetIDs(parser, attrs, transition); + } else if (parser.getDepth() - 1 == depth && transitionGroup != null) { + // add targets to the group + getTargetIDs(parser, attrs, transitionGroup); + } + } + if (transition != null || "targets".equals(name)) { + if (newTransition) { + loadTransition(transition, attrs); + if (transitionGroup != null) { + transitionGroup.addTransitions(transition); + } + } + } else { + throw new RuntimeException("Unknown scene name: " + parser.getName()); + } + } + + return transition; + } + + private void getTargetIDs(XmlPullParser parser, + AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException { + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + + ArrayList targetIds = new ArrayList(); + while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + if (name.equals("target")) { + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Transition); + int id = a.getResourceId(com.android.internal.R.styleable.Transition_targetID, -1); + if (id >= 0) { + targetIds.add(id); + } + } else { + throw new RuntimeException("Unknown scene name: " + parser.getName()); + } + } + int numTargets = targetIds.size(); + if (numTargets > 0) { + int[] targetsArray = new int[numTargets]; + for (int i = 0; i < targetIds.size(); ++i) { + targetsArray[i] = targetIds.get(i); + } + transition.setTargetIds(targetsArray); + } + } + + private Transition loadTransition(Transition transition, AttributeSet attrs) + throws Resources.NotFoundException { + + TypedArray a = + mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Transition); + long duration = a.getInt(com.android.internal.R.styleable.Transition_duration, -1); + if (duration >= 0) { + transition.setDuration(duration); + } + long startOffset = a.getInt(com.android.internal.R.styleable.Transition_startOffset, -1); + if (startOffset > 0) { + transition.setStartDelay(startOffset); + } + final int resID = + a.getResourceId(com.android.internal.R.styleable.Animator_interpolator, 0); + if (resID > 0) { + transition.setInterpolator(AnimationUtils.loadInterpolator(mContext, resID)); + } + a.recycle(); + return transition; + } + + // + // TransitionManager loading + // + + private TransitionManager createTransitionManagerFromXml(XmlPullParser parser, + AttributeSet attrs, ViewGroup sceneRoot) throws XmlPullParserException, IOException { + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + TransitionManager transitionManager = null; + + while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + if (name.equals("transitionManager")) { + transitionManager = new TransitionManager(); + } else if (name.equals("transition") && (transitionManager != null)) { + loadTransition(attrs, sceneRoot, transitionManager); + } else { + throw new RuntimeException("Unknown scene name: " + parser.getName()); + } + } + return transitionManager; + } + + private void loadTransition(AttributeSet attrs, ViewGroup sceneRoot, + TransitionManager transitionManager) + throws Resources.NotFoundException { + + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.TransitionManager); + int transitionId = attrs.getAttributeResourceValue( + com.android.internal.R.styleable.TransitionManager_transition, -1); + Scene fromScene = null, toScene = null; + int fromId = attrs.getAttributeResourceValue( + com.android.internal.R.styleable.TransitionManager_fromScene, -1); + if (fromId >= 0) fromScene = inflateScene(fromId, sceneRoot); + int toId = attrs.getAttributeResourceValue( + com.android.internal.R.styleable.TransitionManager_toScene, -1); + if (toId >= 0) toScene = inflateScene(toId, sceneRoot); + if (transitionId >= 0) { + Transition transition = inflateTransition(transitionId); + if (transition != null) { + if (fromScene != null) { + if (toScene == null){ + throw new RuntimeException("No matching toScene for given fromScene " + + "for transition ID " + transitionId); + } else { + transitionManager.setTransition(fromScene, toScene, transition); + } + } else if (toId >= 0) { + transitionManager.setTransition(toScene, transition); + } + } + } + a.recycle(); + } + + // + // Scene loading + // + + private Scene createSceneFromXml(XmlPullParser parser, AttributeSet attrs, ViewGroup parent) + throws XmlPullParserException, IOException { + Scene scene = null; + + // Make sure we are on a start tag. + int type; + int depth = parser.getDepth(); + + while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) + && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + String name = parser.getName(); + if (name.equals("scene")) { + scene = loadScene(attrs, parent); + } else { + throw new RuntimeException("Unknown scene name: " + parser.getName()); + } + } + + return scene; + } + + private Scene loadScene(AttributeSet attrs, ViewGroup parent) + throws Resources.NotFoundException { + + Scene scene; + TypedArray a = mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.Scene); + int layoutId = a.getResourceId(com.android.internal.R.styleable.Scene_layout, -1); + if (layoutId >= 0) { + scene = new Scene(parent, layoutId, mContext); + } else { + scene = new Scene(parent); + } + a.recycle(); + return scene; + } +} diff --git a/core/java/android/view/transition/TransitionManager.java b/core/java/android/view/transition/TransitionManager.java new file mode 100644 index 0000000000000..5a1991cdcf90d --- /dev/null +++ b/core/java/android/view/transition/TransitionManager.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import java.util.HashMap; + +/** + * This class manages the set of transitions that fire when there is a + * change of {@link Scene}. To use the manager, add scenes along with + * transition objects with calls to {@link #setTransition(Scene, Transition)} + * or {@link #setTransition(Scene, Scene, Transition)}. Setting specific + * transitions for scene changes is not required; by default, a Scene change + * will use {@link AutoTransition} to do something reasonable for most + * situations. Specifying other transitions for particular scene changes is + * only necessary if the application wants different transition behavior + * in these situations. + */ +public class TransitionManager { + // TODO: how to handle enter/exit? + + private static final Transition sDefaultTransition = new AutoTransition(); + private Transition mDefaultTransition = new AutoTransition(); + + HashMap mSceneTransitions = new HashMap(); + HashMap> mScenePairTransitions = + new HashMap>(); + + /** + * Sets the transition to be used for any scene change for which no + * other transition is explicitly set. The initial value is + * an {@link AutoTransition} instance. + * + * @param transition The default transition to be used for scene changes. + */ + public void setDefaultTransition(Transition transition) { + mDefaultTransition = transition; + } + + /** + * Gets the current default transition. The initial value is an {@link + * AutoTransition} instance. + * + * @return The current default transition. + * @see #setDefaultTransition(Transition) + */ + public Transition getDefaultTransition() { + return mDefaultTransition; + } + + /** + * Sets a specific transition to occur when the given scene is entered. + * + * @param scene The scene which, when applied, will cause the given + * transition to run. + * @param transition The transition that will play when the given scene is + * entered. A value of null will result in the default behavior of + * using {@link AutoTransition}. + */ + public void setTransition(Scene scene, Transition transition) { + mSceneTransitions.put(scene, transition); + } + + /** + * Sets a specific transition to occur when the given pair of scenes is + * exited/entered. + * + * @param fromScene The scene being exited when the given transition will + * be run + * @param toScene The scene being entered when the given transition will + * be run + * @param transition The transition that will play when the given scene is + * entered. A value of null will result in the default behavior of + * using {@link AutoTransition}. + */ + public void setTransition(Scene fromScene, Scene toScene, Transition transition) { + HashMap sceneTransitionMap = mScenePairTransitions.get(toScene); + if (sceneTransitionMap == null) { + sceneTransitionMap = new HashMap(); + mScenePairTransitions.put(toScene, sceneTransitionMap); + } + sceneTransitionMap.put(fromScene, transition); + } + + /** + * Returns the Transition for the given scene being entered. The result + * depends not only on the given scene, but also the scene which the + * {@link Scene#getSceneRoot() sceneRoot} of the Scene is currently in. + * + * @param scene The scene being entered + * @return The Transition to be used for the given scene change. If no + * Transition was specified for this scene change, {@link AutoTransition} + * will be used instead. + */ + private Transition getTransition(Scene scene) { + Transition transition = null; + ViewGroup sceneRoot = scene.getSceneRoot(); + if (sceneRoot != null) { + // TODO: cached in Scene instead? long-term, cache in View itself + Scene currScene = sceneRoot.getCurrentScene(); + if (currScene != null) { + HashMap sceneTransitionMap = mScenePairTransitions.get(scene); + if (sceneTransitionMap != null) { + transition = sceneTransitionMap.get(currScene); + if (transition != null) { + return transition; + } + } + } + } + transition = mSceneTransitions.get(scene); + return (transition != null) ? transition : new AutoTransition(); + } + + /** + * This is where all of the work of a transition/scene-change is + * orchestrated. This method captures the start values for the given + * transition, exits the current Scene, enters the new scene, captures + * the end values for the transition, and finally plays the + * resulting values-populated transition. + * + * @param scene The scene being entered + * @param transition The transition to play for this scene change + */ + private static void changeScene(Scene scene, final Transition transition) { + + final ViewGroup sceneRoot = scene.getSceneRoot(); + + // Capture current values + if (transition != null) { + transition.captureValues(sceneRoot, true); + } + + // Notify previous scene that it is being exited + Scene previousScene = sceneRoot.getCurrentScene(); + if (previousScene != null) { + previousScene.exit(); + } + + scene.enter(); + + if (transition != null) { + final ViewTreeObserver observer = sceneRoot.getViewTreeObserver(); + observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + transition.captureValues(sceneRoot, false); + transition.play(sceneRoot); + return true; + } + }); + } + } + + /** + * Change to the given scene, using the + * appropriate transition for this particular scene change + * (as specified to the TransitionManager, or the default + * if no such transition exists). + * + * @param scene The Scene to change to + */ + public void transitionTo(Scene scene) { + // Auto transition if there is no transition declared for the Scene, but there is + // a root or parent view + changeScene(scene, getTransition(scene)); + + } + + /** + * Static utility method to simply change to the given scene using + * the default transition for TransitionManager. + * + * @param scene The Scene to change to + */ + public static void go(Scene scene) { + changeScene(scene, sDefaultTransition); + } + + /** + * Static utility method to simply change to the given scene using + * the given transition. + * + *

Passing in null for the transition parameter will + * result in the scene changing without any transition running, and is + * equivalent to calling {@link Scene#exit()} on the scene root's + * {@link ViewGroup#getCurrentScene() current scene}, followed by + * {@link Scene#enter()} on the scene specified by the scene + * parameter.

+ * + * @param scene The Scene to change to + * @param transition The transition to use for this scene change. A + * value of null causes the scene change to happen with no transition. + */ + public static void go(Scene scene, Transition transition) { + changeScene(scene, transition); + } + + /** + * Static utility method to simply change to a scene defined by the + * code in the given runnable, which will be executed after + * the current values have been captured for the transition. + * This is equivalent to creating a Scene and calling {@link + * Scene#setEnterAction(Runnable)} with the runnable, then calling + * {@link #go(Scene, Transition)}. The transition used will be the + * default provided by TransitionManager. + * + * @param sceneRoot The root of the View hierarchy used when this scene + * runs a transition automatically. + * @param action The runnable whose {@link Runnable#run() run()} method will + * be called. + */ + public static void go(ViewGroup sceneRoot, Runnable action) { + Scene scene = new Scene(sceneRoot); + scene.setEnterAction(action); + changeScene(scene, sDefaultTransition); + } + + /** + * Static utility method to simply change to a scene defined by the + * code in the given runnable, which will be executed after + * the current values have been captured for the transition. + * This is equivalent to creating a Scene and calling {@link + * Scene#setEnterAction(Runnable)} with the runnable, then calling + * {@link #go(Scene, Transition)}. The given transition will be + * used to animate the changes. + * + *

Passing in null for the transition parameter will + * result in the scene changing without any transition running, and is + * equivalent to calling {@link Scene#exit()} on the scene root's + * {@link ViewGroup#getCurrentScene() current scene}, followed by + * {@link Scene#enter()} on a new scene specified by the + * action parameter.

+ * + * @param sceneRoot The root of the View hierarchy to run the transition on. + * @param action The runnable whose {@link Runnable#run() run()} method will + * be called. + * @param transition The transition to use for this change. A + * value of null causes the change to happen with no transition. + */ + public static void go(ViewGroup sceneRoot, Runnable action, Transition transition) { + Scene scene = new Scene(sceneRoot); + scene.setEnterAction(action); + changeScene(scene, transition); + } +} diff --git a/core/java/android/view/transition/TransitionValues.java b/core/java/android/view/transition/TransitionValues.java new file mode 100644 index 0000000000000..120ace80ae36c --- /dev/null +++ b/core/java/android/view/transition/TransitionValues.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.view.View; +import android.view.ViewGroup; + +import java.util.HashMap; + +/** + * Data structure which holds cached values for the transition. + * The view field is the target which all of the values pertain to. + * The values field is a hashmap which holds information for fields + * according to names selected by the transitions. These names should + * be unique to avoid clobbering values stored by other transitions, + * such as the convention project:transition_name:property_name. For + * example, the platform might store a property "alpha" in a transition + * "Fader" as "android:fader:alpha". + * + *

These values are cached during the + * {@link Transition#captureValues(TransitionValues, boolean)} + * capture} phases of a scene change, once when the start values are captured + * and again when the end values are captured. These start/end values are then + * passed into the transitions during the play phase of the scene change, + * for {@link Transition#prePlay(ViewGroup, TransitionValues, TransitionValues)} and + * for {@link Transition#play(ViewGroup, TransitionValues, TransitionValues)}.

+ */ +public class TransitionValues { + + /** + * The View with these values + */ + public View view; + + /** + * The set of values tracked by transitions for this scene + */ + public final HashMap values = new HashMap(); + + @Override + public String toString() { + String returnValue = "TransitionValues@" + Integer.toHexString(hashCode()) + ":\n"; + returnValue += " view = " + view + "\n"; + returnValue += " values = " + values + "\n"; + return returnValue; + } +} \ No newline at end of file diff --git a/core/java/android/view/transition/Visibility.java b/core/java/android/view/transition/Visibility.java new file mode 100644 index 0000000000000..a3e6e7704792b --- /dev/null +++ b/core/java/android/view/transition/Visibility.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2013 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.view.transition; + +import android.animation.Animator; +import android.view.View; +import android.view.ViewGroup; + +/** + * This transition tracks changes to the visibility of target views in the + * start and end scenes. Visibility is determined not just by the + * {@link View#setVisibility(int)} state of views, but also whether + * views exist in the current view hierarchy. The class is intended to be a + * utility for subclasses such as {@link Fade}, which use this visibility + * information to determine the specific animations to run when visibility + * changes occur. Subclasses should implement one or more of the methods + * {@link #preAppear(ViewGroup, View, int, View, int)}, + * {@link #preDisappear(ViewGroup, View, int, View, int)}, + * {@link #appear(ViewGroup, View, int, View, int)}, and + * {@link #disappear(ViewGroup, View, int, View, int)}. + */ +public abstract class Visibility extends Transition { + + private static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; + private static final String PROPNAME_PARENT = "android:visibility:parent"; + + @Override + protected void captureValues(TransitionValues values, boolean start) { + int visibility = values.view.getVisibility(); + values.values.put(PROPNAME_VISIBILITY, visibility); + values.values.put(PROPNAME_PARENT, values.view.getParent()); + } + + @Override + protected boolean prePlay(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + boolean visibilityChange = false; + boolean fadeIn = false; + int startVisibility, endVisibility; + View startParent, endParent; + if (startValues != null) { + startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); + startParent = (View) startValues.values.get(PROPNAME_PARENT); + } else { + startVisibility = -1; + startParent = null; + } + if (endValues != null) { + endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); + endParent = (View) endValues.values.get(PROPNAME_PARENT); + } else { + endVisibility = -1; + endParent = null; + } + boolean existenceChange = false; + if (startValues != null && endValues != null) { + if (startVisibility == endVisibility && startParent == endParent) { + return false; + } else { + if (startVisibility != endVisibility) { + if (startVisibility == View.VISIBLE) { + fadeIn = false; + visibilityChange = true; + } else if (endVisibility == View.VISIBLE) { + fadeIn = true; + visibilityChange = true; + } + // no visibilityChange if going between INVISIBLE and GONE + } else if (startParent != endParent) { + existenceChange = true; + if (endParent == null) { + fadeIn = false; + visibilityChange = true; + } else if (startParent == null) { + fadeIn = true; + visibilityChange = true; + } + } + } + } + if (startValues == null) { + existenceChange = true; + fadeIn = true; + visibilityChange = true; + } else if (endValues == null) { + existenceChange = true; + fadeIn = false; + visibilityChange = true; + } + if (visibilityChange) { + if (fadeIn) { + return preAppear(sceneRoot, existenceChange ? null : startValues.view, + startVisibility, endValues.view, endVisibility); + } else { + return preDisappear(sceneRoot, startValues.view, startVisibility, + existenceChange ? null : endValues.view, endVisibility); + } + } else { + return false; + } + } + + @Override + protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + boolean visibilityChange = false; + boolean fadeIn = false; + int startVisibility, endVisibility; + View startParent, endParent; + if (startValues != null) { + startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); + startParent = (View) startValues.values.get(PROPNAME_PARENT); + } else { + startVisibility = -1; + startParent = null; + } + if (endValues != null) { + endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); + endParent = (View) endValues.values.get(PROPNAME_PARENT); + } else { + endVisibility = -1; + endParent = null; + } + boolean existenceChange = false; + if (startValues != null && endValues != null) { + if (startVisibility == endVisibility && startParent == endParent) { + return null; + } else { + if (startVisibility != endVisibility) { + if (startVisibility == View.VISIBLE) { + fadeIn = false; + visibilityChange = true; + } else if (endVisibility == View.VISIBLE) { + fadeIn = true; + visibilityChange = true; + } + // no visibilityChange if going between INVISIBLE and GONE + } else if (startParent != endParent) { + existenceChange = true; + if (endParent == null) { + fadeIn = false; + visibilityChange = true; + } else if (startParent == null) { + fadeIn = true; + visibilityChange = true; + } + } + } + } + if (startValues == null) { + existenceChange = true; + fadeIn = true; + visibilityChange = true; + } else if (endValues == null) { + existenceChange = true; + fadeIn = false; + visibilityChange = true; + } + if (visibilityChange) { + if (fadeIn) { + return appear(sceneRoot, existenceChange ? null : startValues.view, startVisibility, + endValues.view, endVisibility); + } else { + return disappear(sceneRoot, startValues.view, startVisibility, + existenceChange ? null : endValues.view, endVisibility); + } + } + return null; + } + + /** + * The default implementation of this method does nothing. Subclasses + * should override if they need to set up anything prior to the + * transition starting. + * + * @param sceneRoot + * @param startView + * @param startVisibility + * @param endView + * @param endVisibility + * @return + */ + protected boolean preAppear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + return true; + } + + /** + * The default implementation of this method does nothing. Subclasses + * should override if they need to set up anything prior to the + * transition starting. + * @param sceneRoot + * @param startView + * @param startVisibility + * @param endView + * @param endVisibility + * @return + */ + protected boolean preDisappear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { + return true; + } + + /** + * The default implementation of this method does nothing. Subclasses + * should override if they need to do anything when target objects + * appear during the scene change. + * @param sceneRoot + * @param startView + * @param startVisibility + * @param endView + * @param endVisibility + */ + protected Animator appear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { return null; } + + /** + * The default implementation of this method does nothing. Subclasses + * should override if they need to do anything when target objects + * disappear during the scene change. + * @param sceneRoot + * @param startView + * @param startVisibility + * @param endView + * @param endVisibility + */ + protected Animator disappear(ViewGroup sceneRoot, View startView, int startVisibility, + View endView, int endVisibility) { return null; } + +} diff --git a/core/java/android/view/transition/package.html b/core/java/android/view/transition/package.html new file mode 100644 index 0000000000000..37dc0ec3f4bf4 --- /dev/null +++ b/core/java/android/view/transition/package.html @@ -0,0 +1,25 @@ + + +

The classes in this package enable "scenes & transitions" functionality for +view hiearchies.

+ +

A Scene is an encapsulation of the state of a view hiearchy, +including the views in that hierarchy and the various values (layout-related +and otherwise) that those views have. A scene be defined by a layout hierarchy +directly or some code which sets up the scene dynamically as it is entered.

+ +

A Transition is a mechanism to automatically animate changes that occur +when a new scene is entered. Some transition capabilities are automatic. That +is, entering a scene may cause animations to run which fade out views that +go away, move and resize existing views that change, and fade in views that +become visible. There are additional transitions that can animate other +attributes, such as color changes, and which can optionally be specified +to take place during particular scene changes. Finally, developers can +define their own Transition subclasses which monitor particular property +changes and which run custom animations when those properties change values.

+ +

TransitionManager is used to specify custom transitions for particular +scene changes, and to cause scene changes with transitions to take place.

+ + + diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 8c7a37462c31b..939d43517281d 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4377,6 +4377,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 03d72079f8a76..1f088a50f6a9d 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2058,5 +2058,9 @@ + + + + diff --git a/tests/TransitionTests/Android.mk b/tests/TransitionTests/Android.mk new file mode 100644 index 0000000000000..22fa63889261e --- /dev/null +++ b/tests/TransitionTests/Android.mk @@ -0,0 +1,18 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := optional + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := TransitionTests + +LOCAL_STATIC_JAVA_LIBRARIES += android-common + +LOCAL_PROGUARD_ENABLED := disabled + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tests/TransitionTests/AndroidManifest.xml b/tests/TransitionTests/AndroidManifest.xml new file mode 100644 index 0000000000000..8cd36bf4f429b --- /dev/null +++ b/tests/TransitionTests/AndroidManifest.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/TransitionTests/res/drawable-hdpi/icon.png b/tests/TransitionTests/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000..8074c4c571b8c Binary files /dev/null and b/tests/TransitionTests/res/drawable-hdpi/icon.png differ diff --git a/tests/TransitionTests/res/drawable-ldpi/icon.png b/tests/TransitionTests/res/drawable-ldpi/icon.png new file mode 100644 index 0000000000000..1095584ec21f7 Binary files /dev/null and b/tests/TransitionTests/res/drawable-ldpi/icon.png differ diff --git a/tests/TransitionTests/res/drawable-mdpi/icon.png b/tests/TransitionTests/res/drawable-mdpi/icon.png new file mode 100644 index 0000000000000..a07c69fa5a0f4 Binary files /dev/null and b/tests/TransitionTests/res/drawable-mdpi/icon.png differ diff --git a/tests/TransitionTests/res/drawable-nodpi/arrow_thumbnail.png b/tests/TransitionTests/res/drawable-nodpi/arrow_thumbnail.png new file mode 100644 index 0000000000000..5cae8f25f6539 Binary files /dev/null and b/tests/TransitionTests/res/drawable-nodpi/arrow_thumbnail.png differ diff --git a/tests/TransitionTests/res/drawable-nodpi/self_portrait_square.jpg b/tests/TransitionTests/res/drawable-nodpi/self_portrait_square.jpg new file mode 100644 index 0000000000000..8cce1c1a35212 Binary files /dev/null and b/tests/TransitionTests/res/drawable-nodpi/self_portrait_square.jpg differ diff --git a/tests/TransitionTests/res/drawable-nodpi/self_portrait_square_100.jpg b/tests/TransitionTests/res/drawable-nodpi/self_portrait_square_100.jpg new file mode 100644 index 0000000000000..26c0a8511e454 Binary files /dev/null and b/tests/TransitionTests/res/drawable-nodpi/self_portrait_square_100.jpg differ diff --git a/tests/TransitionTests/res/drawable-nodpi/self_portrait_square_200.jpg b/tests/TransitionTests/res/drawable-nodpi/self_portrait_square_200.jpg new file mode 100644 index 0000000000000..f18ae5bd16292 Binary files /dev/null and b/tests/TransitionTests/res/drawable-nodpi/self_portrait_square_200.jpg differ diff --git a/tests/TransitionTests/res/layout/activity_login.xml b/tests/TransitionTests/res/layout/activity_login.xml new file mode 100644 index 0000000000000..2aaafc03000f9 --- /dev/null +++ b/tests/TransitionTests/res/layout/activity_login.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/TransitionTests/res/layout/changing_text_1.xml b/tests/TransitionTests/res/layout/changing_text_1.xml new file mode 100644 index 0000000000000..88086a3021591 --- /dev/null +++ b/tests/TransitionTests/res/layout/changing_text_1.xml @@ -0,0 +1,39 @@ + + + + +