1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.util;
17 
18 import android.content.Context;
19 import android.util.SparseIntArray;
20 import android.view.View;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Px;
24 import androidx.recyclerview.widget.GridLayoutManager;
25 import androidx.recyclerview.widget.RecyclerView;
26 import androidx.recyclerview.widget.RecyclerView.Adapter;
27 import androidx.recyclerview.widget.RecyclerView.State;
28 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
29 
30 /**
31  * Extension of {@link GridLayoutManager} with support for smooth scrolling
32  */
33 public class ScrollableLayoutManager extends GridLayoutManager {
34 
35     public static final float PREDICTIVE_BACK_MIN_SCALE = 0.9f;
36     public static final float EXTRA_BOTTOM_SPACE_BY_HEIGHT_PERCENT =
37             (1 - PREDICTIVE_BACK_MIN_SCALE) / 2;
38 
39     // keyed on item type
40     protected final SparseIntArray mCachedSizes = new SparseIntArray();
41 
42     private RecyclerView mRv;
43 
44     /**
45      * Precalculated total height keyed on the item position. This is always incremental.
46      * Subclass can override {@link #incrementTotalHeight} to incorporate the layout logic.
47      * For example all-apps should have same values for items in same row,
48      *     sample values: 0, 10, 10, 10, 10, 20, 20, 20, 20
49      * whereas widgets will have strictly increasing values
50      *     sample values: 0, 10, 50, 60, 110
51      */
52     private int[] mTotalHeightCache = new int[1];
53     private int mLastValidHeightIndex = 0;
54 
ScrollableLayoutManager(Context context)55     public ScrollableLayoutManager(Context context) {
56         super(context, 1, GridLayoutManager.VERTICAL, false);
57     }
58 
59     @Override
onAttachedToWindow(RecyclerView view)60     public void onAttachedToWindow(RecyclerView view) {
61         super.onAttachedToWindow(view);
62         mRv = view;
63     }
64 
65     @Override
layoutDecorated(@onNull View child, int left, int top, int right, int bottom)66     public void layoutDecorated(@NonNull View child, int left, int top, int right, int bottom) {
67         super.layoutDecorated(child, left, top, right, bottom);
68         updateCachedSize(child);
69     }
70 
71     @Override
layoutDecoratedWithMargins(@onNull View child, int left, int top, int right, int bottom)72     public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
73             int bottom) {
74         super.layoutDecoratedWithMargins(child, left, top, right, bottom);
75         updateCachedSize(child);
76     }
77 
updateCachedSize(@onNull View child)78     private void updateCachedSize(@NonNull View child) {
79         int viewType = mRv.getChildViewHolder(child).getItemViewType();
80         int size = child.getMeasuredHeight();
81         if (mCachedSizes.get(viewType, -1) != size) {
82             invalidateScrollCache();
83         }
84         mCachedSizes.put(viewType, size);
85     }
86 
87     @Override
computeVerticalScrollExtent(State state)88     public int computeVerticalScrollExtent(State state) {
89         return mRv == null ? 0 : mRv.getHeight();
90     }
91 
92     @Override
computeVerticalScrollOffset(State state)93     public int computeVerticalScrollOffset(State state) {
94         Adapter adapter = mRv == null ? null : mRv.getAdapter();
95         if (adapter == null) {
96             return 0;
97         }
98         if (adapter.getItemCount() == 0 || getChildCount() == 0) {
99             return 0;
100         }
101         View child = getChildAt(0);
102         ViewHolder holder = mRv.findContainingViewHolder(child);
103         if (holder == null) {
104             return 0;
105         }
106         int itemPosition = holder.getLayoutPosition();
107         if (itemPosition < 0) {
108             return 0;
109         }
110         return getPaddingTop() + getItemsHeight(adapter, itemPosition) - getDecoratedTop(child);
111     }
112 
113     @Override
computeVerticalScrollRange(State state)114     public int computeVerticalScrollRange(State state) {
115         Adapter adapter = mRv == null ? null : mRv.getAdapter();
116         return adapter == null ? 0 : getItemsHeight(adapter, adapter.getItemCount());
117     }
118 
119     @Override
calculateExtraLayoutSpace(RecyclerView.State state, int[] extraLayoutSpace)120     protected void calculateExtraLayoutSpace(RecyclerView.State state, int[] extraLayoutSpace) {
121         super.calculateExtraLayoutSpace(state, extraLayoutSpace);
122         @Px int extraSpacePx = (int) (getHeight() * EXTRA_BOTTOM_SPACE_BY_HEIGHT_PERCENT);
123         extraLayoutSpace[1] = Math.max(extraLayoutSpace[1], extraSpacePx);
124     }
125 
126     /**
127      * Returns the sum of the height, in pixels, of this list adapter's items from index
128      * 0 (inclusive) until {@code untilIndex} (exclusive). If untilIndex is same as the itemCount,
129      * it returns the full height of all the items.
130      *
131      * <p>If the untilIndex is larger than the total number of items in this adapter, returns the
132      * sum of all items' height.
133      */
getItemsHeight(Adapter adapter, int untilIndex)134     private int getItemsHeight(Adapter adapter, int untilIndex) {
135         final int totalItems = adapter.getItemCount();
136         if (mTotalHeightCache.length < (totalItems + 1)) {
137             mTotalHeightCache = new int[totalItems + 1];
138             mLastValidHeightIndex = 0;
139         }
140         if (untilIndex > totalItems) {
141             untilIndex = totalItems;
142         } else if (untilIndex < 0) {
143             untilIndex = 0;
144         }
145         if (untilIndex <= mLastValidHeightIndex) {
146             return mTotalHeightCache[untilIndex];
147         }
148 
149         int totalItemsHeight = mTotalHeightCache[mLastValidHeightIndex];
150         for (int i = mLastValidHeightIndex; i < untilIndex; i++) {
151             totalItemsHeight = incrementTotalHeight(adapter, i, totalItemsHeight);
152             mTotalHeightCache[i + 1] = totalItemsHeight;
153         }
154         mLastValidHeightIndex = untilIndex;
155         return totalItemsHeight;
156     }
157 
158     /**
159      * The current implementation assumes a linear list with every item taking up the whole row.
160      * Subclasses should override this method to account for any spanning logic
161      */
incrementTotalHeight(Adapter adapter, int position, int heightUntilLastPos)162     protected int incrementTotalHeight(Adapter adapter, int position, int heightUntilLastPos) {
163         return heightUntilLastPos + mCachedSizes.get(adapter.getItemViewType(position));
164     }
165 
invalidateScrollCache()166     private void invalidateScrollCache() {
167         mLastValidHeightIndex = 0;
168     }
169 
170     @Override
onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)171     public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
172         super.onItemsAdded(recyclerView, positionStart, itemCount);
173         invalidateScrollCache();
174     }
175 
176     @Override
onItemsChanged(RecyclerView recyclerView)177     public void onItemsChanged(RecyclerView recyclerView) {
178         super.onItemsChanged(recyclerView);
179         invalidateScrollCache();
180     }
181 
182     @Override
onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)183     public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
184         super.onItemsRemoved(recyclerView, positionStart, itemCount);
185         invalidateScrollCache();
186     }
187 
188     @Override
onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)189     public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
190         super.onItemsMoved(recyclerView, from, to, itemCount);
191         invalidateScrollCache();
192     }
193 
194     @Override
onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, Object payload)195     public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
196             Object payload) {
197         super.onItemsUpdated(recyclerView, positionStart, itemCount, payload);
198         invalidateScrollCache();
199     }
200 }
201