Update pageUp behavior to create consistent scroll to target view

Bug: 170401207
Bug: 168827981
Test: atest CarUILibUnitTests

Change-Id: If9dbf7a129839c7ade4e32f7c708f2e142f07e39
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java
index 136dc6e..a7fff75 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSnapHelper.java
@@ -246,7 +246,7 @@
      * @param helper An {@link OrientationHelper} to aid with calculation.
      * @return A float indicating the percentage of the given view that is visible.
      */
-    private static float getPercentageVisible(View view, OrientationHelper helper) {
+    static float getPercentageVisible(View view, OrientationHelper helper) {
         int start = helper.getStartAfterPadding();
         int end = helper.getEndAfterPadding();
 
@@ -340,6 +340,80 @@
     }
 
     /**
+     * Estimates a position to which CarUiSnapHelper will try to snap to for a requested scroll
+     * distance.
+     *
+     * @param helper         The {@link OrientationHelper} that is created from the LayoutManager.
+     * @param scrollDistance The intended scroll distance.
+     *
+     * @return The diff between the target snap position and the current position.
+     */
+    public int estimateNextPositionDiffForScrollDistance(OrientationHelper helper,
+            int scrollDistance) {
+        float distancePerChild = computeDistancePerChild(helper.getLayoutManager(), helper);
+        if (distancePerChild <= 0) {
+            return 0;
+        }
+        return (int) Math.round(scrollDistance / distancePerChild);
+    }
+
+    /**
+     * This method is taken verbatim from the [androidx] {@link LinearSnapHelper} private method
+     * implementation.
+     *
+     * Computes an average pixel value to pass a single child.
+     * <p>
+     * Returns a negative value if it cannot be calculated.
+     *
+     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}.
+     * @param helper        The relevant {@link OrientationHelper} for the attached
+     *                      {@link RecyclerView.LayoutManager}.
+     *
+     * @return A float value that is the average number of pixels needed to scroll by one view in
+     * the relevant direction.
+     */
+    private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
+            OrientationHelper helper) {
+        View minPosView = null;
+        View maxPosView = null;
+        int minPos = Integer.MAX_VALUE;
+        int maxPos = Integer.MIN_VALUE;
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return 1;
+        }
+
+        for (int i = 0; i < childCount; i++) {
+            View child = layoutManager.getChildAt(i);
+            final int pos = layoutManager.getPosition(child);
+            if (pos == RecyclerView.NO_POSITION) {
+                continue;
+            }
+            if (pos < minPos) {
+                minPos = pos;
+                minPosView = child;
+            }
+            if (pos > maxPos) {
+                maxPos = pos;
+                maxPosView = child;
+            }
+        }
+        if (minPosView == null || maxPosView == null) {
+            return 1;
+        }
+        int start = Math.min(helper.getDecoratedStart(minPosView),
+                helper.getDecoratedStart(maxPosView));
+        int end = Math.max(helper.getDecoratedEnd(minPosView),
+                helper.getDecoratedEnd(maxPosView));
+        int distance = end - start;
+        if (distance == 0) {
+            return 0;
+        }
+        return 1f * distance / ((maxPos - minPos) + 1);
+    }
+
+    /**
      * Returns {@code true} if the RecyclerView is completely displaying the first item.
      */
     public boolean isAtStart(@Nullable LayoutManager layoutManager) {
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 ec579c3..72b2093 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
@@ -324,6 +324,7 @@
                 getOrientationHelper(getRecyclerView().getLayoutManager());
         int screenSize = orientationHelper.getTotalSpace();
         int scrollDistance = screenSize;
+        boolean isPageUpOverLongItem;
         // The iteration order matters. In case where there are 2 items longer than screen size, we
         // want to focus on upcoming view.
         for (int i = 0; i < getRecyclerView().getChildCount(); i++) {
@@ -343,13 +344,32 @@
                     // is less than a full scroll. Align child top with parent top.
                     scrollDistance = Math.abs(orientationHelper.getDecoratedStart(child));
                 }
+
                 // There can be two items that are longer than the screen. We stop at the first one.
                 // This is affected by the iteration order.
-                break;
+                // Distance should always be positive. Negate its value to scroll up.
+                mRecyclerView.smoothScrollBy(0, -scrollDistance);
+                return;
             }
         }
-        // Distance should always be positive. Negate its value to scroll up.
-        mRecyclerView.smoothScrollBy(0, -scrollDistance);
+
+        int nextPos = mSnapHelper.estimateNextPositionDiffForScrollDistance(orientationHelper,
+                -scrollDistance);
+        View currentPosView = getFirstFullyVisibleChild(orientationHelper);
+        int currentPos = currentPosView != null ? mRecyclerView.getLayoutManager().getPosition(
+                currentPosView) : 0;
+        mRecyclerView.smoothScrollToPosition(Math.max(0, currentPos + nextPos));
+    }
+
+    private View getFirstFullyVisibleChild(OrientationHelper helper) {
+        for (int i = 0; i < getRecyclerView().getChildCount(); i++) {
+            View child = getRecyclerView().getChildAt(i);
+            if (CarUiSnapHelper.getPercentageVisible(child, helper) == 1f) {
+                return getRecyclerView().getChildAt(i);
+            }
+        }
+
+        return null;
     }
 
     /**