Unifying scroll calculation logic for both widgets and apps recycler view
Also using itemType instead of item object for widget size cache

Bug: 234008165
Test: Verified on device
Change-Id: Ia4b4a00a11627c0c454e4a699570e8ab1667a390
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index f117069..94903f2 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -24,6 +24,7 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
 import com.android.launcher3.compat.AccessibilityManagerCompat;
@@ -86,15 +87,20 @@
      * Returns the available scroll height:
      *   AvailableScrollHeight = Total height of the all items - last page height
      */
-    protected abstract int getAvailableScrollHeight();
+    protected int getAvailableScrollHeight() {
+        // AvailableScrollHeight = Total height of the all items - first page height
+        int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
+        int totalHeightOfAllItems = getItemsHeight(/* untilIndex= */ getAdapter().getItemCount());
+        int availableScrollHeight = totalHeightOfAllItems - firstPageHeight;
+        return Math.max(0, availableScrollHeight);
+    }
 
     /**
      * Returns the available scroll bar height:
      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
      */
     protected int getAvailableScrollBarHeight() {
-        int availableScrollBarHeight = getScrollbarTrackHeight() - mScrollbar.getThumbHeight();
-        return availableScrollBarHeight;
+        return getScrollbarTrackHeight() - mScrollbar.getThumbHeight();
     }
 
     /**
@@ -152,12 +158,51 @@
     }
 
     /**
-     * Maps the touch (from 0..1) to the adapter position that should be visible.
-     * <p>Override in each subclass of this base class.
-     *
      * @return the scroll top of this recycler view.
      */
-    public abstract int getCurrentScrollY();
+    public int getCurrentScrollY() {
+        Adapter adapter = getAdapter();
+        if (adapter == null) {
+            return -1;
+        }
+        if (adapter.getItemCount() == 0 || getChildCount() == 0) {
+            return -1;
+        }
+
+        int itemPosition = NO_POSITION;
+        View child = null;
+
+        LayoutManager layoutManager = getLayoutManager();
+        if (layoutManager instanceof LinearLayoutManager) {
+            // Use the LayoutManager as the source of truth for visible positions. During
+            // animations, the view group child may not correspond to the visible views that appear
+            // at the top.
+            itemPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
+            child = layoutManager.findViewByPosition(itemPosition);
+        }
+
+        if (child == null) {
+            // If the layout manager returns null for any reason, which can happen before layout
+            // has occurred for the position, then look at the child of this view as a ViewGroup.
+            child = getChildAt(0);
+            itemPosition = getChildAdapterPosition(child);
+        }
+        if (itemPosition == NO_POSITION) {
+            return -1;
+        }
+        return getPaddingTop() + getItemsHeight(itemPosition)
+                - layoutManager.getDecoratedTop(child);
+    }
+
+    /**
+     * Returns the sum of the height, in pixels, of this list adapter's items from index
+     * 0 (inclusive) until {@code untilIndex} (exclusive). If untilIndex is same as the itemCount,
+     * it returns the full height of all the items.
+     *
+     * <p>If the untilIndex is larger than the total number of items in this adapter, returns the
+     * sum of all items' height.
+     */
+    protected abstract int getItemsHeight(int untilIndex);
 
     /**
      * Maps the touch (from 0..1) to the adapter position that should be visible.
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index af17cf7..de34416 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -31,7 +31,6 @@
 import android.util.Log;
 import android.util.SparseIntArray;
 import android.view.MotionEvent;
-import android.view.View;
 
 import androidx.recyclerview.widget.RecyclerView;
 
@@ -342,24 +341,7 @@
     }
 
     @Override
-    public int getCurrentScrollY() {
-        // Return early if there are no items or we haven't been measured
-        List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems();
-        if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) {
-            return -1;
-        }
-
-        // Calculate the y and offset for the item
-        View child = getChildAt(0);
-        int position = getChildAdapterPosition(child);
-        if (position == NO_POSITION) {
-            return -1;
-        }
-        return getPaddingTop() +
-                getCurrentScrollY(position, getLayoutManager().getDecoratedTop(child));
-    }
-
-    public int getCurrentScrollY(int position, int offset) {
+    protected int getItemsHeight(int position) {
         List<AllAppsGridAdapter.AdapterItem> items = mApps.getAdapterItems();
         AllAppsGridAdapter.AdapterItem posItem = position < items.size()
                 ? items.get(position) : null;
@@ -400,17 +382,7 @@
             }
             mCachedScrollPositions.put(position, y);
         }
-        return y - offset;
-    }
-
-    /**
-     * Returns the available scroll height:
-     * AvailableScrollHeight = Total height of the all items - last page height
-     */
-    @Override
-    protected int getAvailableScrollHeight() {
-        return getPaddingTop() + getCurrentScrollY(getAdapter().getItemCount(), 0)
-                - getHeight() + getPaddingBottom();
+        return y;
     }
 
     public int getScrollBarTop() {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
index 0e5a7d7..e6b9dca 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.recyclerview.ViewHolderBinder.POSITION_LAST;
 
 import android.content.Context;
-import android.graphics.Rect;
 import android.os.Process;
 import android.util.Log;
 import android.util.SparseArray;
@@ -36,7 +35,6 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.Adapter;
-import androidx.recyclerview.widget.RecyclerView.LayoutParams;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
 import com.android.launcher3.R;
@@ -80,10 +78,10 @@
     private static final boolean DEBUG = false;
 
     /** Uniquely identifies widgets list view type within the app. */
-    private static final int VIEW_TYPE_WIDGETS_SPACE = R.id.view_type_widgets_space;
-    private static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
-    private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
-    private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;
+    public static final int VIEW_TYPE_WIDGETS_SPACE = R.id.view_type_widgets_space;
+    public static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
+    public static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
+    public static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;
 
     private final Context mContext;
     private final WidgetsDiffReporter mDiffReporter;
@@ -103,7 +101,6 @@
     @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
     @Nullable private RecyclerView mRecyclerView;
     @Nullable private PackageUserKey mPendingClickHeader;
-    private final int mSpacingBetweenEntries;
     private int mMaxSpanSize = 4;
 
     public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
@@ -133,28 +130,11 @@
         mViewHolderBinders.put(
                 VIEW_TYPE_WIDGETS_SPACE,
                 new WidgetsSpaceViewHolderBinder(emptySpaceHeightProvider));
-        mSpacingBetweenEntries =
-                context.getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
     }
 
     @Override
     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
         mRecyclerView = recyclerView;
