Add the support to drag the thumb in scrollbar.

Bug: 154244647
Bug: 156488147
Test: Manual

Change-Id: I2ad24e2500a0fe35aca102c5fbc6bbc795a22c4f
Merged-In: I2ad24e2500a0fe35aca102c5fbc6bbc795a22c4f
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
index 63ed070..b75e904 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiRecyclerView.java
@@ -183,41 +183,35 @@
                         context.getDrawable(R.drawable.car_ui_divider),
                         mNumOfColumns);
 
+        int topOffset = a.getInteger(R.styleable.CarUiRecyclerView_topOffset, /* defValue= */0);
+        int bottomOffset = a.getInteger(
+                R.styleable.CarUiRecyclerView_bottomOffset, /* defValue= */0);
         if (mCarUiRecyclerViewLayout == CarUiRecyclerViewLayout.LINEAR) {
 
-            int linearTopOffset =
-                    a.getInteger(R.styleable.CarUiRecyclerView_topOffset, /* defValue= */ 0);
-            int linearBottomOffset =
-                    a.getInteger(R.styleable.CarUiRecyclerView_bottomOffset, /* defValue= */ 0);
-
             if (enableDivider) {
                 addItemDecoration(mDividerItemDecorationLinear);
             }
             RecyclerView.ItemDecoration topOffsetItemDecoration =
-                    new LinearOffsetItemDecoration(linearTopOffset, OffsetPosition.START);
+                    new LinearOffsetItemDecoration(topOffset, OffsetPosition.START);
 
             RecyclerView.ItemDecoration bottomOffsetItemDecoration =
-                    new LinearOffsetItemDecoration(linearBottomOffset, OffsetPosition.END);
+                    new LinearOffsetItemDecoration(bottomOffset, OffsetPosition.END);
 
             addItemDecoration(topOffsetItemDecoration);
             addItemDecoration(bottomOffsetItemDecoration);
             setLayoutManager(new LinearLayoutManager(getContext()));
         } else {
-            int gridTopOffset =
-                    a.getInteger(R.styleable.CarUiRecyclerView_topOffset, /* defValue= */ 0);
-            int gridBottomOffset =
-                    a.getInteger(R.styleable.CarUiRecyclerView_bottomOffset, /* defValue= */ 0);
 
             if (enableDivider) {
                 addItemDecoration(mDividerItemDecorationGrid);
             }
 
             mOffsetItemDecoration =
-                    new GridOffsetItemDecoration(gridTopOffset, mNumOfColumns,
+                    new GridOffsetItemDecoration(topOffset, mNumOfColumns,
                             OffsetPosition.START);
 
             GridOffsetItemDecoration bottomOffsetItemDecoration =
-                    new GridOffsetItemDecoration(gridBottomOffset, mNumOfColumns,
+                    new GridOffsetItemDecoration(bottomOffset, mNumOfColumns,
                             OffsetPosition.END);
 
             addItemDecoration(mOffsetItemDecoration);
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
index 0e68c4b..7052820 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -91,6 +91,10 @@
         getRecyclerView().setOnFlingListener(null);
         mSnapHelper.attachToRecyclerView(getRecyclerView());
 
+        // enables fast scrolling.
+        FastScroller fastScroller = new FastScroller(mRecyclerView, mScrollTrack, mScrollView);
+        fastScroller.enable();
+
         mScrollView.addOnLayoutChangeListener(
                 (View v,
                         int left,
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/FastScroller.java b/car-ui-lib/src/com/android/car/ui/recyclerview/FastScroller.java
new file mode 100644
index 0000000..9a44110
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/FastScroller.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 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.car.ui.recyclerview;
+
+import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+
+/**
+ * Class responsible for fast scrolling. This class offers two functionalities.
+ * <ul>
+ *     <li>User can hold the thumb and drag.</li>
+ *     <li>User can click anywhere on the track and thumb will scroll to that position.</li>
+ * </ul>
+ */
+class FastScroller implements View.OnTouchListener {
+
+    private float mTouchDownY = -1;
+
+    private View mScrollTrackView;
+    private boolean mIsDragging;
+    private View mScrollThumb;
+    private RecyclerView mRecyclerView;
+
+    FastScroller(@NonNull RecyclerView recyclerView, @NonNull View scrollTrackView,
+            @NonNull View scrollView) {
+        mRecyclerView = recyclerView;
+        mScrollTrackView = scrollTrackView;
+        mScrollThumb = requireViewByRefId(scrollView, R.id.car_ui_scrollbar_thumb);
+    }
+
+    void enable() {
+        if (mRecyclerView != null) {
+            mScrollTrackView.setOnTouchListener(this);
+        }
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent me) {
+        switch (me.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mTouchDownY = me.getY();
+                mIsDragging = false;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                mIsDragging = true;
+                float thumbBottom = mScrollThumb.getY() + mScrollThumb.getHeight();
+                // check if the move coordinates are within the bounds of the thumb. i.e user is
+                // holding and dragging the thumb.
+                if (!(me.getY() + mScrollTrackView.getY() < thumbBottom
+                        && me.getY() + mScrollTrackView.getY() > mScrollThumb.getY())) {
+                    // don't do anything if touch is detected outside the thumb
+                    return true;
+                }
+                // calculate where the center of the thumb is on the screen.
+                float thumbCenter = mScrollThumb.getY() + mScrollThumb.getHeight() / 2.0f;
+                // me.getY() returns the coordinates relative to the view. For example, if we
+                // click the top left of the scroll track the coordinates will be 0,0. Hence, we
+                // need to add the relative coordinates to the actual coordinates computed by the
+                // thumb center and add them to get the final Y coordinate. "(me.getY() -
+                // mTouchDownY)" calculates the distance that is moved from the previous touch
+                // event.
+                verticalScrollTo(thumbCenter + (me.getY() - mTouchDownY));
+                mTouchDownY = me.getY();
+                break;
+            case MotionEvent.ACTION_UP:
+            default:
+                mTouchDownY = -1;
+                // if not dragged then it's a click. When a click is detected on the track and
+                // within the range we want to move the center of the thumb to the Y coordinate
+                // of the clicked position.
+                if (!mIsDragging) {
+                    verticalScrollTo(me.getY() + mScrollTrackView.getY());
+                }
+        }
+        return true;
+    }
+
+    private void verticalScrollTo(float y) {
+        int scrollingBy = calculateScrollDistance(y);
+        if (scrollingBy != 0) {
+            mRecyclerView.scrollBy(0, scrollingBy);
+        }
+    }
+
+    private int calculateScrollDistance(float newDragPos) {
+        final int[] scrollbarRange = getVerticalRange();
+        int scrollbarLength = scrollbarRange[1] - scrollbarRange[0];
+
+        float thumbCenter = mScrollThumb.getY() + mScrollThumb.getHeight() / 2.0f;
+
+        if (scrollbarLength == 0) {
+            return 0;
+        }
+        // percentage of data to be scrolled.
+        float percentage = ((newDragPos - thumbCenter) / (float) scrollbarLength);
+        int totalPossibleOffset =
+                mRecyclerView.computeVerticalScrollRange() - mRecyclerView.getHeight();
+        int scrollingBy = (int) (percentage * totalPossibleOffset);
+        int absoluteOffset = mRecyclerView.computeVerticalScrollOffset() + scrollingBy;
+        if (absoluteOffset < 0) {
+            return 0;
+        }
+        return scrollingBy;
+    }
+
+    /**
+     * Gets the (min, max) vertical positions of the vertical scroll bar. The range starts from the
+     * center of thumb when thumb is top aligned to center of the thumb when thumb is bottom
+     * aligned.
+     */
+    private int[] getVerticalRange() {
+        int[] verticalRange = new int[2];
+        verticalRange[0] = (int) mScrollTrackView.getY() + mScrollThumb.getHeight() / 2;
+        verticalRange[1] = (int) mScrollTrackView.getY() + mScrollTrackView.getHeight()
+                - mScrollThumb.getHeight() / 2;
+        return verticalRange;
+    }
+}