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