Highlighting sectioned apps on fast-scroll.

- This CL fixes an old assumption we had about the height
  of rows in AllApps, and ensures that we account for the
  difference in height between the predictive icons and the
  normal icons.
- In addition, we refactor FastBitmapDrawable to have multiple
  states, which it manages in drawing itself, including the
  press state and fast scroll focus states.  And we also refactor
  some of the fast scroll logic in the all apps recycler view
  out to its own class.

Change-Id: I1988159b2767df733bbbfc7dc601859cde6c9943
diff --git a/proguard.flags b/proguard.flags
index 5a3dfd1..7725800 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -19,11 +19,6 @@
   public float getAlpha();
 }
 
--keep class com.android.launcher3.BubbleTextView {
-  public void setFastScrollFocus(float);
-  public float getFastScrollFocus();
-}
-
 -keep class com.android.launcher3.ButtonDropTarget {
   public int getTextColor();
 }
@@ -56,8 +51,10 @@
 }
 
 -keep class com.android.launcher3.FastBitmapDrawable {
-  public int getBrightness();
-  public void setBrightness(int);
+  public void setDesaturation(float);
+  public float getDesaturation();
+  public void setBrightness(float);
+  public float getBrightness();
 }
 
 -keep class com.android.launcher3.MemoryDumpActivity {
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 9d713e3..f8ef1e1 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -52,8 +52,8 @@
         public int rowIndex;
         // The offset of the first visible row
         public int rowTopOffset;
-        // The height of a given row (they are currently all the same height)
-        public int rowHeight;
+        // The adapter position of the first visible item
+        public int itemPos;
     }
 
     protected BaseRecyclerViewFastScrollBar mScrollbar;
@@ -187,15 +187,21 @@
     }
 
     /**
+     * Returns the visible height of the recycler view:
+     *   VisibleHeight = View height - top padding - bottom padding
+     */
+    protected int getVisibleHeight() {
+        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
+        return visibleHeight;
+    }
+
+    /**
      * Returns the available scroll height:
      *   AvailableScrollHeight = Total height of the all items - last page height
-     *
-     * This assumes that all rows are the same height.
      */
-    protected int getAvailableScrollHeight(int rowCount, int rowHeight) {
-        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
-        int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom();
-        int availableScrollHeight = scrollHeight - visibleHeight;
+    protected int getAvailableScrollHeight(int rowCount) {
+        int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom();
+        int availableScrollHeight = totalHeight - getVisibleHeight();
         return availableScrollHeight;
     }
 
@@ -204,8 +210,7 @@
      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
      */
     protected int getAvailableScrollBarHeight() {
-        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
-        int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
+        int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight();
         return availableScrollBarHeight;
     }
 
@@ -223,6 +228,13 @@
         return defaultInactiveThumbColor;
     }
 
+    /**
+     * Returns the scrollbar for this recycler view.
+     */
+    public BaseRecyclerViewFastScrollBar getScrollBar() {
+        return mScrollbar;
+    }
+
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
@@ -243,7 +255,7 @@
             int rowCount) {
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
-        int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight);
+        int availableScrollHeight = getAvailableScrollHeight(rowCount);
         if (availableScrollHeight <= 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -252,8 +264,7 @@
         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
         // padding)
-        int scrollY = getPaddingTop() +
-                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
+        int scrollY = getScrollTop(scrollPosState);
         int scrollBarY = mBackgroundPadding.top +
                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
 
@@ -268,7 +279,7 @@
     }
 
     /**
-     * Returns whether fast scrolling is supported in the current state.
+     * @return whether fast scrolling is supported in the current state.
      */
     protected boolean supportsFastScrolling() {
         return true;
@@ -277,22 +288,38 @@
     /**
      * Maps the touch (from 0..1) to the adapter position that should be visible.
      * <p>Override in each subclass of this base class.
+     *
+     * @return the scroll top of this recycler view.
      */
-    public abstract String scrollToPositionAtProgress(float touchFraction);
+    protected int getScrollTop(ScrollPositionState scrollPosState) {
+        return getPaddingTop() + getTop(scrollPosState.rowIndex) -
+                scrollPosState.rowTopOffset;
+    }
+
+    /**
+     * Returns information about the item that the recycler view is currently scrolled to.
+     */
+    protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask);
+
+    /**
+     * Returns the top (or y position) of the row at the specified index.
+     */
+    protected abstract int getTop(int rowIndex);
+
+    /**
+     * Maps the touch (from 0..1) to the adapter position that should be visible.
+     * <p>Override in each subclass of this base class.
+     */
+    protected abstract String scrollToPositionAtProgress(float touchFraction);
 
     /**
      * Updates the bounds for the scrollbar.
      * <p>Override in each subclass of this base class.
      */
-    public abstract void onUpdateScrollbar(int dy);
+    protected abstract void onUpdateScrollbar(int dy);
 
     /**
      * <p>Override in each subclass of this base class.
      */
-    public void onFastScrollCompleted() {}
-
-    /**
-     * Returns information about the item that the recycler view is currently scrolled to.
-     */
-    protected abstract void getCurScrollState(ScrollPositionState stateOut);
+    protected void onFastScrollCompleted() {}
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
index 32ea576..a680169 100644
--- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
+++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
@@ -27,6 +27,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.view.MotionEvent;
+import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.util.Thunk;
@@ -37,7 +38,7 @@
 public class BaseRecyclerViewFastScrollBar {
 
     public interface FastScrollFocusableView {
-        void setFastScrollFocused(boolean focused, boolean animated);
+        void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
     }
 
     private final static int MAX_TRACK_ALPHA = 30;
@@ -199,7 +200,7 @@
                     }
                     mTouchOffset += (lastY - downY);
                     mPopup.animateVisibility(true);
-                    animateScrollbar(true);
+                    showActiveScrollbar(true);
                 }
                 if (mIsDragging) {
                     // Update the fastscroller section name at this touch position
@@ -210,7 +211,7 @@
                             (bottom - top));
                     mPopup.setSectionName(sectionName);
                     mPopup.animateVisibility(!sectionName.isEmpty());
-                    mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY));
+                    mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
                     mLastTouchY = boundedY;
                 }
                 break;
