Merge "Cleaning up scrollbar logic to properly calculate stable extents." into ub-launcher3-calgary
diff --git a/res/drawable/all_apps_divider.xml b/res/drawable/all_apps_divider.xml
new file mode 100644
index 0000000..3fe5295
--- /dev/null
+++ b/res/drawable/all_apps_divider.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/all_apps_divider_color" />
+    <size android:height="1dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/all_apps_search_divider.xml b/res/drawable/all_apps_search_divider.xml
new file mode 100644
index 0000000..99905e4
--- /dev/null
+++ b/res/drawable/all_apps_search_divider.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="?android:attr/colorAccent" />
+    <size android:height="1dp" />
+</shape>
\ No newline at end of file
diff --git a/res/layout/all_apps_divider.xml b/res/layout/all_apps_divider.xml
index b2ee7c1..1eaf685 100644
--- a/res/layout/all_apps_divider.xml
+++ b/res/layout/all_apps_divider.xml
@@ -13,13 +13,14 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<View xmlns:android="http://schemas.android.com/apk/res/android"
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
     android:importantForAccessibility="no"
     android:layout_width="match_parent"
-    android:layout_height="@dimen/all_apps_divider_height"
-    android:layout_marginBottom="@dimen/all_apps_divider_margin_vertical"
-    android:layout_marginLeft="@dimen/container_fastscroll_thumb_max_width"
-    android:layout_marginRight="@dimen/container_fastscroll_thumb_max_width"
-    android:layout_marginTop="@dimen/all_apps_divider_margin_vertical"
-    android:background="@color/all_apps_divider_color"
+    android:layout_height="wrap_content"
+    android:paddingTop="@dimen/all_apps_divider_margin_vertical"
+    android:paddingBottom="@dimen/all_apps_divider_margin_vertical"
+    android:paddingLeft="@dimen/container_fastscroll_thumb_max_width"
+    android:paddingRight="@dimen/container_fastscroll_thumb_max_width"
+    android:src="@drawable/all_apps_divider"
+    android:scaleType="fitXY"
     android:focusable="false" />
\ No newline at end of file
diff --git a/res/layout/all_apps_prediction_bar_icon.xml b/res/layout/all_apps_search_divider.xml
similarity index 61%
rename from res/layout/all_apps_prediction_bar_icon.xml
rename to res/layout/all_apps_search_divider.xml
index 3836fed..d2ef691 100644
--- a/res/layout/all_apps_prediction_bar_icon.xml
+++ b/res/layout/all_apps_search_divider.xml
@@ -13,16 +13,13 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.launcher3.BubbleTextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    style="@style/Icon.AllApps"
-    android:id="@+id/icon"
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:importantForAccessibility="no"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_gravity="center"
-    android:paddingTop="@dimen/all_apps_icon_top_bottom_padding"
-    android:paddingBottom="@dimen/all_apps_icon_top_bottom_padding"
-    android:focusable="true"
-    launcher:iconDisplay="all_apps" />
-
+    android:paddingBottom="@dimen/all_apps_divider_margin_vertical"
+    android:paddingLeft="@dimen/container_fastscroll_thumb_max_width"
+    android:paddingRight="@dimen/container_fastscroll_thumb_max_width"
+    android:src="@drawable/all_apps_search_divider"
+    android:scaleType="fitXY"
+    android:focusable="false" />
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 9abe3e6..1775d09 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -80,7 +80,6 @@
     <dimen name="all_apps_header_scroll_to_elevation">16dp</dimen>
     <dimen name="all_apps_header_shadow_height">6dp</dimen>
 
-    <dimen name="all_apps_divider_height">1dp</dimen>
     <dimen name="all_apps_divider_margin_vertical">8dp</dimen>
 
     <dimen name="all_apps_bezel_swipe_height">24dp</dimen>
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 4cb050e..8bd5eba 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -41,21 +41,6 @@
     @Thunk int mDy = 0;
     private float mDeltaThreshold;
 
-    /**
-     * The current scroll state of the recycler view.  We use this in onUpdateScrollbar()
-     * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so
-     * that we can calculate what the scroll bar looks like, and where to jump to from the fast
-     * scroller.
-     */
-    public static class ScrollPositionState {
-        // The index of the first visible row
-        public int rowIndex;
-        // The offset of the first visible row
-        public int rowTopOffset;
-        // The adapter position of the first visible item
-        public int itemPos;
-    }
-
     protected BaseRecyclerViewFastScrollBar mScrollbar;
 
     private int mDownX;
