Making the scrollbar scrubbable.

- This change in behavior will remove the ability to scrub anywhere in the gutter to
  start fast-scrolling, but requires the user to touch near the scroll bar to start
  fast-scrolling.
- Also fixes issue with wonky scrollbar due to the fake section breaks

Change-Id: I34b08ac46ea93f7f4cad7ccde1048a388ee6a55d
diff --git a/res/drawable-ldrtl/apps_list_fastscroll_bg.xml b/res/drawable-ldrtl/apps_list_fastscroll_bg.xml
new file mode 100644
index 0000000..772975a
--- /dev/null
+++ b/res/drawable-ldrtl/apps_list_fastscroll_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/apps_view_scrollbar_thumb_color" />
+    <size
+        android:width="64dp"
+        android:height="64dp" />
+    <corners
+        android:topLeftRadius="64dp"
+        android:topRightRadius="64dp"
+        android:bottomRightRadius="64dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_list_scrollbar_thumb.xml b/res/drawable/apps_list_scrollbar_thumb.xml
index 59383a5..318d406 100644
--- a/res/drawable/apps_list_scrollbar_thumb.xml
+++ b/res/drawable/apps_list_scrollbar_thumb.xml
@@ -17,5 +17,5 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
     <solid android:color="@color/apps_view_scrollbar_thumb_color" />
-    <size android:width="@dimen/apps_view_fast_scroll_bar_size" />
+    <size android:width="@dimen/apps_view_fast_scroll_bar_width" />
 </shape>
\ No newline at end of file
diff --git a/res/layout/apps_list_view.xml b/res/layout/apps_list_view.xml
index 59c0410..3e42f84 100644
--- a/res/layout/apps_list_view.xml
+++ b/res/layout/apps_list_view.xml
@@ -36,7 +36,7 @@
         android:textSize="16sp"
         android:textColor="#4c4c4c"
         android:textColorHint="#9c9c9c"
-        android:imeOptions="flagNoExtractUi"
+        android:imeOptions="actionDone|flagNoExtractUi"
         android:background="@drawable/apps_list_search_bg"
         android:elevation="4dp" />
     <com.android.launcher3.AppsContainerRecyclerView
@@ -47,9 +47,6 @@
         android:paddingTop="12dp"
         android:paddingBottom="12dp"
         android:clipToPadding="false"
-        android:fadeScrollbars="false"
-        android:scrollbars="vertical"
-        android:scrollbarThumbVertical="@drawable/apps_list_scrollbar_thumb"
         android:focusable="true"
         android:descendantFocusability="afterDescendants"
         android:background="@drawable/apps_list_bg" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index b9b9a24..c327ec2 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -52,8 +52,9 @@
     <dimen name="apps_grid_view_start_margin">52dp</dimen>
     <dimen name="apps_view_row_height">64dp</dimen>
     <dimen name="apps_view_section_text_size">24sp</dimen>
-    <dimen name="apps_view_fast_scroll_bar_size">6dp</dimen>
-    <dimen name="apps_view_fast_scroll_gutter_size">40dp</dimen>
+    <dimen name="apps_view_fast_scroll_bar_width">6dp</dimen>
+    <dimen name="apps_view_fast_scroll_bar_min_height">64dp</dimen>
+    <dimen name="apps_view_fast_scroll_scrubber_touch_inset">-16dp</dimen>
     <dimen name="apps_view_fast_scroll_popup_size">64dp</dimen>
     <dimen name="apps_view_fast_scroll_text_size">40dp</dimen>
 
diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java
index c5a508c..4d6b9d4 100644
--- a/src/com/android/launcher3/AppsContainerRecyclerView.java
+++ b/src/com/android/launcher3/AppsContainerRecyclerView.java
@@ -26,6 +26,7 @@
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.ViewConfiguration;
 
 import java.util.List;
@@ -42,7 +43,9 @@
     private AlphabeticalAppsList mApps;
     private int mNumAppsPerRow;
 
+    private Drawable mScrollbar;
     private Drawable mFastScrollerBg;
+    private Rect mVerticalScrollbarBounds = new Rect();
     private boolean mDraggingFastScroller;
     private String mFastScrollSectionName;
     private Paint mFastScrollTextPaint;
@@ -52,7 +55,9 @@
     private int mDownY;
     private int mLastX;
     private int mLastY;
-    private int mGutterSize;
+    private int mScrollbarWidth;
+    private int mScrollbarMinHeight;
+    private int mScrollbarInset;
 
     public AppsContainerRecyclerView(Context context) {
         this(context, null);
@@ -72,14 +77,19 @@
 
         Resources res = context.getResources();
         int fastScrollerSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_popup_size);
