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     * 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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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
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
368        private void ensureNext() {
369            if (next == null) {
370                next = new Bucket();
371            }
372        }
373
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
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
394        void reset() {
395            mData = 0;
396            if (next != null) {
397                next.reset();
398            }
399        }
400
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
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
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
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
469        int getChildCount();
470
471        void addView(View child, int index);
472
473        int indexOfChild(View view);
474
475        void removeViewAt(int index);
476
477        View getChildAt(int offset);
478
479        void removeAllViews();
480
481        RecyclerView.ViewHolder getChildViewHolder(View view);
482
483        void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
484
485        void detachViewFromParent(int offset);
486    }
487}
488