@@ -199,11 +184,7 @@
      * Returns the available scroll height:
      *   AvailableScrollHeight = Total height of the all items - last page height
      */
-    protected int getAvailableScrollHeight(int rowCount) {
-        int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom();
-        int availableScrollHeight = totalHeight - getVisibleHeight();
-        return availableScrollHeight;
-    }
+    protected abstract int getAvailableScrollHeight();
 
     /**
      * Returns the available scroll bar height:
@@ -247,15 +228,12 @@
      * this by mapping the available scroll area of the recycler view to the available space for the
      * scroll bar.
      *
-     * @param scrollPosState the current scroll position
-     * @param rowCount the number of rows, used to calculate the total scroll height (assumes that
-     *                 all rows are the same height)
+     * @param scrollY the current scroll y
      */
-    protected void synchronizeScrollBarThumbOffsetToViewScroll(ScrollPositionState scrollPosState,
-            int rowCount) {
+    protected void synchronizeScrollBarThumbOffsetToViewScroll(int scrollY,
+            int availableScrollHeight) {
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
-        int availableScrollHeight = getAvailableScrollHeight(rowCount);
         if (availableScrollHeight <= 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -264,7 +242,6 @@
         // 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 = Math.max(0, getScrollTop(scrollPosState));
         int scrollBarY = mBackgroundPadding.top +
                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
 
@@ -291,20 +268,7 @@
      *
      * @return the scroll top of this recycler view.
      */
-    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);
+    protected abstract int getCurrentScrollY();
 
     /**
      * Maps the touch (from 0..1) to the adapter position that should be visible.
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index a0cb1ef..fc1288d 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -366,26 +366,9 @@
 
         FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mAppsRecyclerView);
         mAppsRecyclerView.addItemDecoration(focusedItemDecorator);
+        mAppsRecyclerView.preMeasureViews(mAdapter);
         mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
 
-        // Precalculate the prediction icon and normal icon sizes
-        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
-        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
-                getResources().getDisplayMetrics().widthPixels, MeasureSpec.AT_MOST);
-        final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(
-                getResources().getDisplayMetrics().heightPixels, MeasureSpec.AT_MOST);
-
-        BubbleTextView icon = (BubbleTextView) layoutInflater.inflate(
-                R.layout.all_apps_icon, this, false);
-        icon.applyDummyInfo();
-        icon.measure(widthMeasureSpec, heightMeasureSpec);
-        BubbleTextView predIcon = (BubbleTextView) layoutInflater.inflate(
-                R.layout.all_apps_prediction_bar_icon, this, false);
-        predIcon.applyDummyInfo();
-        predIcon.measure(widthMeasureSpec, heightMeasureSpec);
-        mAppsRecyclerView.setPremeasuredIconHeights(predIcon.getMeasuredHeight(),
-                icon.getMeasuredHeight());
-
         // TODO(hyunyoungs): clean up setting the content and the reveal view.
         if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
             getContentView().setBackground(null);
diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
index 73de45e..6d9094f 100644
--- a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
+++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
@@ -18,7 +18,6 @@
 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;
@@ -144,8 +143,8 @@
 
         // 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 newPosition = info.fastScrollToItem.position;
+        int newScrollY = Math.min(availableScrollHeight, mRv.getCurrentScrollY(newPosition, 0));
         int numFrames = mFastScrollFrames.length;
         for (int i = 0; i < numFrames; i++) {
             // TODO(winsonc): We can interpolate this as well.
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 7d856c0..2680197 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -334,7 +334,6 @@
     private final int mSectionNamesMargin;
     private final int mSectionHeaderOffset;
     private final Paint mSectionTextPaint;
-    private int mAccentColor;
 
     private int mAppsPerRow;
 
@@ -364,12 +363,10 @@
         mSectionHeaderOffset = res.getDimensionPixelSize(R.dimen.all_apps_grid_section_y_offset);
         mIsRtl = Utilities.isRtl(res);
 
-        mAccentColor = Utilities.getColorAccent(launcher);
-
         mSectionTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
         mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
                 R.dimen.all_apps_grid_section_text_size));
-        mSectionTextPaint.setColor(mAccentColor);
+        mSectionTextPaint.setColor(Utilities.getColorAccent(launcher));
     }
 
     public static boolean isDividerViewType(int viewType) {
@@ -380,6 +377,10 @@
         return isViewType(viewType, VIEW_TYPE_MASK_ICON);
     }
 
+    public static boolean isPredictionIconViewType(int viewType) {
+        return isViewType(viewType, VIEW_TYPE_PREDICTION_ICON);
+    }
+
     public static boolean isViewType(int viewType, int viewTypeMask) {
         return (viewType & viewTypeMask) != 0;
     }
@@ -449,8 +450,7 @@
                 /* falls through */
             case VIEW_TYPE_PREDICTION_ICON: {
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
-                        viewType == VIEW_TYPE_ICON ? R.layout.all_apps_icon :
-                                R.layout.all_apps_prediction_bar_icon, parent, false);
+                        R.layout.all_apps_icon, parent, false);
                 icon.setOnClickListener(mIconClickListener);
                 icon.setOnLongClickListener(mIconLongClickListener);
                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
@@ -472,14 +472,8 @@
                 });
                 return new ViewHolder(searchMarketView);
             case VIEW_TYPE_SEARCH_DIVIDER:
-                final View searchDivider =
-                        mLayoutInflater.inflate(R.layout.all_apps_divider, parent, false);
-                searchDivider.setBackgroundColor(mAccentColor);
-                final GridLayoutManager.LayoutParams searchDividerParams =
-                        (GridLayoutManager.LayoutParams) searchDivider.getLayoutParams();
-                searchDividerParams.topMargin = 0;
-                searchDivider.setLayoutParams(searchDividerParams);
-                return new ViewHolder(searchDivider);
+                return new ViewHolder(mLayoutInflater.inflate(
+                        R.layout.all_apps_search_divider, parent, false));
             case VIEW_TYPE_PREDICTION_DIVIDER:
                 /* falls through */
             case VIEW_TYPE_SEARCH_MARKET_DIVIDER:
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index ac88113..3a44853 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -21,6 +21,7 @@
 import android.graphics.drawable.Drawable;
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
+import android.util.SparseIntArray;
 import android.view.View;
 
 import com.android.launcher3.BaseRecyclerView;
@@ -42,13 +43,11 @@
 
     private AlphabeticalAppsList mApps;
     private AllAppsFastScrollHelper mFastScrollHelper;
-    private BaseRecyclerView.ScrollPositionState mScrollPosState =
-            new BaseRecyclerView.ScrollPositionState();
     private int mNumAppsPerRow;
 
-    // The specific icon heights that we use to calculate scroll
-    private int mPredictionIconHeight;
-    private int mIconHeight;
+    // The specific view heights that we use to calculate scroll
+    private SparseIntArray mViewHeights = new SparseIntArray();
+    private SparseIntArray mCachedScrollPositions = new SparseIntArray();
 
     // The empty-search result background
     private AllAppsBackgroundDrawable mEmptySearchBackground;
@@ -109,11 +108,52 @@
     }
 
     /**
-     * Sets the heights of the icons in this view (for scroll calculations).
+     * Ensures that we can present a stable scrollbar for views of varying types by pre-measuring
+     * all the different view types.
      */