-        mFastScrollerBg = res.getDrawable(R.drawable.apps_list_fastscroll_bg);
+        mScrollbar = context.getDrawable(R.drawable.apps_list_scrollbar_thumb);
+        mFastScrollerBg = context.getDrawable(R.drawable.apps_list_fastscroll_bg);
         mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
         mFastScrollTextPaint = new Paint();
         mFastScrollTextPaint.setColor(Color.WHITE);
         mFastScrollTextPaint.setAntiAlias(true);
         mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
                 R.dimen.apps_view_fast_scroll_text_size));
-        mGutterSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_gutter_size);
+        mScrollbarWidth = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_width);
+        mScrollbarMinHeight =
+                res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_min_height);
+        mScrollbarInset =
+                res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_scrubber_touch_inset);
         setFastScrollerAlpha(getFastScrollerAlpha());
     }
 
@@ -112,6 +122,13 @@
         return mFastScrollAlpha;
     }
 
+    /**
+     * Returns the scroll bar width.
+     */
+    public int getScrollbarWidth() {
+        return mScrollbarWidth;
+    }
+
     @Override
     protected void onFinishInflate() {
         addOnItemTouchListener(this);
@@ -120,38 +137,13 @@
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
-
-        if (mFastScrollAlpha > 0f) {
-            boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
-                    LAYOUT_DIRECTION_RTL);
-            Rect bgBounds = mFastScrollerBg.getBounds();
-            int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
-            int x;
-            if (isRtl) {
-                x = getPaddingLeft() + getScrollBarSize();
-            } else {
-                x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
-            }
-            int y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
-            y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
-                    bgBounds.height()));
-            canvas.translate(x, y);
-            mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
-            mFastScrollerBg.draw(canvas);
-            mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
-            mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
-                    mFastScrollSectionName.length(), mFastScrollTextBounds);
-            canvas.drawText(mFastScrollSectionName,
-                    (bgBounds.width() - mFastScrollTextBounds.width()) / 2,
-                    bgBounds.height() -  (bgBounds.height() - mFastScrollTextBounds.height()) / 2,
-                    mFastScrollTextPaint);
-            canvas.restoreToCount(restoreCount);
-        }
+        drawVerticalScrubber(canvas);
+        drawFastScrollerPopup(canvas);
     }
 
     /**
      * We intercept the touch handling only to support fast scrolling when initiated from the
-     * gutter.  Otherwise, we fall back to the default RecyclerView touch handling.
+     * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
      */
     @Override
     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
@@ -182,15 +174,7 @@
                 break;
             case MotionEvent.ACTION_MOVE:
                 // Check if we are scrolling
