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