-
-        mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
-            @Override
-            public void getItemOffsets(
-                    @NonNull Rect outRect,
-                    @NonNull View view,
-                    @NonNull RecyclerView parent,
-                    @NonNull RecyclerView.State state) {
-                super.getItemOffsets(outRect, view, parent, state);
-                int position = ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
-                boolean isHeader =
-                        view.getTag(R.id.tag_widget_entry) instanceof WidgetsListBaseEntry.Header;
-                outRect.top += position > 0 && isHeader ? mSpacingBetweenEntries : 0;
-            }
-        });
     }
 
     @Override
@@ -286,7 +266,6 @@
             listPos |= POSITION_LAST;
         }
         viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), listPos, payloads);
-        holder.itemView.setTag(R.id.tag_widget_entry, entry);
     }
 
     @Override
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
index bdf646b..daa67a9 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java
@@ -16,27 +16,24 @@
 
 package com.android.launcher3.widget.picker;
 
+import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_HEADER;
+import static com.android.launcher3.widget.picker.WidgetsListAdapter.VIEW_TYPE_WIDGETS_SEARCH_HEADER;
+
 import android.content.Context;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.SparseIntArray;
 import android.view.MotionEvent;
 import android.view.View;
-import android.widget.TableLayout;
 
+import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.FastScrollRecyclerView;
 import com.android.launcher3.R;
-import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.widget.model.WidgetListSpaceEntry;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import com.android.launcher3.widget.model.WidgetsListContentEntry;
-import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
-import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
-import com.android.launcher3.widget.picker.WidgetsSpaceViewHolderBinder.EmptySpaceView;
 
 /**
  * The widgets recycler view.
@@ -51,12 +48,14 @@
     private boolean mTouchDownOnScroller;
     private HeaderViewDimensionsProvider mHeaderViewDimensionsProvider;
 
-    // Cached sizes
-    private int mLastVisibleWidgetContentTableHeight = 0;
-    private int mWidgetHeaderHeight = 0;
-    private int mWidgetEmptySpaceHeight = 0;
-
-    private final int mSpacingBetweenEntries;
+    /**
+     * There is always 1 or 0 item of VIEW_TYPE_WIDGETS_LIST. Other types have fixes sizes, so the
+     * the size can be used for all other items of same type. Caching the last know size for
+     * VIEW_TYPE_WIDGETS_LIST allows us to use it to estimate full size even when
+     * VIEW_TYPE_WIDGETS_LIST is not visible on the screen.
+     */
+    private final SparseIntArray mCachedSizes = new SparseIntArray();
+    private final SpacingDecoration mSpacingDecoration;
 
     public WidgetsRecyclerView(Context context) {
         this(context, null);
@@ -72,12 +71,8 @@
         mScrollbarTop = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
         addOnItemTouchListener(this);
 
-        ActivityContext activity = ActivityContext.lookupContext(getContext());
-        DeviceProfile grid = activity.getDeviceProfile();
-
-        // The spacing used between entries.
-        mSpacingBetweenEntries =
-                getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
+        mSpacingDecoration = new SpacingDecoration(context);
+        addItemDecoration(mSpacingDecoration);
     }
 
     @Override
@@ -138,67 +133,6 @@
         synchronizeScrollBarThumbOffsetToViewScroll(scrollY, getAvailableScrollHeight());
     }
 
