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