1 /* 2 * Copyright 2018 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 androidx.recyclerview.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 /** Not in call to removeView/removeViewAt/removeViewIfHidden. */ 43 private static final int REMOVE_STATUS_NONE = 0; 44 45 /** Within a call to removeView/removeViewAt. */ 46 private static final int REMOVE_STATUS_IN_REMOVE = 1; 47 48 /** Within a call to removeViewIfHidden. */ 49 private static final int REMOVE_STATUS_IN_REMOVE_IF_HIDDEN = 2; 50 51 final Callback mCallback; 52 53 final Bucket mBucket; 54 55 final List<View> mHiddenViews; 56 57 /** 58 * One of REMOVE_STATUS_NONE, REMOVE_STATUS_IN_REMOVE, REMOVE_STATUS_IN_REMOVE_IF_HIDDEN. 59 * removeView and removeViewIfHidden may call each other: 60 * 1. removeView triggers removeViewIfHidden: this happens when removeView stops the item 61 * animation. removeViewIfHidden should do nothing. 62 * 2. removeView triggers removeView: this should not happen. 63 * 3. removeViewIfHidden triggers removeViewIfHidden: this should not happen, since the 64 * animation was stopped before the first removeViewIfHidden, it won't trigger another 65 * removeViewIfHidden. 66 * 4. removeViewIfHidden triggers removeView: this should not happen. 67 */ 68 private int mRemoveStatus = REMOVE_STATUS_NONE; 69 /** The view to remove in REMOVE_STATUS_IN_REMOVE. */ 70 private View mViewInRemoveView; 71 ChildHelper(Callback callback)72 ChildHelper(Callback callback) { 73 mCallback = callback; 74 mBucket = new Bucket(); 75 mHiddenViews = new ArrayList<View>(); 76 } 77 78 /** 79 * Marks a child view as hidden 80 * 81 * @param child View to hide. 82 */ hideViewInternal(View child)83 private void hideViewInternal(View child) { 84 mHiddenViews.add(child); 85 mCallback.onEnteredHiddenState(child); 86 } 87 88 /** 89 * Unmarks a child view as hidden. 90 * 91 * @param child View to hide. 92 */ unhideViewInternal(View child)93 private boolean unhideViewInternal(View child) { 94 if (mHiddenViews.remove(child)) { 95 mCallback.onLeftHiddenState(child); 96 return true; 97 } else { 98 return false; 99 } 100 } 101 102 /** 103 * Adds a view to the ViewGroup 104 * 105 * @param child View to add. 106 * @param hidden If set to true, this item will be invisible from regular methods. 107 */ addView(View child, boolean hidden)108 void addView(View child, boolean hidden) { 109 addView(child, -1, hidden); 110 } 111 112 /** 113 * Add a view to the ViewGroup at an index 114 * 115 * @param child View to add. 116 * @param index Index of the child from the regular perspective (excluding hidden views). 117 * ChildHelper offsets this index to actual ViewGroup index. 118 * @param hidden If set to true, this item will be invisible from regular methods. 119 */ addView(View child, int index, boolean hidden)120 void addView(View child, int index, boolean hidden) { 121 final int offset; 122 if (index < 0) { 123 offset = mCallback.getChildCount(); 124 } else { 125 offset = getOffset(index); 126 } 127 mBucket.insert(offset, hidden); 128 if (hidden) { 129 hideViewInternal(child); 130 } 131 mCallback.addView(child, offset); 132 if (DEBUG) { 133 Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this); 134 } 135 } 136 getOffset(int index)137 private int getOffset(int index) { 138 if (index < 0) { 139 return -1; //anything below 0 won't work as diff will be undefined. 140 } 141 final int limit = mCallback.getChildCount(); 142 int offset = index; 143 while (offset < limit) { 144 final int removedBefore = mBucket.countOnesBefore(offset); 145 final int diff = index - (offset - removedBefore); 146 if (diff == 0) { 147 while (mBucket.get(offset)) { // ensure this offset is not hidden 148 offset++; 149 } 150 return offset; 151 } else { 152 offset += diff; 153 } 154 } 155 return -1; 156 } 157 158 /** 159 * Removes the provided View from underlying RecyclerView. 160 * 161 * @param view The view to remove. 162 */ removeView(View view)163 void removeView(View view) { 164 if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) { 165 throw new IllegalStateException("Cannot call removeView(At) within removeView(At)"); 166 } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) { 167 throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden"); 168 } 169 try { 170 mRemoveStatus = REMOVE_STATUS_IN_REMOVE; 171 mViewInRemoveView = view; 172 int index = mCallback.indexOfChild(view); 173 if (index < 0) { 174 return; 175 } 176 if (mBucket.remove(index)) { 177 unhideViewInternal(view); 178 } 179 mCallback.removeViewAt(index); 180 if (DEBUG) { 181 Log.d(TAG, "remove View off:" + index + "," + this); 182 } 183 } finally { 184 mRemoveStatus = REMOVE_STATUS_NONE; 185 mViewInRemoveView = null; 186 } 187 } 188 189 /** 190 * Removes the view at the provided index from RecyclerView. 191 * 192 * @param index Index of the child from the regular perspective (excluding hidden views). 193 * ChildHelper offsets this index to actual ViewGroup index. 194 */ removeViewAt(int index)195 void removeViewAt(int index) { 196 if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) { 197 throw new IllegalStateException("Cannot call removeView(At) within removeView(At)"); 198 } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) { 199 throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden"); 200 } 201 try { 202 final int offset = getOffset(index); 203 final View view = mCallback.getChildAt(offset); 204 if (view == null) { 205 return; 206 } 207 mRemoveStatus = REMOVE_STATUS_IN_REMOVE; 208 mViewInRemoveView = view; 209 if (mBucket.remove(offset)) { 210 unhideViewInternal(view); 211 } 212 mCallback.removeViewAt(offset); 213 if (DEBUG) { 214 Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this); 215 } 216 } finally { 217 mRemoveStatus = REMOVE_STATUS_NONE; 218 mViewInRemoveView = null; 219 } 220 } 221 222 /** 223 * Returns the child at provided index. 224 * 225 * @param index Index of the child to return in regular perspective. 226 */ getChildAt(int index)227 View getChildAt(int index) { 228 final int offset = getOffset(index); 229 return mCallback.getChildAt(offset); 230 } 231 232 /** 233 * Removes all views from the ViewGroup including the hidden ones. 234 */ removeAllViewsUnfiltered()235 void removeAllViewsUnfiltered() { 236 mBucket.reset(); 237 for (int i = mHiddenViews.size() - 1; i >= 0; i--) { 238 mCallback.onLeftHiddenState(mHiddenViews.get(i)); 239 mHiddenViews.remove(i); 240 } 241 mCallback.removeAllViews(); 242 if (DEBUG) { 243 Log.d(TAG, "removeAllViewsUnfiltered"); 244 } 245 } 246 247 /** 248 * This can be used to find a disappearing view by position. 249 * 250 * @param position The adapter position of the item. 251 * @return A hidden view with a valid ViewHolder that matches the position. 252 */ findHiddenNonRemovedView(int position)253 View findHiddenNonRemovedView(int position) { 254 final int count = mHiddenViews.size(); 255 for (int i = 0; i < count; i++) { 256 final View view = mHiddenViews.get(i); 257 RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view); 258 if (holder.getLayoutPosition() == position 259 && !holder.isInvalid() 260 && !holder.isRemoved()) { 261 return view; 262 } 263 } 264 return null; 265 } 266 267 /** 268 * Attaches the provided view to the underlying ViewGroup. 269 * 270 * @param child Child to attach. 271 * @param index Index of the child to attach in regular perspective. 272 * @param layoutParams LayoutParams for the child. 273 * @param hidden If set to true, this item will be invisible to the regular methods. 274 */ attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, boolean hidden)275 void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, 276 boolean hidden) { 277 final int offset; 278 if (index < 0) { 279 offset = mCallback.getChildCount(); 280 } else { 281 offset = getOffset(index); 282 } 283 mBucket.insert(offset, hidden); 284 if (hidden) { 285 hideViewInternal(child); 286 } 287 mCallback.attachViewToParent(child, offset, layoutParams); 288 if (DEBUG) { 289 Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," 290 + "h:" + hidden + ", " + this); 291 } 292 } 293 294 /** 295 * Returns the number of children that are not hidden. 296 * 297 * @return Number of children that are not hidden. 298 * @see #getChildAt(int) 299 */ getChildCount()300 int getChildCount() { 301 return mCallback.getChildCount() - mHiddenViews.size(); 302 } 303 304 /** 305 * Returns the total number of children. 306 * 307 * @return The total number of children including the hidden views. 308 * @see #getUnfilteredChildAt(int) 309 */ getUnfilteredChildCount()310 int getUnfilteredChildCount() { 311 return mCallback.getChildCount(); 312 } 313 314 /** 315 * Returns a child by ViewGroup offset. ChildHelper won't offset this index. 316 * 317 * @param index ViewGroup index of the child to return. 318 * @return The view in the provided index. 319 */ getUnfilteredChildAt(int index)320 View getUnfilteredChildAt(int index) { 321 return mCallback.getChildAt(index); 322 } 323 324 /** 325 * Detaches the view at the provided index. 326 * 327 * @param index Index of the child to return in regular perspective. 328 */ detachViewFromParent(int index)329 void detachViewFromParent(int index) { 330 final int offset = getOffset(index); 331 mBucket.remove(offset); 332 mCallback.detachViewFromParent(offset); 333 if (DEBUG) { 334 Log.d(TAG, "detach view from parent " + index + ", off:" + offset); 335 } 336 } 337 338 /** 339 * Returns the index of the child in regular perspective. 340 * 341 * @param child The child whose index will be returned. 342 * @return The regular perspective index of the child or -1 if it does not exists. 343 */ indexOfChild(View child)344 int indexOfChild(View child) { 345 final int index = mCallback.indexOfChild(child); 346 if (index == -1) { 347 return -1; 348 } 349 if (mBucket.get(index)) { 350 if (DEBUG) { 351 throw new IllegalArgumentException("cannot get index of a hidden child"); 352 } else { 353 return -1; 354 } 355 } 356 // reverse the index 357 return index - mBucket.countOnesBefore(index); 358 } 359 360 /** 361 * Returns whether a View is visible to LayoutManager or not. 362 * 363 * @param view The child view to check. Should be a child of the Callback. 364 * @return True if the View is not visible to LayoutManager 365 */ isHidden(View view)366 boolean isHidden(View view) { 367 return mHiddenViews.contains(view); 368 } 369 370 /** 371 * Marks a child view as hidden. 372 * 373 * @param view The view to hide. 374 */ hide(View view)375 void hide(View view) { 376 final int offset = mCallback.indexOfChild(view); 377 if (offset < 0) { 378 throw new IllegalArgumentException("view is not a child, cannot hide " + view); 379 } 380 if (DEBUG && mBucket.get(offset)) { 381 throw new RuntimeException("trying to hide same view twice, how come ? " + view); 382 } 383 mBucket.set(offset); 384 hideViewInternal(view); 385 if (DEBUG) { 386 Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this); 387 } 388 } 389 390 /** 391 * Moves a child view from hidden list to regular list. 392 * Calling this method should probably be followed by a detach, otherwise, it will suddenly 393 * show up in LayoutManager's children list. 394 * 395 * @param view The hidden View to unhide 396 */ unhide(View view)397 void unhide(View view) { 398 final int offset = mCallback.indexOfChild(view); 399 if (offset < 0) { 400 throw new IllegalArgumentException("view is not a child, cannot hide " + view); 401 } 402 if (!mBucket.get(offset)) { 403 throw new RuntimeException("trying to unhide a view that was not hidden" + view); 404 } 405 mBucket.clear(offset); 406 unhideViewInternal(view); 407 } 408 409 @Override toString()410 public String toString() { 411 return mBucket.toString() + ", hidden list:" + mHiddenViews.size(); 412 } 413 414 /** 415 * Removes a view from the ViewGroup if it is hidden. 416 * 417 * @param view The view to remove. 418 * @return True if the View is found and it is hidden. False otherwise. 419 */ removeViewIfHidden(View view)420 boolean removeViewIfHidden(View view) { 421 if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) { 422 if (mViewInRemoveView != view) { 423 throw new IllegalStateException("Cannot call removeViewIfHidden within removeView" 424 + "(At) for a different view"); 425 } 426 // removeView ends the ItemAnimation and triggers removeViewIfHidden 427 return false; 428 } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) { 429 throw new IllegalStateException("Cannot call removeViewIfHidden within" 430 + " removeViewIfHidden"); 431 } 432 try { 433 mRemoveStatus = REMOVE_STATUS_IN_REMOVE_IF_HIDDEN; 434 final int index = mCallback.indexOfChild(view); 435 if (index == -1) { 436 if (unhideViewInternal(view) && DEBUG) { 437 throw new IllegalStateException("view is in hidden list but not in view group"); 438 } 439 return true; 440 } 441 if (mBucket.get(index)) { 442 mBucket.remove(index); 443 if (!unhideViewInternal(view) && DEBUG) { 444 throw new IllegalStateException( 445 "removed a hidden view but it is not in hidden views list"); 446 } 447 mCallback.removeViewAt(index); 448 return true; 449 } 450 return false; 451 } finally { 452 mRemoveStatus = REMOVE_STATUS_NONE; 453 } 454 } 455 456 /** 457 * Bitset implementation that provides methods to offset indices. 458 */ 459 static class Bucket { 460 461 static final int BITS_PER_WORD = Long.SIZE; 462 463 static final long LAST_BIT = 1L << (Long.SIZE - 1); 464 465 long mData = 0; 466 467 Bucket mNext; 468 set(int index)469 void set(int index) { 470 if (index >= BITS_PER_WORD) { 471 ensureNext(); 472 mNext.set(index - BITS_PER_WORD); 473 } else { 474 mData |= 1L << index; 475 } 476 } 477 ensureNext()478 private void ensureNext() { 479 if (mNext == null) { 480 mNext = new Bucket(); 481 } 482 } 483 clear(int index)484 void clear(int index) { 485 if (index >= BITS_PER_WORD) { 486 if (mNext != null) { 487 mNext.clear(index - BITS_PER_WORD); 488 } 489 } else { 490 mData &= ~(1L << index); 491 } 492 493 } 494 get(int index)495 boolean get(int index) { 496 if (index >= BITS_PER_WORD) { 497 ensureNext(); 498 return mNext.get(index - BITS_PER_WORD); 499 } else { 500 return (mData & (1L << index)) != 0; 501 } 502 } 503 reset()504 void reset() { 505 mData = 0; 506 if (mNext != null) { 507 mNext.reset(); 508 } 509 } 510 insert(int index, boolean value)511 void insert(int index, boolean value) { 512 if (index >= BITS_PER_WORD) { 513 ensureNext(); 514 mNext.insert(index - BITS_PER_WORD, value); 515 } else { 516 final boolean lastBit = (mData & LAST_BIT) != 0; 517 long mask = (1L << index) - 1; 518 final long before = mData & mask; 519 final long after = (mData & ~mask) << 1; 520 mData = before | after; 521 if (value) { 522 set(index); 523 } else { 524 clear(index); 525 } 526 if (lastBit || mNext != null) { 527 ensureNext(); 528 mNext.insert(0, lastBit); 529 } 530 } 531 } 532 remove(int index)533 boolean remove(int index) { 534 if (index >= BITS_PER_WORD) { 535 ensureNext(); 536 return mNext.remove(index - BITS_PER_WORD); 537 } else { 538 long mask = (1L << index); 539 final boolean value = (mData & mask) != 0; 540 mData &= ~mask; 541 mask = mask - 1; 542 final long before = mData & mask; 543 // cannot use >> because it adds one. 544 final long after = Long.rotateRight(mData & ~mask, 1); 545 mData = before | after; 546 if (mNext != null) { 547 if (mNext.get(0)) { 548 set(BITS_PER_WORD - 1); 549 } 550 mNext.remove(0); 551 } 552 return value; 553 } 554 } 555 countOnesBefore(int index)556 int countOnesBefore(int index) { 557 if (mNext == null) { 558 if (index >= BITS_PER_WORD) { 559 return Long.bitCount(mData); 560 } 561 return Long.bitCount(mData & ((1L << index) - 1)); 562 } 563 if (index < BITS_PER_WORD) { 564 return Long.bitCount(mData & ((1L << index) - 1)); 565 } else { 566 return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); 567 } 568 } 569 570 @Override toString()571 public String toString() { 572 return mNext == null ? Long.toBinaryString(mData) 573 : mNext.toString() + "xx" + Long.toBinaryString(mData); 574 } 575 } 576 577 interface Callback { 578 getChildCount()579 int getChildCount(); 580 addView(View child, int index)581 void addView(View child, int index); 582 indexOfChild(View view)583 int indexOfChild(View view); 584 removeViewAt(int index)585 void removeViewAt(int index); 586 getChildAt(int offset)587 View getChildAt(int offset); 588 removeAllViews()589 void removeAllViews(); 590 getChildViewHolder(View view)591 RecyclerView.ViewHolder getChildViewHolder(View view); 592 attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams)593 void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); 594 detachViewFromParent(int offset)595 void detachViewFromParent(int offset); 596 onEnteredHiddenState(View child)597 void onEnteredHiddenState(View child); 598 onLeftHiddenState(View child)599 void onLeftHiddenState(View child); 600 } 601 } 602