-    @Override
-    public int getCurrentScrollY() {
-        // Skip early if widgets are not bound.
-        if (isModelNotReady() || getChildCount() == 0) {
-            return -1;
-        }
-
-        int rowIndex = -1;
-        View child = null;
-
-        LayoutManager layoutManager = getLayoutManager();
-        if (layoutManager instanceof LinearLayoutManager) {
-            // Use the LayoutManager as the source of truth for visible positions. During
-            // animations, the view group child may not correspond to the visible views that appear
-            // at the top.
-            rowIndex = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
-            child = layoutManager.findViewByPosition(rowIndex);
-        }
-
-        if (child == null) {
-            // If the layout manager returns null for any reason, which can happen before layout
-            // has occurred for the position, then look at the child of this view as a ViewGroup.
-            child = getChildAt(0);
-            rowIndex = getChildPosition(child);
-        }
-
-        for (int i = 0; i < getChildCount(); i++) {
-            View view = getChildAt(i);
-            if (view instanceof TableLayout) {
-                // This assumes there is ever only one content shown in this recycler view.
-                mLastVisibleWidgetContentTableHeight = view.getMeasuredHeight();
-            } else if (view instanceof WidgetsListHeader
-                    && mWidgetHeaderHeight == 0
-                    && view.getMeasuredHeight() > 0) {
-                // This assumes all header views are of the same height.
-                mWidgetHeaderHeight = view.getMeasuredHeight();
-            } else if (view instanceof EmptySpaceView && view.getMeasuredHeight() > 0) {
-                mWidgetEmptySpaceHeight = view.getMeasuredHeight();
-            }
-        }
-
-        int scrollPosition = getItemsHeight(rowIndex);
-        int offset = getLayoutManager().getDecoratedTop(child);
-
-        return getPaddingTop() + scrollPosition - offset;
-    }
-
-    /**
-     * Returns the available scroll height, in pixel.
-     *
-     * <p>If the recycler view can't be scrolled, returns 0.
-     */
-    @Override
-    protected int getAvailableScrollHeight() {
-        // AvailableScrollHeight = Total height of the all items - first page height
-        int firstPageHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
-        int totalHeightOfAllItems = getItemsHeight(/* untilIndex= */ mAdapter.getItemCount());
-        int availableScrollHeight = totalHeightOfAllItems - firstPageHeight;
-        return Math.max(0, availableScrollHeight);
-    }
-
     private boolean isModelNotReady() {
         return mAdapter.getItemCount() == 0;
     }
@@ -246,28 +180,27 @@
      * <p>If the untilIndex is larger than the total number of items in this adapter, returns the
      * sum of all items' height.
      */
-    private int getItemsHeight(int untilIndex) {
+    @Override
+    protected int getItemsHeight(int untilIndex) {
+        // Initialize cache
+        int childCount = getChildCount();
+        int startPosition;
+        if (childCount > 0
+                && ((startPosition = getChildAdapterPosition(getChildAt(0))) != NO_POSITION)) {
+            for (int i = 0; i < childCount; i++) {
+                mCachedSizes.put(
+                        mAdapter.getItemViewType(startPosition + i),
+                        getChildAt(i).getMeasuredHeight());
+            }
+        }
+
         if (untilIndex > mAdapter.getItems().size()) {
             untilIndex = mAdapter.getItems().size();
         }
         int totalItemsHeight = 0;
         for (int i = 0; i < untilIndex; i++) {
-            WidgetsListBaseEntry entry = mAdapter.getItems().get(i);
-            if (entry instanceof WidgetsListHeaderEntry
-                    || entry instanceof WidgetsListSearchHeaderEntry) {
-                totalItemsHeight += mWidgetHeaderHeight;
-                if (i > 0) {
-                    // Each header contains the spacing between entries as top decoration, except
-                    // the first one.
-                    totalItemsHeight += mSpacingBetweenEntries;
-                }
-            } else if (entry instanceof WidgetsListContentEntry) {
-                totalItemsHeight += mLastVisibleWidgetContentTableHeight;
-            } else if (entry instanceof WidgetListSpaceEntry) {
-                totalItemsHeight += mWidgetEmptySpaceHeight;
-            } else {
-                throw new UnsupportedOperationException("Can't estimate height for " + entry);
-            }
+            int type = mAdapter.getItemViewType(i);
+            totalItemsHeight += mCachedSizes.get(type) + mSpacingDecoration.getSpacing(i, type);
         }
         return totalItemsHeight;
     }
@@ -283,4 +216,31 @@
          */
         int getHeaderViewHeight();
     }
+
+    private static class SpacingDecoration extends RecyclerView.ItemDecoration {
+
+        private final int mSpacingBetweenEntries;
+
+        SpacingDecoration(@NonNull Context context) {
+            mSpacingBetweenEntries =
+                    context.getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
+        }
+
+        @Override
+        public void getItemOffsets(
+                @NonNull Rect outRect,
+                @NonNull View view,
+                @NonNull RecyclerView parent,
+                @NonNull RecyclerView.State state) {
+            super.getItemOffsets(outRect, view, parent, state);
+            int position = parent.getChildAdapterPosition(view);
+            outRect.top += getSpacing(position, parent.getAdapter().getItemViewType(position));
+        }
+
+        public int getSpacing(int position, int type) {
+            boolean isHeader = type == VIEW_TYPE_WIDGETS_SEARCH_HEADER
+                    || type == VIEW_TYPE_WIDGETS_HEADER;
+            return position > 0 && isHeader ? mSpacingBetweenEntries : 0;
+        }
+    }
 }