ChangeTransform.java revision df81a97346c6617a3de1f54d7d13eecd5a3200ee
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.ValueAnimator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Matrix;
26import android.os.Build;
27import android.support.annotation.NonNull;
28import android.support.v4.content.res.TypedArrayUtils;
29import android.support.v4.view.ViewCompat;
30import android.util.AttributeSet;
31import android.util.Property;
32import android.view.View;
33import android.view.ViewGroup;
34
35import org.xmlpull.v1.XmlPullParser;
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 */
45public class ChangeTransform extends Transition {
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    private static final Property<View, Matrix> ANIMATION_MATRIX_PROPERTY =
63            new Property<View, Matrix>(Matrix.class, "animationMatrix") {
64                @Override
65                public Matrix get(View view) {
66                    return null;
67                }
68
69                @Override
70                public void set(View view, Matrix matrix) {
71                    ViewUtils.setAnimationMatrix(view, matrix);
72                }
73            };
74
75    /**
76     * Newer platforms suppress view removal at the beginning of the animation.
77     */
78    private static final boolean SUPPORTS_VIEW_REMOVAL_SUPPRESSION = Build.VERSION.SDK_INT >= 21;
79
80    private boolean mUseOverlay = true;
81    private boolean mReparent = true;
82    private Matrix mTempMatrix = new Matrix();
83
84    public ChangeTransform() {
85    }
86
87    public ChangeTransform(Context context, AttributeSet attrs) {
88        super(context, attrs);
89        TypedArray a = context.obtainStyledAttributes(attrs, Styleable.CHANGE_TRANSFORM);
90        mUseOverlay = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs,
91                "reparentWithOverlay", Styleable.ChangeTransform.REPARENT_WITH_OVERLAY, true);
92        mReparent = TypedArrayUtils.getNamedBoolean(a, (XmlPullParser) attrs,
93                "reparent", Styleable.ChangeTransform.REPARENT, true);
94        a.recycle();
95    }
96
97    /**
98     * Returns whether changes to parent should use an overlay or not. When the parent
99     * change doesn't use an overlay, it affects the transforms of the child. The
100     * default value is <code>true</code>.
101     *
102     * <p>Note: when Overlays are not used when a parent changes, a view can be clipped when
103     * it moves outside the bounds of its parent. Setting
104     * {@link android.view.ViewGroup#setClipChildren(boolean)} and
105     * {@link android.view.ViewGroup#setClipToPadding(boolean)} can help. Also, when
106     * Overlays are not used and the parent is animating its location, the position of the
107     * child view will be relative to its parent's final position, so it may appear to "jump"
108     * at the beginning.</p>
109     *
110     * @return <code>true</code> when a changed parent should execute the transition
111     * inside the scene root's overlay or <code>false</code> if a parent change only
112     * affects the transform of the transitioning view.
113     */
114    public boolean getReparentWithOverlay() {
115        return mUseOverlay;
116    }
117
118    /**
119     * Sets 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     * @param reparentWithOverlay <code>true</code> when a changed parent should execute the
132     *                            transition inside the scene root's overlay or <code>false</code>
133     *                            if a parent change only affects the transform of the
134     *                            transitioning view.
135     */
136    public void setReparentWithOverlay(boolean reparentWithOverlay) {
137        mUseOverlay = reparentWithOverlay;
138    }
139
140    /**
141     * Returns whether parent changes will be tracked by the ChangeTransform. If parent
142     * changes are tracked, then the transform will adjust to the transforms of the
143     * different parents. If they aren't tracked, only the transforms of the transitioning
144     * view will be tracked. Default is true.
145     *
146     * @return whether parent changes will be tracked by the ChangeTransform.
147     */
148    public boolean getReparent() {
149        return mReparent;
150    }
151
152    /**
153     * Sets 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     * @param reparent Set to true to track parent changes or false to only track changes
159     *                 of the transitioning view without considering the parent change.
160     */
161    public void setReparent(boolean reparent) {
162        mReparent = reparent;
163    }
164
165    @Override
166    public String[] getTransitionProperties() {
167        return sTransitionProperties;
168    }
169
170    private void captureValues(TransitionValues transitionValues) {
171        View view = transitionValues.view;
172        if (view.getVisibility() == View.GONE) {
173            return;
174        }
175        transitionValues.values.put(PROPNAME_PARENT, view.getParent());
176        Transforms transforms = new Transforms(view);
177        transitionValues.values.put(PROPNAME_TRANSFORMS, transforms);
178        Matrix matrix = view.getMatrix();
179        if (matrix == null || matrix.isIdentity()) {
180            matrix = null;
181        } else {
182            matrix = new Matrix(matrix);
183        }
184        transitionValues.values.put(PROPNAME_MATRIX, matrix);
185        if (mReparent) {
186            Matrix parentMatrix = new Matrix();
187            ViewGroup parent = (ViewGroup) view.getParent();
188            ViewUtils.transformMatrixToGlobal(parent, parentMatrix);
189            parentMatrix.preTranslate(-parent.getScrollX(), -parent.getScrollY());
190            transitionValues.values.put(PROPNAME_PARENT_MATRIX, parentMatrix);
191            transitionValues.values.put(PROPNAME_INTERMEDIATE_MATRIX,
192                    view.getTag(R.id.transition_transform));
193            transitionValues.values.put(PROPNAME_INTERMEDIATE_PARENT_MATRIX,
194                    view.getTag(R.id.parent_matrix));
195        }
196    }
197
198    @Override
199    public void captureStartValues(@NonNull TransitionValues transitionValues) {
200        captureValues(transitionValues);
201        if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
202            // We still don't know if the view is removed or not, but we need to do this here, or
203            // the view will be actually removed, resulting in flickering at the beginning of the
204            // animation. We are canceling this afterwards.
205            ((ViewGroup) transitionValues.view.getParent()).startViewTransition(
206                    transitionValues.view);
207        }
208    }
209
210    @Override
211    public void captureEndValues(@NonNull TransitionValues transitionValues) {
212        captureValues(transitionValues);
213    }
214
215    @Override
216    public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
217            TransitionValues endValues) {
218        if (startValues == null || endValues == null
219                || !startValues.values.containsKey(PROPNAME_PARENT)
220                || !endValues.values.containsKey(PROPNAME_PARENT)) {
221            return null;
222        }
223
224        ViewGroup startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
225        ViewGroup endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
226        boolean handleParentChange = mReparent && !parentsMatch(startParent, endParent);
227
228        Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_INTERMEDIATE_MATRIX);
229        if (startMatrix != null) {
230            startValues.values.put(PROPNAME_MATRIX, startMatrix);
231        }
232
233        Matrix startParentMatrix = (Matrix)
234                startValues.values.get(PROPNAME_INTERMEDIATE_PARENT_MATRIX);
235        if (startParentMatrix != null) {
236            startValues.values.put(PROPNAME_PARENT_MATRIX, startParentMatrix);
237        }
238
239        // First handle the parent change:
240        if (handleParentChange) {
241            setMatricesForParent(startValues, endValues);
242        }
243
244        // Next handle the normal matrix transform:
245        ObjectAnimator transformAnimator = createTransformAnimator(startValues, endValues,
246                handleParentChange);
247
248        if (handleParentChange && transformAnimator != null && mUseOverlay) {
249            createGhostView(sceneRoot, startValues, endValues);
250        } else if (!SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
251            // We didn't need to suppress the view removal in this case. Cancel the suppression.
252            startParent.endViewTransition(startValues.view);
253        }
254
255        return transformAnimator;
256    }
257
258    private ObjectAnimator createTransformAnimator(TransitionValues startValues,
259            TransitionValues endValues, final boolean handleParentChange) {
260        Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX);
261        Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX);
262
263        if (startMatrix == null) {
264            startMatrix = MatrixUtils.IDENTITY_MATRIX;
265        }
266
267        if (endMatrix == null) {
268            endMatrix = MatrixUtils.IDENTITY_MATRIX;
269        }
270
271        if (startMatrix.equals(endMatrix)) {
272            return null;
273        }
274
275        final Transforms transforms = (Transforms) endValues.values.get(PROPNAME_TRANSFORMS);
276
277        // clear the transform properties so that we can use the animation matrix instead
278        final View view = endValues.view;
279        setIdentityTransforms(view);
280
281        ObjectAnimator animator = ObjectAnimator.ofObject(view, ANIMATION_MATRIX_PROPERTY,
282                new TransitionUtils.MatrixEvaluator(), startMatrix, endMatrix);
283
284        final Matrix finalEndMatrix = endMatrix;
285
286        AnimatorListenerAdapter listener = new AnimatorListenerAdapter() {
287            private boolean mIsCanceled;
288            private Matrix mTempMatrix = new Matrix();
289
290            @Override
291            public void onAnimationCancel(Animator animation) {
292                mIsCanceled = true;
293            }
294
295            @Override
296            public void onAnimationEnd(Animator animation) {
297                if (!mIsCanceled) {
298                    if (handleParentChange && mUseOverlay) {
299                        setCurrentMatrix(finalEndMatrix);
300                    } else {
301                        view.setTag(R.id.transition_transform, null);
302                        view.setTag(R.id.parent_matrix, null);
303                    }
304                }
305                ANIMATION_MATRIX_PROPERTY.set(view, null);
306                transforms.restore(view);
307            }
308
309            @Override
310            public void onAnimationPause(Animator animation) {
311                ValueAnimator animator = (ValueAnimator) animation;
312                Matrix currentMatrix = (Matrix) animator.getAnimatedValue();
313                setCurrentMatrix(currentMatrix);
314            }
315
316            @Override
317            public void onAnimationResume(Animator animation) {
318                setIdentityTransforms(view);
319            }
320
321            private void setCurrentMatrix(Matrix currentMatrix) {
322                mTempMatrix.set(currentMatrix);
323                view.setTag(R.id.transition_transform, mTempMatrix);
324                transforms.restore(view);
325            }
326        };
327
328        animator.addListener(listener);
329        AnimatorUtils.addPauseListener(animator, listener);
330        return animator;
331    }
332
333    private boolean parentsMatch(ViewGroup startParent, ViewGroup endParent) {
334        boolean parentsMatch = false;
335        if (!isValidTarget(startParent) || !isValidTarget(endParent)) {
336            parentsMatch = startParent == endParent;
337        } else {
338            TransitionValues endValues = getMatchedTransitionValues(startParent, true);
339            if (endValues != null) {
340                parentsMatch = endParent == endValues.view;
341            }
342        }
343        return parentsMatch;
344    }
345
346    private void createGhostView(final ViewGroup sceneRoot, TransitionValues startValues,
347            TransitionValues endValues) {
348        View view = endValues.view;
349
350        Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
351        Matrix localEndMatrix = new Matrix(endMatrix);
352        ViewUtils.transformMatrixToLocal(sceneRoot, localEndMatrix);
353
354        GhostViewImpl ghostView = GhostViewUtils.addGhost(view, sceneRoot, localEndMatrix);
355        if (ghostView == null) {
356            return;
357        }
358        // Ask GhostView to actually remove the start view when it starts drawing the animation.
359        ghostView.reserveEndViewTransition((ViewGroup) startValues.values.get(PROPNAME_PARENT),
360                startValues.view);
361
362        Transition outerTransition = this;
363        while (outerTransition.mParent != null) {
364            outerTransition = outerTransition.mParent;
365        }
366
367        GhostListener listener = new GhostListener(view, ghostView);
368        outerTransition.addListener(listener);
369
370        // We cannot do this for older platforms or it invalidates the view and results in
371        // flickering, but the view will still be invisible by actually removing it from the parent.
372        if (SUPPORTS_VIEW_REMOVAL_SUPPRESSION) {
373            if (startValues.view != endValues.view) {
374                ViewUtils.setTransitionAlpha(startValues.view, 0);
375            }
376            ViewUtils.setTransitionAlpha(view, 1);
377        }
378    }
379
380    private void setMatricesForParent(TransitionValues startValues, TransitionValues endValues) {
381        Matrix endParentMatrix = (Matrix) endValues.values.get(PROPNAME_PARENT_MATRIX);
382        endValues.view.setTag(R.id.parent_matrix, endParentMatrix);
383
384        Matrix toLocal = mTempMatrix;
385        toLocal.reset();
386        endParentMatrix.invert(toLocal);
387
388        Matrix startLocal = (Matrix) startValues.values.get(PROPNAME_MATRIX);
389        if (startLocal == null) {
390            startLocal = new Matrix();
391            startValues.values.put(PROPNAME_MATRIX, startLocal);
392        }
393
394        Matrix startParentMatrix = (Matrix) startValues.values.get(PROPNAME_PARENT_MATRIX);
395        startLocal.postConcat(startParentMatrix);
396        startLocal.postConcat(toLocal);
397    }
398
399    private static void setIdentityTransforms(View view) {
400        setTransforms(view, 0, 0, 0, 1, 1, 0, 0, 0);
401    }
402
403    private static void setTransforms(View view, float translationX, float translationY,
404            float translationZ, float scaleX, float scaleY, float rotationX,
405            float rotationY, float rotationZ) {
406        view.setTranslationX(translationX);
407        view.setTranslationY(translationY);
408        ViewCompat.setTranslationZ(view, translationZ);
409        view.setScaleX(scaleX);
410        view.setScaleY(scaleY);
411        view.setRotationX(rotationX);
412        view.setRotationY(rotationY);
413        view.setRotation(rotationZ);
414    }
415
416    private static class Transforms {
417
418        final float mTranslationX;
419        final float mTranslationY;
420        final float mTranslationZ;
421        final float mScaleX;
422        final float mScaleY;
423        final float mRotationX;
424        final float mRotationY;
425        final float mRotationZ;
426
427        Transforms(View view) {
428            mTranslationX = view.getTranslationX();
429            mTranslationY = view.getTranslationY();
430            mTranslationZ = ViewCompat.getTranslationZ(view);
431            mScaleX = view.getScaleX();
432            mScaleY = view.getScaleY();
433            mRotationX = view.getRotationX();
434            mRotationY = view.getRotationY();
435            mRotationZ = view.getRotation();
436        }
437
438        public void restore(View view) {
439            setTransforms(view, mTranslationX, mTranslationY, mTranslationZ, mScaleX, mScaleY,
440                    mRotationX, mRotationY, mRotationZ);
441        }
442
443        @Override
444        public boolean equals(Object that) {
445            if (!(that instanceof Transforms)) {
446                return false;
447            }
448            Transforms thatTransform = (Transforms) that;
449            return thatTransform.mTranslationX == mTranslationX
450                    && thatTransform.mTranslationY == mTranslationY
451                    && thatTransform.mTranslationZ == mTranslationZ
452                    && thatTransform.mScaleX == mScaleX
453                    && thatTransform.mScaleY == mScaleY
454                    && thatTransform.mRotationX == mRotationX
455                    && thatTransform.mRotationY == mRotationY
456                    && thatTransform.mRotationZ == mRotationZ;
457        }
458
459        @Override
460        public int hashCode() {
461            int code = mTranslationX != +0.0f ? Float.floatToIntBits(mTranslationX) : 0;
462            code = 31 * code + (mTranslationY != +0.0f ? Float.floatToIntBits(mTranslationY) : 0);
463            code = 31 * code + (mTranslationZ != +0.0f ? Float.floatToIntBits(mTranslationZ) : 0);
464            code = 31 * code + (mScaleX != +0.0f ? Float.floatToIntBits(mScaleX) : 0);
465            code = 31 * code + (mScaleY != +0.0f ? Float.floatToIntBits(mScaleY) : 0);
466            code = 31 * code + (mRotationX != +0.0f ? Float.floatToIntBits(mRotationX) : 0);
467            code = 31 * code + (mRotationY != +0.0f ? Float.floatToIntBits(mRotationY) : 0);
468            code = 31 * code + (mRotationZ != +0.0f ? Float.floatToIntBits(mRotationZ) : 0);
469            return code;
470        }
471
472    }
473
474    private static class GhostListener extends Transition.TransitionListenerAdapter {
475
476        private View mView;
477        private GhostViewImpl mGhostView;
478
479        GhostListener(View view, GhostViewImpl ghostView) {
480            mView = view;
481            mGhostView = ghostView;
482        }
483
484        @Override
485        public void onTransitionEnd(@NonNull Transition transition) {
486            transition.removeListener(this);
487            GhostViewUtils.removeGhost(mView);
488            mView.setTag(R.id.transition_transform, null);
489            mView.setTag(R.id.parent_matrix, null);
490        }
491
492        @Override
493        public void onTransitionPause(@NonNull Transition transition) {
494            mGhostView.setVisibility(View.INVISIBLE);
495        }
496
497        @Override
498        public void onTransitionResume(@NonNull Transition transition) {
499            mGhostView.setVisibility(View.VISIBLE);
500        }
501
502    }
503
504}
505