@@ -222,7 +223,7 @@
                 if (mIsDragging) {
                     mIsDragging = false;
                     mPopup.animateVisibility(false);
-                    animateScrollbar(false);
+                    showActiveScrollbar(false);
                 }
                 break;
         }
@@ -246,7 +247,7 @@
     /**
      * Animates the width and color of the scrollbar.
      */
-    private void animateScrollbar(boolean isScrolling) {
+    private void showActiveScrollbar(boolean isScrolling) {
         if (mScrollbarAnimator != null) {
             mScrollbarAnimator.cancel();
         }
diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java
index aeeb515..ebaba18 100644
--- a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java
+++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java
@@ -77,26 +77,26 @@
      * Updates the bounds for the fast scroller.
      * @return the invalidation rect for this update.
      */
-    public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) {
+    public Rect updateFastScrollerBounds(int lastTouchY) {
         mInvalidateRect.set(mBgBounds);
 
         if (isVisible()) {
             // Calculate the dimensions and position of the fast scroller popup
-            int edgePadding = rv.getMaxScrollbarWidth();
+            int edgePadding = mRv.getMaxScrollbarWidth();
             int bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2;
             int bgHeight = mBgOriginalSize;
             int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding));
             if (Utilities.isRtl(mRes)) {
-                mBgBounds.left = rv.getBackgroundPadding().left + (2 * rv.getMaxScrollbarWidth());
+                mBgBounds.left = mRv.getBackgroundPadding().left + (2 * mRv.getMaxScrollbarWidth());
                 mBgBounds.right = mBgBounds.left + bgWidth;
             } else {
-                mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right -
-                        (2 * rv.getMaxScrollbarWidth());
+                mBgBounds.right = mRv.getWidth() - mRv.getBackgroundPadding().right -
+                        (2 * mRv.getMaxScrollbarWidth());
                 mBgBounds.left = mBgBounds.right - bgWidth;
             }
             mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight);
             mBgBounds.top = Math.max(edgePadding,
-                    Math.min(mBgBounds.top, rv.getHeight() - edgePadding - bgHeight));
+                    Math.min(mBgBounds.top, mRv.getHeight() - edgePadding - bgHeight));
             mBgBounds.bottom = mBgBounds.top + bgHeight;
         } else {
             mBgBounds.setEmpty();
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 5070878..0742b0e 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -25,7 +24,6 @@
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Paint;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -37,8 +35,6 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewParent;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
 import android.widget.TextView;
 
 import com.android.launcher3.IconCache.IconLoadRequest;
@@ -63,13 +59,6 @@
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
 
-    private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f;
-    private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0;
-    private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1;
-    private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2;
-    private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175;
-    private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125;
-
     private final Launcher mLauncher;
     private Drawable mIcon;
     private final Drawable mBackground;
@@ -93,12 +82,6 @@
     private boolean mIgnorePressedStateChange;
     private boolean mDisableRelayout = false;
 
-    private ObjectAnimator mFastScrollFocusAnimator;
-    private Paint mFastScrollFocusBgPaint;
-    private float mFastScrollFocusFraction;
-    private boolean mFastScrollFocused;
-    private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON;
-
     private IconLoadRequest mIconLoadRequest;
 
     public BubbleTextView(Context context) {
@@ -151,13 +134,6 @@
             setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
         }
 
-        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) {
-            mFastScrollFocusBgPaint = new Paint();
-            mFastScrollFocusBgPaint.setAntiAlias(true);
-            mFastScrollFocusBgPaint.setColor(
-                    getResources().getColor(R.color.container_fastscroll_thumb_active_color));
-        }
-
         setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
     }
 
@@ -170,8 +146,9 @@
         Bitmap b = info.getIcon(iconCache);
 
         FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b);
-        iconDrawable.setGhostModeEnabled(info.isDisabled != 0);
-
+        if (info.isDisabled != 0) {
+            iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
+        }
         setIcon(iconDrawable, mIconSize);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
@@ -259,7 +236,12 @@
 
     private void updateIconState() {
         if (mIcon instanceof FastBitmapDrawable) {
-            ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed);
+            FastBitmapDrawable d = (FastBitmapDrawable) mIcon;
+            if (isPressed() || mStayPressed) {
+                d.animateState(FastBitmapDrawable.State.PRESSED);
+            } else {
+                d.animateState(FastBitmapDrawable.State.NORMAL);
+            }
         }
     }
 
@@ -362,18 +344,7 @@
     @Override
     public void draw(Canvas canvas) {
         if (!mCustomShadowsEnabled) {
-            // Draw the fast scroll focus bg if we have one
-            if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG &&
-                    mFastScrollFocusFraction > 0f) {
-                DeviceProfile grid = mLauncher.getDeviceProfile();
-                int iconCenterX = getScrollX() + (getWidth() / 2);
-                int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2);
-                canvas.drawCircle(iconCenterX, iconCenterY,
-                        mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint);
-            }
-
             super.draw(canvas);
-
             return;
         }
 
@@ -533,8 +504,13 @@
      */
     public void reapplyItemInfo(final ItemInfo info) {
         if (getTag() == info) {
+            FastBitmapDrawable.State prevState = FastBitmapDrawable.State.NORMAL;
+            if (mIcon instanceof FastBitmapDrawable) {
+                prevState = ((FastBitmapDrawable) mIcon).getCurrentState();
+            }
             mIconLoadRequest = null;
             mDisableRelayout = true;
+
             if (info instanceof AppInfo) {
                 applyFromApplicationInfo((AppInfo) info);
             } else if (info instanceof ShortcutInfo) {
@@ -550,6 +526,13 @@
             } else if (info instanceof PackageItemInfo) {
                 applyFromPackageItemInfo((PackageItemInfo) info);
             }
+
+            // If we are reapplying over an old icon, then we should update the new icon to the same
+            // state as the old icon
+            if (mIcon instanceof FastBitmapDrawable) {
+                ((FastBitmapDrawable) mIcon).setState(prevState);
+            }
+
             mDisableRelayout = false;
         }
     }
@@ -583,55 +566,53 @@
         }
     }
 
-    // Setters & getters for the animation
-    public void setFastScrollFocus(float fraction) {
-        mFastScrollFocusFraction = fraction;
-        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) {
-            setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f));
-            setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f));
-        } else {
-            invalidate();
-        }
-    }
-
-    public float getFastScrollFocus() {
-        return mFastScrollFocusFraction;
-    }
-
     @Override
