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