Cleaning up scrollbar logic to properly calculate stable extents.

- Removing old logic which assumed that views were the same size,
  especially now we can have variable dividers, etc.
- Simplifying old scroll position logic.
- Removing unnecessary prediction icon layout (same as normal icon)

Bug: 30023608
Change-Id: I39e1126fa586a76a9bdd3ff38cd6e360ac3021e6
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 717ce74..a9cc8f3 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -364,26 +364,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() {