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