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