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 17 package com.android.deskclock; 18 19 import android.os.Bundle; 20 import androidx.annotation.NonNull; 21 import androidx.recyclerview.widget.RecyclerView; 22 import android.util.SparseArray; 23 import android.view.View; 24 import android.view.ViewGroup; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 29 import static androidx.recyclerview.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 */ 35 public 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 */ setHasStableIds()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 */ withViewTypes(ItemViewHolder.Factory factory, OnItemClickedListener listener, int... viewTypes)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 */ getItems()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 */ setItems(List<T> itemHolders)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 */ addItem(int position, @NonNull T itemHolder)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 */ removeItem(@onNull T itemHolder)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 */ setOnItemChangedListener(OnItemChangedListener listener)232 public void setOnItemChangedListener(OnItemChangedListener listener) { 233 mOnItemChangedListener = listener; 234 } 235 236 @Override getItemCount()237 public int getItemCount() { 238 return mItemHolders == null ? 0 : mItemHolders.size(); 239 } 240 241 @Override getItemId(int position)242 public long getItemId(int position) { 243 return hasStableIds() ? mItemHolders.get(position).itemId : NO_ID; 244 } 245 findItemById(long id)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 getItemViewType(int position)256 public int getItemViewType(int position) { 257 return mItemHolders.get(position).getItemViewType(); 258 } 259 260 @Override onCreateViewHolder(ViewGroup parent, int viewType)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") onBindViewHolder(ItemViewHolder viewHolder, int position)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 onViewRecycled(ItemViewHolder viewHolder)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 */ ItemHolder(T item, long itemId)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 */ getItemViewType()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 */ addOnItemChangedListener(OnItemChangedListener listener)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 */ removeOnItemChangedListener(OnItemChangedListener listener)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 */ notifyItemChanged()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 */ notifyItemChanged(Object payload)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 */ onSaveInstanceState(Bundle bundle)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 */ onRestoreInstanceState(Bundle bundle)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 */ ItemViewHolder(View itemView)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 */ getItemHolder()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 */ bindItemView(T itemHolder)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 */ onBindItemView(T itemHolder)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 */ recycleItemView()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 */ onRecycleItemView()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 */ setOnItemClickedListener(OnItemClickedListener listener)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 */ notifyItemClicked(int id)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 */ createViewHolder(ViewGroup parent, int viewType)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 */ onItemChanged(ItemHolder<?> itemHolder)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 */ onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload)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 */ onItemClicked(ItemViewHolder<?> viewHolder, int id)542 void onItemClicked(ItemViewHolder<?> viewHolder, int id); 543 } 544 }