Added some smoother transitions to the editor

 - Transition when removing a field
 - Transition when pushing the "Add organization" button
 - Transition for visibility of "Add xxx" when entering text
 - Added pushed state on "Add organization" button
 - Increased the height of "Add xxx" to match the expanded size for simple
   Editors from 40dip to 48dip

Bug:6009430
Change-Id: Ia4524006b528f49d587c6277ecc98b11f77ddc0d
diff --git a/src/com/android/contacts/editor/EditorAnimator.java b/src/com/android/contacts/editor/EditorAnimator.java
new file mode 100644
index 0000000..e1ad6d9
--- /dev/null
+++ b/src/com/android/contacts/editor/EditorAnimator.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.editor;
+
+import com.google.common.collect.Lists;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.LinearLayout;
+
+import java.util.List;
+
+/**
+ * Configures animations for typical use-cases
+ */
+public class EditorAnimator {
+    private static EditorAnimator sInstance = new EditorAnimator();
+
+    public static  EditorAnimator getInstance() {
+        return sInstance;
+    }
+
+    /** Private constructor for singleton */
+    private EditorAnimator() { }
+
+    private AnimatorRunner mRunner = new AnimatorRunner();
+
+    public void removeEditorView(final View victim) {
+        mRunner.endOldAnimation();
+        final int offset = victim.getHeight();
+
+        final List<View> viewsToMove = getViewsBelowOf(victim);
+        final List<Animator> animators = Lists.newArrayList();
+
+        // Fade out
+        final ObjectAnimator fadeOutAnimator =
+                ObjectAnimator.ofFloat(victim, View.ALPHA, 1.0f, 0.0f);
+        fadeOutAnimator.setDuration(200);
+        animators.add(fadeOutAnimator);
+
+        // Translations
+        translateViews(animators, viewsToMove, 0.0f, -offset, 100, 200);
+
+        mRunner.run(animators, new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                // Clean up: Remove all the translations
+                for (int i = 0; i < viewsToMove.size(); i++) {
+                    final View view = viewsToMove.get(i);
+                    view.setTranslationY(0.0f);
+                }
+                // Remove our target view (if parent is null, we were run several times by quick
+                // fingers. Just ignore)
+                final ViewGroup victimParent = (ViewGroup) victim.getParent();
+                if (victimParent != null) {
+                    victimParent.removeView(victim);
+                }
+            }
+        });
+    }
+
+    public void expandOrganization(final View addOrganizationButton,
+            final ViewGroup organizationSectionViewContainer) {
+        mRunner.endOldAnimation();
+        // Make the new controls visible and do one layout pass (so that we can measure)
+        organizationSectionViewContainer.setVisibility(View.VISIBLE);
+        organizationSectionViewContainer.setAlpha(0.0f);
+        organizationSectionViewContainer.requestFocus();
+        doAfterLayout(addOrganizationButton, new Runnable() {
+            @Override
+            public void run() {
+                // How many pixels extra do we need?
+                final int offset = organizationSectionViewContainer.getHeight() -
+                        addOrganizationButton.getHeight();
+
+                final List<Animator> animators = Lists.newArrayList();
+
+                // Fade out
+                final ObjectAnimator fadeOutAnimator = ObjectAnimator.ofFloat(
+                        addOrganizationButton, View.ALPHA, 1.0f, 0.0f);
+                fadeOutAnimator.setDuration(200);
+                animators.add(fadeOutAnimator);
+
+                // Translations
+                final List<View> viewsToMove = getViewsBelowOf(organizationSectionViewContainer);
+                translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200);
+
+                // Fade in
+                final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(
+                        organizationSectionViewContainer, View.ALPHA, 0.0f, 1.0f);
+                fadeInAnimator.setDuration(200);
+                fadeInAnimator.setStartDelay(200);
+                animators.add(fadeInAnimator);
+
+                mRunner.run(animators);
+            }
+        });
+    }
+
+    public void showAddFieldFooter(final View view) {
+        mRunner.endOldAnimation();
+        if (view.getVisibility() == View.VISIBLE) return;
+        // Make the new controls visible and do one layout pass (so that we can measure)
+        view.setVisibility(View.VISIBLE);
+        view.setAlpha(0.0f);
+        doAfterLayout(view, new Runnable() {
+            @Override
+            public void run() {
+                // How many pixels extra do we need?
+                final int offset = view.getHeight();
+
+                final List<Animator> animators = Lists.newArrayList();
+
+                // Translations
+                final List<View> viewsToMove = getViewsBelowOf(view);
+                translateViews(animators, viewsToMove, -offset, 0.0f, 0, 200);
+
+                // Fade in
+                final ObjectAnimator fadeInAnimator = ObjectAnimator.ofFloat(
+                        view, View.ALPHA, 0.0f, 1.0f);
+                fadeInAnimator.setDuration(200);
+                fadeInAnimator.setStartDelay(200);
+                animators.add(fadeInAnimator);
+
+                mRunner.run(animators);
+            }
+        });
+    }
+
+    public void hideAddFieldFooter(final View victim) {
+        mRunner.endOldAnimation();
+        if (victim.getVisibility() == View.GONE) return;
+        final int offset = victim.getHeight();
+
+        final List<View> viewsToMove = getViewsBelowOf(victim);
+        final List<Animator> animators = Lists.newArrayList();
+
+        // Fade out
+        final ObjectAnimator fadeOutAnimator =
+                ObjectAnimator.ofFloat(victim, View.ALPHA, 1.0f, 0.0f);
+        fadeOutAnimator.setDuration(200);
+        animators.add(fadeOutAnimator);
+
+        // Translations
+        translateViews(animators, viewsToMove, 0.0f, -offset, 100, 200);
+
+        // Combine
+        mRunner.run(animators, new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                // Clean up: Remove all the translations
+                for (int i = 0; i < viewsToMove.size(); i++) {
+                    final View view = viewsToMove.get(i);
+                    view.setTranslationY(0.0f);
+                }
+
+                // Restore alpha (for next time), but hide the view for good now
+                victim.setAlpha(1.0f);
+                victim.setVisibility(View.GONE);
+            }
+        });
+    }
+
+    /** Runs a piece of code after the next layout run */
+    private static void doAfterLayout(final View view, final Runnable runnable) {
+        final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                // Layout pass done, unregister for further events
+                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                runnable.run();
+            }
+        };
+        view.getViewTreeObserver().addOnGlobalLayoutListener(listener);
+    }
+
+    /**
+     * Creates a translation-animation for the given views
+     */
+    private static void translateViews(List<Animator> animators, List<View> views, float fromY,
+            float toY, int startDelay, int duration) {
+        for (int i = 0; i < views.size(); i++) {
+            final View child = views.get(i);
+            final ObjectAnimator translateAnimator =
+                    ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, fromY, toY);
+            translateAnimator.setStartDelay(startDelay);
+            translateAnimator.setDuration(duration);
+            animators.add(translateAnimator);
+        }
+    }
+
+    /**
+     * Traverses up the view hierarchy and returns all views below this item. Stops
+     * once a parent is not a vertical LinearLayout
+     */
+    private static List<View> getViewsBelowOf(View view) {
+        final ViewGroup victimParent = (ViewGroup) view.getParent();
+        final List<View> result = Lists.newArrayList();
+        final int index = victimParent.indexOfChild(view);
+        getViewsBelowOfRecursive(result, victimParent, index + 1);
+        return result;
+    }
+
+    private static void getViewsBelowOfRecursive(List<View> result, ViewGroup container,
+            int index) {
+        for (int i = index; i < container.getChildCount(); i++) {
+            result.add(container.getChildAt(i));
+        }
+
+        final ViewParent parent = container.getParent();
+        if (parent instanceof LinearLayout) {
+            final LinearLayout parentLayout = (LinearLayout) parent;
+            if (parentLayout.getOrientation() == LinearLayout.VERTICAL) {
+                int containerIndex = parentLayout.indexOfChild(container);
+                getViewsBelowOfRecursive(result, parentLayout, containerIndex+1);
+            }
+        }
+    }
+
+    /**
+     * Keeps a reference to the last animator, so that we can end that early if the user
+     * quickly pushes buttons. Removes the reference once the animation has finished
+     */
+    /* package */ static class AnimatorRunner extends AnimatorListenerAdapter {
+        private Animator mLastAnimator;
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mLastAnimator = null;
+        }
+
+        public void run(List<Animator> animators) {
+            run(animators, null);
+        }
+
+        public void run(List<Animator> animators, AnimatorListener listener) {
+            final AnimatorSet set = new AnimatorSet();
+            set.playTogether(animators);
+            if (listener != null) set.addListener(listener);
+            set.addListener(this);
+            mLastAnimator = set;
+            set.start();
+        }
+
+        public void endOldAnimation() {
+            if (mLastAnimator != null) {
+                mLastAnimator.end();
+            }
+        }
+    }
+}