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 
17 package androidx.recyclerview.widget;
18 
19 import android.util.Log;
20 import android.view.View;
21 import android.view.ViewGroup;
22 
23 import java.util.ArrayList;
24 import java.util.List;
25 
26 /**
27  * Helper class to manage children.
28  * <p>
29  * It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
30  * provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
31  * like getChildAt, getChildCount etc. These methods ignore hidden children.
32  * <p>
33  * When RecyclerView needs direct access to the view group children, it can call unfiltered
34  * methods like get getUnfilteredChildCount or getUnfilteredChildAt.
35  */
36 class ChildHelper {
37 
38     private static final boolean DEBUG = false;
39 
40     private static final String TAG = "ChildrenHelper";
41 
42     /** Not in call to removeView/removeViewAt/removeViewIfHidden. */
43     private static final int REMOVE_STATUS_NONE = 0;
44 
45     /** Within a call to removeView/removeViewAt. */
46     private static final int REMOVE_STATUS_IN_REMOVE = 1;
47 
48      /** Within a call to removeViewIfHidden. */
49     private static final int REMOVE_STATUS_IN_REMOVE_IF_HIDDEN = 2;
50 
51     final Callback mCallback;
52 
53     final Bucket mBucket;
54 
55     final List<View> mHiddenViews;
56 
57     /**
58      * One of REMOVE_STATUS_NONE, REMOVE_STATUS_IN_REMOVE, REMOVE_STATUS_IN_REMOVE_IF_HIDDEN.
59      * removeView and removeViewIfHidden may call each other:
60      * 1. removeView triggers removeViewIfHidden: this happens when removeView stops the item
61      *    animation.  removeViewIfHidden should do nothing.
62      * 2. removeView triggers removeView: this should not happen.
63      * 3. removeViewIfHidden triggers removeViewIfHidden: this should not happen, since the
64      *    animation was stopped before the first removeViewIfHidden, it won't trigger another
65      *    removeViewIfHidden.
66      * 4. removeViewIfHidden triggers removeView: this should not happen.
67      */
68     private int mRemoveStatus = REMOVE_STATUS_NONE;
69     /** The view to remove in REMOVE_STATUS_IN_REMOVE. */
70     private View mViewInRemoveView;
71 
ChildHelper(Callback callback)72     ChildHelper(Callback callback) {
73         mCallback = callback;
74         mBucket = new Bucket();
75         mHiddenViews = new ArrayList<View>();
76     }
77 
78     /**
79      * Marks a child view as hidden
80      *
81      * @param child  View to hide.
82      */
hideViewInternal(View child)83     private void hideViewInternal(View child) {
84         mHiddenViews.add(child);
85         mCallback.onEnteredHiddenState(child);
86     }
87 
88     /**
89      * Unmarks a child view as hidden.
90      *
91      * @param child  View to hide.
92      */
unhideViewInternal(View child)93     private boolean unhideViewInternal(View child) {
94         if (mHiddenViews.remove(child)) {
95             mCallback.onLeftHiddenState(child);
96             return true;
97         } else {
98             return false;
99         }
100     }
101 
102     /**
103      * Adds a view to the ViewGroup
104      *
105      * @param child  View to add.
106      * @param hidden If set to true, this item will be invisible from regular methods.
107      */
addView(View child, boolean hidden)108     void addView(View child, boolean hidden) {
109         addView(child, -1, hidden);
110     }
111 
112     /**
113      * Add a view to the ViewGroup at an index
114      *
115      * @param child  View to add.
116      * @param index  Index of the child from the regular perspective (excluding hidden views).
117      *               ChildHelper offsets this index to actual ViewGroup index.
118      * @param hidden If set to true, this item will be invisible from regular methods.
119      */
addView(View child, int index, boolean hidden)120     void addView(View child, int index, boolean hidden) {
121         final int offset;
122         if (index < 0) {
123             offset = mCallback.getChildCount();
124         } else {
125             offset = getOffset(index);
126         }
127         mBucket.insert(offset, hidden);
128         if (hidden) {
129             hideViewInternal(child);
130         }
131         mCallback.addView(child, offset);
132         if (DEBUG) {
133             Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
134         }
135     }
136 
getOffset(int index)137     private int getOffset(int index) {
138         if (index < 0) {
139             return -1; //anything below 0 won't work as diff will be undefined.
140         }
141         final int limit = mCallback.getChildCount();
142         int offset = index;
143         while (offset < limit) {
144             final int removedBefore = mBucket.countOnesBefore(offset);
145             final int diff = index - (offset - removedBefore);
146             if (diff == 0) {
147                 while (mBucket.get(offset)) { // ensure this offset is not hidden
148                     offset++;
149                 }
150                 return offset;
151             } else {
152                 offset += diff;
153             }
154         }
155         return -1;
156     }
157 
158     /**
159      * Removes the provided View from underlying RecyclerView.
160      *
161      * @param view The view to remove.
162      */
removeView(View view)163     void removeView(View view) {
164         if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) {
165             throw new IllegalStateException("Cannot call removeView(At) within removeView(At)");
166         } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) {
167             throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden");
168         }
169         try {
170             mRemoveStatus = REMOVE_STATUS_IN_REMOVE;
171             mViewInRemoveView = view;
172             int index = mCallback.indexOfChild(view);
173             if (index < 0) {
174                 return;
175             }
176             if (mBucket.remove(index)) {
177                 unhideViewInternal(view);
178             }
179             mCallback.removeViewAt(index);
180             if (DEBUG) {
181                 Log.d(TAG, "remove View off:" + index + "," + this);
182             }
183         } finally {
184             mRemoveStatus = REMOVE_STATUS_NONE;
185             mViewInRemoveView = null;
186         }
187     }
188 
189     /**
190      * Removes the view at the provided index from RecyclerView.
191      *
192      * @param index Index of the child from the regular perspective (excluding hidden views).
193      *              ChildHelper offsets this index to actual ViewGroup index.
194      */
removeViewAt(int index)195     void removeViewAt(int index) {
196         if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) {
197             throw new IllegalStateException("Cannot call removeView(At) within removeView(At)");
198         } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) {
199             throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden");
200         }
201         try {
202             final int offset = getOffset(index);
203             final View view = mCallback.getChildAt(offset);
204             if (view == null) {
205                 return;
206             }
207             mRemoveStatus = REMOVE_STATUS_IN_REMOVE;
208             mViewInRemoveView = view;
209             if (mBucket.remove(offset)) {
210                 unhideViewInternal(view);
211             }
212             mCallback.removeViewAt(offset);
213             if (DEBUG) {
214                 Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
215             }
216         } finally {
217             mRemoveStatus = REMOVE_STATUS_NONE;
218             mViewInRemoveView = null;
219         }
220     }
221 
222     /**
223      * Returns the child at provided index.
224      *
225      * @param index Index of the child to return in regular perspective.
226      */
getChildAt(int index)227     View getChildAt(int index) {
228         final int offset = getOffset(index);
229         return mCallback.getChildAt(offset);
230     }
231 
232     /**
233      * Removes all views from the ViewGroup including the hidden ones.
234      */
removeAllViewsUnfiltered()235     void removeAllViewsUnfiltered() {
236         mBucket.reset();
237         for (int i = mHiddenViews.size() - 1; i >= 0; i--) {
238             mCallback.onLeftHiddenState(mHiddenViews.get(i));
239             mHiddenViews.remove(i);
240         }
241         mCallback.removeAllViews();
242         if (DEBUG) {
243             Log.d(TAG, "removeAllViewsUnfiltered");
244         }
245     }
246 
247     /**
248      * This can be used to find a disappearing view by position.
249      *
250      * @param position The adapter position of the item.
251      * @return         A hidden view with a valid ViewHolder that matches the position.
252      */
findHiddenNonRemovedView(int position)253     View findHiddenNonRemovedView(int position) {
254         final int count = mHiddenViews.size();
255         for (int i = 0; i < count; i++) {
256             final View view = mHiddenViews.get(i);
257             RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
258             if (holder.getLayoutPosition() == position
259                     && !holder.isInvalid()
260                     && !holder.isRemoved()) {
261                 return view;
262             }
263         }
264         return null;
265     }
266 
267     /**
268      * Attaches the provided view to the underlying ViewGroup.
269      *
270      * @param child        Child to attach.
271      * @param index        Index of the child to attach in regular perspective.
272      * @param layoutParams LayoutParams for the child.
273      * @param hidden       If set to true, this item will be invisible to the regular methods.
274      */
attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, boolean hidden)275     void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
276             boolean hidden) {
277         final int offset;
278         if (index < 0) {
279             offset = mCallback.getChildCount();
280         } else {
281             offset = getOffset(index);
282         }
283         mBucket.insert(offset, hidden);
284         if (hidden) {
285             hideViewInternal(child);
286         }
287         mCallback.attachViewToParent(child, offset, layoutParams);
288         if (DEBUG) {
289             Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + ","
290                     + "h:" + hidden + ", " + this);
291         }
292     }
293 
294     /**
295      * Returns the number of children that are not hidden.
296      *
297      * @return Number of children that are not hidden.
298      * @see #getChildAt(int)
299      */
getChildCount()300     int getChildCount() {
301         return mCallback.getChildCount() - mHiddenViews.size();
302     }
303 
304     /**
305      * Returns the total number of children.
306      *
307      * @return The total number of children including the hidden views.
308      * @see #getUnfilteredChildAt(int)
309      */
getUnfilteredChildCount()310     int getUnfilteredChildCount() {
311         return mCallback.getChildCount();
312     }
313 
314     /**
315      * Returns a child by ViewGroup offset. ChildHelper won't offset this index.
316      *
317      * @param index ViewGroup index of the child to return.
318      * @return The view in the provided index.
319      */
getUnfilteredChildAt(int index)320     View getUnfilteredChildAt(int index) {
321         return mCallback.getChildAt(index);
322     }
323 
324     /**
325      * Detaches the view at the provided index.
326      *
327      * @param index Index of the child to return in regular perspective.
328      */
detachViewFromParent(int index)329     void detachViewFromParent(int index) {
330         final int offset = getOffset(index);
331         mBucket.remove(offset);
332         mCallback.detachViewFromParent(offset);
333         if (DEBUG) {
334             Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
335         }
336     }
337 
338     /**
339      * Returns the index of the child in regular perspective.
340      *
341      * @param child The child whose index will be returned.
342      * @return The regular perspective index of the child or -1 if it does not exists.
343      */
indexOfChild(View child)344     int indexOfChild(View child) {
345         final int index = mCallback.indexOfChild(child);
346         if (index == -1) {
347             return -1;
348         }
349         if (mBucket.get(index)) {
350             if (DEBUG) {
351                 throw new IllegalArgumentException("cannot get index of a hidden child");
352             } else {
353                 return -1;
354             }
355         }
356         // reverse the index
357         return index - mBucket.countOnesBefore(index);
358     }
359 
360     /**
361      * Returns whether a View is visible to LayoutManager or not.
362      *
363      * @param view The child view to check. Should be a child of the Callback.
364      * @return True if the View is not visible to LayoutManager
365      */
isHidden(View view)366     boolean isHidden(View view) {
367         return mHiddenViews.contains(view);
368     }
369 
370     /**
371      * Marks a child view as hidden.
372      *
373      * @param view The view to hide.
374      */
hide(View view)375     void hide(View view) {
376         final int offset = mCallback.indexOfChild(view);
377         if (offset < 0) {
378             throw new IllegalArgumentException("view is not a child, cannot hide " + view);
379         }
380         if (DEBUG && mBucket.get(offset)) {
381             throw new RuntimeException("trying to hide same view twice, how come ? " + view);
382         }
383         mBucket.set(offset);
384         hideViewInternal(view);
385         if (DEBUG) {
386             Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this);
387         }
388     }
389 
390     /**
391      * Moves a child view from hidden list to regular list.
392      * Calling this method should probably be followed by a detach, otherwise, it will suddenly
393      * show up in LayoutManager's children list.
394      *
395      * @param view The hidden View to unhide
396      */
unhide(View view)397     void unhide(View view) {
398         final int offset = mCallback.indexOfChild(view);
399         if (offset < 0) {
400             throw new IllegalArgumentException("view is not a child, cannot hide " + view);
401         }
402         if (!mBucket.get(offset)) {
403             throw new RuntimeException("trying to unhide a view that was not hidden" + view);
404         }
405         mBucket.clear(offset);
406         unhideViewInternal(view);
407     }
408 
409     @Override
toString()410     public String toString() {
411         return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
412     }
413 
414     /**
415      * Removes a view from the ViewGroup if it is hidden.
416      *
417      * @param view The view to remove.
418      * @return True if the View is found and it is hidden. False otherwise.
419      */
removeViewIfHidden(View view)420     boolean removeViewIfHidden(View view) {
421         if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) {
422             if (mViewInRemoveView != view) {
423                 throw new IllegalStateException("Cannot call removeViewIfHidden within removeView"
424                         + "(At) for a different view");
425             }
426             // removeView ends the ItemAnimation and triggers removeViewIfHidden
427             return false;
428         } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) {
429             throw new IllegalStateException("Cannot call removeViewIfHidden within"
430                     + " removeViewIfHidden");
431         }
432         try {
433             mRemoveStatus = REMOVE_STATUS_IN_REMOVE_IF_HIDDEN;
434             final int index = mCallback.indexOfChild(view);
435             if (index == -1) {
436                 if (unhideViewInternal(view) && DEBUG) {
437                     throw new IllegalStateException("view is in hidden list but not in view group");
438                 }
439                 return true;
440             }
441             if (mBucket.get(index)) {
442                 mBucket.remove(index);
443                 if (!unhideViewInternal(view) && DEBUG) {
444                     throw new IllegalStateException(
445                             "removed a hidden view but it is not in hidden views list");
446                 }
447                 mCallback.removeViewAt(index);
448                 return true;
449             }
450             return false;
451         } finally {
452             mRemoveStatus = REMOVE_STATUS_NONE;
453         }
454     }
455 
456     /**
457      * Bitset implementation that provides methods to offset indices.
458      */
459     static class Bucket {
460 
461         static final int BITS_PER_WORD = Long.SIZE;
462 
463         static final long LAST_BIT = 1L << (Long.SIZE - 1);
464 
465         long mData = 0;
466 
467         Bucket mNext;
468 
set(int index)469         void set(int index) {
470             if (index >= BITS_PER_WORD) {
471                 ensureNext();
472                 mNext.set(index - BITS_PER_WORD);
473             } else {
474                 mData |= 1L << index;
475             }
476         }
477 
ensureNext()478         private void ensureNext() {
479             if (mNext == null) {
480                 mNext = new Bucket();
481             }
482         }
483 
clear(int index)484         void clear(int index) {
485             if (index >= BITS_PER_WORD) {
486                 if (mNext != null) {
487                     mNext.clear(index - BITS_PER_WORD);
488                 }
489             } else {
490                 mData &= ~(1L << index);
491             }
492 
493         }
494 
get(int index)495         boolean get(int index) {
496             if (index >= BITS_PER_WORD) {
497                 ensureNext();
498                 return mNext.get(index - BITS_PER_WORD);
499             } else {
500                 return (mData & (1L << index)) != 0;
501             }
502         }
503 
reset()504         void reset() {
505             mData = 0;
506             if (mNext != null) {
507                 mNext.reset();
508             }
509         }
510 
insert(int index, boolean value)511         void insert(int index, boolean value) {
512             if (index >= BITS_PER_WORD) {
513                 ensureNext();
514                 mNext.insert(index - BITS_PER_WORD, value);
515             } else {
516                 final boolean lastBit = (mData & LAST_BIT) != 0;
517                 long mask = (1L << index) - 1;
518                 final long before = mData & mask;
519                 final long after = (mData & ~mask) << 1;
520                 mData = before | after;
521                 if (value) {
522                     set(index);
523                 } else {
524                     clear(index);
525                 }
526                 if (lastBit || mNext != null) {
527                     ensureNext();
528                     mNext.insert(0, lastBit);
529                 }
530             }
531         }
532 
remove(int index)533         boolean remove(int index) {
534             if (index >= BITS_PER_WORD) {
535                 ensureNext();
536                 return mNext.remove(index - BITS_PER_WORD);
537             } else {
538                 long mask = (1L << index);
539                 final boolean value = (mData & mask) != 0;
540                 mData &= ~mask;
541                 mask = mask - 1;
542                 final long before = mData & mask;
543                 // cannot use >> because it adds one.
544                 final long after = Long.rotateRight(mData & ~mask, 1);
545                 mData = before | after;
546                 if (mNext != null) {
547                     if (mNext.get(0)) {
548                         set(BITS_PER_WORD - 1);
549                     }
550                     mNext.remove(0);
551                 }
552                 return value;
553             }
554         }
555 
countOnesBefore(int index)556         int countOnesBefore(int index) {
557             if (mNext == null) {
558                 if (index >= BITS_PER_WORD) {
559                     return Long.bitCount(mData);
560                 }
561                 return Long.bitCount(mData & ((1L << index) - 1));
562             }
563             if (index < BITS_PER_WORD) {
564                 return Long.bitCount(mData & ((1L << index) - 1));
565             } else {
566                 return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
567             }
568         }
569 
570         @Override
toString()571         public String toString() {
572             return mNext == null ? Long.toBinaryString(mData)
573                     : mNext.toString() + "xx" + Long.toBinaryString(mData);
574         }
575     }
576 
577     interface Callback {
578 
getChildCount()579         int getChildCount();
580 
addView(View child, int index)581         void addView(View child, int index);
582 
indexOfChild(View view)583         int indexOfChild(View view);
584 
removeViewAt(int index)585         void removeViewAt(int index);
586 
getChildAt(int offset)587         View getChildAt(int offset);
588 
removeAllViews()589         void removeAllViews();
590 
getChildViewHolder(View view)591         RecyclerView.ViewHolder getChildViewHolder(View view);
592 
attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams)593         void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
594 
detachViewFromParent(int offset)595         void detachViewFromParent(int offset);
596 
onEnteredHiddenState(View child)597         void onEnteredHiddenState(View child);
598 
onLeftHiddenState(View child)599         void onLeftHiddenState(View child);
600     }
601 }
602