-    public void setFastScrollFocused(final boolean focused, boolean animated) {
-        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) {
+    public void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated) {
+        // We can only set the fast scroll focus state on a FastBitmapDrawable
+        if (!(mIcon instanceof FastBitmapDrawable)) {
             return;
         }
 
-        if (mFastScrollFocused != focused) {
-            mFastScrollFocused = focused;
-
-            if (animated) {
-                // Clean up the previous focus animator
-                if (mFastScrollFocusAnimator != null) {
-                    mFastScrollFocusAnimator.cancel();
-                }
-                mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus",
-                        focused ? 1f : 0f);
-                if (focused) {
-                    mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator());
-                } else {
-                    mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator());
-                }
-                mFastScrollFocusAnimator.setDuration(focused ?
-                        FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION);
-                mFastScrollFocusAnimator.start();
-            } else {
-                mFastScrollFocusFraction = focused ? 1f : 0f;
+        FastBitmapDrawable d = (FastBitmapDrawable) mIcon;
+        if (animated) {
+            FastBitmapDrawable.State prevState = d.getCurrentState();
+            if (d.animateState(focusState)) {
+                // If the state was updated, then update the view accordingly
+                animate().scaleX(focusState.viewScale)
+                        .scaleY(focusState.viewScale)
+                        .setStartDelay(getStartDelayForStateChange(prevState, focusState))
+                        .setDuration(d.getDurationForStateChange(prevState, focusState))
+                        .start();
+            }
+        } else {
+            if (d.setState(focusState)) {
+                // If the state was updated, then update the view accordingly
+                animate().cancel();
+                setScaleX(focusState.viewScale);
+                setScaleY(focusState.viewScale);
             }
         }
     }
 
     /**
+     * Returns the start delay when animating between certain {@link FastBitmapDrawable} states.
+     */
+    private static int getStartDelayForStateChange(final FastBitmapDrawable.State fromState,
+            final FastBitmapDrawable.State toState) {
+        switch (toState) {
+            case NORMAL:
+                switch (fromState) {
+                    case FAST_SCROLL_HIGHLIGHTED:
+                        return FastBitmapDrawable.FAST_SCROLL_INACTIVE_DURATION / 4;
+                }
+        }
+        return 0;
+    }
+
+    /**
      * Interface to be implemented by the grand parent to allow click shadow effect.
      */
-    public static interface BubbleTextShadowHandler {
+    public interface BubbleTextShadowHandler {
         void setPressedIcon(BubbleTextView icon, Bitmap background);
     }
 }
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index 28e923e..30bc7ea 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.graphics.Bitmap;
@@ -28,13 +29,40 @@
 import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.SparseArray;
+import android.view.animation.DecelerateInterpolator;
 
 public class FastBitmapDrawable extends Drawable {
 
-    static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
+    /**
+     * The possible states that a FastBitmapDrawable can be in.
+     */
+    public enum State {
+
+        NORMAL                      (0f, 0f, 1f, new DecelerateInterpolator()),
+        PRESSED                     (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR),
+        FAST_SCROLL_HIGHLIGHTED     (0f, 0f, 1.1f, new DecelerateInterpolator()),
+        FAST_SCROLL_UNHIGHLIGHTED   (0.8f, 0.35f, 1f, new DecelerateInterpolator()),
+        DISABLED                    (1f, 0.5f, 1f, new DecelerateInterpolator());
+
+        public final float desaturation;
+        public final float brightness;
+        /**
+         * Used specifically by the view drawing this FastBitmapDrawable.
+         */
+        public final float viewScale;
+        public final TimeInterpolator interpolator;
+
+        State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) {
+            this.desaturation = desaturation;
+            this.brightness = brightness;
+            this.viewScale = viewScale;
+            this.interpolator = interpolator;
+        }
+    }
+
+    public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
 
         @Override
         public float getInterpolation(float input) {
@@ -47,42 +75,46 @@
             }
         }
     };
-    static final long CLICK_FEEDBACK_DURATION = 2000;
+    public static final int CLICK_FEEDBACK_DURATION = 2000;
+    public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225;
+    public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150;
+    public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225;
+    public static final int FAST_SCROLL_INACTIVE_DURATION = 275;
 
-    private static final int PRESSED_BRIGHTNESS = 100;
-    private static ColorMatrix sGhostModeMatrix;
-    private static final ColorMatrix sTempMatrix = new ColorMatrix();
+    // Since we don't need 256^2 values for combinations of both the brightness and saturation, we
+    // reduce the value space to a smaller value V, which reduces the number of cached
+    // ColorMatrixColorFilters that we need to keep to V^2
+    private static final int REDUCED_FILTER_VALUE_SPACE = 48;
 
-    /**
-     * Store the brightness colors filters to optimize animations during icon press. This
-     * only works for non-ghost-mode icons.
-     */
-    private static final SparseArray<ColorFilter> sCachedBrightnessFilter =
-            new SparseArray<ColorFilter>();
+    // A cache of ColorFilters for optimizing brightness and saturation animations
+    private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>();
 
-    private static final int GHOST_MODE_MIN_COLOR_RANGE = 130;
+    // Temporary matrices used for calculation
+    private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix();
+    private static final ColorMatrix sTempFilterMatrix = new ColorMatrix();
 
     private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
     private final Bitmap mBitmap;
-    private int mAlpha;
+    private State mState = State.NORMAL;
 
+    // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and
+    // as a result, can be used to compose the key for the cached ColorMatrixColorFilters
+    private int mDesaturation = 0;
     private int mBrightness = 0;
-    private boolean mGhostModeEnabled = false;
+    private int mAlpha = 255;
+    private int mPrevUpdateKey = Integer.MAX_VALUE;
 
-    private boolean mPressed = false;
-    private ObjectAnimator mPressedAnimator;
+    // Animators for the fast bitmap drawable's properties
+    private AnimatorSet mPropertyAnimator;
 
     public FastBitmapDrawable(Bitmap b) {
-        mAlpha = 255;
         mBitmap = b;
         setBounds(0, 0, b.getWidth(), b.getHeight());
     }
 
     @Override
     public void draw(Canvas canvas) {
-        final Rect r = getBounds();
-        // Draw the bitmap into the bounding rect
-        canvas.drawBitmap(mBitmap, null, r, mPaint);
+        canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
     }
 
     @Override
