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