-    public void setPremeasuredIconHeights(int predictionIconHeight, int iconHeight) {
-        mPredictionIconHeight = predictionIconHeight;
-        mIconHeight = iconHeight;
+    public void preMeasureViews(AllAppsGridAdapter adapter) {
+        final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+                getResources().getDisplayMetrics().widthPixels, View.MeasureSpec.AT_MOST);
+        final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+                getResources().getDisplayMetrics().heightPixels, View.MeasureSpec.AT_MOST);
+
+        // Icons
+        BubbleTextView icon = (BubbleTextView) adapter.onCreateViewHolder(this,
+                AllAppsGridAdapter.VIEW_TYPE_ICON).mContent;
+        icon.applyDummyInfo();
+        icon.measure(widthMeasureSpec, heightMeasureSpec);
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, icon.getMeasuredHeight());
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, icon.getMeasuredHeight());
+
+        // Search divider
+        View searchDivider = adapter.onCreateViewHolder(this,
+                AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER).mContent;
+        searchDivider.measure(widthMeasureSpec, heightMeasureSpec);
+        int searchDividerHeight = searchDivider.getMeasuredHeight();
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_DIVIDER, searchDividerHeight);
+
+        // Generic dividers
+        View divider = adapter.onCreateViewHolder(this,
+                AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER).mContent;
+        divider.measure(widthMeasureSpec, heightMeasureSpec);
+        int dividerHeight = divider.getMeasuredHeight();
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, dividerHeight);
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, dividerHeight);
+
+        // Search views
+        View emptySearch = adapter.onCreateViewHolder(this,
+                AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH).mContent;
+        emptySearch.measure(widthMeasureSpec, heightMeasureSpec);
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH,
+                emptySearch.getMeasuredHeight());
+        View searchMarket = adapter.onCreateViewHolder(this,
+                AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET).mContent;
+        searchMarket.measure(widthMeasureSpec, heightMeasureSpec);
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET,
+                searchMarket.getMeasuredHeight());
+
+        // Section breaks
+        mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_SECTION_BREAK, 0);
     }
 
     /**
@@ -234,8 +274,8 @@
         }
 
         // Update the fast scroll
-        int scrollY = getScrollTop(mScrollPosState);
-        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
+        int scrollY = getCurrentScrollY();
+        int availableScrollHeight = getAvailableScrollHeight();
         mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
         return lastInfo.sectionName;
     }
@@ -249,6 +289,11 @@
     @Override
     public void setAdapter(Adapter adapter) {
         super.setAdapter(adapter);
+        adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
+            public void onChanged() {
+                mCachedScrollPositions.clear();
+            }
+        });
         mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
     }
 
@@ -265,17 +310,16 @@
             return;
         }
 
-        // Find the index and height of the first visible row (all rows have the same height)
-        int rowCount = mApps.getNumAppRows();
-        getCurScrollState(mScrollPosState, AllAppsGridAdapter.VIEW_TYPE_MASK_ICON);
-        if (mScrollPosState.rowIndex < 0) {
+        // Skip early if, there no child laid out in the container.
+        int scrollY = getCurrentScrollY();
+        if (scrollY < 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
         }
 
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
-        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
+        int availableScrollHeight = getAvailableScrollHeight();
         if (availableScrollHeight <= 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -284,7 +328,6 @@
         // 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 = getScrollTop(mScrollPosState);
         int scrollBarY = mBackgroundPadding.top +
                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
 
@@ -330,41 +373,10 @@
                 }
             }
         } else {
-            synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, rowCount);
+            synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight);
         }
     }
 
-    /**
-     * Returns the current scroll state of the apps rows.
-     */
-    protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) {
-        stateOut.rowIndex = -1;
-        stateOut.rowTopOffset = -1;
-        stateOut.itemPos = -1;
-
-        // Return early if there are no items or we haven't been measured
-        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
-        if (items.isEmpty() || mNumAppsPerRow == 0) {
-            return;
-        }
-
-        int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View child = getChildAt(i);
-            int position = getChildPosition(child);
-            if (position != NO_POSITION) {
-                AlphabeticalAppsList.AdapterItem item = items.get(position);
-                if (AllAppsGridAdapter.isViewType(item.viewType, viewTypeMask)) {
-                    stateOut.rowIndex = item.rowIndex;
-                    stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
-                    stateOut.itemPos = position;
-                    return;
-                }
-            }
-        }
-        return;
-    }
-
     @Override
     protected boolean supportsFastScrolling() {
         // Only allow fast scrolling when the user is not searching, since the results are not
@@ -372,13 +384,63 @@
         return !mApps.hasFilter();
     }
 
