• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.deskclock;
18 
19 import android.os.Bundle;
20 import androidx.annotation.NonNull;
21 import androidx.recyclerview.widget.RecyclerView;
22 import android.util.SparseArray;
23 import android.view.View;
24 import android.view.ViewGroup;
25 
26 import java.util.ArrayList;
27 import java.util.List;
28 
29 import static androidx.recyclerview.widget.RecyclerView.NO_ID;
30 
31 /**
32  * Base adapter class for displaying a collection of items. Provides functionality for handling
33  * changing items, persistent item state, item click events, and re-usable item views.
34  */
35 public class ItemAdapter<T extends ItemAdapter.ItemHolder>
36         extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
37 
38     /**
39      * Finds the position of the changed item holder and invokes {@link #notifyItemChanged(int)} or
40      * {@link #notifyItemChanged(int, Object)} if payloads are present (in order to do in-place
41      * change animations).
42      */
43     private final OnItemChangedListener mItemChangedNotifier = new OnItemChangedListener() {
44         @Override
45         public void onItemChanged(ItemHolder<?> itemHolder) {
46             if (mOnItemChangedListener != null) {
47                 mOnItemChangedListener.onItemChanged(itemHolder);
48             }
49             final int position = mItemHolders.indexOf(itemHolder);
50             if (position != RecyclerView.NO_POSITION) {
51                 notifyItemChanged(position);
52             }
53         }
54 
55         @Override
56         public void onItemChanged(ItemHolder<?> itemHolder, Object payload) {
57             if (mOnItemChangedListener != null) {
58                 mOnItemChangedListener.onItemChanged(itemHolder, payload);
59             }
60             final int position = mItemHolders.indexOf(itemHolder);
61             if (position != RecyclerView.NO_POSITION) {
62                 notifyItemChanged(position, payload);
63             }
64         }
65     };
66 
67     /**
68      * Invokes the {@link OnItemClickedListener} in {@link #mListenersByViewType} corresponding
69      * to {@link ItemViewHolder#getItemViewType()}
70      */
71     private final OnItemClickedListener mOnItemClickedListener = new OnItemClickedListener() {
72         @Override
73         public void onItemClicked(ItemViewHolder<?> viewHolder, int id) {
74             final OnItemClickedListener listener =
75                     mListenersByViewType.get(viewHolder.getItemViewType());
76             if (listener != null) {
77                 listener.onItemClicked(viewHolder, id);
78             }
79         }
80     };
81 
82     /**
83      * Invoked when any item changes.
84      */
85     private OnItemChangedListener mOnItemChangedListener;
86 
87     /**
88      * Factories for creating new {@link ItemViewHolder} entities.
89      */
90     private final SparseArray<ItemViewHolder.Factory> mFactoriesByViewType = new SparseArray<>();
91 
92     /**
93      * Listeners to invoke in {@link #mOnItemClickedListener}.
94      */
95     private final SparseArray<OnItemClickedListener> mListenersByViewType = new SparseArray<>();
96 
97     /**
98      * List of current item holders represented by this adapter.
99      */
100     private List<T> mItemHolders;
101 
102     /**
103      * Convenience for calling {@link #setHasStableIds(boolean)} with {@code true}.
104      *
105      * @return this object, allowing calls to methods in this class to be chained
106      */
setHasStableIds()107     public ItemAdapter setHasStableIds() {
108         setHasStableIds(true);
109         return this;
110     }
111 
112     /**
113      * Sets the {@link ItemViewHolder.Factory} and {@link OnItemClickedListener} used to create
114      * new item view holders in {@link #onCreateViewHolder(ViewGroup, int)}.
115      *
116      * @param factory   the {@link ItemViewHolder.Factory} used to create new item view holders
117      * @param listener  the {@link OnItemClickedListener} to be invoked by
118      *                  {@link #mItemChangedNotifier}
119      * @param viewTypes the unique identifier for the view types to be created
120      * @return this object, allowing calls to methods in this class to be chained
121      */
withViewTypes(ItemViewHolder.Factory factory, OnItemClickedListener listener, int... viewTypes)122     public ItemAdapter withViewTypes(ItemViewHolder.Factory factory,
123             OnItemClickedListener listener, int... viewTypes) {
124         for (int viewType : viewTypes) {
125             mFactoriesByViewType.put(viewType, factory);
126             mListenersByViewType.put(viewType, listener);
127         }
128         return this;
129     }
130 
131     /**
132      * @return the current list of item holders represented by this adapter
133      */
getItems()134     public final List<T> getItems() {
135         return mItemHolders;
136     }
137 
138     /**
139      * Sets the list of item holders to serve as the dataset for this adapter and invokes
140      * {@link #notifyDataSetChanged()} to update the UI.
141      * <p/>
142      * If {@link #hasStableIds()} returns {@code true}, then the instance state will preserved
143      * between new and old holders that have matching {@link ItemHolder#itemId} values.
144      *
145      * @param itemHolders the new list of item holders
146      * @return this object, allowing calls to methods in this class to be chained
147      */
setItems(List<T> itemHolders)148     public ItemAdapter setItems(List<T> itemHolders) {
149         final List<T> oldItemHolders = mItemHolders;
150         if (oldItemHolders != itemHolders) {
151             if (oldItemHolders != null) {
152                 // remove the item change listener from the old item holders
153                 for (T oldItemHolder : oldItemHolders) {
154                     oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier);
155                 }
156             }
157 
158             if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
159                 // transfer instance state from old to new item holders based on item id,
160                 // we use a simple O(N^2) implementation since we assume the number of items is
161                 // relatively small and generating a temporary map would be more expensive
162                 final Bundle bundle = new Bundle();
163                 for (ItemHolder newItemHolder : itemHolders) {
164                     for (ItemHolder oldItemHolder : oldItemHolders) {
165                         if (newItemHolder.itemId == oldItemHolder.itemId
166                                 && newItemHolder != oldItemHolder) {
167                             // clear any existing state from the bundle
168                             bundle.clear();
169 
170                             // transfer instance state from old to new item holder
171                             oldItemHolder.onSaveInstanceState(bundle);
172                             newItemHolder.onRestoreInstanceState(bundle);
173 
174                             break;
175                         }
176                     }
177                 }
178             }
179 
180             if (itemHolders != null) {
181                 // add the item change listener to the new item holders
182                 for (ItemHolder newItemHolder : itemHolders) {
183                     newItemHolder.addOnItemChangedListener(mItemChangedNotifier);
184                 }
185             }
186 
187             // finally update the current list of item holders and inform the RV to update the UI
188             mItemHolders = itemHolders;
189             notifyDataSetChanged();
190         }
191 
192         return this;
193     }
194 
195     /**
196      * Inserts the specified item holder at the specified position. Invokes
197      * {@link #notifyItemInserted} to update the UI.
198      *
199      * @param position   the index to which to add the item holder
200      * @param itemHolder the item holder to add
201      * @return this object, allowing calls to methods in this class to be chained
202      */
addItem(int position, @NonNull T itemHolder)203     public ItemAdapter addItem(int position, @NonNull T itemHolder) {
204         itemHolder.addOnItemChangedListener(mItemChangedNotifier);
205         position = Math.min(position, mItemHolders.size());
206         mItemHolders.add(position, itemHolder);
207         notifyItemInserted(position);
208         return this;
209     }
210 
211     /**
212      * Removes the first occurrence of the specified element from this list, if it is present
213      * (optional operation). If this list does not contain the element, it is unchanged. Invokes
214      * {@link #notifyItemRemoved} to update the UI.
215      *
216      * @param itemHolder the item holder to remove
217      * @return this object, allowing calls to methods in this class to be chained
218      */
removeItem(@onNull T itemHolder)219     public ItemAdapter removeItem(@NonNull T itemHolder) {
220         final int index = mItemHolders.indexOf(itemHolder);
221         if (index >= 0) {
222             itemHolder = mItemHolders.remove(index);
223             itemHolder.removeOnItemChangedListener(mItemChangedNotifier);
224             notifyItemRemoved(index);
225         }
226         return this;
227     }
228 
229     /**
230      * Sets the listener to be invoked whenever any item changes.
231      */
setOnItemChangedListener(OnItemChangedListener listener)232     public void setOnItemChangedListener(OnItemChangedListener listener) {
233         mOnItemChangedListener = listener;
234     }
235 
236     @Override
getItemCount()237     public int getItemCount() {
238         return mItemHolders == null ? 0 : mItemHolders.size();
239     }
240 
241     @Override
getItemId(int position)242     public long getItemId(int position) {
243         return hasStableIds() ? mItemHolders.get(position).itemId : NO_ID;
244     }
245 
findItemById(long id)246     public T findItemById(long id) {
247         for (T holder : mItemHolders) {
248             if (holder.itemId == id) {
249                 return holder;
250             }
251         }
252         return null;
253     }
254 
255     @Override
getItemViewType(int position)256     public int getItemViewType(int position) {
257         return mItemHolders.get(position).getItemViewType();
258     }
259 
260     @Override
onCreateViewHolder(ViewGroup parent, int viewType)261     public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
262         final ItemViewHolder.Factory factory = mFactoriesByViewType.get(viewType);
263         if (factory != null) {
264             return factory.createViewHolder(parent, viewType);
265         }
266         throw new IllegalArgumentException("Unsupported view type: " + viewType);
267     }
268 
269     @Override
270     @SuppressWarnings("unchecked")
onBindViewHolder(ItemViewHolder viewHolder, int position)271     public void onBindViewHolder(ItemViewHolder viewHolder, int position) {
272         // suppress any unchecked warnings since it is up to the subclass to guarantee
273         // compatibility of their view holders with the item holder at the corresponding position
274         viewHolder.bindItemView(mItemHolders.get(position));
275         viewHolder.setOnItemClickedListener(mOnItemClickedListener);
276     }
277 
278     @Override
onViewRecycled(ItemViewHolder viewHolder)279     public void onViewRecycled(ItemViewHolder viewHolder) {
280         viewHolder.setOnItemClickedListener(null);
281         viewHolder.recycleItemView();
282     }
283 
284     /**
285      * Base class for wrapping an item for compatibility with an {@link ItemHolder}.
286      * <p/>
287      * An {@link ItemHolder} serves as bridge between the model and view layer; subclassers should
288      * implement properties that fall beyond the scope of their model layer but are necessary for
289      * the view layer. Properties that should be persisted across dataset changes can be
290      * preserved via the {@link #onSaveInstanceState(Bundle)} and
291      * {@link #onRestoreInstanceState(Bundle)} methods.
292      * <p/>
293      * Note: An {@link ItemHolder} can be used by multiple {@link ItemHolder} and any state changes
294      * should simultaneously be reflected in both UIs.  It is not thread-safe however and should
295      * only be used on a single thread at a given time.
296      *
297      * @param <T> the item type wrapped by the holder
298      */
299     public static abstract class ItemHolder<T> {
300 
301         /**
302          * The item held by this holder.
303          */
304         public final T item;
305 
306         /**
307          * Globally unique id corresponding to the item.
308          */
309         public final long itemId;
310 
311         /**
312          * Listeners to be invoked by {@link #notifyItemChanged()}.
313          */
314         private final List<OnItemChangedListener> mOnItemChangedListeners = new ArrayList<>();
315 
316         /**
317          * Designated constructor.
318          *
319          * @param item   the {@link T} item to be held by this holder
320          * @param itemId the globally unique id corresponding to the item
321          */
ItemHolder(T item, long itemId)322         public ItemHolder(T item, long itemId) {
323             this.item = item;
324             this.itemId = itemId;
325         }
326 
327         /**
328          * @return the unique identifier for the view that should be used to represent the item,
329          * e.g. the layout resource id.
330          */
getItemViewType()331         public abstract int getItemViewType();
332 
333         /**
334          * Adds the listener to the current list of registered listeners if it is not already
335          * registered.
336          *
337          * @param listener the listener to add
338          */
addOnItemChangedListener(OnItemChangedListener listener)339         public final void addOnItemChangedListener(OnItemChangedListener listener) {
340             if (!mOnItemChangedListeners.contains(listener)) {
341                 mOnItemChangedListeners.add(listener);
342             }
343         }
344 
345         /**
346          * Removes the listener from the current list of registered listeners.
347          *
348          * @param listener the listener to remove
349          */
removeOnItemChangedListener(OnItemChangedListener listener)350         public final void removeOnItemChangedListener(OnItemChangedListener listener) {
351             mOnItemChangedListeners.remove(listener);
352         }
353 
354         /**
355          * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder)} for all listeners added
356          * via {@link #addOnItemChangedListener(OnItemChangedListener)}.
357          */
notifyItemChanged()358         public final void notifyItemChanged() {
359             for (OnItemChangedListener listener : mOnItemChangedListeners) {
360                 listener.onItemChanged(this);
361             }
362         }
363 
364         /**
365          * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder, Object)} for all
366          * listeners added via {@link #addOnItemChangedListener(OnItemChangedListener)}.
367          */
notifyItemChanged(Object payload)368         public final void notifyItemChanged(Object payload) {
369             for (OnItemChangedListener listener : mOnItemChangedListeners) {
370                 listener.onItemChanged(this, payload);
371             }
372         }
373 
374         /**
375          * Called to retrieve per-instance state when the item may disappear or change so that
376          * state can be restored in {@link #onRestoreInstanceState(Bundle)}.
377          * <p/>
378          * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
379          * reused for other items in the {@link ItemHolder}.
380          *
381          * @param bundle the {@link Bundle} in which to place saved state
382          */
onSaveInstanceState(Bundle bundle)383         public void onSaveInstanceState(Bundle bundle) {
384             // for subclassers
385         }
386 
387         /**
388          * Called to restore any per-instance state which was previously saved in
389          * {@link #onSaveInstanceState(Bundle)} for an item with a matching {@link #itemId}.
390          * <p/>
391          * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
392          * reused for other items in the {@link ItemHolder}.
393          *
394          * @param bundle the {@link Bundle} in which to retrieve saved state
395          */
onRestoreInstanceState(Bundle bundle)396         public void onRestoreInstanceState(Bundle bundle) {
397             // for subclassers
398         }
399     }
400 
401     /**
402      * Base class for a reusable {@link RecyclerView.ViewHolder} compatible with an
403      * {@link ItemViewHolder}. Provides an interface for binding to an {@link ItemHolder} and later
404      * being recycled.
405      */
406     public static class ItemViewHolder<T extends ItemHolder> extends RecyclerView.ViewHolder {
407 
408         /**
409          * The current {@link ItemHolder} bound to this holder.
410          */
411         private T mItemHolder;
412 
413         /**
414          * The current {@link OnItemClickedListener} associated with this holder.
415          */
416         private OnItemClickedListener mOnItemClickedListener;
417 
418         /**
419          * Designated constructor.
420          *
421          * @param itemView the item {@link View} to associate with this holder
422          */
ItemViewHolder(View itemView)423         public ItemViewHolder(View itemView) {
424             super(itemView);
425         }
426 
427         /**
428          * @return the current {@link ItemHolder} bound to this holder, or {@code null} if unbound
429          */
getItemHolder()430         public final T getItemHolder() {
431             return mItemHolder;
432         }
433 
434         /**
435          * Binds the holder's {@link #itemView} to a particular item.
436          *
437          * @param itemHolder the {@link ItemHolder} to bind
438          */
bindItemView(T itemHolder)439         public final void bindItemView(T itemHolder) {
440             mItemHolder = itemHolder;
441             onBindItemView(itemHolder);
442         }
443 
444         /**
445          * Called when a new item is bound to the holder. Subclassers should override to bind any
446          * relevant data to their {@link #itemView} in this method.
447          *
448          * @param itemHolder the {@link ItemHolder} to bind
449          */
onBindItemView(T itemHolder)450         protected void onBindItemView(T itemHolder) {
451             // for subclassers
452         }
453 
454         /**
455          * Recycles the current item view, unbinding the current item holder and state.
456          */
recycleItemView()457         public final void recycleItemView() {
458             mItemHolder = null;
459             mOnItemClickedListener = null;
460 
461             onRecycleItemView();
462         }
463 
464         /**
465          * Called when the current item view is recycled. Subclassers should override to release
466          * any bound item state and prepare their {@link #itemView} for reuse.
467          */
onRecycleItemView()468         protected void onRecycleItemView() {
469             // for subclassers
470         }
471 
472         /**
473          * Sets the current {@link OnItemClickedListener} to be invoked via
474          * {@link #notifyItemClicked}.
475          *
476          * @param listener the new {@link OnItemClickedListener}, or {@code null} to clear
477          */
setOnItemClickedListener(OnItemClickedListener listener)478         public final void setOnItemClickedListener(OnItemClickedListener listener) {
479             mOnItemClickedListener = listener;
480         }
481 
482         /**
483          * Called by subclasses to invoke the current {@link OnItemClickedListener} for a
484          * particular click event so it can be handled at a higher level.
485          *
486          * @param id the unique identifier for the click action that has occurred
487          */
notifyItemClicked(int id)488         public final void notifyItemClicked(int id) {
489             if (mOnItemClickedListener != null) {
490                 mOnItemClickedListener.onItemClicked(this, id);
491             }
492         }
493 
494         /**
495          * Factory interface used by {@link ItemAdapter} for creating new {@link ItemViewHolder}.
496          */
497         public interface Factory {
498             /**
499              * Used by {@link ItemAdapter#createViewHolder(ViewGroup, int)} to make new
500              * {@link ItemViewHolder} for a given view type.
501              *
502              * @param parent   the {@code ViewGroup} that the {@link ItemViewHolder#itemView} will
503              *                 be attached
504              * @param viewType the unique id of the item view to create
505              * @return a new initialized {@link ItemViewHolder}
506              */
createViewHolder(ViewGroup parent, int viewType)507             public ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType);
508         }
509     }
510 
511     /**
512      * Callback interface for when an item changes and should be re-bound.
513      */
514     public interface OnItemChangedListener {
515         /**
516          * Invoked by {@link ItemHolder#notifyItemChanged()}.
517          *
518          * @param itemHolder the item holder that has changed
519          */
onItemChanged(ItemHolder<?> itemHolder)520         void onItemChanged(ItemHolder<?> itemHolder);
521 
522 
523         /**
524          * Invoked by {@link ItemHolder#notifyItemChanged(Object payload)}.
525          *
526          * @param itemHolder the item holder that has changed
527          * @param payload the payload object
528          */
onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload)529         void onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload);
530     }
531 
532     /**
533      * Callback interface for handling when an item is clicked.
534      */
535     public interface OnItemClickedListener {
536         /**
537          * Invoked by {@link ItemViewHolder#notifyItemClicked(int)}
538          *
539          * @param viewHolder the {@link ItemViewHolder} containing the view that was clicked
540          * @param id         the unique identifier for the click action that has occurred
541          */
onItemClicked(ItemViewHolder<?> viewHolder, int id)542         void onItemClicked(ItemViewHolder<?> viewHolder, int id);
543     }
544 }