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