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