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