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