Home | History | Annotate | Download | only in transition
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.transition;
     17 
     18 import android.animation.Animator;
     19 import android.animation.AnimatorListenerAdapter;
     20 import android.animation.FloatArrayEvaluator;
     21 import android.animation.ObjectAnimator;
     22 import android.animation.PropertyValuesHolder;
     23 import android.content.Context;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Matrix;
     26 import android.graphics.Path;
     27 import android.graphics.PointF;
     28 import android.util.AttributeSet;
     29 import android.util.Property;
     30 import android.view.GhostView;
     31 import android.view.View;
     32 import android.view.ViewGroup;
     33 import com.android.internal.R;
     34 
     35 /**
     36  * This Transition captures scale and rotation for Views before and after the
     37  * scene change and animates those changes during the transition.
     38  *
     39  * A change in parent is handled as well by capturing the transforms from
     40  * the parent before and after the scene change and animating those during the
     41  * transition.
     42  */
     43 public class ChangeTransform extends Transition {
     44 
     45     private static final String TAG = "ChangeTransform";
     46 
     47     private static final String PROPNAME_MATRIX = "android:changeTransform:matrix";
     48     private static final String PROPNAME_TRANSFORMS = "android:changeTransform:transforms";
     49     private static final String PROPNAME_PARENT = "android:changeTransform:parent";
     50     private static final String PROPNAME_PARENT_MATRIX = "android:changeTransform:parentMatrix";
     51     private static final String PROPNAME_INTERMEDIATE_PARENT_MATRIX =
     52             "android:changeTransform:intermediateParentMatrix";
     53     private static final String PROPNAME_INTERMEDIATE_MATRIX =
     54             "android:changeTransform:intermediateMatrix";
     55 
     56     private static final String[] sTransitionProperties = {
     57             PROPNAME_MATRIX,
     58             PROPNAME_TRANSFORMS,
     59             PROPNAME_PARENT_MATRIX,
     60     };
     61 
     62     /**
     63      * This property sets the animation matrix properties that are not translations.
     64      */
     65     private static final Property<PathAnimatorMatrix, float[]> NON_TRANSLATIONS_PROPERTY =
     66             new Property<PathAnimatorMatrix, float[]>(float[].class, "nonTranslations") {
     67                 @Override
     68                 public float[] get(PathAnimatorMatrix object) {
     69                     return null;
     70                 }
     71 
     72                 @Override
     73                 public void set(PathAnimatorMatrix object, float[] value) {
     74                     object.setValues(value);
     75                 }
     76             };
     77 
     78     /**
     79      * This property sets the translation animation matrix properties.
     80      */
     81     private static final Property<PathAnimatorMatrix, PointF> TRANSLATIONS_PROPERTY =
     82             new Property<PathAnimatorMatrix, PointF>(PointF.class, "translations") {
     83                 @Override
     84                 public PointF get(PathAnimatorMatrix object) {
     85                     return null;
     86                 }
     87 
     88                 @Override
     89                 public void set(PathAnimatorMatrix object, PointF value) {
     90                     object.setTranslation(value);
     91                 }
     92             };
     93 
     94     private boolean mUseOverlay = true;
     95     private boolean mReparent = true;
     96     private Matrix mTempMatrix = new Matrix();
     97 
     98     public ChangeTransform() {}
     99 
    100     public ChangeTransform(Context context, AttributeSet attrs) {
    101         super(context, attrs);
    102         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeTransform);
    103         mUseOverlay = a.getBoolean(R.styleable.ChangeTransform_reparentWithOverlay, true);
    104         mReparent = a.getBoolean(R.styleable.ChangeTransform_reparent, true);
    105         a.recycle();
    106     }
    107 
    108     /**
    109      * Returns whether changes to parent should use an overlay or not. When the parent
    110      * change doesn't use an overlay, it affects the transforms of the child. The
    111      * default value is <code>true</code>.
    112      *
    113      * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when
    114      * it moves outside the bounds of its parent. Setting
    115      * {@link android.view.ViewGroup#setClipChildren(boolean)} and
    116      * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when
    117      * Overlays are not used and the parent is animating its location, the position of the
    118      * child view will be relative to its parent's final position, so it may appear to "jump"
    119      * at the beginning.</p>
    120      *
    121      * @return <code>true</code> when a changed parent should execute the transition
    122      * inside the scene root's overlay or <code>false</code> if a parent change only
    123      * affects the transform of the transitioning view.
    124      * @attr ref android.R.styleable#ChangeTransform_reparentWithOverlay
    125      */
    126     public boolean getReparentWithOverlay() {
    127         return mUseOverlay;
    128     }
    129 
    130     /**
    131      * Sets whether changes to parent should use an overlay or not. When the parent
    132      * change doesn't use an overlay, it affects the transforms of the child. The
    133      * default value is <code>true</code>.
    134      *
    135      * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when
    136      * it moves outside the bounds of its parent. Setting
    137      * {@link android.view.ViewGroup#setClipChildren(boolean)} and
    138      * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when
    139      * Overlays are not used and the parent is animating its location, the position of the
    140      * child view will be relative to its parent's final position, so it may appear to "jump"
    141      * at the beginning.</p>
    142      *
    143      * @return <code>true</code> when a changed parent should execute the transition
    144      * inside the scene root's overlay or <code>false</code> if a parent change only
    145      * affects the transform of the transitioning view.
    146      * @attr ref android.R.styleable#ChangeTransform_reparentWithOverlay
    147      */
    148     public void setReparentWithOverlay(boolean reparentWithOverlay) {
    149         mUseOverlay = reparentWithOverlay;
    150     }
    151 
    152     /**
    153      * Returns whether parent changes will be tracked by the ChangeTransform. If parent
    154      * changes are tracked, then the transform will adjust to the transforms of the
    155      * different parents. If they aren't tracked, only the transforms of the transitioning
    156      * view will be tracked. Default is true.
    157      *
    158      * @return whether parent changes will be tracked by the ChangeTransform.
    159      * @attr ref android.R.styleable#ChangeTransform_reparent
    160      */
    161     public boolean getReparent() {
    162         return mReparent;
    163     }
    164 
    165     /**
    166      * Sets whether parent changes will be tracked by the ChangeTransform. If parent
    167      * changes are tracked, then the transform will adjust to the transforms of the
    168      * different parents. If they aren't tracked, only the transforms of the transitioning
    169      * view will be tracked. Default is true.
    170      *
    171      * @param reparent Set to true to track parent changes or false to only track changes
    172      *                 of the transitioning view without considering the parent change.
    173      * @attr ref android.R.styleable#ChangeTransform_reparent
    174      */
    175     public void setReparent(boolean reparent) {
    176         mReparent = reparent;
    177     }
    178 
    179     @Override
    180     public String[] getTransitionProperties() {
    181         return sTransitionProperties;
    182     }
    183 
    184     private void captureValues(TransitionValues transitionValues) {
    185         View view = transitionValues.view;
    186         if (view.getVisibility() == View.GONE) {
    187             return;
    188         }
    189         transitionValues.values.put(PROPNAME_PARENT, view.getParent());
    190         Transforms transforms = new Transforms(view);
    191         transitionValues.values.put(PROPNAME_TRANSFORMS, transforms);
    192         Matrix matrix = view.getMatrix();
    193         if (matrix == null || matrix.isIdentity()) {
    194             matrix = null;
    195         } else {
    196             matrix = new Matrix(matrix);
    197         }
    198         transitionValues.values.put(PROPNAME_MATRIX, matrix);
    199         if (mReparent) {
    200             Matrix parentMatrix = new Matrix();
    201             ViewGroup parent = (ViewGroup) view.getParent();
    202             parent.transformMatrixToGlobal(parentMatrix);
    203             parentMatrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
    204             transitionValues.values.put(PROPNAME_PARENT_MATRIX, parentMatrix);
    205             transitionValues.values.put(PROPNAME_INTERMEDIATE_MATRIX,
    206                     view.getTag(R.id.transitionTransform));
    207             transitionValues.values.put(PROPNAME_INTERMEDIATE_PARENT_MATRIX,
    208                     view.getTag(R.id.parentMatrix));
    209         }
    210         return;
    211     }
    212 
    213     @Override
    214     public void captureStartValues(TransitionValues transitionValues) {
    215         captureValues(transitionValues);
    216     }
    217 
    218     @Override
    219     public void captureEndValues(TransitionValues transitionValues) {
    220         captureValues(transitionValues);
    221     }
    222 
    223     @Override
    224     public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
    225             TransitionValues endValues) {
    226         if (startValues == null || endValues == null ||
    227                 !startValues.values.containsKey(PROPNAME_PARENT) ||
    228                 !endValues.values.containsKey(PROPNAME_PARENT)) {
    229             return null;
    230         }
    231 
    232         ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
    233         ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
    234         boolean handleParentChange = mReparent && !parentsMatch(startParent, endParent);
    235 
    236         Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_INTERMEDIATE_MATRIX);
    237         if (startMatrix != null) {
    238             startValues.values.put(PROPNAME_MATRIX, startMatrix);
    239         }
    240 
    241         Matrix startParentMatrix = (Matrix)
    242                 startValues.values.get(PROPNAME_INTERMEDIATE_PARENT_MATRIX);
    243         if (startParentMatrix != null) {
    244             startValues.values.put(PROPNAME_PARENT_MATRIX, startParentMatrix);
    245         }
    246 
    247         // First handle the parent change:
    248         if (handleParentChange) {
    249             setMatricesForParent(startValues, endValues);
    250         }
    251 
    252         // Next handle the normal matrix transform:
    253         ObjectAnimator transformAnimator = createTransformAnimator(startValues, endValues,
    254                 handleParentChange);
    255 
    256         if (handleParentChange && transformAnimator != null && mUseOverlay) {
    257             createGhostView(sceneRoot, startValues, endValues);
    258         }
    259 
    260         return transformAnimator;
    261     }
    262 
    263     private ObjectAnimator createTransformAnimator(TransitionValues startValues,
    264             TransitionValues endValues, final boolean handleParentChange) {
    265         Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX);
    266         Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX);
    267 
    268         if (startMatrix == null) {
    269             startMatrix = Matrix.IDENTITY_MATRIX;
    270         }
    271 
    272         if (endMatrix == null) {
    273             endMatrix = Matrix.IDENTITY_MATRIX;
    274         }
    275 
    276         if (startMatrix.equals(endMatrix)) {
    277             return null;
    278         }
    279 
    280         final Transforms transforms = (Transforms) endValues.values.get(PROPNAME_TRANSFORMS);
    281 
    282         // clear the transform properties so that we can use the animation matrix instead
    283         final View view = endValues.view;
    284         setIdentityTransforms(view);
    285 
    286         final float[] startMatrixValues = new float[9];
    287         startMatrix.getValues(startMatrixValues);
    288         final float[] endMatrixValues = new float[9];
    289         endMatrix.getValues(endMatrixValues);
    290         final PathAnimatorMatrix pathAnimatorMatrix =
    291                 new PathAnimatorMatrix(view, startMatrixValues);
    292 
    293         PropertyValuesHolder valuesProperty = PropertyValuesHolder.ofObject(
    294                 NON_TRANSLATIONS_PROPERTY, new FloatArrayEvaluator(new float[9]),
    295                 startMatrixValues, endMatrixValues);
    296         Path path = getPathMotion().getPath(startMatrixValues[Matrix.MTRANS_X],
    297                 startMatrixValues[Matrix.MTRANS_Y], endMatrixValues[Matrix.MTRANS_X],
    298                 endMatrixValues[Matrix.MTRANS_Y]);
    299         PropertyValuesHolder translationProperty = PropertyValuesHolder.ofObject(
    300                 TRANSLATIONS_PROPERTY, null, path);
    301         ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(pathAnimatorMatrix,
    302                 valuesProperty, translationProperty);
    303 
    304         final Matrix finalEndMatrix = endMatrix;
    305 
    306         AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
    307             private boolean mIsCanceled;
    308             private Matrix mTempMatrix = new Matrix();
    309 
    310             @Override
    311             public void onAnimationCancel(Animator animation) {
    312                 mIsCanceled = true;
    313             }
    314 
    315             @Override
    316             public void onAnimationEnd(Animator animation) {
    317                 if (!mIsCanceled) {
    318                     if (handleParentChange && mUseOverlay) {
    319                         setCurrentMatrix(finalEndMatrix);
    320                     } else {
    321                         view.setTagInternal(R.id.transitionTransform, null);
    322                         view.setTagInternal(R.id.parentMatrix, null);
    323                     }
    324                 }
    325                 view.setAnimationMatrix(null);
    326                 transforms.restore(view);
    327             }
    328 
    329             @Override
    330             public void onAnimationPause(Animator animation) {
    331                 Matrix currentMatrix = pathAnimatorMatrix.getMatrix();
    332                 setCurrentMatrix(currentMatrix);
    333             }
    334 
    335             @Override
    336             public void onAnimationResume(Animator animation) {
    337                 setIdentityTransforms(view);
    338             }
    339 
    340             private void setCurrentMatrix(Matrix currentMatrix) {
    341                 mTempMatrix.set(currentMatrix);
    342                 view.setTagInternal(R.id.transitionTransform, mTempMatrix);
    343                 transforms.restore(view);
    344             }
    345         };
    346 
    347         animator.addListener(listener);
    348         animator.addPauseListener(listener);
    349         return animator;
    350     }
    351 
    352     private boolean parentsMatch(ViewGroup startParent, ViewGroup endParent) {
    353         boolean parentsMatch = false;
    354         if (!isValidTarget(startParent) || !isValidTarget(endParent)) {
    355             parentsMatch = startParent == endParent;
    356         } else {
    357             TransitionValues endValues = getMatchedTransitionValues(startParent, true);
    358             if (endValues != null) {
    359                 parentsMatch = endParent == endValues.view;
    360             }
    361         }
    362         return parentsMatch;
    363     }
    364 
    365     private void createGhostView(final ViewGroup sceneRoot, TransitionValues startValues,
    366             TransitionValues endValues) {
    367         View view = endValues.view;
    368 
    369         Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
    370         Matrix localEndMatrix = new Matrix(endMatrix);
    371         sceneRoot.transformMatrixToLocal(localEndMatrix);
    372 
    373         GhostView ghostView = GhostView.addGhost(view, sceneRoot, localEndMatrix);
    374 
    375         Transition outerTransition = this;
    376         while (outerTransition.mParent != null) {
    377             outerTransition = outerTransition.mParent;
    378         }
    379         GhostListener listener = new GhostListener(view, startValues.view, ghostView);
    380         outerTransition.addListener(listener);
    381 
    382         if (startValues.view != endValues.view) {
    383             startValues.view.setTransitionAlpha(0);
    384         }
    385         view.setTransitionAlpha(1);
    386     }
    387 
    388     private void setMatricesForParent(TransitionValues startValues, TransitionValues endValues) {
    389         Matrix endParentMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
    390         endValues.view.setTagInternal(R.id.parentMatrix, endParentMatrix);
    391 
    392         Matrix toLocal = mTempMatrix;
    393         toLocal.reset();
    394         endParentMatrix.invert(toLocal);
    395 
    396         Matrix startLocal = (Matrix) startValues.values.get(PROPNAME_MATRIX);
    397         if (startLocal == null) {
    398             startLocal = new Matrix();
    399             startValues.values.put(PROPNAME_MATRIX, startLocal);
    400         }
    401 
    402         Matrix startParentMatrix = (Matrix) startValues.values.get(PROPNAME_PARENT_MATRIX);
    403         startLocal.postConcat(startParentMatrix);
    404         startLocal.postConcat(toLocal);
    405     }
    406 
    407     private static void setIdentityTransforms(View view) {
    408         setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0);
    409     }
    410 
    411     private static void setTransforms(View view, float translationX, float translationY,
    412             float translationZ, float scaleX, float scaleY, float rotationX,
    413             float rotationY, float rotationZ) {
    414         view.setTranslationX(translationX);
    415         view.setTranslationY(translationY);
    416         view.setTranslationZ(translationZ);
    417         view.setScaleX(scaleX);
    418         view.setScaleY(scaleY);
    419         view.setRotationX(rotationX);
    420         view.setRotationY(rotationY);
    421         view.setRotation(rotationZ);
    422     }
    423 
    424     private static class Transforms {
    425         public final float translationX;
    426         public final float translationY;
    427         public final float translationZ;
    428         public final float scaleX;
    429         public final float scaleY;
    430         public final float rotationX;
    431         public final float rotationY;
    432         public final float rotationZ;
    433 
    434         public Transforms(View view) {
    435             translationX = view.getTranslationX();
    436             translationY = view.getTranslationY();
    437             translationZ = view.getTranslationZ();
    438             scaleX = view.getScaleX();
    439             scaleY = view.getScaleY();
    440             rotationX = view.getRotationX();
    441             rotationY = view.getRotationY();
    442             rotationZ = view.getRotation();
    443         }
    444 
    445         public void restore(View view) {
    446             setTransforms(view, translationX, translationY, translationZ, scaleX, scaleY,
    447                     rotationX, rotationY, rotationZ);
    448         }
    449 
    450         @Override
    451         public boolean equals(Object that) {
    452             if (!(that instanceof Transforms)) {
    453                 return false;
    454             }
    455             Transforms thatTransform = (Transforms) that;
    456             return thatTransform.translationX == translationX &&
    457                     thatTransform.translationY == translationY &&
    458                     thatTransform.translationZ == translationZ &&
    459                     thatTransform.scaleX == scaleX &&
    460                     thatTransform.scaleY == scaleY &&
    461                     thatTransform.rotationX == rotationX &&
    462                     thatTransform.rotationY == rotationY &&
    463                     thatTransform.rotationZ == rotationZ;
    464         }
    465     }
    466 
    467     private static class GhostListener extends Transition.TransitionListenerAdapter {
    468         private View mView;
    469         private View mStartView;
    470         private GhostView mGhostView;
    471 
    472         public GhostListener(View view, View startView, GhostView ghostView) {
    473             mView = view;
    474             mStartView = startView;
    475             mGhostView = ghostView;
    476         }
    477 
    478         @Override
    479         public void onTransitionEnd(Transition transition) {
    480             transition.removeListener(this);
    481             GhostView.removeGhost(mView);
    482             mView.setTagInternal(R.id.transitionTransform, null);
    483             mView.setTagInternal(R.id.parentMatrix, null);
    484             mStartView.setTransitionAlpha(1);
    485         }
    486 
    487         @Override
    488         public void onTransitionPause(Transition transition) {
    489             mGhostView.setVisibility(View.INVISIBLE);
    490         }
    491 
    492         @Override
    493         public void onTransitionResume(Transition transition) {
    494             mGhostView.setVisibility(View.VISIBLE);
    495         }
    496     }
    497 
    498     /**
    499      * PathAnimatorMatrix allows the translations and the rest of the matrix to be set
    500      * separately. This allows the PathMotion to affect the translations while scale
    501      * and rotation are evaluated separately.
    502      */
    503     private static class PathAnimatorMatrix {
    504         private final Matrix mMatrix = new Matrix();
    505         private final View mView;
    506         private final float[] mValues;
    507         private float mTranslationX;
    508         private float mTranslationY;
    509 
    510         public PathAnimatorMatrix(View view, float[] values) {
    511             mView = view;
    512             mValues = values.clone();
    513             mTranslationX = mValues[Matrix.MTRANS_X];
    514             mTranslationY = mValues[Matrix.MTRANS_Y];
    515             setAnimationMatrix();
    516         }
    517 
    518         public void setValues(float[] values) {
    519             System.arraycopy(values, 0, mValues, 0, values.length);
    520             setAnimationMatrix();
    521         }
    522 
    523         public void setTranslation(PointF translation) {
    524             mTranslationX = translation.x;
    525             mTranslationY = translation.y;
    526             setAnimationMatrix();
    527         }
    528 
    529         private void setAnimationMatrix() {
    530             mValues[Matrix.MTRANS_X] = mTranslationX;
    531             mValues[Matrix.MTRANS_Y] = mTranslationY;
    532             mMatrix.setValues(mValues);
    533             mView.setAnimationMatrix(mMatrix);
    534         }
    535 
    536         public Matrix getMatrix() {
    537             return mMatrix;
    538         }
    539     }
    540 }
    541