Use scrollToPositionWithOffset in widget list and account for padding

Also, scroll after collapsing as well to preserve the header position.
LinearLayoutManager seems to not count/double count the top padding of
the RecyclerView when scrolling to the position. Scrolling with the
offset and deducting the top padding works around the issue.

There's still some occasional weirdness that needs to be investigated,
but this works well...most of the time.

Bug: 183378651
Test: locally
Change-Id: I0ba85fb65411991ef781f08a69faaa993a7d7fd0
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 3936ec8..5010629 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -20,6 +20,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
+import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLongClickListener;
 import android.view.ViewGroup;
@@ -27,6 +28,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.Adapter;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -268,36 +270,53 @@
             updateVisibleEntries();
             // Scroll the layout manager to the header position to keep it anchored to the same
             // position.
-            scrollToSelectedHeaderPosition();
+            scrollToPositionAndMaintainOffset(getSelectedHeaderPosition());
         } else if (packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) {
+            OptionalInt previouslySelectedPosition = getSelectedHeaderPosition();
+
             mWidgetsContentVisiblePackageUserKey = null;
             updateVisibleEntries();
+
+            // Scroll to the header that was just collapsed so it maintains its scroll offset.
+            scrollToPositionAndMaintainOffset(previouslySelectedPosition);
         }
     }
 
-    private void scrollToSelectedHeaderPosition() {
-        OptionalInt selectedHeaderPosition =
-                IntStream.range(0, mVisibleEntries.size())
-                        .filter(index -> isHeaderForVisibleContent(mVisibleEntries.get(index)))
-                        .findFirst();
-        RecyclerView.LayoutManager layoutManager =
-                mRecyclerView == null ? null : mRecyclerView.getLayoutManager();
-        if (!selectedHeaderPosition.isPresent() || layoutManager == null) {
+    private OptionalInt getSelectedHeaderPosition() {
+        return IntStream.range(0, mVisibleEntries.size())
+                .filter(index -> isHeaderForVisibleContent(mVisibleEntries.get(index)))
+                .findFirst();
+    }
+
+    /**
+     * Scrolls to the selected header position. LinearLayoutManager scrolls the minimum distance
+     * necessary, so this will keep the selected header in place during clicks, without interrupting
+     * the animation.
+     */
+    private void scrollToPositionAndMaintainOffset(OptionalInt positionOptional) {
+        if (!positionOptional.isPresent() || mRecyclerView == null) return;
+        int position = positionOptional.getAsInt();
+
+        LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
+        if (layoutManager == null) return;
+
+        if (position == mVisibleEntries.size() - 2
+                && mVisibleEntries.get(mVisibleEntries.size() - 1)
+                instanceof WidgetsListContentEntry) {
+            // If the selected header is in the last position and its content is showing, then
+            // scroll to the final position so the last list of widgets will show.
+            layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
             return;
         }
 
-        // Scroll to the selected header position. LinearLayoutManager scrolls the minimum distance
-        // necessary, so this will keep the selected header in place during clicks, without
-        // interrupting the animation.
-        int position = selectedHeaderPosition.getAsInt();
-        if (position == mVisibleEntries.size() - 2) {
-            // If the selected header is in the last position (-1 for the content), then scroll to
-            // the final position so the last list of widgets will show.
-            layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
-        } else {
-            // Otherwise, scroll to the position of the selected header.
-            layoutManager.scrollToPosition(position);
-        }
+        // Scroll to the header view's current offset, accounting for the recycler view's padding.
+        // If the header view couldn't be found, then it will appear at the top of the list.
+        View headerView = layoutManager.findViewByPosition(position);
+        int targetHeaderViewTop =
+                headerView == null ? 0 : layoutManager.getDecoratedTop(headerView);
+        layoutManager.scrollToPositionWithOffset(
+                position,
+                targetHeaderViewTop - mRecyclerView.getPaddingTop());
     }
 
     /**