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 }