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 android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.AnimatorSet;
23import android.animation.ObjectAnimator;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.ViewParent;
27import android.widget.LinearLayout;
28import android.widget.ScrollView;
29
30import com.android.contacts.util.SchedulingUtils;
31import com.google.common.collect.Lists;
32
33import java.util.List;
34
35/**
36 * Configures animations for typical use-cases
37 */
38public class EditorAnimator {
39    private static EditorAnimator sInstance = new EditorAnimator();
40
41    public static  EditorAnimator getInstance() {
42        return sInstance;
43    }
44
45    /** Private constructor for singleton */
46    private EditorAnimator() { }
47
48    private AnimatorRunner mRunner = new AnimatorRunner();
49
50    public void removeEditorView(final View victim) {
51        mRunner.endOldAnimation();
52        final int offset = victim.getHeight();
53
54        final List<View> viewsToMove = getViewsBelowOf(victim);
55        final List<Animator> animators = Lists.newArrayList();
56
57        // Fade out
58        final ObjectAnimator fadeOutAnimator =
59                ObjectAnimator.ofFloat(victim, View.ALPHA, 1.0f, 0.0f);
60        fadeOutAnimator.setDuration(200);
61        animators.add(fadeOutAnimator);
62
63        // Translations
64        translateViews(animators, viewsToMove, 0.0f, -offset, 100, 200);
65
66        mRunner.run(animators, new AnimatorListenerAdapter() {
67            @Override
68            public void onAnimationEnd(Animator animation) {
69                // Clean up: Remove all the translations
70                for (int i = 0; i < viewsToMove.size(); i++) {
71                    final View view = viewsToMove.get(i);
72                    view.setTranslationY(0.0f);
73                }
74                // Remove our target view (if parent is null, we were run several times by quick
75                // fingers. Just ignore)
76                final ViewGroup victimParent = (ViewGroup) victim.getParent();
77                if (victimParent != null) {
78                    victimParent.removeView(victim);
79                }
80            }
81        });
82    }
83
84    /**
85     * Slides the view into its new height, while simultaneously fading it into view.
86     *
87     * @param target The target view to perform the animation on.
88     * @param previousHeight The previous height of the view before its height was changed.
89     * Needed because the view does not store any state information about its previous height.
90     */
91    public void slideAndFadeIn(final ViewGroup target, final int previousHeight) {
92        mRunner.endOldAnimation();
93        target.setVisibility(View.VISIBLE);
94        target.setAlpha(0.0f);
95        SchedulingUtils.doAfterLayout(target, new Runnable() {
96            @Override
97            public void run() {
98                final int offset = target.getHeight() - previousHeight;
99                final List<Animator> animators = Lists.newArrayList();
100
101                // Translations
102                final List<View> viewsToMove = getViewsBelowOf(target);
103
104                translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200);
105
106                // Fade in
107                final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(
108                        target, View.ALPHA, 0.0f, 1.0f);
109                fadeInAnimator.setDuration(200);
110                fadeInAnimator.setStartDelay(200);
111                animators.add(fadeInAnimator);
112
113                mRunner.run(animators);
114            }
115        });
116    }
117
118    public void showFieldFooter(final View view) {
119        mRunner.endOldAnimation();
120        if (view.getVisibility() == View.VISIBLE) return;
121        // Make the new controls visible and do one layout pass (so that we can measure)
122        view.setVisibility(View.VISIBLE);
123        view.setAlpha(0.0f);
124        SchedulingUtils.doAfterLayout(view, new Runnable() {
125            @Override
126            public void run() {
127                // How many pixels extra do we need?
128                final int offset = view.getHeight();
129
130                final List<Animator> animators = Lists.newArrayList();
131
132                // Translations
133                final List<View> viewsToMove = getViewsBelowOf(view);
134                translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200);
135
136                // Fade in
137                final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(
138                        view, View.ALPHA, 0.0f, 1.0f);
139                fadeInAnimator.setDuration(200);
140                fadeInAnimator.setStartDelay(200);
141                animators.add(fadeInAnimator);
142
143                mRunner.run(animators);
144            }
145        });
146    }
147
148    /**
149     * Smoothly scroll {@param targetView}'s parent ScrollView to the top of {@param targetView}.
150     */
151    public void scrollViewToTop(final View targetView) {
152        final ScrollView scrollView = getParentScrollView(targetView);
153        SchedulingUtils.doAfterLayout(scrollView, new Runnable() {
154            @Override
155            public void run() {
156                ScrollView scrollView = getParentScrollView(targetView);
157                scrollView.smoothScrollTo(0, offsetFromTopOfViewGroup(targetView, scrollView)
158                        + scrollView.getScrollY());
159            }
160        });
161        // Clear the focused element so it doesn't interfere with scrolling.
162        View view = scrollView.findFocus();
163        if (view != null) {
164            view.clearFocus();
165        }
166    }
167
168    public static void placeFocusAtTopOfScreenAfterReLayout(final View view) {
169        // In order for the focus to be placed at the top of the Window, we need
170        // to wait for layout. Otherwise we don't know where the top of the screen is.
171        SchedulingUtils.doAfterLayout(view, new Runnable() {
172            @Override
173            public void run() {
174                EditorAnimator.getParentScrollView(view).clearFocus();
175            }
176        });
177    }
178
179    private int offsetFromTopOfViewGroup(View view, ViewGroup viewGroup) {
180        int viewLocation[] = new int[2];
181        int viewGroupLocation[] = new int[2];
182        viewGroup.getLocationOnScreen(viewGroupLocation);
183        view.getLocationOnScreen(viewLocation);
184        return viewLocation[1] - viewGroupLocation[1];
185    }
186
187    private static ScrollView getParentScrollView(View view) {
188        while (true) {
189            ViewParent parent = view.getParent();
190            if (parent instanceof ScrollView)
191                return (ScrollView) parent;
192            if (!(parent instanceof View))
193                throw new IllegalArgumentException(
194                        "The editor should be contained inside a ScrollView.");
195            view = (View) parent;
196        }
197    }
198
199    /**
200     * Creates a translation-animation for the given views
201     */
202    private static void translateViews(List<Animator> animators, List<View> views, float fromY,
203            float toY, int startDelay, int duration) {
204        for (int i = 0; i < views.size(); i++) {
205            final View child = views.get(i);
206            final ObjectAnimator translateAnimator =
207                    ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, fromY, toY);
208            translateAnimator.setStartDelay(startDelay);
209            translateAnimator.setDuration(duration);
210            animators.add(translateAnimator);
211        }
212    }
213
214    /**
215     * Traverses up the view hierarchy and returns all views physically below this item.
216     *
217     * @return List of views that are below the given view. Empty list if parent of view is null.
218     */
219    private static List<View> getViewsBelowOf(View view) {
220        final ViewGroup victimParent = (ViewGroup) view.getParent();
221        final List<View> result = Lists.newArrayList();
222        if (victimParent != null) {
223            final int index = victimParent.indexOfChild(view);
224            getViewsBelowOfRecursive(result, victimParent, index + 1, view);
225        }
226        return result;
227    }
228
229    private static void getViewsBelowOfRecursive(List<View> result, ViewGroup container,
230            int index, View target) {
231        for (int i = index; i < container.getChildCount(); i++) {
232            View view = container.getChildAt(i);
233            // consider the child view below the target view only if it is physically
234            // below the view on-screen, using half the height of the target view as the
235            // baseline
236            if (view.getY() > (target.getY() + target.getHeight() / 2)) {
237                result.add(view);
238            }
239        }
240
241        final ViewParent parent = container.getParent();
242        if (parent instanceof LinearLayout) {
243            final LinearLayout parentLayout = (LinearLayout) parent;
244            int containerIndex = parentLayout.indexOfChild(container);
245            getViewsBelowOfRecursive(result, parentLayout, containerIndex + 1, target);
246        }
247    }
248
249    /**
250     * Keeps a reference to the last animator, so that we can end that early if the user
251     * quickly pushes buttons. Removes the reference once the animation has finished
252     */
253    /* package */ static class AnimatorRunner extends AnimatorListenerAdapter {
254        private Animator mLastAnimator;
255
256        @Override
257        public void onAnimationEnd(Animator animation) {
258            mLastAnimator = null;
259        }
260
261        public void run(List<Animator> animators) {
262            run(animators, null);
263        }
264
265        public void run(List<Animator> animators, AnimatorListener listener) {
266            final AnimatorSet set = new AnimatorSet();
267            set.playTogether(animators);
268            if (listener != null) set.addListener(listener);
269            set.addListener(this);
270            mLastAnimator = set;
271            set.start();
272        }
273
274        public void endOldAnimation() {
275            if (mLastAnimator != null) {
276                mLastAnimator.end();
277            }
278        }
279    }
280}
281