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
17package com.android.deskclock;
18
19import android.os.Bundle;
20import android.support.annotation.NonNull;
21import android.support.v7.widget.RecyclerView;
22import android.util.SparseArray;
23import android.view.View;
24import android.view.ViewGroup;
25
26import java.util.ArrayList;
27import java.util.List;
28
29import static android.support.v7.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 */
35public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
232    public void setOnItemChangedListener(OnItemChangedListener listener) {
233        mOnItemChangedListener = listener;
234    }
235
236    @Override
237    public int getItemCount() {
238        return mItemHolders == null ? 0 : mItemHolders.size();
239    }
240
241    @Override
242    public long getItemId(int position) {
243        return hasStableIds() ? mItemHolders.get(position).itemId : NO_ID;
244    }
245
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
256    public int getItemViewType(int position) {
257        return mItemHolders.get(position).getItemViewType();
258    }
259
260    @Override
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")
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
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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             */
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         */
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         */
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         */
542        void onItemClicked(ItemViewHolder<?> viewHolder, int id);
543    }
544}