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