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 }