• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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