EditorAnimator.java revision ca87e9c598929b5b6a62da9b80d2114168e24274
1/*
2 * Copyright (C) 2012 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 com.android.contacts.editor;
18
19import com.google.common.collect.Lists;
20
21import android.animation.Animator;
22import android.animation.Animator.AnimatorListener;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.AnimatorSet;
25import android.animation.ObjectAnimator;
26import android.view.View;
27import android.view.ViewGroup;
28import android.view.ViewParent;
29import android.view.ViewTreeObserver.OnGlobalLayoutListener;
30import android.widget.LinearLayout;
31
32import java.util.List;
33
34/**
35 * Configures animations for typical use-cases
36 */
37public class EditorAnimator {
38    private static EditorAnimator sInstance = new EditorAnimator();
39
40    public static  EditorAnimator getInstance() {
41        return sInstance;
42    }
43
44    /** Private constructor for singleton */
45    private EditorAnimator() { }
46
47    private AnimatorRunner mRunner = new AnimatorRunner();
48
49    public void removeEditorView(final View victim) {
50        mRunner.endOldAnimation();
51        final int offset = victim.getHeight();
52
53        final List<View> viewsToMove = getViewsBelowOf(victim);
54        final List<Animator> animators = Lists.newArrayList();
55
56        // Fade out
57        final ObjectAnimator fadeOutAnimator =
58                ObjectAnimator.ofFloat(victim, View.ALPHA, 1.0f, 0.0f);
59        fadeOutAnimator.setDuration(200);
60        animators.add(fadeOutAnimator);
61
62        // Translations
63        translateViews(animators, viewsToMove, 0.0f, -offset, 100, 200);
64
65        mRunner.run(animators, new AnimatorListenerAdapter() {
66            @Override
67            public void onAnimationEnd(Animator animation) {
68                // Clean up: Remove all the translations
69                for (int i = 0; i < viewsToMove.size(); i++) {
70                    final View view = viewsToMove.get(i);
71                    view.setTranslationY(0.0f);
72                }
73                // Remove our target view (if parent is null, we were run several times by quick
74                // fingers. Just ignore)
75                final ViewGroup victimParent = (ViewGroup) victim.getParent();
76                if (victimParent != null) {
77                    victimParent.removeView(victim);
78                }
79            }
80        });
81    }
82
83    public void expandOrganization(final View addOrganizationButton,
84            final ViewGroup organizationSectionViewContainer) {
85        mRunner.endOldAnimation();
86        // Make the new controls visible and do one layout pass (so that we can measure)
87        organizationSectionViewContainer.setVisibility(View.VISIBLE);
88        organizationSectionViewContainer.setAlpha(0.0f);
89        organizationSectionViewContainer.requestFocus();
90        doAfterLayout(addOrganizationButton, new Runnable() {
91            @Override
92            public void run() {
93                // How many pixels extra do we need?
94                final int offset = organizationSectionViewContainer.getHeight() -
95                        addOrganizationButton.getHeight();
96
97                final List<Animator> animators = Lists.newArrayList();
98
99                // Fade out
100                final ObjectAnimator fadeOutAnimator = ObjectAnimator.ofFloat(
101                        addOrganizationButton, View.ALPHA, 1.0f, 0.0f);
102                fadeOutAnimator.setDuration(200);
103                animators.add(fadeOutAnimator);
104
105                // Translations
106                final List<View> viewsToMove = getViewsBelowOf(organizationSectionViewContainer);
107                translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200);
108
109                // Fade in
110                final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(
111                        organizationSectionViewContainer, View.ALPHA, 0.0f, 1.0f);
112                fadeInAnimator.setDuration(200);
113                fadeInAnimator.setStartDelay(200);
114                animators.add(fadeInAnimator);
115
116                mRunner.run(animators);
117            }
118        });
119    }
120
121    public void showAddFieldFooter(final View view) {
122        mRunner.endOldAnimation();
123        if (view.getVisibility() == View.VISIBLE) return;
124        // Make the new controls visible and do one layout pass (so that we can measure)
125        view.setVisibility(View.VISIBLE);
126        view.setAlpha(0.0f);
127        doAfterLayout(view, new Runnable() {
128            @Override
129            public void run() {
130                // How many pixels extra do we need?
131                final int offset = view.getHeight();
132
133                final List<Animator> animators = Lists.newArrayList();
134
135                // Translations
136                final List<View> viewsToMove = getViewsBelowOf(view);
137                translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200);
138
139                // Fade in
140                final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(
141                        view, View.ALPHA, 0.0f, 1.0f);
142                fadeInAnimator.setDuration(200);
143                fadeInAnimator.setStartDelay(200);
144                animators.add(fadeInAnimator);
145
146                mRunner.run(animators);
147            }
148        });
149    }
150
151    public void hideAddFieldFooter(final View victim) {
152        mRunner.endOldAnimation();
153        if (victim.getVisibility() == View.GONE) return;
154        final int offset = victim.getHeight();
155
156        final List<View> viewsToMove = getViewsBelowOf(victim);
157        final List<Animator> animators = Lists.newArrayList();
158
159        // Fade out
160        final ObjectAnimator fadeOutAnimator =
161                ObjectAnimator.ofFloat(victim, View.ALPHA, 1.0f, 0.0f);
162        fadeOutAnimator.setDuration(200);
163        animators.add(fadeOutAnimator);
164
165        // Translations
166        translateViews(animators, viewsToMove, 0.0f, -offset, 100, 200);
167
168        // Combine
169        mRunner.run(animators, new AnimatorListenerAdapter() {
170            @Override
171            public void onAnimationEnd(Animator animation) {
172                // Clean up: Remove all the translations
173                for (int i = 0; i < viewsToMove.size(); i++) {
174                    final View view = viewsToMove.get(i);
175                    view.setTranslationY(0.0f);
176                }
177
178                // Restore alpha (for next time), but hide the view for good now
179                victim.setAlpha(1.0f);
180                victim.setVisibility(View.GONE);
181            }
182        });
183    }
184
185    /** Runs a piece of code after the next layout run */
186    private static void doAfterLayout(final View view, final Runnable runnable) {
187        final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
188            @Override
189            public void onGlobalLayout() {
190                // Layout pass done, unregister for further events
191                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
192                runnable.run();
193            }
194        };
195        view.getViewTreeObserver().addOnGlobalLayoutListener(listener);
196    }
197
198    /**
199     * Creates a translation-animation for the given views
200     */
201    private static void translateViews(List<Animator> animators, List<View> views, float fromY,
202            float toY, int startDelay, int duration) {
203        for (int i = 0; i < views.size(); i++) {
204            final View child = views.get(i);
205            final ObjectAnimator translateAnimator =
206                    ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, fromY, toY);
207            translateAnimator.setStartDelay(startDelay);
208            translateAnimator.setDuration(duration);
209            animators.add(translateAnimator);
210        }
211    }
212
213    /**
214     * Traverses up the view hierarchy and returns all views below this item. Stops
215     * once a parent is not a vertical LinearLayout
216     */
217    private static List<View> getViewsBelowOf(View view) {
218        final ViewGroup victimParent = (ViewGroup) view.getParent();
219        final List<View> result = Lists.newArrayList();
220        final int index = victimParent.indexOfChild(view);
221        getViewsBelowOfRecursive(result, victimParent, index + 1);
222        return result;
223    }
224
225    private static void getViewsBelowOfRecursive(List<View> result, ViewGroup container,
226            int index) {
227        for (int i = index; i < container.getChildCount(); i++) {
228            result.add(container.getChildAt(i));
229        }
230
231        final ViewParent parent = container.getParent();
232        if (parent instanceof LinearLayout) {
233            final LinearLayout parentLayout = (LinearLayout) parent;
234            if (parentLayout.getOrientation() == LinearLayout.VERTICAL) {
235                int containerIndex = parentLayout.indexOfChild(container);
236                getViewsBelowOfRecursive(result, parentLayout, containerIndex+1);
237            }
238        }
239    }
240
241    /**
242     * Keeps a reference to the last animator, so that we can end that early if the user
243     * quickly pushes buttons. Removes the reference once the animation has finished
244     */
245    /* package */ static class AnimatorRunner extends AnimatorListenerAdapter {
246        private Animator mLastAnimator;
247
248        @Override
249        public void onAnimationEnd(Animator animation) {
250            mLastAnimator = null;
251        }
252
253        public void run(List<Animator> animators) {
254            run(animators, null);
255        }
256
257        public void run(List<Animator> animators, AnimatorListener listener) {
258            final AnimatorSet set = new AnimatorSet();
259            set.playTogether(animators);
260            if (listener != null) set.addListener(listener);
261            set.addListener(this);
262            mLastAnimator = set;
263            set.start();
264        }
265
266        public void endOldAnimation() {
267            if (mLastAnimator != null) {
268                mLastAnimator.end();
269            }
270        }
271    }
272}
273