@@ -136,96 +168,191 @@
     }
 
     /**
-     * When enabled, the icon is grayed out and the contrast is increased to give it a 'ghost'
-     * appearance.
+     * Animates this drawable to a new state.
+     *
+     * @return whether the state has changed.
      */
-    public void setGhostModeEnabled(boolean enabled) {
-        if (mGhostModeEnabled != enabled) {
-            mGhostModeEnabled = enabled;
+    public boolean animateState(State newState) {
+        State prevState = mState;
+        if (mState != newState) {
+            mState = newState;
+
+            mPropertyAnimator = cancelAnimator(mPropertyAnimator);
+            mPropertyAnimator = new AnimatorSet();
+            mPropertyAnimator.playTogether(
+                    ObjectAnimator
+                            .ofFloat(this, "desaturation", newState.desaturation),
+                    ObjectAnimator
+                            .ofFloat(this, "brightness", newState.brightness));
+            mPropertyAnimator.setInterpolator(newState.interpolator);
+            mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState));
+            mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState));
+            mPropertyAnimator.start();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Immediately sets this drawable to a new state.
+     *
+     * @return whether the state has changed.
+     */
+    public boolean setState(State newState) {
+        if (mState != newState) {
+            mState = newState;
+
+            mPropertyAnimator = cancelAnimator(mPropertyAnimator);
+
+            setDesaturation(newState.desaturation);
+            setBrightness(newState.brightness);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the current state.
+     */
+    public State getCurrentState() {
+        return mState;
+    }
+
+    /**
+     * Returns the duration for the state change animation.
+     */
+    public static int getDurationForStateChange(State fromState, State toState) {
+        switch (toState) {
+            case NORMAL:
+                switch (fromState) {
+                    case PRESSED:
+                        return 0;
+                    case FAST_SCROLL_HIGHLIGHTED:
+                    case FAST_SCROLL_UNHIGHLIGHTED:
+                        return FAST_SCROLL_INACTIVE_DURATION;
+                }
+            case PRESSED:
+                return CLICK_FEEDBACK_DURATION;
+            case FAST_SCROLL_HIGHLIGHTED:
+                return FAST_SCROLL_HIGHLIGHT_DURATION;
+            case FAST_SCROLL_UNHIGHLIGHTED:
+                switch (fromState) {
+                    case NORMAL:
+                        // When animating from normal state, take a little longer
+                        return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION;
+                    default:
+                        return FAST_SCROLL_UNHIGHLIGHT_DURATION;
+                }
+        }
+        return 0;
+    }
+
+    /**
+     * Returns the start delay when animating between certain fast scroll states.
+     */
+    public static int getStartDelayForStateChange(State fromState, State toState) {
+        switch (toState) {
+            case FAST_SCROLL_UNHIGHLIGHTED:
+                switch (fromState) {
+                    case NORMAL:
+                        return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4;
+                }
+        }
+        return 0;
+    }
+
+    /**
+     * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated]
+     */
+    public void setDesaturation(float desaturation) {
+        int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE);
+        if (mDesaturation != newDesaturation) {
+            mDesaturation = newDesaturation;
             updateFilter();
         }
     }
 
-    public void setPressed(boolean pressed) {
-        if (mPressed != pressed) {
-            mPressed = pressed;
-            if (mPressed) {
-                mPressedAnimator = ObjectAnimator
-                        .ofInt(this, "brightness", PRESSED_BRIGHTNESS)
-                        .setDuration(CLICK_FEEDBACK_DURATION);
-                mPressedAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR);
-                mPressedAnimator.start();
-            } else if (mPressedAnimator != null) {
-                mPressedAnimator.cancel();
-                setBrightness(0);
-            }
-        }
-        invalidateSelf();
+    public float getDesaturation() {
+        return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE;
     }
 
-    public boolean isGhostModeEnabled() {
-        return mGhostModeEnabled;
-    }
-
-    public int getBrightness() {
-        return mBrightness;
-    }
-
-    public void setBrightness(int brightness) {
-        if (mBrightness != brightness) {
-            mBrightness = brightness;
+    /**
+     * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious]
+     */
+    public void setBrightness(float brightness) {
+        int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE);
+        if (mBrightness != newBrightness) {
+            mBrightness = newBrightness;
             updateFilter();
-            invalidateSelf();
         }
     }
 
+    public float getBrightness() {
+        return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE;
+    }
+
+    /**
+     * Updates the paint to reflect the current brightness and saturation.
+     */
     private void updateFilter() {
-        if (mGhostModeEnabled) {
-            if (sGhostModeMatrix == null) {
-                sGhostModeMatrix = new ColorMatrix();
-                sGhostModeMatrix.setSaturation(0);
+        boolean usePorterDuffFilter = false;
+        int key = -1;
+        if (mDesaturation > 0) {
+            key = (mDesaturation << 16) | mBrightness;
+        } else if (mBrightness > 0) {
+            // Compose a key with a fully saturated icon if we are just animating brightness
+            key = (1 << 16) | mBrightness;
 
-                // For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255]
-                float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f;
-                sTempMatrix.set(new float[] {
-                        range, 0, 0, 0, GHOST_MODE_MIN_COLOR_RANGE,
-                        0, range, 0, 0, GHOST_MODE_MIN_COLOR_RANGE,
-                        0, 0, range, 0, GHOST_MODE_MIN_COLOR_RANGE,
-                        0, 0, 0, 1, 0 });
-                sGhostModeMatrix.preConcat(sTempMatrix);
-            }
+            // We found that in L, ColorFilters cause drawing artifacts with shadows baked into
+            // icons, so just use a PorterDuff filter when we aren't animating saturation
+            usePorterDuffFilter = true;
+        }
 
-            if (mBrightness == 0) {
-                mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix));
-            } else {
-                setBrightnessMatrix(sTempMatrix, mBrightness);
-                sTempMatrix.postConcat(sGhostModeMatrix);
-                mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix));
-            }
-        } else if (mBrightness != 0) {
-            ColorFilter filter = sCachedBrightnessFilter.get(mBrightness);
+        // Debounce multiple updates on the same frame
+        if (key == mPrevUpdateKey) {
+            return;
+        }
+        mPrevUpdateKey = key;
+
+        if (key != -1) {
+            ColorFilter filter = sCachedFilter.get(key);
             if (filter == null) {
-                filter = new PorterDuffColorFilter(Color.argb(mBrightness, 255, 255, 255),
-                        PorterDuff.Mode.SRC_ATOP);
-                sCachedBrightnessFilter.put(mBrightness, filter);
+                float brightnessF = getBrightness();
+                int brightnessI = (int) (255 * brightnessF);
+                if (usePorterDuffFilter) {
+                    filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255),
+                            PorterDuff.Mode.SRC_ATOP);
+                } else {
+                    float saturationF = 1f - getDesaturation();
+                    sTempFilterMatrix.setSaturation(saturationF);
+                    if (mBrightness > 0) {
+                        // Brightness: C-new = C-old*(1-amount) + amount
+                        float scale = 1f - brightnessF;
+                        float[] mat = sTempBrightnessMatrix.getArray();
+                        mat[0] = scale;
+                        mat[6] = scale;
+                        mat[12] = scale;
+                        mat[4] = brightnessI;
+                        mat[9] = brightnessI;
+                        mat[14] = brightnessI;
+                        sTempFilterMatrix.preConcat(sTempBrightnessMatrix);
+                    }
+                    filter = new ColorMatrixColorFilter(sTempFilterMatrix);
+                }
+                sCachedFilter.append(key, filter);
             }
             mPaint.setColorFilter(filter);
         } else {
             mPaint.setColorFilter(null);
         }