-                boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
-                        LAYOUT_DIRECTION_RTL);
-                boolean isInGutter;
-                if (isRtl) {
-                    isInGutter = mDownX < mGutterSize;
-                } else {
-                    isInGutter = mDownX >= (getWidth() - mGutterSize);
-                }
-                if (!mDraggingFastScroller && isInGutter &&
+                if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
                         Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
                     getParent().requestDisallowInterceptTouchEvent(true);
                     mDraggingFastScroller = true;
@@ -230,6 +214,67 @@
     }
 
     /**
+     * Returns whether a given point is near the scrollbar.
+     */
+    private boolean isPointNearScrollbar(int x, int y) {
+        // Check if we are scrolling
+        updateVerticalScrollbarBounds();
+        mVerticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
+        return mVerticalScrollbarBounds.contains(x, y);
+    }
+
+    /**
+     * Draws the fast scroller popup.
+     */
+    private void drawFastScrollerPopup(Canvas canvas) {
+        int x;
+        int y;
+        boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                LAYOUT_DIRECTION_RTL);
+
+        if (mFastScrollAlpha > 0f) {
+            // Calculate the position for the fast scroller popup
+            Rect bgBounds = mFastScrollerBg.getBounds();
+            if (isRtl) {
+                x = getPaddingLeft() + getScrollBarSize();
+            } else {
+                x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
+            }
+            y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
+            y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
+                    bgBounds.height()));
+
+            // Draw the fast scroller popup
+            int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+            canvas.translate(x, y);
+            mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
+            mFastScrollerBg.draw(canvas);
+            mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
+            mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
+                    mFastScrollSectionName.length(), mFastScrollTextBounds);
+            canvas.drawText(mFastScrollSectionName,
+                    (bgBounds.width() - mFastScrollTextBounds.width()) / 2,
+                    bgBounds.height() - (bgBounds.height() - mFastScrollTextBounds.height()) / 2,
+                    mFastScrollTextPaint);
+            canvas.restoreToCount(restoreCount);
+        }
+    }
+
+    /**
+     * Draws the vertical scrollbar.
+     */
+    private void drawVerticalScrubber(Canvas canvas) {
+        updateVerticalScrollbarBounds();
+
+        // Draw the scroll bar
+        int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.translate(mVerticalScrollbarBounds.left, mVerticalScrollbarBounds.top);
+        mScrollbar.setBounds(0, 0, mScrollbarWidth, mVerticalScrollbarBounds.height());
+        mScrollbar.draw(canvas);
+        canvas.restoreToCount(restoreCount);
+    }
+
+    /**
      * Invalidates the fast scroller popup.
      */
     private void invalidateFastScroller() {
@@ -243,11 +288,7 @@
     private String scrollToPositionAtProgress(float progress) {
         List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
         // Get the total number of rows
-        int rowCount = 0;
-        for (AlphabeticalAppsList.SectionInfo info : sections) {
-            int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
-            rowCount += numRowsInSection;
-        }
+        int rowCount = getNumRows();
 
         // Find the index of the first app in that row and scroll to that position
         int rowAtProgress = (int) (progress * rowCount);
@@ -270,4 +311,100 @@
         // Returns the section name of the row
         return mApps.getSectionNameForApp(appInfo);
     }
+
+    /**
+     * Returns the bounds for the scrollbar.
+     */
+    private void updateVerticalScrollbarBounds() {
+        int x;
+        int y;
+        boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                LAYOUT_DIRECTION_RTL);
+
+        // Skip early if there are no items
+        if (mApps.getApps().isEmpty()) {
+            mVerticalScrollbarBounds.setEmpty();
+            return;
+        }
+
+        // Find the index and height of the first visible row (all rows have the same height)
+        int rowIndex = -1;
+        int rowTopOffset = -1;
+        int rowHeight = -1;
+        int rowCount = getNumRows();
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            int position = getChildPosition(child);
+            if (position != NO_POSITION) {
+                AppInfo info = mApps.getApps().get(position);
+                if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
+                    int appIndex = mApps.getAppsWithoutSectionBreaks().indexOf(info);
+                    rowIndex = findRowForAppIndex(appIndex);
+                    rowTopOffset = getLayoutManager().getDecoratedTop(child);
+                    rowHeight = child.getHeight();
+                    break;
+                }
+            }
+        }
+
+        if (rowIndex != -1) {
+            int height = getHeight() - getPaddingTop() - getPaddingBottom();
+            int totalScrollHeight = rowCount * rowHeight;
+            if (totalScrollHeight > height) {
+                int scrollbarHeight = Math.max(mScrollbarMinHeight,
+                        (int) (height / ((float) totalScrollHeight / height)));
+
+                // Calculate the position and size of the scroll bar
+                if (isRtl) {
+                    x = getPaddingLeft();
+                } else {
+                    x = getWidth() - getPaddingRight() - mScrollbarWidth;
+                }
+
+                // To calculate the offset, we compute the percentage of the total scrollable height
+                // that the user has already scrolled and then map that to the scroll bar bounds
+                int availableY = totalScrollHeight - height;
+                int availableScrollY = height - scrollbarHeight;
+                y = (rowIndex * rowHeight) - rowTopOffset;
+                y = getPaddingTop() +
+                        (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
+
+                mVerticalScrollbarBounds.set(x, y, x + mScrollbarWidth, y + scrollbarHeight);
+                return;
+            }
+        }
+        mVerticalScrollbarBounds.setEmpty();
+    }
+
+    /**
+     * Returns the row index for a given position in the list.
+     */
+    private int findRowForAppIndex(int position) {
+        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+        int appIndex = 0;
+        int rowCount = 0;
+        for (AlphabeticalAppsList.SectionInfo info : sections) {
+            int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
+            if (appIndex + info.numAppsInSection > position) {
+                return rowCount + ((position - appIndex) / mNumAppsPerRow);
+            }
+            appIndex += info.numAppsInSection;
+            rowCount += numRowsInSection;
+        }
+        return appIndex;
+    }
+
+    /**
+     * Returns the total number of rows in the list.
+     */
+    private int getNumRows() {
+        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+        int rowCount = 0;
+        for (AlphabeticalAppsList.SectionInfo info : sections) {
+            int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
+            rowCount += numRowsInSection;
+        }
+        return rowCount;
+    }
 }
diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java
index ce092bf..a4688f0 100644
--- a/src/com/android/launcher3/AppsContainerView.java
+++ b/src/com/android/launcher3/AppsContainerView.java
@@ -166,12 +166,16 @@
         mAppsListView.setAdapter(mAdapter);
         mAppsListView.setHasFixedSize(true);
         if (isRtl) {
-            mAppsListView.setPadding(mAppsListView.getPaddingLeft(), mAppsListView.getPaddingTop(),
+            mAppsListView.setPadding(
+                    mAppsListView.getPaddingLeft(),
+                    mAppsListView.getPaddingTop(),
                     mAppsListView.getPaddingRight() + mContentMarginStart,
                     mAppsListView.getPaddingBottom());
         } else {
-            mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart,
-                    mAppsListView.getPaddingTop(), mAppsListView.getPaddingRight(),
+            mAppsListView.setPadding(
+                    mAppsListView.getPaddingLeft() + mContentMarginStart,
+                    mAppsListView.getPaddingTop(),
+                    mAppsListView.getPaddingRight(),
                     mAppsListView.getPaddingBottom());
         }
         if (mItemDecoration != null) {