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      * 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      * @param type     View type, can be {@link RecyclerView#INVALID_TYPE}.
204      * @return         A hidden view with a valid ViewHolder that matches the position and type.
205      */
findHiddenNonRemovedView(int position, int type)206     View findHiddenNonRemovedView(int position, int type) {
207         final int count = mHiddenViews.size();
208         for (int i = 0; i < count; i++) {
209             final View view = mHiddenViews.get(i);
210             RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
211             if (holder.getLayoutPosition() == position && !holder.isInvalid() &&
212                     (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
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     @Override
toString()343     public String toString() {
344         return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
345     }
346 
347     /**
348      * Removes a view from the ViewGroup if it is hidden.
349      *
350      * @param view The view to remove.
351      * @return True if the View is found and it is hidden. False otherwise.
352      */
removeViewIfHidden(View view)353     boolean removeViewIfHidden(View view) {
354         final int index = mCallback.indexOfChild(view);
355         if (index == -1) {
356             if (unhideViewInternal(view) && DEBUG) {
357                 throw new IllegalStateException("view is in hidden list but not in view group");
358             }
359             return true;
360         }
361         if (mBucket.get(index)) {
362             mBucket.remove(index);
363             if (!unhideViewInternal(view) && DEBUG) {
364                 throw new IllegalStateException(
365                         "removed a hidden view but it is not in hidden views list");
366             }
367             mCallback.removeViewAt(index);
368             return true;
369         }
370         return false;
371     }
372 
373     /**
374      * Bitset implementation that provides methods to offset indices.
375      */
376     static class Bucket {
377 
378         final static int BITS_PER_WORD = Long.SIZE;
379 
380         final static long LAST_BIT = 1L << (Long.SIZE - 1);
381 
382         long mData = 0;
383 
384         Bucket next;
385 
set(int index)386         void set(int index) {
387             if (index >= BITS_PER_WORD) {
388                 ensureNext();
389                 next.set(index - BITS_PER_WORD);
390             } else {
391                 mData |= 1L << index;
392             }
393         }
394 
ensureNext()395         private void ensureNext() {
396             if (next == null) {
397                 next = new Bucket();
398             }
399         }
400 
clear(int index)401         void clear(int index) {
402             if (index >= BITS_PER_WORD) {
403                 if (next != null) {
404                     next.clear(index - BITS_PER_WORD);
405                 }
406             } else {
407                 mData &= ~(1L << index);
408             }
409 
410         }
411 
get(int index)412         boolean get(int index) {
413             if (index >= BITS_PER_WORD) {
414                 ensureNext();
415                 return next.get(index - BITS_PER_WORD);
416             } else {
417                 return (mData & (1L << index)) != 0;
418             }
419         }
420 
reset()421         void reset() {
422             mData = 0;
423             if (next != null) {
424                 next.reset();
425             }
426         }
427 
insert(int index, boolean value)428         void insert(int index, boolean value) {
429             if (index >= BITS_PER_WORD) {
430                 ensureNext();
431                 next.insert(index - BITS_PER_WORD, value);
432             } else {
433                 final boolean lastBit = (mData & LAST_BIT) != 0;
434                 long mask = (1L << index) - 1;
435                 final long before = mData & mask;
436                 final long after = ((mData & ~mask)) << 1;
437                 mData = before | after;
438                 if (value) {
439                     set(index);
440                 } else {
441                     clear(index);
442                 }
443                 if (lastBit || next != null) {
444                     ensureNext();
445                     next.insert(0, lastBit);
446                 }
447             }
448         }
449 
remove(int index)450         boolean remove(int index) {
451             if (index >= BITS_PER_WORD) {
452                 ensureNext();
453                 return next.remove(index - BITS_PER_WORD);
454             } else {
455                 long mask = (1L << index);
456                 final boolean value = (mData & mask) != 0;
457                 mData &= ~mask;
458                 mask = mask - 1;
459                 final long before = mData & mask;
460                 // cannot use >> because it adds one.
461                 final long after = Long.rotateRight(mData & ~mask, 1);
462                 mData = before | after;
463                 if (next != null) {
464                     if (next.get(0)) {
465                         set(BITS_PER_WORD - 1);
466                     }
467                     next.remove(0);
468                 }
469                 return value;
470             }
471         }
472 
countOnesBefore(int index)473         int countOnesBefore(int index) {
474             if (next == null) {
475                 if (index >= BITS_PER_WORD) {
476                     return Long.bitCount(mData);
477                 }
478                 return Long.bitCount(mData & ((1L << index) - 1));
479             }
480             if (index < BITS_PER_WORD) {
481                 return Long.bitCount(mData & ((1L << index) - 1));
482             } else {
483                 return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
484             }
485         }
486 
487         @Override
toString()488         public String toString() {
489             return next == null ? Long.toBinaryString(mData)
490                     : next.toString() + "xx" + Long.toBinaryString(mData);
491         }
492     }
493 
494     static interface Callback {
495 
getChildCount()496         int getChildCount();
497 
addView(View child, int index)498         void addView(View child, int index);
499 
indexOfChild(View view)500         int indexOfChild(View view);
501 
removeViewAt(int index)502         void removeViewAt(int index);
503 
getChildAt(int offset)504         View getChildAt(int offset);
505 
removeAllViews()506         void removeAllViews();
507 
getChildViewHolder(View view)508         RecyclerView.ViewHolder getChildViewHolder(View view);
509 
attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams)510         void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
511 
detachViewFromParent(int offset)512         void detachViewFromParent(int offset);
513 
onEnteredHiddenState(View child)514         void onEnteredHiddenState(View child);
515 
onLeftHiddenState(View child)516         void onLeftHiddenState(View child);
517     }
518 }
519