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 
ChangeTransform()99     public ChangeTransform() {}
100 
ChangeTransform(Context context, AttributeSet attrs)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      */
getReparentWithOverlay()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      */
setReparentWithOverlay(boolean reparentWithOverlay)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      */
getReparent()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      */
setReparent(boolean reparent)177     public void setReparent(boolean reparent) {
178         mReparent = reparent;
179     }
180 
181     @Override
getTransitionProperties()182     public String[] getTransitionProperties() {
183         return sTransitionProperties;
184     }
185 
captureValues(TransitionValues transitionValues)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
captureStartValues(TransitionValues transitionValues)216     public void captureStartValues(TransitionValues transitionValues) {
217         captureValues(transitionValues);
218     }
219 
220     @Override
captureEndValues(TransitionValues transitionValues)221     public void captureEndValues(TransitionValues transitionValues) {
222         captureValues(transitionValues);
223     }
224 
225     @Override
createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)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 
createTransformAnimator(TransitionValues startValues, TransitionValues endValues, final boolean handleParentChange)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 
parentsMatch(ViewGroup startParent, ViewGroup endParent)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 
createGhostView(final ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)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 
setMatricesForParent(TransitionValues startValues, TransitionValues endValues)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 
setIdentityTransforms(View view)409     private static void setIdentityTransforms(View view) {
410         setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0);
411     }
412 
setTransforms(View view, float translationX, float translationY, float translationZ, float scaleX, float scaleY, float rotationX, float rotationY, float rotationZ)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 
Transforms(View view)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 
restore(View view)447         public void restore(View view) {
448             setTransforms(view, translationX, translationY, translationZ, scaleX, scaleY,
449                     rotationX, rotationY, rotationZ);
450         }
451 
452         @Override
equals(Object that)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 
GhostListener(View view, View startView, GhostView ghostView)474         public GhostListener(View view, View startView, GhostView ghostView) {
475             mView = view;
476             mStartView = startView;
477             mGhostView = ghostView;
478         }
479 
480         @Override
onTransitionEnd(Transition transition)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
onTransitionPause(Transition transition)490         public void onTransitionPause(Transition transition) {
491             mGhostView.setVisibility(View.INVISIBLE);
492         }
493 
494         @Override
onTransitionResume(Transition transition)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 
PathAnimatorMatrix(View view, float[] values)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 
setValues(float[] values)520         public void setValues(float[] values) {
521             System.arraycopy(values, 0, mValues, 0, values.length);
522             setAnimationMatrix();
523         }
524 
setTranslation(PointF translation)525         public void setTranslation(PointF translation) {
526             mTranslationX = translation.x;
527             mTranslationY = translation.y;
528             setAnimationMatrix();
529         }
530 
setAnimationMatrix()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 
getMatrix()538         public Matrix getMatrix() {
539             return mMatrix;
540         }
541     }
542 }
543