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