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
17package android.support.v7.widget;
18
19import android.util.Log;
20import android.view.View;
21import android.view.ViewGroup;
22
23import java.util.ArrayList;
24import 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 */
36class 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
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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() && !holder.isRemoved()
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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        final static int BITS_PER_WORD = Long.SIZE;
398
399        final static long LAST_BIT = 1L << (Long.SIZE - 1);
400
401        long mData = 0;
402
403        Bucket next;
404
405        void set(int index) {
406            if (index >= BITS_PER_WORD) {
407                ensureNext();
408                next.set(index - BITS_PER_WORD);
409            } else {
410                mData |= 1L << index;
411            }
412        }
413
414        private void ensureNext() {
415            if (next == null) {
416                next = new Bucket();
417            }
418        }
419
420        void clear(int index) {
421            if (index >= BITS_PER_WORD) {
422                if (next != null) {
423                    next.clear(index - BITS_PER_WORD);
424                }
425            } else {
426                mData &= ~(1L << index);
427            }
428
429        }
430
431        boolean get(int index) {
432            if (index >= BITS_PER_WORD) {
433                ensureNext();
434                return next.get(index - BITS_PER_WORD);
435            } else {
436                return (mData & (1L << index)) != 0;
437            }
438        }
439
440        void reset() {
441            mData = 0;
442            if (next != null) {
443                next.reset();
444            }
445        }
446
447        void insert(int index, boolean value) {
448            if (index >= BITS_PER_WORD) {
449                ensureNext();
450                next.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 || next != null) {
463                    ensureNext();
464                    next.insert(0, lastBit);
465                }
466            }
467        }
468
469        boolean remove(int index) {
470            if (index >= BITS_PER_WORD) {
471                ensureNext();
472                return next.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 (next != null) {
483                    if (next.get(0)) {
484                        set(BITS_PER_WORD - 1);
485                    }
486                    next.remove(0);
487                }
488                return value;
489            }
490        }
491
492        int countOnesBefore(int index) {
493            if (next == 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 next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
503            }
504        }
505
506        @Override
507        public String toString() {
508            return next == null ? Long.toBinaryString(mData)
509                    : next.toString() + "xx" + Long.toBinaryString(mData);
510        }
511    }
512
513    static interface Callback {
514
515        int getChildCount();
516
517        void addView(View child, int index);
518
519        int indexOfChild(View view);
520
521        void removeViewAt(int index);
522
523        View getChildAt(int offset);
524
525        void removeAllViews();
526
527        RecyclerView.ViewHolder getChildViewHolder(View view);
528
529        void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
530
531        void detachViewFromParent(int offset);
532
533        void onEnteredHiddenState(View child);
534
535        void onLeftHiddenState(View child);
536    }
537}
538