From bbad97e273018661d803791d57b334ef4a4384f8 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 26 May 2022 07:12:43 -0700 Subject: [PATCH] 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 --- res/values/id.xml | 1 - .../launcher3/FastScrollRecyclerView.java | 59 ++++++- .../allapps/AllAppsRecyclerView.java | 32 +--- .../widget/picker/WidgetsListAdapter.java | 29 +--- .../widget/picker/WidgetsRecyclerView.java | 158 +++++++----------- 5 files changed, 117 insertions(+), 162 deletions(-) diff --git a/res/values/id.xml b/res/values/id.xml index af21b27caf..9fc0ff8277 100644 --- a/res/values/id.xml +++ b/res/values/id.xml @@ -16,7 +16,6 @@ --> - diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java index f117069144..94903f278e 100644 --- a/src/com/android/launcher3/FastScrollRecyclerView.java +++ b/src/com/android/launcher3/FastScrollRecyclerView.java @@ -24,6 +24,7 @@ import android.view.ViewGroup; 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 @@ public abstract class FastScrollRecyclerView extends RecyclerView { * 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 @@ public abstract class FastScrollRecyclerView extends RecyclerView { } /** - * Maps the touch (from 0..1) to the adapter position that should be visible. - *

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. + * + *

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 af17cf72e9..de34416504 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -31,7 +31,6 @@ import android.util.AttributeSet; 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 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { } @Override - public int getCurrentScrollY() { - // Return early if there are no items or we haven't been measured - List 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 items = mApps.getAdapterItems(); AllAppsGridAdapter.AdapterItem posItem = position < items.size() ? items.get(position) : null; @@ -400,17 +382,7 @@ public class AllAppsRecyclerView extends FastScrollRecyclerView { } 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 0e5a7d7ba9..e6b9dcaae1 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_FIRST 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.annotation.Nullable; 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 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC 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 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC @Nullable private Predicate 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 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC 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 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC 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 bdf646bfaf..daa67a973c 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 @@ public class WidgetsRecyclerView extends FastScrollRecyclerView implements OnIte 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 @@ public class WidgetsRecyclerView extends FastScrollRecyclerView implements OnIte 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 @@ public class WidgetsRecyclerView extends FastScrollRecyclerView implements OnIte 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. - * - *

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 @@ public class WidgetsRecyclerView extends FastScrollRecyclerView implements OnIte *

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 @@ public class WidgetsRecyclerView extends FastScrollRecyclerView implements OnIte */ 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; + } + } }