+        invalidateSelf();
     }
 
-    private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) {
-        // Brightness: C-new = C-old*(1-amount) + amount
-        float scale = 1 - brightness / 255.0f;
-        matrix.setScale(scale, scale, scale, 1);
-        float[] array = matrix.getArray();
-
-        // Add the amount to RGB components of the matrix, as per the above formula.
-        // Fifth elements in the array correspond to the constant being added to
-        // red, blue, green, and alpha channel respectively.
-        array[4] = brightness;
-        array[9] = brightness;
-        array[14] = brightness;
+    private AnimatorSet cancelAnimator(AnimatorSet animator) {
+        if (animator != null) {
+            animator.removeAllListeners();
+            animator.cancel();
+        }
+        return null;
     }
 }
diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java
index 8d534d2..d7b55b3 100644
--- a/src/com/android/launcher3/FolderIcon.java
+++ b/src/com/android/launcher3/FolderIcon.java
@@ -520,7 +520,7 @@
     }
 
     class PreviewItemDrawingParams {
-        PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) {
+        PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
             this.transX = transX;
             this.transY = transY;
             this.scale = scale;
@@ -529,7 +529,7 @@
         float transX;
         float transY;
         float scale;
-        int overlayAlpha;
+        float overlayAlpha;
         Drawable drawable;
     }
 
@@ -561,7 +561,7 @@
         float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection) + getPaddingTop();
         float transX = (mAvailableSpaceInPreview - scaledSize) / 2;
         float totalScale = mBaselineIconScale * scale;
-        final int overlayAlpha = (int) (80 * (1 - r));
+        final float overlayAlpha = (80 * (1 - r)) / 255f;
 
         if (params == null) {
             params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
@@ -585,12 +585,12 @@
             d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
             if (d instanceof FastBitmapDrawable) {
                 FastBitmapDrawable fd = (FastBitmapDrawable) d;
-                int oldBrightness = fd.getBrightness();
+                float oldBrightness = fd.getBrightness();
                 fd.setBrightness(params.overlayAlpha);
                 d.draw(canvas);
                 fd.setBrightness(oldBrightness);
             } else {
-                d.setColorFilter(Color.argb(params.overlayAlpha, 255, 255, 255),
+                d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255),
                         PorterDuff.Mode.SRC_ATOP);
                 d.draw(canvas);
                 d.clearColorFilter();
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index e97f017..370f695 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -46,6 +46,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Bitmap;
@@ -4677,6 +4678,18 @@
                 UserHandleCompat.myUserHandle());
     }
 
+    /**
+     * Generates a dummy AppInfo for us to use to calculate BubbleTextView sizes.
+     */
+    public AppInfo createDummyAppInfo() {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(this, Launcher.class));
+        PackageManager pm = getPackageManager();
+        ResolveInfo info = pm.resolveActivity(intent, 0);
+        return new AppInfo(this, LauncherActivityInfoCompat.fromResolveInfo(info, this),
+                UserHandleCompat.myUserHandle(), mIconCache);
+    }
+
     // TODO: This method should be a part of LauncherSearchCallback
     public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) {
         dragView.setTag(dragInfo);
diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java
index 40eadab..1d76945 100644
--- a/src/com/android/launcher3/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/PendingAppWidgetHostView.java
@@ -133,7 +133,7 @@
             //   3) Setup icon in the center and app icon in the top right corner.
             if (mDisabledForSafeMode) {
                 FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon);
-                disabledIcon.setGhostModeEnabled(true);
+                disabledIcon.setState(FastBitmapDrawable.State.DISABLED);
                 mCenterDrawable = disabledIcon;
                 mSettingIconDrawable = null;
             } else if (isReadyForClickSetup()) {
diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java
index 45e4b2c..908c8b9 100644
--- a/src/com/android/launcher3/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/PreloadIconDrawable.java
@@ -179,7 +179,8 @@
             mPaint.setColor(getIndicatorColor());
         }
         if (mIcon instanceof FastBitmapDrawable) {
-            ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0);
+            ((FastBitmapDrawable) mIcon).setState(level <= 0 ?
+                    FastBitmapDrawable.State.DISABLED : FastBitmapDrawable.State.NORMAL);
         }
 
         invalidateSelf();
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 564527e..90f1322 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -28,6 +28,7 @@
 import android.text.method.TextKeyListener;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -35,6 +36,7 @@
 import android.widget.LinearLayout;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BaseContainerView;
+import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeleteDropTarget;
 import com.android.launcher3.DeviceProfile;
@@ -332,6 +334,18 @@
             mAppsRecyclerView.addItemDecoration(mItemDecoration);
         }
 
