ChangeBounds.java revision 8cd89cddc10ee1f02dd68fb7e8a4480c889fcf41
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.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.PropertyValuesHolder;
24import android.animation.RectEvaluator;
25import android.content.Context;
26import android.content.res.TypedArray;
27import android.graphics.Bitmap;
28import android.graphics.Canvas;
29import android.graphics.Path;
30import android.graphics.PointF;
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 com.android.internal.R;
40
41import java.util.Map;
42
43/**
44 * This transition captures the layout bounds of target views before and after
45 * the scene change and animates those changes during the transition.
46 *
47 * <p>A ChangeBounds transition can be described in a resource file by using the
48 * tag <code>changeBounds</code>, using its attributes of
49 * {@link android.R.styleable#ChangeBounds} along with the other standard
50 * attributes of {@link android.R.styleable#Transition}.</p>
51 */
52public class ChangeBounds extends Transition {
53
54    private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
55    private static final String PROPNAME_CLIP = "android:changeBounds:clip";
56    private static final String PROPNAME_PARENT = "android:changeBounds:parent";
57    private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
58    private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
59    private static final String[] sTransitionProperties = {
60            PROPNAME_BOUNDS,
61            PROPNAME_CLIP,
62            PROPNAME_PARENT,
63            PROPNAME_WINDOW_X,
64            PROPNAME_WINDOW_Y
65    };
66
67    private static final Property<Drawable, PointF> DRAWABLE_ORIGIN_PROPERTY =
68            new Property<Drawable, PointF>(PointF.class, "boundsOrigin") {
69                private Rect mBounds = new Rect();
70
71                @Override
72                public void set(Drawable object, PointF value) {
73                    object.copyBounds(mBounds);
74                    mBounds.offsetTo(Math.round(value.x), Math.round(value.y));
75                    object.setBounds(mBounds);
76                }
77
78                @Override
79                public PointF get(Drawable object) {
80                    object.copyBounds(mBounds);
81                    return new PointF(mBounds.left, mBounds.top);
82                }
83    };
84
85    private static final Property<ViewBounds, PointF> TOP_LEFT_PROPERTY =
86            new Property<ViewBounds, PointF>(PointF.class, "topLeft") {
87                @Override
88                public void set(ViewBounds viewBounds, PointF topLeft) {
89                    viewBounds.setTopLeft(topLeft);
90                }
91
92                @Override
93                public PointF get(ViewBounds viewBounds) {
94                    return null;
95                }
96            };
97
98    private static final Property<ViewBounds, PointF> BOTTOM_RIGHT_PROPERTY =
99            new Property<ViewBounds, PointF>(PointF.class, "bottomRight") {
100                @Override
101                public void set(ViewBounds viewBounds, PointF bottomRight) {
102                    viewBounds.setBottomRight(bottomRight);
103                }
104
105                @Override
106                public PointF get(ViewBounds viewBounds) {
107                    return null;
108                }
109            };
110
111    private static final Property<View, PointF> BOTTOM_RIGHT_ONLY_PROPERTY =
112            new Property<View, PointF>(PointF.class, "bottomRight") {
113                @Override
114                public void set(View view, PointF bottomRight) {
115                    int left = view.getLeft();
116                    int top = view.getTop();
117                    int right = Math.round(bottomRight.x);
118                    int bottom = Math.round(bottomRight.y);
119                    view.setLeftTopRightBottom(left, top, right, bottom);
120                }
121
122                @Override
123                public PointF get(View view) {
124                    return null;
125                }
126            };
127
128    private static final Property<View, PointF> TOP_LEFT_ONLY_PROPERTY =
129            new Property<View, PointF>(PointF.class, "topLeft") {
130                @Override
131                public void set(View view, PointF topLeft) {
132                    int left = Math.round(topLeft.x);
133                    int top = Math.round(topLeft.y);
134                    int right = view.getRight();
135                    int bottom = view.getBottom();
136                    view.setLeftTopRightBottom(left, top, right, bottom);
137                }
138
139                @Override
140                public PointF get(View view) {
141                    return null;
142                }
143            };
144
145    private static final Property<View, PointF> POSITION_PROPERTY =
146            new Property<View, PointF>(PointF.class, "position") {
147                @Override
148                public void set(View view, PointF topLeft) {
149                    int left = Math.round(topLeft.x);
150                    int top = Math.round(topLeft.y);
151                    int right = left + view.getWidth();
152                    int bottom = top + view.getHeight();
153                    view.setLeftTopRightBottom(left, top, right, bottom);
154                }
155
156                @Override
157                public PointF get(View view) {
158                    return null;
159                }
160            };
161
162    int[] tempLocation = new int[2];
163    boolean mResizeClip = false;
164    boolean mReparent = false;
165    private static final String LOG_TAG = "ChangeBounds";
166
167    private static RectEvaluator sRectEvaluator = new RectEvaluator();
168
169    public ChangeBounds() {}
170
171    public ChangeBounds(Context context, AttributeSet attrs) {
172        super(context, attrs);
173
174        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ChangeBounds);
175        boolean resizeClip = a.getBoolean(R.styleable.ChangeBounds_resizeClip, false);
176        a.recycle();
177        setResizeClip(resizeClip);
178    }
179
180    @Override
181    public String[] getTransitionProperties() {
182        return sTransitionProperties;
183    }
184
185    /**
186     * When <code>resizeClip</code> is true, ChangeBounds resizes the view using the clipBounds
187     * instead of changing the dimensions of the view during the animation. When
188     * <code>resizeClip</code> is false, ChangeBounds resizes the View by changing its dimensions.
189     *
190     * <p>When resizeClip is set to true, the clip bounds is modified by ChangeBounds. Therefore,
191     * {@link android.transition.ChangeClipBounds} is not compatible with ChangeBounds
192     * in this mode.</p>
193     *
194     * @param resizeClip Used to indicate whether the view bounds should be modified or the
195     *                   clip bounds should be modified by ChangeBounds.
196     * @see android.view.View#setClipBounds(android.graphics.Rect)
197     * @attr ref android.R.styleable#ChangeBounds_resizeClip
198     */
199    public void setResizeClip(boolean resizeClip) {
200        mResizeClip = resizeClip;
201    }
202
203    /**
204     * Returns true when the ChangeBounds will resize by changing the clip bounds during the
205     * view animation or false when bounds are changed. The default value is false.
206     *
207     * @return true when the ChangeBounds will resize by changing the clip bounds during the
208     * view animation or false when bounds are changed. The default value is false.
209     * @attr ref android.R.styleable#ChangeBounds_resizeClip
210     */
211    public boolean getResizeClip() {
212        return mResizeClip;
213    }
214
215    /**
216     * Setting this flag tells ChangeBounds to track the before/after parent
217     * of every view using this transition. The flag is not enabled by
218     * default because it requires the parent instances to be the same
219     * in the two scenes or else all parents must use ids to allow
220     * the transition to determine which parents are the same.
221     *
222     * @param reparent true if the transition should track the parent
223     * container of target views and animate parent changes.
224     * @deprecated Use {@link android.transition.ChangeTransform} to handle
225     * transitions between different parents.
226     */
227    @Deprecated
228    public void setReparent(boolean reparent) {
229        mReparent = reparent;
230    }
231
232    private void captureValues(TransitionValues values) {
233        View view = values.view;
234
235        if (view.isLaidOut() || view.getWidth() != 0 || view.getHeight() != 0) {
236            values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
237                    view.getRight(), view.getBottom()));
238            values.values.put(PROPNAME_PARENT, values.view.getParent());
239            if (mReparent) {
240                values.view.getLocationInWindow(tempLocation);
241                values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
242                values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
243            }
244            if (mResizeClip) {
245                values.values.put(PROPNAME_CLIP, view.getClipBounds());
246            }
247        }
248    }
249
250    @Override
251    public void captureStartValues(TransitionValues transitionValues) {
252        captureValues(transitionValues);
253    }
254
255    @Override
256    public void captureEndValues(TransitionValues transitionValues) {
257        captureValues(transitionValues);
258    }
259
260    private boolean parentMatches(View startParent, View endParent) {
261        boolean parentMatches = true;
262        if (mReparent) {
263            TransitionValues endValues = getMatchedTransitionValues(startParent, true);
264            if (endValues == null) {
265                parentMatches = startParent == endParent;
266            } else {
267                parentMatches = endParent == endValues.view;
268            }
269        }
270        return parentMatches;
271    }
272
273    @Override
274    public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
275            TransitionValues endValues) {
276        if (startValues == null || endValues == null) {
277            return null;
278        }
279        Map<String, Object> startParentVals = startValues.values;
280        Map<String, Object> endParentVals = endValues.values;
281        ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
282        ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
283        if (startParent == null || endParent == null) {
284            return null;
285        }
286        final View view = endValues.view;
287        if (parentMatches(startParent, endParent)) {
288            Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
289            Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
290            final int startLeft = startBounds.left;
291            final int endLeft = endBounds.left;
292            final int startTop = startBounds.top;
293            final int endTop = endBounds.top;
294            final int startRight = startBounds.right;
295            final int endRight = endBounds.right;
296            final int startBottom = startBounds.bottom;
297            final int endBottom = endBounds.bottom;
298            final int startWidth = startRight - startLeft;
299            final int startHeight = startBottom - startTop;
300            final int endWidth = endRight - endLeft;
301            final int endHeight = endBottom - endTop;
302            Rect startClip = (Rect) startValues.values.get(PROPNAME_CLIP);
303            Rect endClip = (Rect) endValues.values.get(PROPNAME_CLIP);
304            int numChanges = 0;
305            if ((startWidth != 0 && startHeight != 0) || (endWidth != 0 && endHeight != 0)) {
306                if (startLeft != endLeft || startTop != endTop) ++numChanges;
307                if (startRight != endRight || startBottom != endBottom) ++numChanges;
308            }
309            if ((startClip != null && !startClip.equals(endClip)) ||
310                    (startClip == null && endClip != null)) {
311                ++numChanges;
312            }
313            if (numChanges > 0) {
314                if (view.getParent() instanceof ViewGroup) {
315                    final ViewGroup parent = (ViewGroup) view.getParent();
316                    parent.suppressLayout(true);
317                    TransitionListener transitionListener = new TransitionListenerAdapter() {
318                        boolean mCanceled = false;
319
320                        @Override
321                        public void onTransitionCancel(Transition transition) {
322                            parent.suppressLayout(false);
323                            mCanceled = true;
324                        }
325
326                        @Override
327                        public void onTransitionEnd(Transition transition) {
328                            if (!mCanceled) {
329                                parent.suppressLayout(false);
330                            }
331                            transition.removeListener(this);
332                        }
333
334                        @Override
335                        public void onTransitionPause(Transition transition) {
336                            parent.suppressLayout(false);
337                        }
338
339                        @Override
340                        public void onTransitionResume(Transition transition) {
341                            parent.suppressLayout(true);
342                        }
343                    };
344                    addListener(transitionListener);
345                }
346                Animator anim;
347                if (!mResizeClip) {
348                    view.setLeftTopRightBottom(startLeft, startTop, startRight, startBottom);
349                    if (numChanges == 2) {
350                        if (startWidth == endWidth && startHeight == endHeight) {
351                            Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
352                                    endTop);
353                            anim = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null,
354                                    topLeftPath);
355                        } else {
356                            final ViewBounds viewBounds = new ViewBounds(view);
357                            Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
358                                    endLeft, endTop);
359                            ObjectAnimator topLeftAnimator = ObjectAnimator
360                                    .ofObject(viewBounds, TOP_LEFT_PROPERTY, null, topLeftPath);
361
362                            Path bottomRightPath = getPathMotion().getPath(startRight, startBottom,
363                                    endRight, endBottom);
364                            ObjectAnimator bottomRightAnimator = ObjectAnimator.ofObject(viewBounds,
365                                    BOTTOM_RIGHT_PROPERTY, null, bottomRightPath);
366                            AnimatorSet set = new AnimatorSet();
367                            set.playTogether(topLeftAnimator, bottomRightAnimator);
368                            anim = set;
369                            set.addListener(new AnimatorListenerAdapter() {
370                                // We need a strong reference to viewBounds until the
371                                // animator ends.
372                                private ViewBounds mViewBounds = viewBounds;
373                            });
374                        }
375                    } else if (startLeft != endLeft || startTop != endTop) {
376                        Path topLeftPath = getPathMotion().getPath(startLeft, startTop,
377                                endLeft, endTop);
378                        anim = ObjectAnimator.ofObject(view, TOP_LEFT_ONLY_PROPERTY, null,
379                                topLeftPath);
380                    } else {
381                        Path bottomRight = getPathMotion().getPath(startRight, startBottom,
382                                endRight, endBottom);
383                        anim = ObjectAnimator.ofObject(view, BOTTOM_RIGHT_ONLY_PROPERTY, null,
384                                bottomRight);
385                    }
386                } else {
387                    int maxWidth = Math.max(startWidth, endWidth);
388                    int maxHeight = Math.max(startHeight, endHeight);
389
390                    view.setLeftTopRightBottom(startLeft, startTop, startLeft + maxWidth,
391                            startTop + maxHeight);
392
393                    ObjectAnimator positionAnimator = null;
394                    if (startLeft != endLeft || startTop != endTop) {
395                        Path topLeftPath = getPathMotion().getPath(startLeft, startTop, endLeft,
396                                endTop);
397                        positionAnimator = ObjectAnimator.ofObject(view, POSITION_PROPERTY, null,
398                                topLeftPath);
399                    }
400                    final Rect finalClip = endClip;
401                    if (startClip == null) {
402                        startClip = new Rect(0, 0, startWidth, startHeight);
403                    }
404                    if (endClip == null) {
405                        endClip = new Rect(0, 0, endWidth, endHeight);
406                    }
407                    ObjectAnimator clipAnimator = null;
408                    if (!startClip.equals(endClip)) {
409                        view.setClipBounds(startClip);
410                        clipAnimator = ObjectAnimator.ofObject(view, "clipBounds", sRectEvaluator,
411                                startClip, endClip);
412                        clipAnimator.addListener(new AnimatorListenerAdapter() {
413                            private boolean mIsCanceled;
414
415                            @Override
416                            public void onAnimationCancel(Animator animation) {
417                                mIsCanceled = true;
418                            }
419
420                            @Override
421                            public void onAnimationEnd(Animator animation) {
422                                if (!mIsCanceled) {
423                                    view.setClipBounds(finalClip);
424                                    view.setLeftTopRightBottom(endLeft, endTop, endRight,
425                                            endBottom);
426                                }
427                            }
428                        });
429                    }
430                    anim = TransitionUtils.mergeAnimators(positionAnimator,
431                            clipAnimator);
432                }
433                return anim;
434            }
435        } else {
436            sceneRoot.getLocationInWindow(tempLocation);
437            int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
438            int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
439            int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X) - tempLocation[0];
440            int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y) - tempLocation[1];
441            // TODO: also handle size changes: check bounds and animate size changes
442            if (startX != endX || startY != endY) {
443                final int width = view.getWidth();
444                final int height = view.getHeight();
445                Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
446                Canvas canvas = new Canvas(bitmap);
447                view.draw(canvas);
448                final BitmapDrawable drawable = new BitmapDrawable(bitmap);
449                drawable.setBounds(startX, startY, startX + width, startY + height);
450                final float transitionAlpha = view.getTransitionAlpha();
451                view.setTransitionAlpha(0);
452                sceneRoot.getOverlay().add(drawable);
453                Path topLeftPath = getPathMotion().getPath(startX, startY, endX, endY);
454                PropertyValuesHolder origin = PropertyValuesHolder.ofObject(
455                        DRAWABLE_ORIGIN_PROPERTY, null, topLeftPath);
456                ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, origin);
457                anim.addListener(new AnimatorListenerAdapter() {
458                    @Override
459                    public void onAnimationEnd(Animator animation) {
460                        sceneRoot.getOverlay().remove(drawable);
461                        view.setTransitionAlpha(transitionAlpha);
462                    }
463                });
464                return anim;
465            }
466        }
467        return null;
468    }
469
470    private static class ViewBounds {
471        private int mLeft;
472        private int mTop;
473        private int mRight;
474        private int mBottom;
475        private View mView;
476        private int mTopLeftCalls;
477        private int mBottomRightCalls;
478
479        public ViewBounds(View view) {
480            mView = view;
481        }
482
483        public void setTopLeft(PointF topLeft) {
484            mLeft = Math.round(topLeft.x);
485            mTop = Math.round(topLeft.y);
486            mTopLeftCalls++;
487            if (mTopLeftCalls == mBottomRightCalls) {
488                setLeftTopRightBottom();
489            }
490        }
491
492        public void setBottomRight(PointF bottomRight) {
493            mRight = Math.round(bottomRight.x);
494            mBottom = Math.round(bottomRight.y);
495            mBottomRightCalls++;
496            if (mTopLeftCalls == mBottomRightCalls) {
497                setLeftTopRightBottom();
498            }
499        }
500
501        private void setLeftTopRightBottom() {
502            mView.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
503            mTopLeftCalls = 0;
504            mBottomRightCalls = 0;
505        }
506    }
507}
508