1 /* 2 * Copyright 2018 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 package androidx.recyclerview.widget; 17 18 import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR; 19 import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR; 20 import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST; 21 import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED; 22 import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_POST; 23 import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_PRE; 24 import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST; 25 26 import androidx.annotation.VisibleForTesting; 27 import androidx.collection.LongSparseArray; 28 import androidx.collection.SimpleArrayMap; 29 import androidx.core.util.Pools; 30 31 import org.jspecify.annotations.NonNull; 32 import org.jspecify.annotations.Nullable; 33 34 /** 35 * This class abstracts all tracking for Views to run animations. 36 */ 37 class ViewInfoStore { 38 39 private static final boolean DEBUG = false; 40 41 /** 42 * View data records for pre-layout 43 */ 44 @VisibleForTesting 45 final SimpleArrayMap<RecyclerView.ViewHolder, InfoRecord> mLayoutHolderMap = 46 new SimpleArrayMap<>(); 47 48 @VisibleForTesting 49 final LongSparseArray<RecyclerView.ViewHolder> mOldChangedHolders = new LongSparseArray<>(); 50 51 /** 52 * Clears the state and all existing tracking data 53 */ clear()54 void clear() { 55 mLayoutHolderMap.clear(); 56 mOldChangedHolders.clear(); 57 } 58 59 /** 60 * Adds the item information to the prelayout tracking 61 * @param holder The ViewHolder whose information is being saved 62 * @param info The information to save 63 */ addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info)64 void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { 65 InfoRecord record = mLayoutHolderMap.get(holder); 66 if (record == null) { 67 record = InfoRecord.obtain(); 68 mLayoutHolderMap.put(holder, record); 69 } 70 record.preInfo = info; 71 record.flags |= FLAG_PRE; 72 } 73 isDisappearing(RecyclerView.ViewHolder holder)74 boolean isDisappearing(RecyclerView.ViewHolder holder) { 75 final InfoRecord record = mLayoutHolderMap.get(holder); 76 return record != null && ((record.flags & FLAG_DISAPPEARED) != 0); 77 } 78 79 /** 80 * Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it. 81 * 82 * @param vh The ViewHolder whose information is being queried 83 * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist 84 */ popFromPreLayout( RecyclerView.ViewHolder vh)85 RecyclerView.ItemAnimator.@Nullable ItemHolderInfo popFromPreLayout( 86 RecyclerView.ViewHolder vh) { 87 return popFromLayoutStep(vh, FLAG_PRE); 88 } 89 90 /** 91 * Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it. 92 * 93 * @param vh The ViewHolder whose information is being queried 94 * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist 95 */ popFromPostLayout( RecyclerView.ViewHolder vh)96 RecyclerView.ItemAnimator.@Nullable ItemHolderInfo popFromPostLayout( 97 RecyclerView.ViewHolder vh) { 98 return popFromLayoutStep(vh, FLAG_POST); 99 } 100 popFromLayoutStep(RecyclerView.ViewHolder vh, int flag)101 private RecyclerView.ItemAnimator.ItemHolderInfo popFromLayoutStep(RecyclerView.ViewHolder vh, int flag) { 102 int index = mLayoutHolderMap.indexOfKey(vh); 103 if (index < 0) { 104 return null; 105 } 106 final InfoRecord record = mLayoutHolderMap.valueAt(index); 107 if (record != null && (record.flags & flag) != 0) { 108 record.flags &= ~flag; 109 final RecyclerView.ItemAnimator.ItemHolderInfo info; 110 if (flag == FLAG_PRE) { 111 info = record.preInfo; 112 } else if (flag == FLAG_POST) { 113 info = record.postInfo; 114 } else { 115 throw new IllegalArgumentException("Must provide flag PRE or POST"); 116 } 117 // if not pre-post flag is left, clear. 118 if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) { 119 mLayoutHolderMap.removeAt(index); 120 InfoRecord.recycle(record); 121 } 122 return info; 123 } 124 return null; 125 } 126 127 /** 128 * Adds the given ViewHolder to the oldChangeHolders list 129 * @param key The key to identify the ViewHolder. 130 * @param holder The ViewHolder to store 131 */ addToOldChangeHolders(long key, RecyclerView.ViewHolder holder)132 void addToOldChangeHolders(long key, RecyclerView.ViewHolder holder) { 133 mOldChangedHolders.put(key, holder); 134 } 135 136 /** 137 * Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the 138 * LayoutManager during a pre-layout pass. We distinguish them from other views that were 139 * already in the pre-layout so that ItemAnimator can choose to run a different animation for 140 * them. 141 * 142 * @param holder The ViewHolder to store 143 * @param info The information to save 144 */ addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info)145 void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { 146 InfoRecord record = mLayoutHolderMap.get(holder); 147 if (record == null) { 148 record = InfoRecord.obtain(); 149 mLayoutHolderMap.put(holder, record); 150 } 151 record.flags |= FLAG_APPEAR; 152 record.preInfo = info; 153 } 154 155 /** 156 * Checks whether the given ViewHolder is in preLayout list 157 * @param viewHolder The ViewHolder to query 158 * 159 * @return True if the ViewHolder is present in preLayout, false otherwise 160 */ isInPreLayout(RecyclerView.ViewHolder viewHolder)161 boolean isInPreLayout(RecyclerView.ViewHolder viewHolder) { 162 final InfoRecord record = mLayoutHolderMap.get(viewHolder); 163 return record != null && (record.flags & FLAG_PRE) != 0; 164 } 165 166 /** 167 * Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns 168 * null. 169 * @param key The key to be used to find the ViewHolder. 170 * 171 * @return A ViewHolder if exists or null if it does not exist. 172 */ getFromOldChangeHolders(long key)173 RecyclerView.ViewHolder getFromOldChangeHolders(long key) { 174 return mOldChangedHolders.get(key); 175 } 176 177 /** 178 * Adds the item information to the post layout list 179 * @param holder The ViewHolder whose information is being saved 180 * @param info The information to save 181 */ addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info)182 void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { 183 InfoRecord record = mLayoutHolderMap.get(holder); 184 if (record == null) { 185 record = InfoRecord.obtain(); 186 mLayoutHolderMap.put(holder, record); 187 } 188 record.postInfo = info; 189 record.flags |= FLAG_POST; 190 } 191 192 /** 193 * A ViewHolder might be added by the LayoutManager just to animate its disappearance. 194 * This list holds such items so that we can animate / recycle these ViewHolders properly. 195 * 196 * @param holder The ViewHolder which disappeared during a layout. 197 */ addToDisappearedInLayout(RecyclerView.ViewHolder holder)198 void addToDisappearedInLayout(RecyclerView.ViewHolder holder) { 199 InfoRecord record = mLayoutHolderMap.get(holder); 200 if (record == null) { 201 record = InfoRecord.obtain(); 202 mLayoutHolderMap.put(holder, record); 203 } 204 record.flags |= FLAG_DISAPPEARED; 205 } 206 207 /** 208 * Removes a ViewHolder from disappearing list. 209 * @param holder The ViewHolder to be removed from the disappearing list. 210 */ removeFromDisappearedInLayout(RecyclerView.ViewHolder holder)211 void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) { 212 InfoRecord record = mLayoutHolderMap.get(holder); 213 if (record == null) { 214 return; 215 } 216 record.flags &= ~FLAG_DISAPPEARED; 217 } 218 process(ProcessCallback callback)219 void process(ProcessCallback callback) { 220 for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { 221 final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); 222 final InfoRecord record = mLayoutHolderMap.removeAt(index); 223 if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { 224 // Appeared then disappeared. Not useful for animations. 225 callback.unused(viewHolder); 226 } else if ((record.flags & FLAG_DISAPPEARED) != 0) { 227 // Set as "disappeared" by the LayoutManager (addDisappearingView) 228 if (record.preInfo == null) { 229 // similar to appear disappear but happened between different layout passes. 230 // this can happen when the layout manager is using auto-measure 231 callback.unused(viewHolder); 232 } else { 233 callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); 234 } 235 } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) { 236 // Appeared in the layout but not in the adapter (e.g. entered the viewport) 237 callback.processAppeared(viewHolder, record.preInfo, record.postInfo); 238 } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) { 239 // Persistent in both passes. Animate persistence 240 callback.processPersistent(viewHolder, record.preInfo, record.postInfo); 241 } else if ((record.flags & FLAG_PRE) != 0) { 242 // Was in pre-layout, never been added to post layout 243 callback.processDisappeared(viewHolder, record.preInfo, null); 244 } else if ((record.flags & FLAG_POST) != 0) { 245 // Was not in pre-layout, been added to post layout 246 callback.processAppeared(viewHolder, record.preInfo, record.postInfo); 247 } else if ((record.flags & FLAG_APPEAR) != 0) { 248 // Scrap view. RecyclerView will handle removing/recycling this. 249 } else if (DEBUG) { 250 throw new IllegalStateException("record without any reasonable flag combination:/"); 251 } 252 InfoRecord.recycle(record); 253 } 254 } 255 256 /** 257 * Removes the ViewHolder from all list 258 * @param holder The ViewHolder which we should stop tracking 259 */ removeViewHolder(RecyclerView.ViewHolder holder)260 void removeViewHolder(RecyclerView.ViewHolder holder) { 261 for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) { 262 if (holder == mOldChangedHolders.valueAt(i)) { 263 mOldChangedHolders.removeAt(i); 264 break; 265 } 266 } 267 final InfoRecord info = mLayoutHolderMap.remove(holder); 268 if (info != null) { 269 InfoRecord.recycle(info); 270 } 271 } 272 onDetach()273 void onDetach() { 274 InfoRecord.drainCache(); 275 } 276 onViewDetached(RecyclerView.ViewHolder viewHolder)277 public void onViewDetached(RecyclerView.ViewHolder viewHolder) { 278 removeFromDisappearedInLayout(viewHolder); 279 } 280 281 interface ProcessCallback { processDisappeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.@NonNull ItemHolderInfo preInfo, RecyclerView.ItemAnimator.@Nullable ItemHolderInfo postInfo)282 void processDisappeared(RecyclerView.ViewHolder viewHolder, 283 RecyclerView.ItemAnimator.@NonNull ItemHolderInfo preInfo, 284 RecyclerView.ItemAnimator.@Nullable ItemHolderInfo postInfo); processAppeared(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.@Nullable ItemHolderInfo preInfo, RecyclerView.ItemAnimator.ItemHolderInfo postInfo)285 void processAppeared(RecyclerView.ViewHolder viewHolder, 286 RecyclerView.ItemAnimator.@Nullable ItemHolderInfo preInfo, 287 RecyclerView.ItemAnimator.ItemHolderInfo postInfo); processPersistent(RecyclerView.ViewHolder viewHolder, RecyclerView.ItemAnimator.@NonNull ItemHolderInfo preInfo, RecyclerView.ItemAnimator.@NonNull ItemHolderInfo postInfo)288 void processPersistent(RecyclerView.ViewHolder viewHolder, 289 RecyclerView.ItemAnimator.@NonNull ItemHolderInfo preInfo, 290 RecyclerView.ItemAnimator.@NonNull ItemHolderInfo postInfo); unused(RecyclerView.ViewHolder holder)291 void unused(RecyclerView.ViewHolder holder); 292 } 293 294 static class InfoRecord { 295 // disappearing list 296 static final int FLAG_DISAPPEARED = 1; 297 // appear in pre layout list 298 static final int FLAG_APPEAR = 1 << 1; 299 // pre layout, this is necessary to distinguish null item info 300 static final int FLAG_PRE = 1 << 2; 301 // post layout, this is necessary to distinguish null item info 302 static final int FLAG_POST = 1 << 3; 303 static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED; 304 static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST; 305 static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST; 306 int flags; 307 RecyclerView.ItemAnimator.@Nullable ItemHolderInfo preInfo; 308 RecyclerView.ItemAnimator.@Nullable ItemHolderInfo postInfo; 309 static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20); 310 InfoRecord()311 private InfoRecord() { 312 } 313 obtain()314 static InfoRecord obtain() { 315 InfoRecord record = sPool.acquire(); 316 return record == null ? new InfoRecord() : record; 317 } 318 recycle(InfoRecord record)319 static void recycle(InfoRecord record) { 320 record.flags = 0; 321 record.preInfo = null; 322 record.postInfo = null; 323 sPool.release(record); 324 } 325 drainCache()326 static void drainCache() { 327 //noinspection StatementWithEmptyBody 328 while (sPool.acquire() != null); 329 } 330 } 331 } 332