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.ObjectAnimator;
22import android.animation.PropertyValuesHolder;
23import android.animation.RectEvaluator;
24import android.graphics.Bitmap;
25import android.graphics.Canvas;
26import android.graphics.Rect;
27import android.graphics.drawable.BitmapDrawable;
28import android.view.View;
29import android.view.ViewGroup;
30
31import java.util.Map;
32
33/**
34 * This transition captures the layout bounds of target views before and after
35 * the scene change and animates those changes during the transition.
36 *
37 * <p>A ChangeBounds transition can be described in a resource file by using the
38 * tag <code>changeBounds</code>, along with the other standard
39 * attributes of {@link android.R.styleable#Transition}.</p>
40 */
41public class ChangeBounds extends Transition {
42
43    private static final String PROPNAME_BOUNDS = "android:changeBounds:bounds";
44    private static final String PROPNAME_PARENT = "android:changeBounds:parent";
45    private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
46    private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
47    private static final String[] sTransitionProperties = {
48            PROPNAME_BOUNDS,
49            PROPNAME_PARENT,
50            PROPNAME_WINDOW_X,
51            PROPNAME_WINDOW_Y
52    };
53
54    int[] tempLocation = new int[2];
55    boolean mResizeClip = false;
56    boolean mReparent = false;
57    private static final String LOG_TAG = "ChangeBounds";
58
59    private static RectEvaluator sRectEvaluator = new RectEvaluator();
60
61    @Override
62    public String[] getTransitionProperties() {
63        return sTransitionProperties;
64    }
65
66    public void setResizeClip(boolean resizeClip) {
67        mResizeClip = resizeClip;
68    }
69
70    /**
71     * Setting this flag tells ChangeBounds to track the before/after parent
72     * of every view using this transition. The flag is not enabled by
73     * default because it requires the parent instances to be the same
74     * in the two scenes or else all parents must use ids to allow
75     * the transition to determine which parents are the same.
76     *
77     * @param reparent true if the transition should track the parent
78     * container of target views and animate parent changes.
79     */
80    public void setReparent(boolean reparent) {
81        mReparent = reparent;
82    }
83
84    private void captureValues(TransitionValues values) {
85        View view = values.view;
86        values.values.put(PROPNAME_BOUNDS, new Rect(view.getLeft(), view.getTop(),
87                view.getRight(), view.getBottom()));
88        values.values.put(PROPNAME_PARENT, values.view.getParent());
89        values.view.getLocationInWindow(tempLocation);
90        values.values.put(PROPNAME_WINDOW_X, tempLocation[0]);
91        values.values.put(PROPNAME_WINDOW_Y, tempLocation[1]);
92    }
93
94    @Override
95    public void captureStartValues(TransitionValues transitionValues) {
96        captureValues(transitionValues);
97    }
98
99    @Override
100    public void captureEndValues(TransitionValues transitionValues) {
101        captureValues(transitionValues);
102    }
103
104    @Override
105    public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
106            TransitionValues endValues) {
107        if (startValues == null || endValues == null) {
108            return null;
109        }
110        Map<String, Object> startParentVals = startValues.values;
111        Map<String, Object> endParentVals = endValues.values;
112        ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
113        ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
114        if (startParent == null || endParent == null) {
115            return null;
116        }
117        final View view = endValues.view;
118        boolean parentsEqual = (startParent == endParent) ||
119                (startParent.getId() == endParent.getId());
120        // TODO: Might want reparenting to be separate/subclass transition, or at least
121        // triggered by a property on ChangeBounds. Otherwise, we're forcing the requirement that
122        // all parents in layouts have IDs to avoid layout-inflation resulting in a side-effect
123        // of reparenting the views.
124        if (!mReparent || parentsEqual) {
125            Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
126            Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
127            int startLeft = startBounds.left;
128            int endLeft = endBounds.left;
129            int startTop = startBounds.top;
130            int endTop = endBounds.top;
131            int startRight = startBounds.right;
132            int endRight = endBounds.right;
133            int startBottom = startBounds.bottom;
134            int endBottom = endBounds.bottom;
135            int startWidth = startRight - startLeft;
136            int startHeight = startBottom - startTop;
137            int endWidth = endRight - endLeft;
138            int endHeight = endBottom - endTop;
139            int numChanges = 0;
140            if (startWidth != 0 && startHeight != 0 && endWidth != 0 && endHeight != 0) {
141                if (startLeft != endLeft) ++numChanges;
142                if (startTop != endTop) ++numChanges;
143                if (startRight != endRight) ++numChanges;
144                if (startBottom != endBottom) ++numChanges;
145            }
146            if (numChanges > 0) {
147                if (!mResizeClip) {
148                    PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges];
149                    int pvhIndex = 0;
150                    if (startLeft != endLeft) view.setLeft(startLeft);
151                    if (startTop != endTop) view.setTop(startTop);
152                    if (startRight != endRight) view.setRight(startRight);
153                    if (startBottom != endBottom) view.setBottom(startBottom);
154                    if (startLeft != endLeft) {
155                        pvh[pvhIndex++] = PropertyValuesHolder.ofInt("left", startLeft, endLeft);
156                    }
157                    if (startTop != endTop) {
158                        pvh[pvhIndex++] = PropertyValuesHolder.ofInt("top", startTop, endTop);
159                    }
160                    if (startRight != endRight) {
161                        pvh[pvhIndex++] = PropertyValuesHolder.ofInt("right",
162                                startRight, endRight);
163                    }
164                    if (startBottom != endBottom) {
165                        pvh[pvhIndex++] = PropertyValuesHolder.ofInt("bottom",
166                                startBottom, endBottom);
167                    }
168                    ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh);
169                    if (view.getParent() instanceof ViewGroup) {
170                        final ViewGroup parent = (ViewGroup) view.getParent();
171                        parent.suppressLayout(true);
172                        TransitionListener transitionListener = new TransitionListenerAdapter() {
173                            boolean mCanceled = false;
174
175                            @Override
176                            public void onTransitionCancel(Transition transition) {
177                                parent.suppressLayout(false);
178                                mCanceled = true;
179                            }
180
181                            @Override
182                            public void onTransitionEnd(Transition transition) {
183                                if (!mCanceled) {
184                                    parent.suppressLayout(false);
185                                }
186                            }
187
188                            @Override
189                            public void onTransitionPause(Transition transition) {
190                                parent.suppressLayout(false);
191                            }
192
193                            @Override
194                            public void onTransitionResume(Transition transition) {
195                                parent.suppressLayout(true);
196                            }
197                        };
198                        addListener(transitionListener);
199                    }
200                    return anim;
201                } else {
202                    if (startWidth != endWidth) view.setRight(endLeft +
203                            Math.max(startWidth, endWidth));
204                    if (startHeight != endHeight) view.setBottom(endTop +
205                            Math.max(startHeight, endHeight));
206                    // TODO: don't clobber TX/TY
207                    if (startLeft != endLeft) view.setTranslationX(startLeft - endLeft);
208                    if (startTop != endTop) view.setTranslationY(startTop - endTop);
209                    // Animate location with translationX/Y and size with clip bounds
210                    float transXDelta = endLeft - startLeft;
211                    float transYDelta = endTop - startTop;
212                    int widthDelta = endWidth - startWidth;
213                    int heightDelta = endHeight - startHeight;
214                    numChanges = 0;
215                    if (transXDelta != 0) numChanges++;
216                    if (transYDelta != 0) numChanges++;
217                    if (widthDelta != 0 || heightDelta != 0) numChanges++;
218                    PropertyValuesHolder pvh[] = new PropertyValuesHolder[numChanges];
219                    int pvhIndex = 0;
220                    if (transXDelta != 0) {
221                        pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationX",
222                                view.getTranslationX(), 0);
223                    }
224                    if (transYDelta != 0) {
225                        pvh[pvhIndex++] = PropertyValuesHolder.ofFloat("translationY",
226                                view.getTranslationY(), 0);
227                    }
228                    if (widthDelta != 0 || heightDelta != 0) {
229                        Rect tempStartBounds = new Rect(0, 0, startWidth, startHeight);
230                        Rect tempEndBounds = new Rect(0, 0, endWidth, endHeight);
231                        pvh[pvhIndex++] = PropertyValuesHolder.ofObject("clipBounds",
232                                sRectEvaluator, tempStartBounds, tempEndBounds);
233                    }
234                    ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(view, pvh);
235                    if (view.getParent() instanceof ViewGroup) {
236                        final ViewGroup parent = (ViewGroup) view.getParent();
237                        parent.suppressLayout(true);
238                        TransitionListener transitionListener = new TransitionListenerAdapter() {
239                            boolean mCanceled = false;
240
241                            @Override
242                            public void onTransitionCancel(Transition transition) {
243                                parent.suppressLayout(false);
244                                mCanceled = true;
245                            }
246
247                            @Override
248                            public void onTransitionEnd(Transition transition) {
249                                if (!mCanceled) {
250                                    parent.suppressLayout(false);
251                                }
252                            }
253
254                            @Override
255                            public void onTransitionPause(Transition transition) {
256                                parent.suppressLayout(false);
257                            }
258
259                            @Override
260                            public void onTransitionResume(Transition transition) {
261                                parent.suppressLayout(true);
262                            }
263                        };
264                        addListener(transitionListener);
265                    }
266                    anim.addListener(new AnimatorListenerAdapter() {
267                        @Override
268                        public void onAnimationEnd(Animator animation) {
269                            view.setClipBounds(null);
270                        }
271                    });
272                    return anim;
273                }
274            }
275        } else {
276            int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X);
277            int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y);
278            int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X);
279            int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y);
280            // TODO: also handle size changes: check bounds and animate size changes
281            if (startX != endX || startY != endY) {
282                sceneRoot.getLocationInWindow(tempLocation);
283                Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
284                        Bitmap.Config.ARGB_8888);
285                Canvas canvas = new Canvas(bitmap);
286                view.draw(canvas);
287                final BitmapDrawable drawable = new BitmapDrawable(bitmap);
288                view.setVisibility(View.INVISIBLE);
289                sceneRoot.getOverlay().add(drawable);
290                Rect startBounds1 = new Rect(startX - tempLocation[0], startY - tempLocation[1],
291                        startX - tempLocation[0] + view.getWidth(),
292                        startY - tempLocation[1] + view.getHeight());
293                Rect endBounds1 = new Rect(endX - tempLocation[0], endY - tempLocation[1],
294                        endX - tempLocation[0] + view.getWidth(),
295                        endY - tempLocation[1] + view.getHeight());
296                ObjectAnimator anim = ObjectAnimator.ofObject(drawable, "bounds",
297                        sRectEvaluator, startBounds1, endBounds1);
298                anim.addListener(new AnimatorListenerAdapter() {
299                    @Override
300                    public void onAnimationEnd(Animator animation) {
301                        sceneRoot.getOverlay().remove(drawable);
302                        view.setVisibility(View.VISIBLE);
303                    }
304                });
305                return anim;
306            }
307        }
308        return null;
309    }
310}
311