+        // Precalculate the prediction icon and normal icon sizes
+        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
+        BubbleTextView icon = (BubbleTextView) layoutInflater.inflate(R.layout.all_apps_icon, this, false);
+        icon.applyFromApplicationInfo(mLauncher.createDummyAppInfo());
+        icon.measure(MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST));
+        BubbleTextView predIcon = (BubbleTextView) layoutInflater.inflate(R.layout.all_apps_prediction_bar_icon, this, false);
+        predIcon.applyFromApplicationInfo(mLauncher.createDummyAppInfo());
+        predIcon.measure(MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST));
+        mAppsRecyclerView.setPremeasuredIconHeights(predIcon.getMeasuredHeight(), icon.getMeasuredHeight());
+
         updateBackgroundAndPaddings();
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
new file mode 100644
index 0000000..1045342
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2015 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.launcher3.allapps;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.BaseRecyclerViewFastScrollBar;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.util.Thunk;
+
+import java.util.HashSet;
+
+public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
+
+    private static final int INITIAL_TOUCH_SETTLING_DURATION = 300;
+    private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
+    private static final float FAST_SCROLL_TOUCH_VELOCITY_BARRIER = 1900f;
+
+    private AllAppsRecyclerView mRv;
+    private AlphabeticalAppsList mApps;
+
+    // Keeps track of the current and targetted fast scroll section (the section to scroll to after
+    // the initial delay)
+    int mTargetFastScrollPosition = -1;
+    @Thunk String mCurrentFastScrollSection;
+    @Thunk String mTargetFastScrollSection;
+
+    // The settled states affect the delay before the fast scroll animation is applied
+    private boolean mHasFastScrollTouchSettled;
+    private boolean mHasFastScrollTouchSettledAtLeastOnce;
+
+    // Set of all views animated during fast scroll.  We keep track of these ourselves since there
+    // is no way to reset a view once it gets scrapped or recycled without other hacks
+    private HashSet<BaseRecyclerViewFastScrollBar.FastScrollFocusableView> mTrackedFastScrollViews =
+            new HashSet<>();
+
+    // Smooth fast-scroll animation frames
+    @Thunk int mFastScrollFrameIndex;
+    @Thunk final int[] mFastScrollFrames = new int[10];
+
+    /**
+     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
+     * if necessary.
+     */
+    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
+                mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
+                mFastScrollFrameIndex++;
+                mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
+            }
+        }
+    };
+
+    /**
+     * This runnable updates the current fast scroll section to the target fastscroll section.
+     */
+    Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
+        @Override
+        public void run() {
+            // Update to the target section
+            mCurrentFastScrollSection = mTargetFastScrollSection;
+            mHasFastScrollTouchSettled = true;
+            mHasFastScrollTouchSettledAtLeastOnce = true;
+            updateTrackedViewsFastScrollFocusState();
+        }
+    };
+
+    public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
+        mRv = rv;
+        mApps = apps;
+    }
+
+    public void onSetAdapter(AllAppsGridAdapter adapter) {
+        adapter.setBindViewCallback(this);
+    }
+
+    /**
+     * Smooth scrolls the recycler view to the given section.
+     *
+     * @return whether the fastscroller can scroll to the new section.
+     */
+    public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
+            AlphabeticalAppsList.FastScrollSectionInfo info) {
+        if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
+            mTargetFastScrollPosition = info.fastScrollToItem.position;
+            smoothSnapToPosition(scrollY, availableScrollHeight, info);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
+     * ourselves and animating the scroll on the recycler view.
+     */
+    private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
+            AlphabeticalAppsList.FastScrollSectionInfo info) {
+        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
+        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
+
+        trackAllChildViews();
+        if (mHasFastScrollTouchSettled) {
+            // In this case, the user has already settled once (and the fast scroll state has
+            // animated) and they are just fine-tuning their section from the last section, so
+            // we should make it feel fast and update immediately.
+            mCurrentFastScrollSection = info.sectionName;
+            mTargetFastScrollSection = null;
+            updateTrackedViewsFastScrollFocusState();
+        } else {
+            // Otherwise, the user has scrubbed really far, and we don't want to distract the user
+            // with the flashing fast scroll state change animation in addition to the fast scroll
+            // section popup, so reset the views to normal, and wait for the touch to settle again
+            // before animating the fast scroll state.
+            mCurrentFastScrollSection = null;
+            mTargetFastScrollSection = info.sectionName;
+            mHasFastScrollTouchSettled = false;
+            updateTrackedViewsFastScrollFocusState();
+
+            // Delay scrolling to a new section until after some duration.  If the user has been
+            // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
+            // fast scroll to settle so it doesn't feel so long.
+            mRv.postDelayed(mFastScrollToTargetSectionRunnable,
+                    mHasFastScrollTouchSettledAtLeastOnce ?
+                            REPEAT_TOUCH_SETTLING_DURATION :
+                            INITIAL_TOUCH_SETTLING_DURATION);
+        }
+
+        // Calculate the full animation from the current scroll position to the final scroll
+        // position, and then run the animation for the duration.
+        int newScrollY = Math.min(availableScrollHeight,
+                mRv.getPaddingTop() + mRv.getTop(info.fastScrollToItem.rowIndex));
+        int numFrames = mFastScrollFrames.length;
+        for (int i = 0; i < numFrames; i++) {
+            // TODO(winsonc): We can interpolate this as well.
+            mFastScrollFrames[i] = (newScrollY - scrollY) / numFrames;
+        }
+        mFastScrollFrameIndex = 0;
+        mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
+    }
+
+    public void onFastScrollCompleted() {
+        // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
+        //                runs
+
+        // Stop animating the fast scroll position and state
+        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
+        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
+
+        // Reset the tracking variables
+        mHasFastScrollTouchSettled = false;
+        mHasFastScrollTouchSettledAtLeastOnce = false;
+        mCurrentFastScrollSection = null;
+        mTargetFastScrollSection = null;
+        mTargetFastScrollPosition = -1;
+
+        updateTrackedViewsFastScrollFocusState();
+        mTrackedFastScrollViews.clear();
+    }
+
+    @Override
+    public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
+        // Update newly bound views to the current fast scroll state if we are fast scrolling
+        if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
+            if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+                BaseRecyclerViewFastScrollBar.FastScrollFocusableView v =
+                        (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent;
+                updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */);
+                mTrackedFastScrollViews.add(v);
+            }
+        }
+    }
+
+    /**
+     * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
+     */
+    private void trackAllChildViews() {
+        int childCount = mRv.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View v = mRv.getChildAt(i);
+            if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+                mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v);
+            }
+        }
+    }
+
+    /**
+     * Updates the fast scroll focus on all the children.
+     */
+    private void updateTrackedViewsFastScrollFocusState() {
+        for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) {
+            RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v);
+            int pos = (viewHolder != null) ? viewHolder.getPosition() : -1;
+            updateViewFastScrollFocusState(v, pos, true);
+        }
+    }
+
+    /**
+     * Updates the fast scroll focus on all a given view.
+     */
+    private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v,
+                                                int pos, boolean animated) {
+        FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL;
+        if (mCurrentFastScrollSection != null && pos > -1) {
+            AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
+            newState = item.sectionName.equals(mCurrentFastScrollSection) ?
+                    FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED :
+                    FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED;
+        }
+        v.setFastScrollFocusState(newState, animated);
+    }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 1f95133..99004f2 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -69,6 +69,10 @@
     // The message to continue to a market search when there are no filtered results
     public static final int SEARCH_MARKET_VIEW_TYPE = 5;
 
