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