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.deskclock
18 
19 import android.os.Bundle
20 import android.util.SparseArray
21 import android.view.View
22 import android.view.ViewGroup
23 import androidx.recyclerview.widget.RecyclerView
24 import androidx.recyclerview.widget.RecyclerView.NO_ID
25 
26 import com.android.deskclock.ItemAdapter.ItemHolder
27 import com.android.deskclock.ItemAdapter.ItemViewHolder
28 
29 import kotlin.math.min
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 class ItemAdapter<T : ItemHolder<*>> : RecyclerView.Adapter<ItemViewHolder<T>>() {
36     /**
37      * Finds the position of the changed item holder and invokes [.notifyItemChanged] or
38      * [.notifyItemChanged] if payloads are present (in order to do in-place
39      * change animations).
40      */
41     private val mItemChangedNotifier: OnItemChangedListener = object : OnItemChangedListener {
onItemChangednull42         override fun onItemChanged(itemHolder: ItemHolder<*>) {
43             mOnItemChangedListener?.onItemChanged(itemHolder)
44             val position = items!!.indexOf(itemHolder)
45             if (position != RecyclerView.NO_POSITION) {
46                 notifyItemChanged(position)
47             }
48         }
49 
onItemChangednull50         override fun onItemChanged(itemHolder: ItemHolder<*>, payload: Any) {
51             mOnItemChangedListener?.onItemChanged(itemHolder, payload)
52             val position = items!!.indexOf(itemHolder)
53             if (position != RecyclerView.NO_POSITION) {
54                 notifyItemChanged(position, payload)
55             }
56         }
57     }
58 
59     /**
60      * Invokes the [OnItemClickedListener] in [.mListenersByViewType] corresponding
61      * to [ItemViewHolder.getItemViewType]
62      */
63     private val mOnItemClickedListener: OnItemClickedListener = object : OnItemClickedListener {
onItemClickednull64         override fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int) {
65             val listener = mListenersByViewType[viewHolder.getItemViewType()]
66             listener?.onItemClicked(viewHolder, id)
67         }
68     }
69 
70     /**
71      * Invoked when any item changes.
72      */
73     private var mOnItemChangedListener: OnItemChangedListener? = null
74 
75     /**
76      * Factories for creating new [ItemViewHolder] entities.
77      */
78     private val mFactoriesByViewType: SparseArray<ItemViewHolder.Factory> = SparseArray()
79 
80     /**
81      * Listeners to invoke in [.mOnItemClickedListener].
82      */
83     private val mListenersByViewType: SparseArray<OnItemClickedListener?> = SparseArray()
84 
85     /**
86      * List of current item holders represented by this adapter.
87      */
88     @JvmField var items: MutableList<T>? = null
89 
90     /**
91      * Convenience for calling [.setHasStableIds] with `true`.
92      *
93      * @return this object, allowing calls to methods in this class to be chained
94      */
setHasStableIdsnull95     fun setHasStableIds(): ItemAdapter<T> {
96         setHasStableIds(true)
97         return this
98     }
99 
100     /**
101      * Sets the [ItemViewHolder.Factory] and [OnItemClickedListener] used to create
102      * new item view holders in [.onCreateViewHolder].
103      *
104      * @param factory the [ItemViewHolder.Factory] used to create new item view holders
105      * @param listener the [OnItemClickedListener] to be invoked by [.mItemChangedNotifier]
106      * @param viewTypes the unique identifier for the view types to be created
107      * @return this object, allowing calls to methods in this class to be chained
108      */
withViewTypesnull109     fun withViewTypes(
110         factory: ItemViewHolder.Factory,
111         listener: OnItemClickedListener?,
112         vararg viewTypes: Int
113     ): ItemAdapter<T> {
114         for (viewType in viewTypes) {
115             mFactoriesByViewType.put(viewType, factory)
116             mListenersByViewType.put(viewType, listener)
117         }
118         return this
119     }
120 
121     /**
122      * Sets the list of item holders to serve as the dataset for this adapter and invokes
123      * [.notifyDataSetChanged] to update the UI.
124      *
125      * If [.hasStableIds] returns `true`, then the instance state will preserved
126      * between new and old holders that have matching [itemId] values.
127      *
128      * @param itemHolders the new list of item holders
129      * @return this object, allowing calls to methods in this class to be chained
130      */
setItemsnull131     fun setItems(itemHolders: List<T>?): ItemAdapter<T> {
132         val oldItemHolders = items
133         if (oldItemHolders !== itemHolders) {
134             if (oldItemHolders != null) {
135                 // remove the item change listener from the old item holders
136                 for (oldItemHolder in oldItemHolders) {
137                     oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
138                 }
139             }
140 
141             if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
142                 // transfer instance state from old to new item holders based on item id,
143                 // we use a simple O(N^2) implementation since we assume the number of items is
144                 // relatively small and generating a temporary map would be more expensive
145                 val bundle = Bundle()
146                 for (newItemHolder in itemHolders) {
147                     for (oldItemHolder in oldItemHolders) {
148                         if (newItemHolder.itemId == oldItemHolder.itemId &&
149                                 newItemHolder !== oldItemHolder) {
150                             // clear any existing state from the bundle
151                             bundle.clear()
152 
153                             // transfer instance state from old to new item holder
154                             oldItemHolder.onSaveInstanceState(bundle)
155                             newItemHolder.onRestoreInstanceState(bundle)
156                             break
157                         }
158                     }
159                 }
160             }
161 
162             if (itemHolders != null) {
163                 // add the item change listener to the new item holders
164                 for (newItemHolder in itemHolders) {
165                     newItemHolder.addOnItemChangedListener(mItemChangedNotifier)
166                 }
167             }
168 
169             // finally update the current list of item holders and inform the RV to update the UI
170             items = itemHolders?.toMutableList()
171             notifyDataSetChanged()
172         }
173 
174         return this
175     }
176 
177     /**
178      * Inserts the specified item holder at the specified position. Invokes
179      * [.notifyItemInserted] to update the UI.
180      *
181      * @param position the index to which to add the item holder
182      * @param itemHolder the item holder to add
183      * @return this object, allowing calls to methods in this class to be chained
184      */
addItemnull185     fun addItem(position: Int, itemHolder: T): ItemAdapter<T> {
186         var variablePosition = position
187         itemHolder.addOnItemChangedListener(mItemChangedNotifier)
188         variablePosition = min(variablePosition, items!!.size)
189         items!!.add(variablePosition, itemHolder)
190         notifyItemInserted(variablePosition)
191         return this
192     }
193 
194     /**
195      * Removes the first occurrence of the specified element from this list, if it is present
196      * (optional operation). If this list does not contain the element, it is unchanged. Invokes
197      * [.notifyItemRemoved] to update the UI.
198      *
199      * @param itemHolder the item holder to remove
200      * @return this object, allowing calls to methods in this class to be chained
201      */
removeItemnull202     fun removeItem(itemHolder: T): ItemAdapter<T> {
203         var variableItemHolder = itemHolder
204         val index = items!!.indexOf(variableItemHolder)
205         if (index >= 0) {
206             variableItemHolder = items!!.removeAt(index)
207             variableItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
208             notifyItemRemoved(index)
209         }
210         return this
211     }
212 
213     /**
214      * Sets the listener to be invoked whenever any item changes.
215      */
setOnItemChangedListenernull216     fun setOnItemChangedListener(listener: OnItemChangedListener) {
217         mOnItemChangedListener = listener
218     }
219 
getItemCountnull220     override fun getItemCount(): Int = items?.size ?: 0
221 
222     override fun getItemId(position: Int): Long {
223         return if (hasStableIds()) items!![position].itemId else NO_ID
224     }
225 
findItemByIdnull226     fun findItemById(id: Long): T? {
227         for (holder in items!!) {
228             if (holder.itemId == id) {
229                 return holder
230             }
231         }
232         return null
233     }
234 
getItemViewTypenull235     override fun getItemViewType(position: Int): Int {
236         return items!![position].getItemViewType()
237     }
238 
onCreateViewHoldernull239     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<T> {
240         val factory = mFactoriesByViewType[viewType]
241         if (factory != null) {
242             return factory.createViewHolder(parent, viewType) as ItemViewHolder<T>
243         }
244         throw IllegalArgumentException("Unsupported view type: $viewType")
245     }
246 
onBindViewHoldernull247     override fun onBindViewHolder(viewHolder: ItemViewHolder<T>, position: Int) {
248         // suppress any unchecked warnings since it is up to the subclass to guarantee
249         // compatibility of their view holders with the item holder at the corresponding position
250         viewHolder.bindItemView(items!![position])
251         viewHolder.setOnItemClickedListener(mOnItemClickedListener)
252     }
253 
onViewRecyclednull254     override fun onViewRecycled(viewHolder: ItemViewHolder<T>) {
255         viewHolder.setOnItemClickedListener(null)
256         viewHolder.recycleItemView()
257     }
258 
259     /**
260      * Base class for wrapping an item for compatibility with an [ItemHolder].
261      *
262      * An [ItemHolder] serves as bridge between the model and view layer; subclassers should
263      * implement properties that fall beyond the scope of their model layer but are necessary for
264      * the view layer. Properties that should be persisted across dataset changes can be
265      * preserved via the [.onSaveInstanceState] and
266      * [.onRestoreInstanceState] methods.
267      *
268      * Note: An [ItemHolder] can be used by multiple [ItemHolder] and any state changes
269      * should simultaneously be reflected in both UIs.  It is not thread-safe however and should
270      * only be used on a single thread at a given time.
271      *
272      * @param <T> the item type wrapped by the holder
273     </T> */
274     abstract class ItemHolder<T>(
275         /** The item held by this holder. */
276         val item: T,
277         /** Globally unique id corresponding to the item. */
278         val itemId: Long
279     ) {
280         /** Listeners to be invoked by [.notifyItemChanged]. */
281         private val mOnItemChangedListeners: MutableList<OnItemChangedListener> = ArrayList()
282 
283         /**
284          * @return the unique identifier for the view that should be used to represent the item,
285          * e.g. the layout resource id.
286          */
getItemViewTypenull287         abstract fun getItemViewType(): Int
288 
289         /**
290          * Adds the listener to the current list of registered listeners if it is not already
291          * registered.
292          *
293          * @param listener the listener to add
294          */
295         fun addOnItemChangedListener(listener: OnItemChangedListener) {
296             if (!mOnItemChangedListeners.contains(listener)) {
297                 mOnItemChangedListeners.add(listener)
298             }
299         }
300 
301         /**
302          * Removes the listener from the current list of registered listeners.
303          *
304          * @param listener the listener to remove
305          */
removeOnItemChangedListenernull306         fun removeOnItemChangedListener(listener: OnItemChangedListener) {
307             mOnItemChangedListeners.remove(listener)
308         }
309 
310         /**
311          * Invokes [OnItemChangedListener.onItemChanged] for all listeners added
312          * via [.addOnItemChangedListener].
313          */
notifyItemChangednull314         fun notifyItemChanged() {
315             for (listener in mOnItemChangedListeners) {
316                 listener.onItemChanged(this)
317             }
318         }
319 
320         /**
321          * Invokes [OnItemChangedListener.onItemChanged] for all
322          * listeners added via [.addOnItemChangedListener].
323          */
notifyItemChangednull324         fun notifyItemChanged(payload: Any) {
325             for (listener in mOnItemChangedListeners) {
326                 listener.onItemChanged(this, payload)
327             }
328         }
329 
330         /**
331          * Called to retrieve per-instance state when the item may disappear or change so that
332          * state can be restored in [.onRestoreInstanceState].
333          *
334          * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
335          * reused for other items in the [ItemHolder].
336          *
337          * @param bundle the [Bundle] in which to place saved state
338          */
onSaveInstanceStatenull339         open fun onSaveInstanceState(bundle: Bundle) {
340             // for subclassers
341         }
342 
343         /**
344          * Called to restore any per-instance state which was previously saved in
345          * [.onSaveInstanceState] for an item with a matching [.itemId].
346          *
347          * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
348          * reused for other items in the [ItemHolder].
349          *
350          * @param bundle the [Bundle] in which to retrieve saved state
351          */
onRestoreInstanceStatenull352         open fun onRestoreInstanceState(bundle: Bundle) {
353             // for subclassers
354         }
355     }
356 
357     /**
358      * Base class for a reusable [RecyclerView.ViewHolder] compatible with an
359      * [ItemViewHolder]. Provides an interface for binding to an [ItemHolder] and later
360      * being recycled.
361      */
362     open class ItemViewHolder<T : ItemHolder<*>>(itemView: View)
363         : RecyclerView.ViewHolder(itemView) {
364         /**
365          * The current [ItemHolder] bound to this holder, or `null` if unbound.
366          */
367         var itemHolder: T? = null
368             private set
369 
370         /**
371          * The current [OnItemClickedListener] associated with this holder.
372          */
373         private var mOnItemClickedListener: OnItemClickedListener? = null
374 
375         /**
376          * Binds the holder's [.itemView] to a particular item.
377          *
378          * @param itemHolder the [ItemHolder] to bind
379          */
bindItemViewnull380         fun bindItemView(itemHolder: T) {
381             this.itemHolder = itemHolder
382             onBindItemView(itemHolder)
383         }
384 
385         /**
386          * Called when a new item is bound to the holder. Subclassers should override to bind any
387          * relevant data to their [.itemView] in this method.
388          *
389          * @param itemHolder the [ItemHolder] to bind
390          */
onBindItemViewnull391         protected open fun onBindItemView(itemHolder: T) {
392             // for subclassers
393         }
394 
395         /**
396          * Recycles the current item view, unbinding the current item holder and state.
397          */
recycleItemViewnull398         fun recycleItemView() {
399             itemHolder = null
400             mOnItemClickedListener = null
401 
402             onRecycleItemView()
403         }
404 
405         /**
406          * Called when the current item view is recycled. Subclassers should override to release
407          * any bound item state and prepare their [.itemView] for reuse.
408          */
onRecycleItemViewnull409         protected fun onRecycleItemView() {
410             // for subclassers
411         }
412 
413         /**
414          * Sets the current [OnItemClickedListener] to be invoked via
415          * [.notifyItemClicked].
416          *
417          * @param listener the new [OnItemClickedListener], or `null` to clear
418          */
setOnItemClickedListenernull419         fun setOnItemClickedListener(listener: OnItemClickedListener?) {
420             mOnItemClickedListener = listener
421         }
422 
423         /**
424          * Called by subclasses to invoke the current [OnItemClickedListener] for a
425          * particular click event so it can be handled at a higher level.
426          *
427          * @param id the unique identifier for the click action that has occurred
428          */
notifyItemClickednull429         fun notifyItemClicked(id: Int) {
430             mOnItemClickedListener?.onItemClicked(this, id)
431         }
432 
433         /**
434          * Factory interface used by [ItemAdapter] for creating new [ItemViewHolder].
435          */
436         interface Factory {
437             /**
438              * Used by [ItemAdapter.createViewHolder] to make new
439              * [ItemViewHolder] for a given view type.
440              *
441              * @param parent the `ViewGroup` that the [ItemViewHolder.itemView] will be attached
442              * @param viewType the unique id of the item view to create
443              * @return a new initialized [ItemViewHolder]
444              */
createViewHoldernull445             fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*>
446         }
447     }
448 
449     /**
450      * Callback interface for when an item changes and should be re-bound.
451      */
452     interface OnItemChangedListener {
453         /**
454          * Invoked by [ItemHolder.notifyItemChanged].
455          *
456          * @param itemHolder the item holder that has changed
457          */
458         fun onItemChanged(itemHolder: ItemHolder<*>)
459 
460         /**
461          * Invoked by [ItemHolder.notifyItemChanged].
462          *
463          * @param itemHolder the item holder that has changed
464          * @param payload the payload object
465          */
466         fun onItemChanged(itemHolder: ItemHolder<*>, payload: Any)
467     }
468 
469     /**
470      * Callback interface for handling when an item is clicked.
471      */
472     interface OnItemClickedListener {
473         /**
474          * Invoked by [ItemViewHolder.notifyItemClicked]
475          *
476          * @param viewHolder the [ItemViewHolder] containing the view that was clicked
477          * @param id the unique identifier for the click action that has occurred
478          */
onItemClickednull479         fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int)
480     }
481 }