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