Animation for smart dialing suggestions

Suggestions now appear with a fade in and slide up animation.
Suggestions vanish with a fade out and slide down animation.
If a suggestion is moved into the middle, it slides to the left/right
as appropriate.

Change the layout containing suggestions to a LinearLayout, in order to
better support animations.

Renamed SmartDialAdapter to SmartDialController, and also refactored
it to handle entries for a LinearLayout instead of a GridView, as well
as adding animation support and view management.

Use null object pattern in SmartDialEntry to better handle null entries.

Start displaying suggestions on the first digit entered.

Bug 8840240

Change-Id: If4e16006c0b36d2244434e0b2d8f3d3b997b0ad2
diff --git a/res/layout/dialpad_fragment.xml b/res/layout/dialpad_fragment.xml
index 13d91bd..f3bd2a2 100644
--- a/res/layout/dialpad_fragment.xml
+++ b/res/layout/dialpad_fragment.xml
@@ -57,17 +57,29 @@
             android:src="@drawable/ic_dial_action_delete" />
     </LinearLayout>
 
-    <!-- Smard dial suggestion section -->
-    <GridView
-        android:id="@+id/dialpad_smartdial_list"
+    <!-- Smart dial suggestion section.
+         sp is used here for this layout instead of dp in order for it to resize as
+         appropriate when the font size increases. This is a one-time exception that is
+         ok in this case because there is space for the suggestion strip to expand. -->
+    <RelativeLayout
+        android:id="@+id/dialpad_smartdial_container"
         android:layout_width="match_parent"
         android:layout_height="50sp"
-        android:columnWidth="0dp"
-        android:numColumns="3"
-        android:stretchMode="columnWidth"
-        android:gravity="center"
-        android:layout_marginTop="@dimen/dialpad_vertical_margin"
-        android:background="@drawable/dialpad_background"/>
+        android:layout_marginTop="@dimen/dialpad_vertical_margin">
+        <View
+            android:id="@+id/dialpad_smartdial_list_background"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/dialpad_background">
+        </View>
+        <LinearLayout
+            android:id="@+id/dialpad_smartdial_list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="horizontal"
+            android:gravity="center">
+        </LinearLayout>
+    </RelativeLayout>
 
     <!-- Keypad section -->
     <include layout="@layout/dialpad" />
diff --git a/res/layout/dialpad_smartdial_item.xml b/res/layout/dialpad_smartdial_item.xml
index 54f2a08..32d801e 100644
--- a/res/layout/dialpad_smartdial_item.xml
+++ b/res/layout/dialpad_smartdial_item.xml
@@ -16,15 +16,19 @@
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="46sp">
+    android:layout_width="0dp"
+    android:layout_weight="1"
+    android:layout_height="match_parent"
+    android:layout_marginTop="2dp"
+    android:layout_marginBottom="2dp"
+    android:background="?android:attr/selectableItemBackground">
 
     <com.android.dialer.dialpad.SmartDialTextView
         android:id="@+id/contact_name"
         android:layout_width="match_parent"
         android:layout_height="28sp"
         android:padding="@dimen/smartdial_suggestions_padding"
-        android:textColor="@color/smartdial_primary_text_color"
+        android:textColor="@color/smartdial_name_primary_text_color"
         android:textSize="16sp"
         android:singleLine="true"
         android:ellipsize="none"
@@ -34,7 +38,7 @@
         android:id="@+id/contact_number"
         android:layout_width="match_parent"
         android:layout_height="16sp"
-        android:textColor="@color/dialtacts_secondary_text_color"
+        android:textColor="@color/smartdial_number_primary_text_color"
         android:textSize="13sp"
         android:gravity="center"
     />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 288f58b..1aa217f 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -18,9 +18,10 @@
 
     <!-- Secondary text color in the Phone app -->
     <color name="dialtacts_secondary_text_color">#888888</color>
