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