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