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