• 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.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     var items: MutableList<T>? = null
89         private set
90 
91     /**
92      * Convenience for calling [.setHasStableIds] with `true`.
93      *
94      * @return this object, allowing calls to methods in this class to be chained
95      */
setHasStableIdsnull96     fun setHasStableIds(): ItemAdapter<T> {
97         setHasStableIds(true)
98         return this
99     }
100 
101     /**
102      * Sets the [ItemViewHolder.Factory] and [OnItemClickedListener] used to create
103      * new item view holders in [.onCreateViewHolder].
104      *
105      * @param factory the [ItemViewHolder.Factory] used to create new item view holders
106      * @param listener the [OnItemClickedListener] to be invoked by [.mItemChangedNotifier]
107      * @param viewTypes the unique identifier for the view types to be created
108      * @return this object, allowing calls to methods in this class to be chained
109      */
withViewTypesnull110     fun withViewTypes(
111         factory: ItemViewHolder.Factory,
112         listener: OnItemClickedListener?,
113         vararg viewTypes: Int
114     ): ItemAdapter<T> {
115         for (viewType in viewTypes) {
116             mFactoriesByViewType.put(viewType, factory)
117             mListenersByViewType.put(viewType, listener)
118         }
119         return this
120     }
121 
122     /**
123      * Sets the list of item holders to serve as the dataset for this adapter and invokes
124      * [.notifyDataSetChanged] to update the UI.
125      *
126      * If [.hasStableIds] returns `true`, then the instance state will preserved
127      * between new and old holders that have matching [itemId] values.
128      *
129      * @param itemHolders the new list of item holders
130      * @return this object, allowing calls to methods in this class to be chained
131      */
setItemsnull132     fun setItems(itemHolders: List<T>?): ItemAdapter<T> {
133         val oldItemHolders = items
134         if (oldItemHolders !== itemHolders) {
135             if (oldItemHolders != null) {
136                 // remove the item change listener from the old item holders
137                 for (oldItemHolder in oldItemHolders) {
138                     oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
139                 }
140             }
141 
142             if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
143                 // transfer instance state from old to new item holders based on item id,
144                 // we use a simple O(N^2) implementation since we assume the number of items is
145                 // relatively small and generating a temporary map would be more expensive
146                 val bundle = Bundle()
147                 for (newItemHolder in itemHolders) {
148                     for (oldItemHolder in oldItemHolders) {
149                         if (newItemHolder.itemId == oldItemHolder.itemId &&
150                                 newItemHolder !== oldItemHolder) {
151                             // clear any existing state from the bundle
152                             bundle.clear()
153 
154                             // transfer instance state from old to new item holder
155                             oldItemHolder.onSaveInstanceState(bundle)
156                             newItemHolder.onRestoreInstanceState(bundle)
157                             break
158                         }
159                     }
160                 }
161             }
162 
163             if (itemHolders != null) {
164                 // add the item change listener to the new item holders
165                 for (newItemHolder in itemHolders) {
166                     newItemHolder.addOnItemChangedListener(mItemChangedNotifier)
167                 }
168             }
169 
170             // finally update the current list of item holders and inform the RV to update the UI
171             items = itemHolders?.toMutableList()
172             notifyDataSetChanged()
173         }
174 
175         return this
176     }
177 
178     /**
179      * Inserts the specified item holder at the specified position. Invokes
180      * [.notifyItemInserted] to update the UI.
181      *
182      * @param position the index to which to add the item holder
183      * @param itemHolder the item holder to add
184      * @return this object, allowing calls to methods in this class to be chained
185      */
addItemnull186     fun addItem(position: Int, itemHolder: T): ItemAdapter<T> {
187         var variablePosition = position
188         itemHolder.addOnItemChangedListener(mItemChangedNotifier)
189         variablePosition = min(variablePosition, items!!.size)
190         items!!.add(variablePosition, itemHolder)
191         notifyItemInserted(variablePosition)
192         return this
193     }
194 
195     /**
196      * Removes the first occurrence of the specified element from this list, if it is present
197      * (optional operation). If this list does not contain the element, it is unchanged. Invokes
198      * [.notifyItemRemoved] to update the UI.
199      *
200      * @param itemHolder the item holder to remove
201      * @return this object, allowing calls to methods in this class to be chained
202      */
removeItemnull203     fun removeItem(itemHolder: T): ItemAdapter<T> {
204         var variableItemHolder = itemHolder
205         val index = items!!.indexOf(variableItemHolder)
206         if (index >= 0) {
207             variableItemHolder = items!!.removeAt(index)
208             variableItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
209             notifyItemRemoved(index)
210         }
211         return this
212     }
213 
214     /**
215      * Sets the listener to be invoked whenever any item changes.
216      */
setOnItemChangedListenernull217     fun setOnItemChangedListener(listener: OnItemChangedListener) {
218         mOnItemChangedListener = listener
219     }
220 
getItemCountnull221     override fun getItemCount(): Int = items?.size ?: 0
222 
223     override fun getItemId(position: Int): Long {
224         return if (hasStableIds()) items!![position].itemId else NO_ID
225     }
226 
findItemByIdnull227     fun findItemById(id: Long): T? {
228         for (holder in items!!) {
229             if (holder.itemId == id) {
230                 return holder
231             }
232         }
233         return null
234     }
235 
getItemViewTypenull236     override fun getItemViewType(position: Int): Int {
237         return items!![position].getItemViewType()
238     }
239 
onCreateViewHoldernull240     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<T> {
241         val factory = mFactoriesByViewType[viewType]
242         if (factory != null) {
243             return factory.createViewHolder(parent, viewType) as ItemViewHolder<T>
244         }
245         throw IllegalArgumentException("Unsupported view type: $viewType")
246     }
247 
onBindViewHoldernull248     override fun onBindViewHolder(viewHolder: ItemViewHolder<T>, position: Int) {
249         // suppress any unchecked warnings since it is up to the subclass to guarantee
250         // compatibility of their view holders with the item holder at the corresponding position
251         viewHolder.bindItemView(items!![position])
252         viewHolder.setOnItemClickedListener(mOnItemClickedListener)
253     }
254 
onViewRecyclednull255     override fun onViewRecycled(viewHolder: ItemViewHolder<T>) {
256         viewHolder.setOnItemClickedListener(null)
257         viewHolder.recycleItemView()
258     }
259 
260     /**
261      * Base class for wrapping an item for compatibility with an [ItemHolder].
262      *
263      * An [ItemHolder] serves as bridge between the model and view layer; subclassers should
264      * implement properties that fall beyond the scope of their model layer but are necessary for
265      * the view layer. Properties that should be persisted across dataset changes can be
266      * preserved via the [.onSaveInstanceState] and
267      * [.onRestoreInstanceState] methods.
268      *
269      * Note: An [ItemHolder] can be used by multiple [ItemHolder] and any state changes
270      * should simultaneously be reflected in both UIs.  It is not thread-safe however and should
271      * only be used on a single thread at a given time.
272      *
273      * @param <T> the item type wrapped by the holder
274     </T> */
275     abstract class ItemHolder<T>(
276         /** The item held by this holder. */
277         val item: T,
278         /** Globally unique id corresponding to the item. */
279         val itemId: Long
280     ) {
281         /** Listeners to be invoked by [.notifyItemChanged]. */
282         private val mOnItemChangedListeners: MutableList<OnItemChangedListener> = ArrayList()
283 
284         /**
285          * @return the unique identifier for the view that should be used to represent the item,
286          * e.g. the layout resource id.
287          */
getItemViewTypenull288         abstract fun getItemViewType(): Int
289 
290         /**
291          * Adds the listener to the current list of registered listeners if it is not already
292          * registered.
293          *
294          * @param listener the listener to add
295          */
296         fun addOnItemChangedListener(listener: OnItemChangedListener) {
297             if (!mOnItemChangedListeners.contains(listener)) {
298                 mOnItemChangedListeners.add(listener)
299             }
300         }
301 
302         /**
303          * Removes the listener from the current list of registered listeners.
304          *
305          * @param listener the listener to remove
306          */
removeOnItemChangedListenernull307         fun removeOnItemChangedListener(listener: OnItemChangedListener) {
308             mOnItemChangedListeners.remove(listener)
309         }
310 
311         /**
312          * Invokes [OnItemChangedListener.onItemChanged] for all listeners added
313          * via [.addOnItemChangedListener].
314          */
notifyItemChangednull315         fun notifyItemChanged() {
316             for (listener in mOnItemChangedListeners) {
317                 listener.onItemChanged(this)
318             }
319         }
320 
321         /**
322          * Invokes [OnItemChangedListener.onItemChanged] for all
323          * listeners added via [.addOnItemChangedListener].
324          */
notifyItemChangednull325         fun notifyItemChanged(payload: Any) {
326             for (listener in mOnItemChangedListeners) {
327                 listener.onItemChanged(this, payload)
328             }
329         }
330 
331         /**
332          * Called to retrieve per-instance state when the item may disappear or change so that
333          * state can be restored in [.onRestoreInstanceState].
334          *
335          * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
336          * reused for other items in the [ItemHolder].
337          *
338          * @param bundle the [Bundle] in which to place saved state
339          */
onSaveInstanceStatenull340         open fun onSaveInstanceState(bundle: Bundle) {
341             // for subclassers
342         }
343 
344         /**
345          * Called to restore any per-instance state which was previously saved in
346          * [.onSaveInstanceState] for an item with a matching [.itemId].
347          *
348          * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
349          * reused for other items in the [ItemHolder].
350          *
351          * @param bundle the [Bundle] in which to retrieve saved state
352          */
onRestoreInstanceStatenull353         open fun onRestoreInstanceState(bundle: Bundle) {
354             // for subclassers
355         }
356     }
357 
358     /**
359      * Base class for a reusable [RecyclerView.ViewHolder] compatible with an
360      * [ItemViewHolder]. Provides an interface for binding to an [ItemHolder] and later
361      * being recycled.
362      */
363     open class ItemViewHolder<T : ItemHolder<*>>(itemView: View)
364         : RecyclerView.ViewHolder(itemView) {
365         /**
366          * The current [ItemHolder] bound to this holder, or `null` if unbound.
367          */
368         var itemHolder: T? = null
369             private set
370 
371         /**
372          * The current [OnItemClickedListener] associated with this holder.
373          */
374         private var mOnItemClickedListener: OnItemClickedListener? = null
375 
376         /**
377          * Binds the holder's [.itemView] to a particular item.
378          *
379          * @param itemHolder the [ItemHolder] to bind
380          */
bindItemViewnull381         fun bindItemView(itemHolder: T) {
382             this.itemHolder = itemHolder
383             onBindItemView(itemHolder)
384         }
385 
386         /**
387          * Called when a new item is bound to the holder. Subclassers should override to bind any
388          * relevant data to their [.itemView] in this method.
389          *
390          * @param itemHolder the [ItemHolder] to bind
391          */
onBindItemViewnull392         protected open fun onBindItemView(itemHolder: T) {
393             // for subclassers
394         }
395 
396         /**
397          * Recycles the current item view, unbinding the current item holder and state.
398          */
recycleItemViewnull399         fun recycleItemView() {
400             itemHolder = null
401             mOnItemClickedListener = null
402 
403             onRecycleItemView()
404         }
405 
406         /**
407          * Called when the current item view is recycled. Subclassers should override to release
408          * any bound item state and prepare their [.itemView] for reuse.
409          */
onRecycleItemViewnull410         protected fun onRecycleItemView() {
411             // for subclassers
412         }
413 
414         /**
415          * Sets the current [OnItemClickedListener] to be invoked via
416          * [.notifyItemClicked].
417          *
418          * @param listener the new [OnItemClickedListener], or `null` to clear
419          */
setOnItemClickedListenernull420         fun setOnItemClickedListener(listener: OnItemClickedListener?) {
421             mOnItemClickedListener = listener
422         }
423 
424         /**
425          * Called by subclasses to invoke the current [OnItemClickedListener] for a
426          * particular click event so it can be handled at a higher level.
427          *
428          * @param id the unique identifier for the click action that has occurred
429          */
notifyItemClickednull430         fun notifyItemClicked(id: Int) {
431             mOnItemClickedListener?.onItemClicked(this, id)
432         }
433 
434         /**
435          * Factory interface used by [ItemAdapter] for creating new [ItemViewHolder].
436          */
437         interface Factory {
438             /**
439              * Used by [ItemAdapter.createViewHolder] to make new
440              * [ItemViewHolder] for a given view type.
441              *
442              * @param parent the `ViewGroup` that the [ItemViewHolder.itemView] will be attached
443              * @param viewType the unique id of the item view to create
444              * @return a new initialized [ItemViewHolder]
445              */
createViewHoldernull446             fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*>
447         }
448     }
449 
450     /**
451      * Callback interface for when an item changes and should be re-bound.
452      */
453     interface OnItemChangedListener {
454         /**
455          * Invoked by [ItemHolder.notifyItemChanged].
456          *
457          * @param itemHolder the item holder that has changed
458          */
459         fun onItemChanged(itemHolder: ItemHolder<*>)
460 
461         /**
462          * Invoked by [ItemHolder.notifyItemChanged].
463          *
464          * @param itemHolder the item holder that has changed
465          * @param payload the payload object
466          */
467         fun onItemChanged(itemHolder: ItemHolder<*>, payload: Any)
468     }
469 
470     /**
471      * Callback interface for handling when an item is clicked.
472      */
473     interface OnItemClickedListener {
474         /**
475          * Invoked by [ItemViewHolder.notifyItemClicked]
476          *
477          * @param viewHolder the [ItemViewHolder] containing the view that was clicked
478          * @param id the unique identifier for the click action that has occurred
479          */
onItemClickednull480         fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int)
481     }
482 }