1 /* 2 * Copyright (C) 2017 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 com.android.internal.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 * Marks a child view as hidden 56 * 57 * @param child View to hide. 58 */ hideViewInternal(View child)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 */ unhideViewInternal(View child)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 */ addView(View child, boolean hidden)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 */ addView(View child, int index, boolean hidden)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 getOffset(int index)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 */ removeView(View view)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 */ removeViewAt(int index)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 */ getChildAt(int index)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 */ removeAllViewsUnfiltered()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 * @return A hidden view with a valid ViewHolder that matches the position. 204 */ findHiddenNonRemovedView(int position)205 View findHiddenNonRemovedView(int position) { 206 final int count = mHiddenViews.size(); 207 for (int i = 0; i < count; i++) { 208 final View view = mHiddenViews.get(i); 209 RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view); 210 if (holder.getLayoutPosition() == position 211 && !holder.isInvalid() 212 && !holder.isRemoved()) { 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 */ attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, boolean hidden)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 */ getChildCount()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 */ getUnfilteredChildCount()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 */ getUnfilteredChildAt(int index)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 */ detachViewFromParent(int index)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 */ indexOfChild(View child)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 */ isHidden(View view)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 */ hide(View view)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 */ unhide(View view)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 toString()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 */ removeViewIfHidden(View view)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 static final int BITS_PER_WORD = Long.SIZE; 398 399 static final long LAST_BIT = 1L << (Long.SIZE - 1); 400 401 long mData = 0; 402 403 Bucket mNext; 404 set(int index)405 void set(int index) { 406 if (index >= BITS_PER_WORD) { 407 ensureNext(); 408 mNext.set(index - BITS_PER_WORD); 409 } else { 410 mData |= 1L << index; 411 } 412 } 413 ensureNext()414 private void ensureNext() { 415 if (mNext == null) { 416 mNext = new Bucket(); 417 } 418 } 419 clear(int index)420 void clear(int index) { 421 if (index >= BITS_PER_WORD) { 422 if (mNext != null) { 423 mNext.clear(index - BITS_PER_WORD); 424 } 425 } else { 426 mData &= ~(1L << index); 427 } 428 429 } 430 get(int index)431 boolean get(int index) { 432 if (index >= BITS_PER_WORD) { 433 ensureNext(); 434 return mNext.get(index - BITS_PER_WORD); 435 } else { 436 return (mData & (1L << index)) != 0; 437 } 438 } 439 reset()440 void reset() { 441 mData = 0; 442 if (mNext != null) { 443 mNext.reset(); 444 } 445 } 446 insert(int index, boolean value)447 void insert(int index, boolean value) { 448 if (index >= BITS_PER_WORD) { 449 ensureNext(); 450 mNext.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 || mNext != null) { 463 ensureNext(); 464 mNext.insert(0, lastBit); 465 } 466 } 467 } 468 remove(int index)469 boolean remove(int index) { 470 if (index >= BITS_PER_WORD) { 471 ensureNext(); 472 return mNext.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 (mNext != null) { 483 if (mNext.get(0)) { 484 set(BITS_PER_WORD - 1); 485 } 486 mNext.remove(0); 487 } 488 return value; 489 } 490 } 491 countOnesBefore(int index)492 int countOnesBefore(int index) { 493 if (mNext == 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 mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); 503 } 504 } 505 506 @Override toString()507 public String toString() { 508 return mNext == null ? Long.toBinaryString(mData) 509 : mNext.toString() + "xx" + Long.toBinaryString(mData); 510 } 511 } 512 513 interface Callback { 514 getChildCount()515 int getChildCount(); 516 addView(View child, int index)517 void addView(View child, int index); 518 indexOfChild(View view)519 int indexOfChild(View view); 520 removeViewAt(int index)521 void removeViewAt(int index); 522 getChildAt(int offset)523 View getChildAt(int offset); 524 removeAllViews()525 void removeAllViews(); 526 getChildViewHolder(View view)527 RecyclerView.ViewHolder getChildViewHolder(View view); 528 attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams)529 void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); 530 detachViewFromParent(int offset)531 void detachViewFromParent(int offset); 532 onEnteredHiddenState(View child)533 void onEnteredHiddenState(View child); 534 onLeftHiddenState(View child)535 void onLeftHiddenState(View child); 536 } 537 } 538 539