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