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,