ChangeBounds.java revision d359952459f96a9b57f50a7434b8660836c6e987
1/*
2 * Copyright (C) 2013 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.transition;
18
19import android.animation.AnimatorSet;
20import android.content.Context;
21import android.graphics.PointF;
22
23import android.animation.Animator;
24import android.animation.AnimatorListenerAdapter;
25import android.animation.ObjectAnimator;
26import android.animation.PropertyValuesHolder;
27import android.animation.RectEvaluator;
28import android.graphics.Bitmap;
29import android.graphics.Canvas;
30import android.graphics.Path;
31import android.graphics.Rect;
32import android.graphics.drawable.BitmapDrawable;
33import android.graphics.drawable.Drawable;
34import android.util.AttributeSet;
35import android.util.Property;
36import android.view.View;
37import android.view.ViewGroup;
38
39import java.util.Map;
40
41/**
42 * This transition captures the layout bounds of target views before and after
43 * the scene change and animates those changes during the transition.
44 *
45 * <p>A ChangeBounds transition can be described in a resource file by using the
46 * tag <code>changeBounds</code>, along with the other standard
47 * attributes of {@link android.R.styleable#Transition}.</p>
48 */
49public class ChangeBounds extends Transition {
50
51    private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
52    private static final String PROPNAME_PARENT = "android:changeBounds:parent";
53    private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
54    private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
55    private static final String[] sTransitionProperties = {
56            PROPNAME_BOUNDS,
57            PROPNAME_PARENT,
58            PROPNAME_WINDOW_X,
59            PROPNAME_WINDOW_Y
60    };
61
62    private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
63            new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
64                private Rect mBounds = new Rect();
65
66                @Override
67                public void set(Drawable object, PointF value) {
68                    object.copyBounds(mBounds);
69                    mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
70                    object.setBounds(mBounds);
71                }
72
73                @Override
74                public PointF get(Drawable object) {
75                    object.copyBounds(mBounds);
76                    return new PointF(mBounds.left, mBounds.top);
77                }
78    };
79
80    private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
81            new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
82                @Override
83                public void set(ViewBounds viewBounds, PointF topLeft) {
84                    viewBounds.setTopLeft(topLeft);
85                }
86
87                @Override
88                public PointF get(ViewBounds viewBounds) {
89                    return null;
90                }
91            };
92
93    private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =
94            new Property<ViewBounds, PointF>(PointF.class, "bottomRight") {
95                @Override
96                public void set(ViewBounds viewBounds, PointF bottomRight) {
97                    viewBounds.setBottomRight(bottomRight);
98                }
99
100                @Override
101                public PointF get(ViewBounds viewBounds) {
102                    return null;
103                }
104            };
105
106    int[] tempLocation = new int[2];
107    boolean mResizeClip = false;
108    boolean mReparent = false;
109    private static final String LOG_TAG = "ChangeBounds";
110
111    private static RectEvaluator sRectEvaluator = new RectEvaluator();
112
113    public ChangeBounds() {}
114
115    public ChangeBounds(Context context, AttributeSet attrs) {
116        super(context, attrs);
117    }
118
119    @Override
120    public String[] getTransitionProperties() {
121        return sTransitionProperties;
122    }
123
124    public void setResizeClip(boolean resizeClip) {
125        mResizeClip = resizeClip;
126    }
127
128    /**
129     * Setting this flag tells ChangeBounds to track the before/after parent
130     * of every view using this transition. The flag is not enabled by
131     * default because it requires the parent instances to be the same
132     * in the two scenes or else all parents must use ids to allow
133     * the transition to determine which parents are the same.
134     *
135     * @param reparent true if the transition should track the parent
136     * container of target views and animate parent changes.
137     * @deprecated Use {@link android.transition.ChangeTransform} to handle
138     * transitions between different parents.
139     */
140    public void setReparent(boolean reparent) {
141        mReparent = reparent;
142    }
143
144    private void captureValues(TransitionValues values) {
145        View view = values.view;
146
147        if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
148            values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
149                    view.getRight(), view.getBottom()));
150            values.values.put(PROPNAME_PARENT, values.view.getParent());
151            if (mReparent) {
152                values.view.getLocationInWindow(tempLocation);
153                values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
154                values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
155            }
156        }
157    }
158
159    @Override
160    public void captureStartValues(TransitionValues transitionValues) {
161        captureValues(transitionValues);
162    }
163
164    @Override
165    public void captureEndValues(TransitionValues transitionValues) {
166        captureValues(transitionValues);
167    }
168
169    private boolean parentMatches(View startParent, View endParent) {
170        boolean parentMatches = true;
171        if (mReparent) {
172            TransitionValues endValues = getMatchedTransitionValues(startParent, true);
173            if (endValues == null) {
174                parentMatches = startParent == endParent;
175            } else {
176                parentMatches = endParent == endValues.view;
177            }
178        }
179        return parentMatches;
180    }
181
182    @Override
183    public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
184            TransitionValues endValues) {
185        if (startValues == null || endValues == null) {
186            return null;
187        }
188        Map<String, Object> startParentVals = startValues.values;
189        Map<String, Object> endParentVals = endValues.values;
190        ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
191        ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
192        if (startParent == null || endParent == null) {
193            return null;
194        }
195        final View view = endValues.view;
196        if (parentMatches(startParent, endParent)) {
197            Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
198            Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
199            int startLeft = startBounds.left;
200            int endLeft = endBounds.left;
201            int startTop = startBounds.top;
202            int endTop = endBounds.top;
203            int startRight = startBounds.right;
204            int endRight = endBounds.right;
205            int startBottom = startBounds.bottom;
206            int endBottom = endBounds.bottom;
207            int startWidth = startRight - startLeft;
208            int startHeight = startBottom - startTop;
209            int endWidth = endRight - endLeft;
210            int endHeight = endBottom - endTop;
211            int numChanges = 0;
212            if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
213                if (startLeft != endLeft || startTop != endTop) ++numChanges;
214                if (startRight != endRight || startBottom != endBottom) ++numChanges;
215            }
216            if (numChanges > 0) {
217                if (!mResizeClip) {
218                    view.setLeftTopRightBottom(startLeft, startTop, startRight, startBottom);
219                    ViewBounds viewBounds = new ViewBounds(view);
220                    Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
221                            endLeft, endTop);
222                    ObjectAnimator topLeftAnimator = ObjectAnimator
223                            .ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath);
224
225                    Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
226                            endRight, endBottom);
227                    ObjectAnimator bottomRightAnimator = ObjectAnimator.ofObject(viewBounds,
228                            BOTTOM_RIGHT_PROPERTY, null, bottomRightPath);
229                    AnimatorSet anim = new AnimatorSet();
230                    anim.playTogether(topLeftAnimator, bottomRightAnimator);
231
232                    if (view.getParent() instanceof ViewGroup) {
233                        final ViewGroup parent = (ViewGroup) view.getParent();
234                        parent.suppressLayout(true);
235                        TransitionListener transitionListener = new TransitionListenerAdapter() {
236                            boolean mCanceled = false;
237
238                            @Override
239                            public void onTransitionCancel(Transition transition) {
240                                parent.suppressLayout(false);
241                                mCanceled = true;
242                            }
243
244                            @Override
245                            public void onTransitionEnd(Transition transition) {
246                                if (!mCanceled) {
247                                    parent.suppressLayout(false);
248                                }
249                            }
250
251                            @Override
252                            public void onTransitionPause(Transition transition) {
253                                parent.suppressLayout(false);
254                            }
255
256                            @Override
257                            public void onTransitionResume(Transition transition) {
258                                parent.suppressLayout(true);
259                            }
260                        };
261                        addListener(transitionListener);
262                    }
263                    return anim;
264                } else {
265                    if (startWidth != endWidth) view.setRight(endLeft +
266                            Math.max(startWidth, endWidth));
267                    if (startHeight != endHeight) view.setBottom(endTop +
268                            Math.max(startHeight, endHeight));
269                    // TODO: don't clobber TX/TY
270                    if (startLeft != endLeft) view.setTranslationX(startLeft - endLeft);
271                    if (startTop != endTop) view.setTranslationY(startTop - endTop);
272                    // Animate location with translationX/Y and size with clip bounds
273                    float transXDelta = endLeft - startLeft;
274                    float transYDelta = endTop - startTop;
275                    int widthDelta = endWidth - startWidth;
276                    int heightDelta = endHeight - startHeight;
277                    numChanges = 0;
278                    if (transXDelta != 0) numChanges++;
279                    if (transYDelta != 0) numChanges++;
280                    if (widthDelta != 0 || heightDelta != 0) numChanges++;
281                    ObjectAnimator translationAnimator = null;
282                    if (transXDelta != 0 || transYDelta != 0) {
283                        Path topLeftPath = getPathMotion().getPath(0, 0, transXDelta, transYDelta);
284                        translationAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X,
285                                View.TRANSLATION_Y, topLeftPath);
286                    }
287                    ObjectAnimator clipAnimator = null;
288                    if (widthDelta != 0 || heightDelta != 0) {
289                        Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight);
290                        Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight);
291                        clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
292                                tempStartBounds, tempEndBounds);
293                    }
294                    Animator anim = TransitionUtils.mergeAnimators(translationAnimator,
295                            clipAnimator);
296                    if (view.getParent() instanceof ViewGroup) {
297                        final ViewGroup parent = (ViewGroup) view.getParent();
298                        parent.suppressLayout(true);
299                        TransitionListener transitionListener = new TransitionListenerAdapter() {
300                            boolean mCanceled = false;
301
302                            @Override
303                            public void onTransitionCancel(Transition transition) {
304                                parent.suppressLayout(false);
305                                mCanceled = true;
306                            }
307
308                            @Override
309                            public void onTransitionEnd(Transition transition) {
310                                if (!mCanceled) {
311                                    parent.suppressLayout(false);
312                                }
313                            }
314
315                            @Override
316                            public void onTransitionPause(Transition transition) {
317                                parent.suppressLayout(false);
318                            }
319
320                            @Override
321                            public void onTransitionResume(Transition transition) {
322                                parent.suppressLayout(true);
323                            }
324                        };
325                        addListener(transitionListener);
326                    }
327                    anim.addListener(new AnimatorListenerAdapter() {
328                        @Override
329                        public void onAnimationEnd(Animator animation) {
330                            view.setClipBounds(null);
331                        }
332                    });
333                    return anim;
334                }
335            }
336        } else {
337            int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X);
338            int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y);
339            int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X);
340            int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y);
341            // TODO: also handle size changes: check bounds and animate size changes
342            if (startX != endX || startY != endY) {
343                sceneRoot.getLocationInWindow(tempLocation);
344                Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
345                        Bitmap.Config.ARGB_8888);
346                Canvas canvas = new Canvas(bitmap);
347                view.draw(canvas);
348                final BitmapDrawable drawable = new BitmapDrawable(bitmap);
349                final float transitionAlpha = view.getTransitionAlpha();
350                view.setTransitionAlpha(0);
351                sceneRoot.getOverlay().add(drawable);
352                Path topLeftPath = getPathMotion().getPath(startX - tempLocation[0],
353                        startY - tempLocation[1], endX - tempLocation[0], endY - tempLocation[1]);
354                PropertyValuesHolder origin = PropertyValuesHolder.ofObject(
355                        DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath);
356                ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
357                anim.addListener(new AnimatorListenerAdapter() {
358                    @Override
359                    public void onAnimationEnd(Animator animation) {
360                        sceneRoot.getOverlay().remove(drawable);
361                        view.setTransitionAlpha(transitionAlpha);
362                    }
363                });
364                return anim;
365            }
366        }
367        return null;
368    }
369
370    private static class ViewBounds {
371        private int mLeft;
372        private int mTop;
373        private int mRight;
374        private int mBottom;
375        private boolean mIsTopLeftSet;
376        private boolean mIsBottomRightSet;
377        private View mView;
378
379        public ViewBounds(View view) {
380            mView = view;
381        }
382
383        public void setTopLeft(PointF topLeft) {
384            mLeft = Math.round(topLeft.x);
385            mTop = Math.round(topLeft.y);
386            mIsTopLeftSet = true;
387            if (mIsBottomRightSet) {
388                setLeftTopRightBottom();
389            }
390        }
391
392        public void setBottomRight(PointF bottomRight) {
393            mRight = Math.round(bottomRight.x);
394            mBottom = Math.round(bottomRight.y);
395            mIsBottomRightSet = true;
396            if (mIsTopLeftSet) {
397                setLeftTopRightBottom();
398            }
399        }
400
401        private void setLeftTopRightBottom() {
402            mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
403            mIsTopLeftSet = false;
404            mIsBottomRightSet = false;
405        }
406    }
407}
408