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