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