1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package androidx.leanback.widget; 15 16 import android.util.Log; 17 18 import androidx.recyclerview.widget.DiffUtil; 19 import androidx.recyclerview.widget.ListUpdateCallback; 20 21 import org.jspecify.annotations.NonNull; 22 import org.jspecify.annotations.Nullable; 23 24 import java.util.ArrayList; 25 import java.util.Collection; 26 import java.util.Collections; 27 import java.util.List; 28 29 /** 30 * An {@link ObjectAdapter} implemented with an {@link ArrayList}. 31 */ 32 public class ArrayObjectAdapter extends ObjectAdapter { 33 34 private static final Boolean DEBUG = false; 35 private static final String TAG = "ArrayObjectAdapter"; 36 37 private final List<Object> mItems = new ArrayList<>(); 38 39 // To compute the payload correctly, we should use a temporary list to hold all the old items. 40 @SuppressWarnings("WeakerAccess") /* synthetic access */ 41 final List<Object> mOldItems = new ArrayList<>(); 42 43 // Un modifiable version of mItems; 44 private List<?> mUnmodifiableItems; 45 46 /** 47 * Constructs an adapter with the given {@link PresenterSelector}. 48 */ ArrayObjectAdapter(@onNull PresenterSelector presenterSelector)49 public ArrayObjectAdapter(@NonNull PresenterSelector presenterSelector) { 50 super(presenterSelector); 51 } 52 53 /** 54 * Constructs an adapter that uses the given {@link Presenter} for all items. 55 */ ArrayObjectAdapter(@onNull Presenter presenter)56 public ArrayObjectAdapter(@NonNull Presenter presenter) { 57 super(presenter); 58 } 59 60 /** 61 * Constructs an adapter. 62 */ ArrayObjectAdapter()63 public ArrayObjectAdapter() { 64 super(); 65 } 66 67 @Override size()68 public int size() { 69 return mItems.size(); 70 } 71 72 @Override get(int index)73 public @Nullable Object get(int index) { 74 return mItems.get(index); 75 } 76 77 /** 78 * Returns the index for the first occurrence of item in the adapter, or -1 if 79 * not found. 80 * 81 * @param item The item to find in the list. 82 * @return Index of the first occurrence of the item in the adapter, or -1 83 * if not found. 84 */ indexOf(@onNull Object item)85 public int indexOf(@NonNull Object item) { 86 return mItems.indexOf(item); 87 } 88 89 /** 90 * Notify that the content of a range of items changed. Note that this is 91 * not same as items being added or removed. 92 * 93 * @param positionStart The position of first item that has changed. 94 * @param itemCount The count of how many items have changed. 95 */ notifyArrayItemRangeChanged(int positionStart, int itemCount)96 public void notifyArrayItemRangeChanged(int positionStart, int itemCount) { 97 notifyItemRangeChanged(positionStart, itemCount); 98 } 99 100 /** 101 * Adds an item to the end of the adapter. 102 * 103 * @param item The item to add to the end of the adapter. 104 */ add(@onNull Object item)105 public void add(@NonNull Object item) { 106 add(mItems.size(), item); 107 } 108 109 /** 110 * Inserts an item into this adapter at the specified index. 111 * If the index is > {@link #size} an exception will be thrown. 112 * 113 * @param index The index at which the item should be inserted. 114 * @param item The item to insert into the adapter. 115 */ add(int index, @NonNull Object item)116 public void add(int index, @NonNull Object item) { 117 mItems.add(index, item); 118 notifyItemRangeInserted(index, 1); 119 } 120 121 /** 122 * Adds the objects in the given collection to the adapter, starting at the 123 * given index. If the index is >= {@link #size} an exception will be thrown. 124 * 125 * @param index The index at which the items should be inserted. 126 * @param items A {@link Collection} of items to insert. 127 */ 128 @SuppressWarnings("unchecked") addAll(int index, @NonNull Collection<?> items)129 public void addAll(int index, @NonNull Collection<?> items) { 130 int itemsCount = items.size(); 131 if (itemsCount == 0) { 132 return; 133 } 134 mItems.addAll(index, items); 135 notifyItemRangeInserted(index, itemsCount); 136 } 137 138 /** 139 * Removes the first occurrence of the given item from the adapter. 140 * 141 * @param item The item to remove from the adapter. 142 * @return True if the item was found and thus removed from the adapter. 143 */ remove(@onNull Object item)144 public boolean remove(@NonNull Object item) { 145 int index = mItems.indexOf(item); 146 if (index >= 0) { 147 mItems.remove(index); 148 notifyItemRangeRemoved(index, 1); 149 } 150 return index >= 0; 151 } 152 153 /** 154 * Moved the item at fromPosition to toPosition. 155 * 156 * @param fromPosition Previous position of the item. 157 * @param toPosition New position of the item. 158 */ move(int fromPosition, int toPosition)159 public void move(int fromPosition, int toPosition) { 160 if (fromPosition == toPosition) { 161 // no-op 162 return; 163 } 164 Object item = mItems.remove(fromPosition); 165 mItems.add(toPosition, item); 166 notifyItemMoved(fromPosition, toPosition); 167 } 168 169 /** 170 * Replaces item at position with a new item and calls notifyItemRangeChanged() 171 * at the given position. Note that this method does not compare new item to 172 * existing item. 173 * 174 * @param position The index of item to replace. 175 * @param item The new item to be placed at given position. 176 */ replace(int position, @NonNull Object item)177 public void replace(int position, @NonNull Object item) { 178 mItems.set(position, item); 179 notifyItemRangeChanged(position, 1); 180 } 181 182 /** 183 * Removes a range of items from the adapter. The range is specified by giving 184 * the starting position and the number of elements to remove. 185 * 186 * @param position The index of the first item to remove. 187 * @param count The number of items to remove. 188 * @return The number of items removed. 189 */ removeItems(int position, int count)190 public int removeItems(int position, int count) { 191 int itemsToRemove = Math.min(count, mItems.size() - position); 192 if (itemsToRemove <= 0) { 193 return 0; 194 } 195 196 for (int i = 0; i < itemsToRemove; i++) { 197 mItems.remove(position); 198 } 199 notifyItemRangeRemoved(position, itemsToRemove); 200 return itemsToRemove; 201 } 202 203 /** 204 * Removes all items from this adapter, leaving it empty. 205 */ clear()206 public void clear() { 207 int itemCount = mItems.size(); 208 if (itemCount == 0) { 209 return; 210 } 211 mItems.clear(); 212 notifyItemRangeRemoved(0, itemCount); 213 } 214 215 /** 216 * Gets a read-only view of the list of object of this ArrayObjectAdapter. 217 */ 218 @SuppressWarnings("unchecked") unmodifiableList()219 public <E> @NonNull List<E> unmodifiableList() { 220 221 // The mUnmodifiableItems will only be created once as long as the content of mItems has not 222 // been changed. 223 if (mUnmodifiableItems == null) { 224 mUnmodifiableItems = Collections.unmodifiableList(mItems); 225 } 226 return (List<E>) mUnmodifiableItems; 227 } 228 229 @Override isImmediateNotifySupported()230 public boolean isImmediateNotifySupported() { 231 return true; 232 } 233 234 ListUpdateCallback mListUpdateCallback; 235 236 /** 237 * Set a new item list to adapter. The DiffUtil will compute the difference and dispatch it to 238 * specified position. 239 * 240 * @param itemList List of new Items 241 * @param callback Optional DiffCallback Object to compute the difference between the old data 242 * set and new data set. When null, {@link #notifyChanged()} will be fired. 243 */ 244 @SuppressWarnings("unchecked") setItems( final @NonNull List itemList, final @Nullable DiffCallback callback )245 public void setItems( 246 final @NonNull List itemList, 247 final @Nullable DiffCallback callback 248 ) { 249 if (callback == null) { 250 // shortcut when DiffCallback is not provided 251 mItems.clear(); 252 mItems.addAll(itemList); 253 notifyChanged(); 254 return; 255 } 256 mOldItems.clear(); 257 mOldItems.addAll(mItems); 258 259 DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() { 260 @Override 261 public int getOldListSize() { 262 return mOldItems.size(); 263 } 264 265 @Override 266 public int getNewListSize() { 267 return itemList.size(); 268 } 269 270 @Override 271 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { 272 return callback.areItemsTheSame(mOldItems.get(oldItemPosition), 273 itemList.get(newItemPosition)); 274 } 275 276 @Override 277 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { 278 return callback.areContentsTheSame(mOldItems.get(oldItemPosition), 279 itemList.get(newItemPosition)); 280 } 281 282 @Override 283 public @Nullable Object getChangePayload(int oldItemPosition, int newItemPosition) { 284 return callback.getChangePayload(mOldItems.get(oldItemPosition), 285 itemList.get(newItemPosition)); 286 } 287 }); 288 289 // update items. 290 mItems.clear(); 291 mItems.addAll(itemList); 292 293 // dispatch diff result 294 if (mListUpdateCallback == null) { 295 mListUpdateCallback = new ListUpdateCallback() { 296 297 @Override 298 public void onInserted(int position, int count) { 299 if (DEBUG) { 300 Log.d(TAG, "onInserted"); 301 } 302 notifyItemRangeInserted(position, count); 303 } 304 305 @Override 306 public void onRemoved(int position, int count) { 307 if (DEBUG) { 308 Log.d(TAG, "onRemoved"); 309 } 310 notifyItemRangeRemoved(position, count); 311 } 312 313 @Override 314 public void onMoved(int fromPosition, int toPosition) { 315 if (DEBUG) { 316 Log.d(TAG, "onMoved"); 317 } 318 notifyItemMoved(fromPosition, toPosition); 319 } 320 321 @Override 322 public void onChanged(int position, int count, Object payload) { 323 if (DEBUG) { 324 Log.d(TAG, "onChanged"); 325 } 326 notifyItemRangeChanged(position, count, payload); 327 } 328 }; 329 } 330 diffResult.dispatchUpdatesTo(mListUpdateCallback); 331 mOldItems.clear(); 332 } 333 } 334