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();
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/editor/KindSectionView.java b/src/com/android/contacts/editor/KindSectionView.java
index 8a01490..b474956 100644
--- a/src/com/android/contacts/editor/KindSectionView.java
+++ b/src/com/android/contacts/editor/KindSectionView.java
@@ -24,6 +24,7 @@
 import com.android.contacts.model.EntityModifier;
 
 import android.content.Context;
+import android.provider.ContactsContract.Data;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -112,13 +113,18 @@
     public void onDeleteRequested(Editor editor) {
         // If there is only 1 editor in the section, then don't allow the user to delete it.
         // Just clear the fields in the editor.
+        final boolean animate;
         if (getEditorCount() == 1) {
             editor.clearAllFields();
+            animate = true;
         } else {
             // Otherwise it's okay to delete this {@link Editor}
             editor.deleteEditor();
+
+            // This is already animated, don't do anything further here
+            animate = false;
         }
-        updateAddFooterVisible();
+        updateAddFooterVisible(animate);
     }
 
     @Override
@@ -126,7 +132,7 @@
         // If a field has become empty or non-empty, then check if another row
         // can be added dynamically.
         if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
-            updateAddFooterVisible();
+            updateAddFooterVisible(true);
         }
     }
 
@@ -145,7 +151,7 @@
         mTitle.setText(mTitleString);
 
         rebuildFromState();
-        updateAddFooterVisible();
+        updateAddFooterVisible(false);
         updateSectionVisible();
     }
 
@@ -225,18 +231,26 @@
         setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
     }
 
-    protected void updateAddFooterVisible() {
+    protected void updateAddFooterVisible(boolean animate) {
         if (!mReadOnly && (mKind.typeOverallMax != 1)) {
             // First determine whether there are any existing empty editors.
             updateEmptyEditors();
             // If there are no existing empty editors and it's possible to add
             // another field, then make the "add footer" field visible.
             if (!hasEmptyEditor() && EntityModifier.canInsert(mState, mKind)) {
-                mAddFieldFooter.setVisibility(View.VISIBLE);
+                if (animate) {
+                    EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter);
+                } else {
+                    mAddFieldFooter.setVisibility(View.VISIBLE);
+                }
                 return;
             }
         }
-        mAddFieldFooter.setVisibility(View.GONE);
+        if (animate) {
+            EditorAnimator.getInstance().hideAddFieldFooter(mAddFieldFooter);
+        } else {
+            mAddFieldFooter.setVisibility(View.GONE);
+        }
     }
 
     /**
diff --git a/src/com/android/contacts/editor/LabeledEditorView.java b/src/com/android/contacts/editor/LabeledEditorView.java
index 5c09b99..1d12978 100644
--- a/src/com/android/contacts/editor/LabeledEditorView.java
+++ b/src/com/android/contacts/editor/LabeledEditorView.java
@@ -178,7 +178,7 @@
         mEntry.markDeleted();
 
         // Remove the view
-        ((ViewGroup) getParent()).removeView(LabeledEditorView.this);
+        EditorAnimator.getInstance().removeEditorView(this);
     }
 
     public boolean isReadOnly() {
diff --git a/src/com/android/contacts/editor/RawContactEditorView.java b/src/com/android/contacts/editor/RawContactEditorView.java
index efb6b13..cb5e838 100644
--- a/src/com/android/contacts/editor/RawContactEditorView.java
+++ b/src/com/android/contacts/editor/RawContactEditorView.java
@@ -280,9 +280,8 @@
                         public void onClick(View v) {
                             // Once the user expands the organization field, the user cannot
                             // collapse them again.
-                            addOrganizationButton.setVisibility(View.GONE);
-                            organizationSectionViewContainer.setVisibility(View.VISIBLE);
-                            organizationSectionViewContainer.requestFocus();
+                            EditorAnimator.getInstance().expandOrganization(addOrganizationButton,
+                                    organizationSectionViewContainer);
                         }
                     });
 
diff --git a/src/com/android/contacts/editor/TextFieldsEditorView.java b/src/com/android/contacts/editor/TextFieldsEditorView.java
index 5b1dd5f..a1f67a8 100644
--- a/src/com/android/contacts/editor/TextFieldsEditorView.java
+++ b/src/com/android/contacts/editor/TextFieldsEditorView.java
@@ -22,23 +22,17 @@
 import com.android.contacts.model.DataKind;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
-import com.android.contacts.util.NameConverter;
 import com.android.contacts.util.PhoneNumberFormatter;
 
 import android.content.Context;
 import android.content.Entity;
 import android.graphics.Rect;
-import android.graphics.Typeface;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.text.Editable;
 import android.text.InputType;
-import android.text.Spannable;
 import android.text.TextUtils;
 import android.text.TextWatcher;
-import android.text.style.StyleSpan;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.Gravity;
@@ -50,8 +44,6 @@
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 
-import java.util.Map;
-
 /**
  * Simple editor that handles labels and any {@link EditField} defined for the
  * entry. Uses {@link ValuesDelta} to read any existing {@link Entity} values,