-    <color name="smartdial_confidence_drawable_color">#39caff</color>
-    <color name="smartdial_primary_text_color">#39caff</color>
-    <color name="smartdial_highlighted_text_color">#ffffff</color>
+    <color name="smartdial_name_primary_text_color">#0099cc</color>
+    <color name="smartdial_name_highlighted_text_color">#39c9ff</color>
+    <color name="smartdial_number_primary_text_color">#bbbbbb</color>
+    <color name="smartdial_number_highlighted_text_color">#ffffff</color>
 
     <!-- Color of the text describing an unconsumed missed call. -->
     <color name="call_log_missed_call_highlight_color">#FF0000</color>
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index f571d89..3d75e39 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -67,8 +67,10 @@
 import android.widget.BaseAdapter;
 import android.widget.EditText;
 import android.widget.ImageView;
+import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.PopupMenu;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import com.android.contacts.common.CallUtil;
@@ -147,13 +149,12 @@
     private DialpadChooserAdapter mDialpadChooserAdapter;
 
     /** Will be set only if the view has the smart dialing section. */
-    private AbsListView mSmartDialList;
+    private RelativeLayout mSmartDialContainer;
 
     /**
-     * Adapter for {@link #mSmartDialList}.
      * Will be set only if the view has the smart dialing section.
      */