+    public interface BindViewCallback {
+        public void onBindView(ViewHolder holder);
+    }
+
     /**
      * ViewHolder for each icon.
      */
@@ -328,6 +332,7 @@
     private View.OnTouchListener mTouchListener;
     private View.OnClickListener mIconClickListener;
     private View.OnLongClickListener mIconLongClickListener;
+    private BindViewCallback mBindViewCallback;
     @Thunk final Rect mBackgroundPadding = new Rect();
     @Thunk int mPredictionBarDividerOffset;
     @Thunk int mAppsPerRow;
@@ -425,6 +430,13 @@
     }
 
     /**
+     * Sets the callback for when views are bound.
+     */
+    public void setBindViewCallback(BindViewCallback cb) {
+        mBindViewCallback = cb;
+    }
+
+    /**
      * Notifies the adapter of the background padding so that it can draw things correctly in the
      * item decorator.
      */
@@ -529,6 +541,15 @@
                 }
                 break;
         }
+        if (mBindViewCallback != null) {
+            mBindViewCallback.onBindView(holder);
+        }
+    }
+
+    @Override
+    public boolean onFailedToRecycleView(ViewHolder holder) {
+        // Always recycle and we will reset the view when it is bound
+        return true;
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 10d10f1..48b9494 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,24 +15,20 @@
  */
 package com.android.launcher3.allapps;
 
-import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.view.View;
 
 import com.android.launcher3.BaseRecyclerView;
-import com.android.launcher3.BaseRecyclerViewFastScrollBar;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Stats;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Thunk;
 
 import java.util.List;
 
@@ -42,25 +38,17 @@
 public class AllAppsRecyclerView extends BaseRecyclerView
         implements Stats.LaunchSourceProvider {
 
-    private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
-    private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
-
-    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0;
-    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1;
-
     private AlphabeticalAppsList mApps;
+    private AllAppsFastScrollHelper mFastScrollHelper;
+    private BaseRecyclerView.ScrollPositionState mScrollPosState =
+            new BaseRecyclerView.ScrollPositionState();
     private int mNumAppsPerRow;
 
-    @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
-    @Thunk int mPrevFastScrollFocusedPosition;
-    @Thunk int mFastScrollFrameIndex;
-    @Thunk final int[] mFastScrollFrames = new int[10];
+    // The specific icon heights that we use to calculate scroll
+    private int mPredictionIconHeight;
+    private int mIconHeight;
 
-    private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
-    private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
-
-    private ScrollPositionState mScrollPosState = new ScrollPositionState();
-
+    // The empty-search result background
     private AllAppsBackgroundDrawable mEmptySearchBackground;
     private int mEmptySearchBackgroundTopOffset;
 
@@ -79,8 +67,8 @@
     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr);
-
         Resources res = getResources();
+        addOnItemTouchListener(this);
         mScrollbar.setDetachThumbOnFastScroll();
         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
                 R.dimen.all_apps_empty_search_bg_top_offset);
@@ -91,6 +79,7 @@
      */
     public void setApps(AlphabeticalAppsList apps) {
         mApps = apps;
+        mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     }
 
     /**
@@ -110,6 +99,14 @@
     }
 
     /**
+     * Sets the heights of the icons in this view (for scroll calculations).
+     */
+    public void setPremeasuredIconHeights(int predictionIconHeight, int iconHeight) {
+        mPredictionIconHeight = predictionIconHeight;
+        mIconHeight = iconHeight;
+    }
+
+    /**
      * Scrolls this recycler view to the top.
      */
     public void scrollToTop() {
@@ -126,6 +123,7 @@
      */
     @Override
     protected void dispatchDraw(Canvas canvas) {
+        // Clip to ensure that we don't draw the overscroll effect beyond the background bounds
         canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
                 getWidth() - mBackgroundPadding.right,
                 getHeight() - mBackgroundPadding.bottom);
@@ -157,14 +155,6 @@
     }
 
     @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        // Bind event handlers
