1 /* 2 * Copyright (C) 2020 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 17 package com.android.car.ui.recyclerview; 18 19 import android.util.Log; 20 import android.view.ViewGroup; 21 22 import androidx.annotation.NonNull; 23 import androidx.annotation.StringRes; 24 import androidx.recyclerview.widget.RecyclerView; 25 26 /** 27 * A {@link RecyclerView.Adapter} that can limit its content based on a given length limit which 28 * can change at run-time. 29 * 30 * @param <T> type of the {@link RecyclerView.ViewHolder} objects used by base classes. 31 */ 32 public abstract class ContentLimitingAdapter<T extends RecyclerView.ViewHolder> 33 extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements ContentLimiting { 34 private static final String TAG = "ContentLimitingAdapter"; 35 36 private static final int SCROLLING_LIMITED_MESSAGE_VIEW_TYPE = Integer.MAX_VALUE; 37 38 private Integer mScrollingLimitedMessageResId; 39 private RangeFilter mRangeFilter = new PassThroughFilter(); 40 private RecyclerView mRecyclerView; 41 private boolean mIsLimiting = false; 42 43 /** 44 * Returns the viewType value to use for the scrolling limited message views. 45 * 46 * Override this method to provide your own alternative value if {@link Integer#MAX_VALUE} is 47 * a viewType value already in-use by your adapter. 48 */ getScrollingLimitedMessageViewType()49 public int getScrollingLimitedMessageViewType() { 50 return SCROLLING_LIMITED_MESSAGE_VIEW_TYPE; 51 } 52 53 @Override 54 @NonNull onCreateViewHolder( @onNull ViewGroup parent, int viewType)55 public final RecyclerView.ViewHolder onCreateViewHolder( 56 @NonNull ViewGroup parent, int viewType) { 57 if (viewType == getScrollingLimitedMessageViewType()) { 58 return ScrollingLimitedViewHolder.create(parent); 59 } 60 61 return onCreateViewHolderImpl(parent, viewType); 62 } 63 64 /** See {@link RangeFilter#indexToPosition}. */ indexToPosition(int index)65 protected int indexToPosition(int index) { 66 return mRangeFilter.indexToPosition(index); 67 } 68 69 /** See {@link RangeFilter#positionToIndex}. */ positionToIndex(int position)70 protected int positionToIndex(int position) { 71 return mRangeFilter.positionToIndex(position); 72 } 73 74 /** 75 * Returns a {@link androidx.recyclerview.widget.RecyclerView.ViewHolder} of type {@code T}. 76 * 77 * <p>It is delegated to by {@link #onCreateViewHolder(ViewGroup, int)} to handle any 78 * {@code viewType}s other than the one corresponding to the "scrolling is limited" message. 79 */ onCreateViewHolderImpl( @onNull ViewGroup parent, int viewType)80 protected abstract T onCreateViewHolderImpl( 81 @NonNull ViewGroup parent, int viewType); 82 83 @Override 84 @SuppressWarnings("unchecked") onBindViewHolder(@onNull RecyclerView.ViewHolder holder, int position)85 public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 86 if (holder instanceof ScrollingLimitedViewHolder) { 87 ScrollingLimitedViewHolder vh = (ScrollingLimitedViewHolder) holder; 88 vh.bind(mScrollingLimitedMessageResId); 89 } else { 90 int index = mRangeFilter.positionToIndex(position); 91 if (index != RangeFilterImpl.INVALID_INDEX) { 92 int size = getUnrestrictedItemCount(); 93 if (0 <= index && index < size) { 94 onBindViewHolderImpl((T) holder, index); 95 } else { 96 Log.e(TAG, "onBindViewHolder pos: " + position + " gave index: " 97 + index + " out of bounds size: " + size 98 + " " + mRangeFilter.toString()); 99 } 100 } else { 101 Log.e(TAG, "onBindViewHolder invalid position " + position 102 + " " + mRangeFilter.toString()); 103 } 104 } 105 } 106 107 /** 108 * Binds {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type {@code T}. 109 * 110 * <p>It is delegated to by {@link #onBindViewHolder(RecyclerView.ViewHolder, int)} to handle 111 * holders that are not of type {@link ScrollingLimitedViewHolder}. 112 */ onBindViewHolderImpl(T holder, int position)113 protected abstract void onBindViewHolderImpl(T holder, int position); 114 115 @Override getItemViewType(int position)116 public final int getItemViewType(int position) { 117 if (mRangeFilter.positionToIndex(position) == RangeFilterImpl.INVALID_INDEX) { 118 return getScrollingLimitedMessageViewType(); 119 } else { 120 return getItemViewTypeImpl(mRangeFilter.positionToIndex(position)); 121 } 122 } 123 124 /** 125 * Returns the view type of the item at {@code position}. 126 * 127 * <p>Defaults to the implementation in {@link RecyclerView.Adapter#getItemViewType(int)}. 128 * 129 * <p>It is delegated to by {@link #getItemViewType(int)} for all positions other than the 130 * {@link #getScrollingLimitedMessagePosition()}. 131 */ getItemViewTypeImpl(int position)132 protected int getItemViewTypeImpl(int position) { 133 return super.getItemViewType(position); 134 } 135 136 /** 137 * Returns the position where the "scrolling is limited" message should be placed. 138 * 139 * <p>The default implementation is to put this item at the very end of the limited list. 140 * Subclasses can override to choose a different position to suit their needs. 141 * 142 * @deprecated limiting message offset is not supported any more. 143 */ 144 @Deprecated getScrollingLimitedMessagePosition()145 protected int getScrollingLimitedMessagePosition() { 146 return getItemCount() - 1; 147 } 148 149 @Override getItemCount()150 public final int getItemCount() { 151 if (mIsLimiting) { 152 return mRangeFilter.getFilteredCount(); 153 } else { 154 return getUnrestrictedItemCount(); 155 } 156 } 157 158 /** 159 * Returns the number of items in the unrestricted list being displayed via this adapter. 160 */ getUnrestrictedItemCount()161 protected abstract int getUnrestrictedItemCount(); 162 163 @Override 164 @SuppressWarnings("unchecked") onViewRecycled(@onNull RecyclerView.ViewHolder holder)165 public final void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { 166 super.onViewRecycled(holder); 167 168 if (!(holder instanceof ScrollingLimitedViewHolder)) { 169 onViewRecycledImpl((T) holder); 170 } 171 } 172 173 /** 174 * Recycles {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type {@code T}. 175 * 176 * <p>It is delegated to by {@link #onViewRecycled(RecyclerView.ViewHolder)} to handle 177 * holders that are not of type {@link ScrollingLimitedViewHolder}. 178 */ 179 @SuppressWarnings("unused") onViewRecycledImpl(@onNull T holder)180 protected void onViewRecycledImpl(@NonNull T holder) { 181 } 182 183 @Override 184 @SuppressWarnings("unchecked") onFailedToRecycleView(@onNull RecyclerView.ViewHolder holder)185 public final boolean onFailedToRecycleView(@NonNull RecyclerView.ViewHolder holder) { 186 if (!(holder instanceof ScrollingLimitedViewHolder)) { 187 return onFailedToRecycleViewImpl((T) holder); 188 } 189 return super.onFailedToRecycleView(holder); 190 } 191 192 /** 193 * Handles failed recycle attempts for 194 * {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type {@code T}. 195 * 196 * <p>It is delegated to by {@link #onFailedToRecycleView(RecyclerView.ViewHolder)} for holders 197 * that are not of type {@link ScrollingLimitedViewHolder}. 198 */ onFailedToRecycleViewImpl(@onNull T holder)199 protected boolean onFailedToRecycleViewImpl(@NonNull T holder) { 200 return super.onFailedToRecycleView(holder); 201 } 202 203 @Override 204 @SuppressWarnings("unchecked") onViewAttachedToWindow(@onNull RecyclerView.ViewHolder holder)205 public final void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) { 206 super.onViewAttachedToWindow(holder); 207 if (!(holder instanceof ScrollingLimitedViewHolder)) { 208 onViewAttachedToWindowImpl((T) holder); 209 } 210 } 211 212 /** 213 * Handles attaching {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type 214 * {@code T} to the application window. 215 * 216 * <p>It is delegated to by {@link #onViewAttachedToWindow(RecyclerView.ViewHolder)} for 217 * holders that are not of type {@link ScrollingLimitedViewHolder}. 218 */ 219 @SuppressWarnings("unused") onViewAttachedToWindowImpl(@onNull T holder)220 protected void onViewAttachedToWindowImpl(@NonNull T holder) { 221 } 222 223 @Override onAttachedToRecyclerView(@onNull RecyclerView recyclerView)224 public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { 225 mRecyclerView = recyclerView; 226 } 227 228 @Override 229 @SuppressWarnings("unchecked") onViewDetachedFromWindow(@onNull RecyclerView.ViewHolder holder)230 public final void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) { 231 super.onViewDetachedFromWindow(holder); 232 if (!(holder instanceof ScrollingLimitedViewHolder)) { 233 onViewDetachedFromWindowImpl((T) holder); 234 } 235 } 236 237 /** 238 * Handles detaching {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}s of type 239 * {@code T} from the application window. 240 * 241 * <p>It is delegated to by {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder)} for 242 * holders that are not of type {@link ScrollingLimitedViewHolder}. 243 */ 244 @SuppressWarnings("unused") onViewDetachedFromWindowImpl(@onNull T holder)245 protected void onViewDetachedFromWindowImpl(@NonNull T holder) { 246 } 247 248 @Override setMaxItems(int maxItems)249 public void setMaxItems(int maxItems) { 250 if (maxItems >= 0) { 251 if (mRangeFilter != null && mIsLimiting) { 252 Log.w(TAG, "A new filter range received before parked"); 253 // remove the original filter first. 254 mRangeFilter.removeFilter(); 255 } 256 mIsLimiting = true; 257 mRangeFilter = new RangeFilterImpl(this, maxItems); 258 mRangeFilter.recompute(getUnrestrictedItemCount(), computeAnchorIndexWhenRestricting()); 259 mRangeFilter.applyFilter(); 260 autoScrollWhenRestricted(); 261 } else { 262 mRangeFilter.removeFilter(); 263 264 mIsLimiting = false; 265 mRangeFilter = new PassThroughFilter(); 266 mRangeFilter.recompute(getUnrestrictedItemCount(), 0); 267 } 268 } 269 270 /** 271 * Returns the position in the truncated list to scroll to when the list is limited. 272 * 273 * Returns -1 to disable the scrolling. 274 */ getScrollToPositionWhenRestricted()275 protected int getScrollToPositionWhenRestricted() { 276 return -1; 277 } 278 autoScrollWhenRestricted()279 private void autoScrollWhenRestricted() { 280 int scrollToPosition = getScrollToPositionWhenRestricted(); 281 if (scrollToPosition >= 0 && mRecyclerView != null) { 282 RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); 283 if (layoutManager != null) { 284 layoutManager.scrollToPosition(scrollToPosition); 285 } 286 } 287 } 288 289 /** 290 * Computes the anchor point index in the original list when limiting starts. 291 * Returns position 0 by default. 292 * 293 * Override this function to return a different anchor point to control the position of the 294 * limiting window. 295 */ computeAnchorIndexWhenRestricting()296 protected int computeAnchorIndexWhenRestricting() { 297 return 0; 298 } 299 300 /** 301 * Updates the changes from underlying data along with a new anchor. 302 */ updateUnderlyingDataChanged(int unrestrictedCount, int newAnchorIndex)303 public void updateUnderlyingDataChanged(int unrestrictedCount, int newAnchorIndex) { 304 mRangeFilter.recompute(unrestrictedCount, newAnchorIndex); 305 } 306 307 /** 308 * Changes the index where the limiting range surrounds. Items that are added and removed will 309 * be notified. 310 */ notifyLimitingAnchorChanged(int newPivotIndex)311 public void notifyLimitingAnchorChanged(int newPivotIndex) { 312 mRangeFilter.notifyPivotIndexChanged(newPivotIndex); 313 } 314 315 @Override setScrollingLimitedMessageResId(@tringRes int resId)316 public void setScrollingLimitedMessageResId(@StringRes int resId) { 317 if (mScrollingLimitedMessageResId == null || mScrollingLimitedMessageResId != resId) { 318 mScrollingLimitedMessageResId = resId; 319 mRangeFilter.invalidateMessagePositions(); 320 } 321 } 322 } 323