-    private SmartDialAdapter mSmartDialAdapter;
+    private SmartDialController mSmartDialAdapter;
 
     private SmartDialCache mSmartDialCache;
     /**
@@ -368,12 +369,12 @@
         mDialpadChooser.setOnItemClickListener(this);
 
         // Smart dial
-        mSmartDialList = (AbsListView) fragmentView.findViewById(R.id.dialpad_smartdial_list);
-        if (mSmartDialList != null) {
-            mSmartDialAdapter = new SmartDialAdapter(getActivity());
-            mSmartDialList.setAdapter(mSmartDialAdapter);
-            mSmartDialList.setOnItemClickListener(new OnSmartDialItemClick());
-            mSmartDialList.setOnItemLongClickListener(new OnSmartDialLongClick());
+        mSmartDialContainer = (RelativeLayout) fragmentView.findViewById(
+                R.id.dialpad_smartdial_container);
+
+        if (mSmartDialContainer != null) {
+            mSmartDialAdapter = new SmartDialController(getActivity(), mSmartDialContainer,
+                    new OnSmartDialShortClick(), new OnSmartDialLongClick());
         }
         return fragmentView;
     }
@@ -1697,7 +1698,7 @@
         }
         mLastDigitsForSmartDial = digits;
 
-        if (digits.length() < 2) {
+        if (digits.length() < 1) {
             mSmartDialAdapter.clear();
         } else {
             final SmartDialLoaderTask task = new SmartDialLoaderTask(this, digits, mSmartDialCache);
@@ -1716,7 +1717,7 @@
     private void initializeSmartDialingState() {
         // Handle smart dialing related state
         if (mSmartDialEnabled) {
-            mSmartDialList.setVisibility(View.VISIBLE);
+            mSmartDialContainer.setVisibility(View.VISIBLE);
             mSmartDialCache = SmartDialCache.getInstance(getActivity(),
                     mContactsPrefs.getDisplayOrder());
             // Don't force recache if this is the first time onResume is being called, since
@@ -1730,14 +1731,14 @@
                 mSmartDialCache.cacheIfNeeded(true);
             }
         } else {
-            mSmartDialList.setVisibility(View.GONE);
+            mSmartDialContainer.setVisibility(View.GONE);
             mSmartDialCache = null;
         }
     }
 
-    private class OnSmartDialLongClick implements AdapterView.OnItemLongClickListener {
+    private class OnSmartDialLongClick implements View.OnLongClickListener {
         @Override
-        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+        public boolean onLongClick(View view) {
             final SmartDialEntry entry = (SmartDialEntry) view.getTag();
             if (entry == null) return false; // just in case.
             mClearDigitsOnStop = true;
@@ -1749,9 +1750,9 @@
         }
     }
 
-    private class OnSmartDialItemClick implements AdapterView.OnItemClickListener {
+    private class OnSmartDialShortClick implements View.OnClickListener {
         @Override
-        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        public void onClick(View view) {
             final SmartDialEntry entry = (SmartDialEntry) view.getTag();
             if (entry == null) return; // just in case.
             // Dial the displayed phone number immediately
diff --git a/src/com/android/dialer/dialpad/SmartDialAdapter.java b/src/com/android/dialer/dialpad/SmartDialAdapter.java
deleted file mode 100644
index 0a246e3..0000000
--- a/src/com/android/dialer/dialpad/SmartDialAdapter.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * 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.dialer.dialpad;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.TextUtils;
-import android.text.style.ForegroundColorSpan;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.LinearLayout;
-
-import com.android.dialer.R;
-import com.google.common.collect.Lists;
-
-import java.util.List;
-
-public class SmartDialAdapter extends BaseAdapter {
-    public static final String LOG_TAG = "SmartDial";
-    private final LayoutInflater mInflater;
-
-    private List<SmartDialEntry> mEntries;
-
-    private final int mHighlightedTextColor;
-
-    public SmartDialAdapter(Context context) {
-        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        final Resources res = context.getResources();
-        mHighlightedTextColor = res.getColor(R.color.smartdial_highlighted_text_color);
-        clear();
-    }
-
-    /** Remove all entries. */
-    public void clear() {
-        mEntries = Lists.newArrayList();
-        notifyDataSetChanged();
-    }
-
-    /** Set entries. */
-    public void setEntries(List<SmartDialEntry> entries) {
-        if (entries == null) throw new IllegalArgumentException();
-        mEntries = entries;
-
-        if (mEntries.size() <= 1) {
-            // add a null entry to push the single entry into the middle
-            mEntries.add(0, null);
-        } else if (mEntries.size() >= 2){
-            // swap the 1st and 2nd entries so that the highest confidence match goes into the
-            // middle
-            final SmartDialEntry temp = mEntries.get(0);
-            mEntries.set(0, mEntries.get(1));
-            mEntries.set(1, temp);
-        }
-
-        notifyDataSetChanged();
-    }
-
-    @Override
-    public boolean isEnabled(int position) {
-        return !(mEntries.get(position) == null);
-    }
-
-    @Override
-    public boolean areAllItemsEnabled() {
-        return false;
-    }
-
-    @Override
-    public int getCount() {
-        return mEntries.size();
-    }
-
-    @Override
-    public Object getItem(int position) {
-        return mEntries.get(position);
-    }
-
-    @Override
-    public long getItemId(int position) {
-        return position; // Just use the position as the ID, so it's not stable.
-    }
-
-    @Override
-    public boolean hasStableIds() {
-        return false; // Not stable because we just use the position as the ID.
-    }
-
-    @Override
-    public View getView(int position, View convertView, ViewGroup parent) {
-        final LinearLayout view;
-        if (convertView == null) {
-            view = (LinearLayout) mInflater.inflate(
-                    R.layout.dialpad_smartdial_item, parent, false);
-        } else {
-            view = (LinearLayout) convertView;
-        }
-
-        final SmartDialTextView nameView = (SmartDialTextView) view.findViewById(R.id.contact_name);
-
-        final SmartDialTextView numberView = (SmartDialTextView) view.findViewById(
-                R.id.contact_number);
-
-        final SmartDialEntry item = mEntries.get(position);
-
-        if (item == null) {
-            // Clear the text in case the view was reused.
-            nameView.setText("");
-            numberView.setText("");
-            // Empty view. We use this to force a single entry to be in the middle
-            return view;
-        }
-
-        // Highlight the display name with the provided match positions
-        if (!TextUtils.isEmpty(item.displayName)) {
-            final SpannableString displayName = new SpannableString(item.displayName);
-            for (final SmartDialMatchPosition p : item.matchPositions) {
-                if (p.start < p.end) {
-                    if (p.end > displayName.length()) {
-                        p.end = displayName.length();
-                    }
-                    // Create a new ForegroundColorSpan for each section of the name to highlight,
-                    // otherwise multiple highlights won't work.
-                    displayName.setSpan(new ForegroundColorSpan(mHighlightedTextColor), p.start,
-                            p.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-                }
-            }
-            nameView.setText(displayName);
-        }
-
-        // Highlight the phone number with the provided match positions
-        if (!TextUtils.isEmpty(item.phoneNumber)) {
-            final SmartDialMatchPosition p = item.phoneNumberMatchPosition;
-            final SpannableString phoneNumber = new SpannableString(item.phoneNumber);
-            if (p != null && p.start < p.end) {
-                if (p.end > phoneNumber.length()) {
-                    p.end = phoneNumber.length();
-                }
-                phoneNumber.setSpan(new ForegroundColorSpan(mHighlightedTextColor), p.start, p.end,
-                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-            }
-            numberView.setText(phoneNumber);
-        }
-        view.setTag(item);
-
-        return view;
-    }
-}
diff --git a/src/com/android/dialer/dialpad/SmartDialCache.java b/src/com/android/dialer/dialpad/SmartDialCache.java
index 51e900a..3294bfb 100644
--- a/src/com/android/dialer/dialpad/SmartDialCache.java
+++ b/src/com/android/dialer/dialpad/SmartDialCache.java
@@ -16,7 +16,7 @@
 
 package com.android.dialer.dialpad;
 
-import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG;
+import static com.android.dialer.dialpad.SmartDialController.LOG_TAG;
 
 import android.content.Context;
 import android.content.SharedPreferences;
diff --git a/src/com/android/dialer/dialpad/SmartDialController.java b/src/com/android/dialer/dialpad/SmartDialController.java
new file mode 100644
index 0000000..5ce993b
--- /dev/null
+++ b/src/com/android/dialer/dialpad/SmartDialController.java
@@ -0,0 +1,389 @@
+/*
+ * 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.dialer.dialpad;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.View.OnLongClickListener;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+* This class controls the display and animation logic behind the smart dialing suggestion strip.
+*
+* It allows a list of SmartDialEntries to be assigned to the suggestion strip via
+* {@link #setEntries}, and also animates the removal of old suggestions.
+*
+* To avoid creating new views every time new entries are assigned, references to 2 *
+* {@link #NUM_SUGGESTIONS} views are kept in {@link #mViews} and {@link #mViewOverlays}.
+*
+* {@code mViews} contains the active views that are currently being displayed to the user,
+* while {@code mViewOverlays} contains the views that are used as view overlays. The view
+* overlays are used to provide the illusion of the former suggestions fading out. These two
+* lists of views are rotated each time a new set of entries is assigned to achieve the appropriate
+* cross fade animations using the new {@link View#getOverlay()} API.
+*/
+public class SmartDialController {
+    public static final String LOG_TAG = "SmartDial";
+
+    /**
+     * Handtuned interpolator used to achieve the bounce effect when suggestions slide up. It
+     * uses a combination of a decelerate interpolator and overshoot interpolator to first
+     * decelerate, and then overshoot its top bounds and bounce back to its final position.
+     */
+    private class DecelerateAndOvershootInterpolator implements Interpolator {
+        private DecelerateInterpolator a;
+        private OvershootInterpolator b;
+
+        public DecelerateAndOvershootInterpolator() {
+            a = new DecelerateInterpolator(1.5f);
+            b = new OvershootInterpolator(1.3f);
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            if (input > 0.6) {
+                return b.getInterpolation(input);
+            } else {
+                return a.getInterpolation(input);
+            }
+        }
+
+    }
+
+    private DecelerateAndOvershootInterpolator mDecelerateAndOvershootInterpolator =
+            new DecelerateAndOvershootInterpolator();
+    private AccelerateDecelerateInterpolator mAccelerateDecelerateInterpolator =
+            new AccelerateDecelerateInterpolator();
+
+    private List<SmartDialEntry> mEntries;
+    private List<SmartDialEntry> mOldEntries;
+
+    private final int mNameHighlightedTextColor;
+    private final int mNumberHighlightedTextColor;
+
+    private final LinearLayout mList;
+    private final View mBackground;
+
+    private final List<LinearLayout> mViewOverlays = Lists.newArrayList();
+    private final List<LinearLayout> mViews = Lists.newArrayList();
+
+    private static final int NUM_SUGGESTIONS = 3;
+
+    private static final long ANIM_DURATION = 200;
+
+    private static final float BACKGROUND_FADE_AMOUNT = 0.25f;
+
+    Resources mResources;
+
+    public SmartDialController(Context context, ViewGroup parent,
+            OnClickListener shortClickListener, OnLongClickListener longClickListener) {
+        final Resources res = context.getResources();
+        mResources = res;
+
+        mNameHighlightedTextColor = res.getColor(R.color.smartdial_name_highlighted_text_color);
+        mNumberHighlightedTextColor = res.getColor(
+                R.color.smartdial_number_highlighted_text_color);
+
+        mList = (LinearLayout) parent.findViewById(R.id.dialpad_smartdial_list);
+        mBackground = parent.findViewById(R.id.dialpad_smartdial_list_background);
+
+        mEntries = Lists.newArrayList();
+        for (int i = 0; i < NUM_SUGGESTIONS; i++) {
+            mEntries.add(SmartDialEntry.NULL);
+        }
+
+        mOldEntries = mEntries;
+
+        final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+
+        for (int i = 0; i < NUM_SUGGESTIONS * 2; i++) {
+            final LinearLayout view = (LinearLayout) inflater.inflate(
+                    R.layout.dialpad_smartdial_item, mList, false);
+            view.setOnClickListener(shortClickListener);
+            view.setOnLongClickListener(longClickListener);
+            if (i < NUM_SUGGESTIONS) {
+                mViews.add(view);
+            } else {
+                mViewOverlays.add(view);
+            }
+            // Add all the views to mList so that they can get measured properly for animation
+            // purposes. Once setEntries is called they will be removed and added as appropriate.
+            view.setEnabled(false);
+            mList.addView(view);
+        }
+    }
+
+    /** Remove all entries. */
+    public void clear() {
+        mOldEntries = mEntries;
+        mEntries = Lists.newArrayList();
+        for (int i = 0; i < NUM_SUGGESTIONS; i++) {
+            mEntries.add(SmartDialEntry.NULL);
+        }
+        updateViews();
+    }
+
+    /** Set entries. At the end of this method {@link #mEntries} should contain exactly
+     *  {@link #NUM_SUGGESTIONS} entries.*/
+    public void setEntries(List<SmartDialEntry> entries) {
+        if (entries == null) throw new IllegalArgumentException();
+        mOldEntries = mEntries;
+        mEntries = entries;
+
+        final int size = mEntries.size();
+        if (size <= 1) {
+            if (size == 0) {
+                mEntries.add(SmartDialEntry.NULL);
+            }
+            // add a null entry to push the single entry into the middle
+            mEntries.add(0, SmartDialEntry.NULL);
+        } else if (size >= 2) {
+            // swap the 1st and 2nd entries so that the highest confidence match goes into the
+            // middle
+            swap(0, 1);
+        }
+
+        while (mEntries.size() < NUM_SUGGESTIONS) {
+            mEntries.add(SmartDialEntry.NULL);
+        }
+
+        updateViews();
+    }
+
+    /**
+     * This method is called every time a new set of SmartDialEntries is to be assigned to the
+     * suggestions view. The current set of active views are to be used as view overlays and
+     * faded out, while the former view overlays are assigned the current entries, added to
+     * {@link #mList} and faded into view.
+     */
+    private void updateViews() {
+        // Remove all views from the root in preparation to swap the two sets of views
+        mList.removeAllViews();
+        try {
+            mList.getOverlay().clear();
+        } catch (NullPointerException e) {
+            // Catch possible NPE b/8895794
+        }
+
+        // Used to track whether or not to animate the overlay. In the case where the suggestion
+        // at position i will slide from the left or right, or if the suggestion at position i
+        // has not changed, the overlay at i should be hidden immediately. Overlay animations are
+        // set in a separate loop from the active views to avoid unnecessarily reanimating the same
+        // overlay multiple times.
+        boolean[] dontAnimateOverlay = new boolean[NUM_SUGGESTIONS];
+        boolean noSuggestions = true;
+
+        // At this point in time {@link #mViews} contains the former active views with old
+        // suggestions that will be swapped out to serve as view overlays, while
+        // {@link #mViewOverlays} contains the former overlays that will now serve as active
+        // views.
+        for (int i = 0; i < NUM_SUGGESTIONS; i++) {
+            // Retrieve the former overlay to be used as the new active view
+            final LinearLayout active = mViewOverlays.get(i);
+            final SmartDialEntry item = mEntries.get(i);
+
+            noSuggestions &= (item == SmartDialEntry.NULL);
+
+            assignEntryToView(active, mEntries.get(i));
+            final SmartDialEntry oldItem = mOldEntries.get(i);
+            // The former active view will now be used as an overlay for the cross-fade effect
+            final LinearLayout overlay = mViews.get(i);
+            show(active);
+            if (!containsSameContact(oldItem, item)) {
+                // Determine what kind of animation to use for the new view
+                if (i == 1) { // Middle suggestion
+                    if (containsSameContact(item, mOldEntries.get(0))) {
+                        // Suggestion went from the left to the middle, slide it left to right
+                        animateSlideFromLeft(active);
+                        dontAnimateOverlay[0] = true;
+                    } else if (containsSameContact(item, mOldEntries.get(2))) {
+                        // Suggestion sent from the right to the middle, slide it right to left
+                        animateSlideFromRight(active);
+                        dontAnimateOverlay[2] = true;
+                    } else {
+                        animateFadeInAndSlideUp(active);
+                    }
+                } else { // Left/Right suggestion
+                    if (i == 2 && containsSameContact(item, mOldEntries.get(1))) {
+                        // Suggestion went from middle to the right, slide it left to right
+                        animateSlideFromLeft(active);
+                        dontAnimateOverlay[1] = true;
+                    } else if (i == 0 && containsSameContact(item, mOldEntries.get(1))) {
+                        // Suggestion went from middle to the left, slide it right to left
+                        animateSlideFromRight(active);
+                        dontAnimateOverlay[1] = true;
+                    } else {
+                        animateFadeInAndSlideUp(active);
+                    }
+                }
+            } else {
+                // Since the same item is in the same spot, don't do any animations and just
+                // show the new view.
+                dontAnimateOverlay[i] = true;
+            }
+            mList.getOverlay().add(overlay);
+            mList.addView(active);
+            // Keep track of active views and view overlays
+            mViews.set(i, active);
+            mViewOverlays.set(i, overlay);
+        }
+
+        // Separate loop for overlay animations. At this point in time {@link #mViewOverlays}
+        // contains the actual overlays.
+        for (int i = 0; i < NUM_SUGGESTIONS; i++) {
+            final LinearLayout overlay = mViewOverlays.get(i);
+            if (!dontAnimateOverlay[i]) {
+                animateFadeOutAndSlideDown(overlay);
+            } else {
+                hide(overlay);
+            }
+        }
+
+        // Fade out the background to 25% opacity if there are suggestions. If there are no
+        // suggestions, display the background as usual.
+        mBackground.animate().withLayer().alpha(noSuggestions ? 1.0f : BACKGROUND_FADE_AMOUNT);
+    }
+
+    private void show(View view) {
+        view.animate().cancel();
+        view.setAlpha(1);
+        view.setTranslationX(0);
+        view.setTranslationY(0);
+    }
+
+    private void hide(View view) {
+        view.animate().cancel();
+        view.setAlpha(0);
+    }
+
+    private void animateFadeInAndSlideUp(View view) {
+        view.animate().cancel();
+        view.setAlpha(0.2f);
+        view.setTranslationY(view.getHeight());
+        view.animate().withLayer().alpha(1).translationY(0).setDuration(ANIM_DURATION).
+                setInterpolator(mDecelerateAndOvershootInterpolator);
+    }
+
+    private void animateFadeOutAndSlideDown(View view) {
+        view.animate().cancel();
+        view.setAlpha(1);
+        view.setTranslationY(0);
+        view.animate().withLayer().alpha(0).translationY(view.getHeight()).setDuration(
+                ANIM_DURATION).setInterpolator(mAccelerateDecelerateInterpolator);
+    }
+
+    private void animateSlideFromLeft(View view) {
+        view.animate().cancel();
+        view.setAlpha(1);
+        view.setTranslationX(-1 * view.getWidth());
+        view.animate().withLayer().translationX(0).setDuration(ANIM_DURATION).setInterpolator(
+                mAccelerateDecelerateInterpolator);
+    }
+
+    private void animateSlideFromRight(View view) {
+        view.animate().cancel();
+        view.setAlpha(1);
+        view.setTranslationX(view.getWidth());
+        view.animate().withLayer().translationX(0).setDuration(ANIM_DURATION).setInterpolator(
+                mAccelerateDecelerateInterpolator);
+    }
+
+    // Swaps the items in pos1 and pos2 of mEntries
+    private void swap(int pos1, int pos2) {
+        if (pos1 == pos2) {
+            return;
+        }
+        final SmartDialEntry temp = mEntries.get(pos1);
+        mEntries.set(pos1, mEntries.get(pos2));
+        mEntries.set(pos2, temp);
+    }
+
+    // Returns whether two SmartDialEntries contain the same contact
+    private boolean containsSameContact(SmartDialEntry x, SmartDialEntry y) {
+        return x.contactUri.equals(y.contactUri);
+    }
+
+    // Sets the information within a SmartDialEntry to the provided view
+    private void assignEntryToView(LinearLayout view, SmartDialEntry item) {
+        final TextView nameView = (TextView) view.findViewById(R.id.contact_name);
+
+        final TextView numberView = (TextView) view.findViewById(
+                R.id.contact_number);
+
+        if (item == SmartDialEntry.NULL) {
+            // Clear the text in case the view was reused.
+            nameView.setText("");
+            numberView.setText("");
+            view.setEnabled(false);
+            return;
+        }
+
+        // Highlight the display name with the provided match positions
+        if (!TextUtils.isEmpty(item.displayName)) {
+            final SpannableString displayName = new SpannableString(item.displayName);
+            for (final SmartDialMatchPosition p : item.matchPositions) {
+                if (p.start < p.end) {
+                    if (p.end > displayName.length()) {
+                        p.end = displayName.length();
+                    }
+                    // Create a new ForegroundColorSpan for each section of the name to highlight,
+                    // otherwise multiple highlights won't work.
+                    displayName.setSpan(new ForegroundColorSpan(mNameHighlightedTextColor), p.start,
+                            p.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                }
+            }
+            nameView.setText(displayName);
+        }
+
+        // Highlight the phone number with the provided match positions
+        if (!TextUtils.isEmpty(item.phoneNumber)) {
+            final SmartDialMatchPosition p = item.phoneNumberMatchPosition;
+            final SpannableString phoneNumber = new SpannableString(item.phoneNumber);
+            if (p != null && p.start < p.end) {
+                if (p.end > phoneNumber.length()) {
+                    p.end = phoneNumber.length();
+                }
+                phoneNumber.setSpan(new ForegroundColorSpan(mNumberHighlightedTextColor), p.start,
+                        p.end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+            }
+            numberView.setText(phoneNumber);
+        }
+        view.setEnabled(true);
+        view.setTag(item);
+    }
+}
diff --git a/src/com/android/dialer/dialpad/SmartDialEntry.java b/src/com/android/dialer/dialpad/SmartDialEntry.java
index d658f3d..9ff4912 100644
--- a/src/com/android/dialer/dialpad/SmartDialEntry.java
+++ b/src/com/android/dialer/dialpad/SmartDialEntry.java
@@ -29,6 +29,9 @@
     public final ArrayList<SmartDialMatchPosition> matchPositions;
     public final SmartDialMatchPosition phoneNumberMatchPosition;
 
+    public static final SmartDialEntry NULL = new SmartDialEntry("", Uri.EMPTY, "",
+            new ArrayList<SmartDialMatchPosition>(), null);
+
     public SmartDialEntry(CharSequence displayName, Uri contactUri, CharSequence phoneNumber,
             ArrayList<SmartDialMatchPosition> matchPositions,
             SmartDialMatchPosition phoneNumberMatchPosition) {
diff --git a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
index 08c5265..216697d 100644
--- a/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
+++ b/src/com/android/dialer/dialpad/SmartDialLoaderTask.java
@@ -16,7 +16,7 @@
 
 package com.android.dialer.dialpad;
 
-import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG;
+import static com.android.dialer.dialpad.SmartDialController.LOG_TAG;
 
 import android.os.AsyncTask;
 import android.provider.ContactsContract;
diff --git a/src/com/android/dialer/dialpad/SmartDialMatchPosition.java b/src/com/android/dialer/dialpad/SmartDialMatchPosition.java
index 3d248cc..4348746 100644
--- a/src/com/android/dialer/dialpad/SmartDialMatchPosition.java
+++ b/src/com/android/dialer/dialpad/SmartDialMatchPosition.java
@@ -16,7 +16,7 @@
 
 package com.android.dialer.dialpad;
 
-import static com.android.dialer.dialpad.SmartDialAdapter.LOG_TAG;
+import static com.android.dialer.dialpad.SmartDialController.LOG_TAG;
 
 import android.util.Log;
 
@@ -25,7 +25,7 @@
 /**
  * Stores information about a range of characters matched in a display name The integers
  * start and end indicate that the range start to end (exclusive) correspond to some characters
- * in the query. Used by {@link SmartDialAdapter} to highlight certain parts of the contact's
+ * in the query. Used by {@link SmartDialController} to highlight certain parts of the contact's
  * display name to indicate that those ranges matched the user's query.
  */
 class SmartDialMatchPosition {
diff --git a/src/com/android/dialer/dialpad/SmartDialTextView.java b/src/com/android/dialer/dialpad/SmartDialTextView.java
index d1d5706..398f99b 100644
--- a/src/com/android/dialer/dialpad/SmartDialTextView.java
+++ b/src/com/android/dialer/dialpad/SmartDialTextView.java
@@ -17,14 +17,8 @@
 package com.android.dialer.dialpad;
 
 import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Paint;
-import android.graphics.Paint.Align;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.widget.TextView;
 
@@ -34,7 +28,6 @@
 
     private final float mPadding;
     private final float mExtraPadding;
-    private static final String HIGH_CONFIDENCE_HINT = "\u2026";
 
     public SmartDialTextView(Context context) {
         this(context, null);
@@ -46,33 +39,6 @@
         mExtraPadding = getResources().getDimension(R.dimen.smartdial_suggestions_extra_padding);
     }
 
-    /**
-     * Returns a drawable that resembles a sideways overflow icon. Used to indicate the presence
-     * of a high confidence match.
-     *
-     * @param res Resources that we will use to create our BitmapDrawable with
-     * @param textSize Size of drawable to create
-     * @param color Color of drawable to create
-     * @return The drawable drawn according to the given parameters
-     */
-    public static Drawable getHighConfidenceHintDrawable(final Resources res, final float textSize,
-            final int color) {
-        final Paint paint = new Paint();
-        paint.setAntiAlias(true);
-        paint.setTextAlign(Align.CENTER);
-        paint.setTextSize(textSize);
-        paint.setColor(color);
-        final Rect bounds = new Rect();
-        paint.getTextBounds(HIGH_CONFIDENCE_HINT, 0, HIGH_CONFIDENCE_HINT.length(), bounds);
-        final int width = bounds.width();
-        final int height = bounds.height();
-        final Bitmap buffer = Bitmap.createBitmap(
-                width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
-        final Canvas canvas = new Canvas(buffer);
-        canvas.drawText(HIGH_CONFIDENCE_HINT, width / 2, height, paint);
-        return new BitmapDrawable(res, buffer);
-    }
-
     @Override
     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter);
@@ -94,8 +60,17 @@
         float width = w - 2 * mPadding - 2 * mExtraPadding;
 
         float ratio = width / paint.measureText(getText().toString());
+        TextUtils.TruncateAt ellipsizeAt = null;
         if (ratio < 1.0f) {
-            setTextScaleX(ratio);
+            if (ratio < 0.8f) {
+                // If the text is too big to fit even after scaling to 80%, just ellipsize it
+                // instead.
+                ellipsizeAt = TextUtils.TruncateAt.END;
+                setTextScaleX(0.8f);
+            } else {
+                setTextScaleX(ratio);
+            }
         }
+        setEllipsize(ellipsizeAt);
     }
 }