-        addOnItemTouchListener(this);
-    }
-
-    @Override
     public void fillInLaunchSourceData(Bundle sourceData) {
         sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
         if (mApps.hasFilter()) {
@@ -212,63 +202,31 @@
         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                 mApps.getFastScrollerSections();
         AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
-        if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) {
-            for (int i = 1; i < fastScrollSections.size(); i++) {
-                AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
-                if (info.touchFraction > touchFraction) {
-                    break;
-                }
-                lastInfo = info;
+        for (int i = 1; i < fastScrollSections.size(); i++) {
+            AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
+            if (info.touchFraction > touchFraction) {
+                break;
             }
-        } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
-            lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
-        } else {
-            throw new RuntimeException("Unexpected scroll bar mode");
+            lastInfo = info;
         }
 
-        // Map the touch position back to the scroll of the recycler view
-        getCurScrollState(mScrollPosState);
-        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
-        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
-        if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
-            layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
-        }
-
-        if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
-            mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
-
-            // Reset the last focused view
-            if (mLastFastScrollFocusedView != null) {
-                mLastFastScrollFocusedView.setFastScrollFocused(false, true);
-                mLastFastScrollFocusedView = null;
-            }
-
-            if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) {
-                smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState);
-            } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
-                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
-                if (vh != null &&
-                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
-                    mLastFastScrollFocusedView =
-                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
-                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
-                }
-            } else {
-                throw new RuntimeException("Unexpected fast scroll mode");
-            }
-        }
+        // Update the fast scroll
+        int scrollY = getScrollTop(mScrollPosState);
+        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
+        mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
         return lastInfo.sectionName;
     }
 
     @Override
     public void onFastScrollCompleted() {
         super.onFastScrollCompleted();
-        // Reset and clean up the last focused view
-        if (mLastFastScrollFocusedView != null) {
-            mLastFastScrollFocusedView.setFastScrollFocused(false, true);
-            mLastFastScrollFocusedView = null;
-        }
-        mPrevFastScrollFocusedPosition = -1;
+        mFastScrollHelper.onFastScrollCompleted();
+    }
+
+    @Override
+    public void setAdapter(Adapter adapter) {
+        super.setAdapter(adapter);
+        mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
     }
 
     /**
@@ -286,7 +244,7 @@
 
         // Find the index and height of the first visible row (all rows have the same height)
         int rowCount = mApps.getNumAppRows();
-        getCurScrollState(mScrollPosState);
+        getCurScrollState(mScrollPosState, -1);
         if (mScrollPosState.rowIndex < 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -294,7 +252,7 @@
 
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
-        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight);
+        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
         if (availableScrollHeight <= 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -303,8 +261,7 @@
         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
         // padding)
-        int scrollY = getPaddingTop() +
-                (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
+        int scrollY = getScrollTop(mScrollPosState);
         int scrollBarY = mBackgroundPadding.top +
                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
 
@@ -355,58 +312,12 @@
     }
 
     /**
-     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
-     * if necessary.
-     */
-    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
-                scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
-                mFastScrollFrameIndex++;
-                postOnAnimation(mSmoothSnapNextFrameRunnable);
-            } else {
-                // Animation completed, set the fast scroll state on the target view
-                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
-                if (vh != null &&
-                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
-                        mLastFastScrollFocusedView != vh.itemView) {
-                    mLastFastScrollFocusedView =
-                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
-                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
-                }
-            }
-        }
-    };
-
-    /**
-     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
-     * ourselves and animating the scroll on the recycler view.
-     */
-    private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
-        removeCallbacks(mSmoothSnapNextFrameRunnable);
-
-        // Calculate the full animation from the current scroll position to the final scroll
-        // position, and then run the animation for the duration.
-        int curScrollY = getPaddingTop() +
-                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
-        int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight);
-        int numFrames = mFastScrollFrames.length;
-        for (int i = 0; i < numFrames; i++) {
-            // TODO(winsonc): We can interpolate this as well.
-            mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames;
-        }
-        mFastScrollFrameIndex = 0;
-        postOnAnimation(mSmoothSnapNextFrameRunnable);
-    }
-
-    /**
      * Returns the current scroll state of the apps rows.
      */
-    protected void getCurScrollState(ScrollPositionState stateOut) {
+    protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) {
         stateOut.rowIndex = -1;
         stateOut.rowTopOffset = -1;
-        stateOut.rowHeight = -1;
+        stateOut.itemPos = -1;
 
         // Return early if there are no items or we haven't been measured
         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
@@ -420,15 +331,15 @@
             int position = getChildPosition(child);
             if (position != NO_POSITION) {
                 AlphabeticalAppsList.AdapterItem item = items.get(position);
-                if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
-                        item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+                if ((item.viewType & viewTypeMask) != 0) {
                     stateOut.rowIndex = item.rowIndex;
                     stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
-                    stateOut.rowHeight = child.getHeight();
-                    break;
+                    stateOut.itemPos = position;
+                    return;
                 }
             }
         }
+        return;
     }
 
     @Override
@@ -438,18 +349,13 @@
         return !mApps.hasFilter();
     }
 
-    /**
-     * Returns the scrollY for the given position in the adapter.
-     */
-    private int getScrollAtPosition(int position, int rowHeight) {
-        AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
-        if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
-                item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
-            int offset = item.rowIndex > 0 ? getPaddingTop() : 0;
-            return offset + item.rowIndex * rowHeight;
-        } else {
+    protected int getTop(int rowIndex) {
+        if (getChildCount() == 0 || rowIndex <= 0) {
             return 0;
         }
+
+        // The prediction bar icons have more padding, so account for that in the row offset
+        return mPredictionIconHeight + (rowIndex - 1) * mIconHeight;
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 884bdc4..fe9c51c 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -102,9 +102,9 @@
         // Stop the scroller if it is scrolling
         stopScroll();
 
-        getCurScrollState(mScrollPosState);
+        getCurScrollState(mScrollPosState, -1);
         float pos = rowCount * touchFraction;
-        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
+        int availableScrollHeight = getAvailableScrollHeight(rowCount);
         LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
         layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
 
@@ -131,7 +131,7 @@
         }
 
         // Skip early if, there no child laid out in the container.
-        getCurScrollState(mScrollPosState);
+        getCurScrollState(mScrollPosState, -1);
         if (mScrollPosState.rowIndex < 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -143,10 +143,10 @@
     /**
      * Returns the current scroll state.
      */
-    protected void getCurScrollState(ScrollPositionState stateOut) {
+    protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) {
         stateOut.rowIndex = -1;
         stateOut.rowTopOffset = -1;
-        stateOut.rowHeight = -1;
+        stateOut.itemPos = -1;
 
         // Skip early if widgets are not bound.
         if (mWidgets == null) {
@@ -163,6 +163,17 @@
 
         stateOut.rowIndex = position;
         stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
-        stateOut.rowHeight = child.getHeight();
+        stateOut.itemPos = position;
+    }
+
+    @Override
+    protected int getTop(int rowIndex) {
+        if (getChildCount() == 0) {
+            return 0;
+        }
+
+        // All the rows are the same height, return any child height
+        View child = getChildAt(0);
+        return child.getMeasuredHeight() * rowIndex;
     }
 }
\ No newline at end of file