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