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