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