-    protected int getTop(int rowIndex) {
-        if (getChildCount() == 0 || rowIndex <= 0) {
-            return 0;
+    @Override
+    protected int getCurrentScrollY() {
+        // Return early if there are no items or we haven't been measured
+        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+        if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) {
+            return -1;
         }
 
-        // The prediction bar icons have more padding, so account for that in the row offset
-        return mPredictionIconHeight + (rowIndex - 1) * mIconHeight;
+        // Calculate the y and offset for the item
+        View child = getChildAt(0);
+        int position = getChildPosition(child);
+        if (position == NO_POSITION) {
+            return -1;
+        }
+        return getCurrentScrollY(position, getLayoutManager().getDecoratedTop(child));
+    }
+
+    public int getCurrentScrollY(int position, int offset) {
+        List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+        AlphabeticalAppsList.AdapterItem posItem = position < items.size() ?
+                items.get(position) : null;
+        int y = mCachedScrollPositions.get(position, -1);
+        if (y < 0) {
+            y = 0;
+            for (int i = 0; i < position; i++) {
+                AlphabeticalAppsList.AdapterItem item = items.get(i);
+                if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
+                    // Break once we reach the desired row
+                    if (posItem != null && posItem.viewType == item.viewType &&
+                            posItem.rowIndex == item.rowIndex) {
+                        break;
+                    }
+                    // Otherwise, only account for the first icon in the row since they are the same
+                    // size within a row
+                    if (item.rowAppIndex == 0) {
+                        y += mViewHeights.get(item.viewType, 0);
+                    }
+                } else {
+                    // Rest of the views span the full width
+                    y += mViewHeights.get(item.viewType, 0);
+                }
+            }
+            mCachedScrollPositions.put(position, y);
+        }
+
+        return getPaddingTop() + y - offset;
+    }
+
+    /**
+     * Returns the available scroll height:
+     *   AvailableScrollHeight = Total height of the all items - last page height
+     */
+    @Override
+    protected int getAvailableScrollHeight() {
+        int paddedHeight = getCurrentScrollY(mApps.getAdapterItems().size(), 0);
+        int totalHeight = paddedHeight + getPaddingBottom();
+        return totalHeight - getVisibleHeight();
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 2e3cc1a..0975206 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -33,7 +33,6 @@
 
     private static final String TAG = "WidgetsRecyclerView";
     private WidgetsModel mWidgets;
-    private ScrollPositionState mScrollPosState = new ScrollPositionState();
 
     public WidgetsRecyclerView(Context context) {
         this(context, null);
@@ -99,9 +98,8 @@
         stopScroll();
 
         int rowCount = mWidgets.getPackageSize();
-        getCurScrollState(mScrollPosState, -1);
         float pos = rowCount * touchFraction;
-        int availableScrollHeight = getAvailableScrollHeight(rowCount);
+        int availableScrollHeight = getAvailableScrollHeight();
         LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
         layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
 
@@ -121,45 +119,41 @@
         }
 
         // Skip early if, there no child laid out in the container.
-        getCurScrollState(mScrollPosState, -1);
-        if (mScrollPosState.rowIndex < 0) {
+        int scrollY = getCurrentScrollY();
+        if (scrollY < 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
         }
 
-        synchronizeScrollBarThumbOffsetToViewScroll(mScrollPosState, mWidgets.getPackageSize());
-    }
-
-    /**
-     * Returns the current scroll state.
-     */
-    protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) {
-        stateOut.rowIndex = -1;
-        stateOut.rowTopOffset = -1;
-        stateOut.itemPos = -1;
-
-        // Skip early if widgets are not bound.
-        if (isModelNotReady()) {
-            return;
-        }
-
-        View child = getChildAt(0);
-        int position = getChildPosition(child);
-
-        stateOut.rowIndex = position;
-        stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
-        stateOut.itemPos = position;
+        synchronizeScrollBarThumbOffsetToViewScroll(scrollY, getAvailableScrollHeight());
     }
 
     @Override
-    protected int getTop(int rowIndex) {
-        if (getChildCount() == 0) {
-            return 0;
+    protected int getCurrentScrollY() {
+        // Skip early if widgets are not bound.
+        if (isModelNotReady() || getChildCount() == 0) {
+            return -1;
         }
 
-        // All the rows are the same height, return any child height
         View child = getChildAt(0);
-        return child.getMeasuredHeight() * rowIndex;
+        int rowIndex = getChildPosition(child);
+        int y = (child.getMeasuredHeight() * rowIndex);
+        int offset = getLayoutManager().getDecoratedTop(child);
+
+        return getPaddingTop() + y - offset;
+    }
+
+    /**
+     * Returns the available scroll height:
+     *   AvailableScrollHeight = Total height of the all items - last page height
+     */
+    @Override
+    protected int getAvailableScrollHeight() {
+        View child = getChildAt(0);
+        int height = child.getMeasuredHeight() * mWidgets.getPackageSize();
+        int totalHeight = getPaddingTop() + height + getPaddingBottom();
+        int availableScrollHeight = totalHeight - getVisibleHeight();
+        return availableScrollHeight;
     }
 
     private boolean isModelNotReady() {