ChildHelper.java revision a5dc6f3eb86aacd2b287f6ea588aa1d900f6702a
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.getPosition() == 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 (DEBUG) { 211 Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," + 212 "h:" + hidden + ", " + this); 213 } 214 } 215 216 /** 217 * Returns the number of children that are not hidden. 218 * 219 * @return Number of children that are not hidden. 220 * @see #getChildAt(int) 221 */ 222 int getChildCount() { 223 return mCallback.getChildCount() - mHiddenViews.size(); 224 } 225 226 /** 227 * Returns the total number of children. 228 * 229 * @return The total number of children including the hidden views. 230 * @see #getUnfilteredChildAt(int) 231 */ 232 int getUnfilteredChildCount() { 233 return mCallback.getChildCount(); 234 } 235 236 /** 237 * Returns a child by ViewGroup offset. ChildHelper won't offset this index. 238 * 239 * @param index ViewGroup index of the child to return. 240 * @return The view in the provided index. 241 */ 242 View getUnfilteredChildAt(int index) { 243 return mCallback.getChildAt(index); 244 } 245 246 /** 247 * Detaches the view at the provided index. 248 * 249 * @param index Index of the child to return in regular perspective. 250 */ 251 void detachViewFromParent(int index) { 252 final int offset = getOffset(index); 253 mCallback.detachViewFromParent(offset); 254 mBucket.remove(offset); 255 if (DEBUG) { 256 Log.d(TAG, "detach view from parent " + index + ", off:" + offset); 257 } 258 } 259 260 /** 261 * Returns the index of the child in regular perspective. 262 * 263 * @param child The child whose index will be returned. 264 * @return The regular perspective index of the child or -1 if it does not exists. 265 */ 266 int indexOfChild(View child) { 267 final int index = mCallback.indexOfChild(child); 268 if (index == -1) { 269 return -1; 270 } 271 if (mBucket.get(index)) { 272 if (DEBUG) { 273 throw new IllegalArgumentException("cannot get index of a hidden child"); 274 } else { 275 return -1; 276 } 277 } 278 // reverse the index 279 return index - mBucket.countOnesBefore(index); 280 } 281 282 /** 283 * Returns whether a View is visible to LayoutManager or not. 284 * 285 * @param view The child view to check. Should be a child of the Callback. 286 * @return True if the View is not visible to LayoutManager 287 */ 288 boolean isHidden(View view) { 289 return mHiddenViews.contains(view); 290 } 291 292 /** 293 * Marks a child view as hidden. 294 * 295 * @param view The view to hide. 296 */ 297 void hide(View view) { 298 final int offset = mCallback.indexOfChild(view); 299 if (offset < 0) { 300 throw new IllegalArgumentException("view is not a child, cannot hide " + view); 301 } 302 if (DEBUG && mBucket.get(offset)) { 303 throw new RuntimeException("trying to hide same view twice, how come ? " + view); 304 } 305 mBucket.set(offset); 306 mHiddenViews.add(view); 307 if (DEBUG) { 308 Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this); 309 } 310 } 311 312 @Override 313 public String toString() { 314 return mBucket.toString(); 315 } 316 317 /** 318 * Removes a view from the ViewGroup if it is hidden. 319 * 320 * @param view The view to remove. 321 * @return True if the View is found and it is hidden. False otherwise. 322 */ 323 boolean removeViewIfHidden(View view) { 324 final int index = mCallback.indexOfChild(view); 325 if (index == -1) { 326 if (mHiddenViews.remove(view) && DEBUG) { 327 throw new IllegalStateException("view is in hidden list but not in view group"); 328 } 329 return true; 330 } 331 if (mBucket.get(index)) { 332 mBucket.remove(index); 333 mCallback.removeViewAt(index); 334 if (!mHiddenViews.remove(view) && DEBUG) { 335 throw new IllegalStateException( 336 "removed a hidden view but it is not in hidden views list"); 337 } 338 return true; 339 } 340 return false; 341 } 342 343 /** 344 * Bitset implementation that provides methods to offset indices. 345 */ 346 static class Bucket { 347 348 final static int BITS_PER_WORD = Long.SIZE; 349 350 final static long LAST_BIT = 1L << (Long.SIZE - 1); 351 352 long mData = 0; 353 354 Bucket next; 355 356 void set(int index) { 357 if (index >= BITS_PER_WORD) { 358 ensureNext(); 359 next.set(index - BITS_PER_WORD); 360 } else { 361 mData |= 1L << index; 362 } 363 } 364 365 private void ensureNext() { 366 if (next == null) { 367 next = new Bucket(); 368 } 369 } 370 371 void clear(int index) { 372 if (index >= BITS_PER_WORD) { 373 if (next != null) { 374 next.clear(index - BITS_PER_WORD); 375 } 376 } else { 377 mData &= ~(1L << index); 378 } 379 380 } 381 382 boolean get(int index) { 383 if (index >= BITS_PER_WORD) { 384 ensureNext(); 385 return next.get(index - BITS_PER_WORD); 386 } else { 387 return (mData & (1L << index)) != 0; 388 } 389 } 390 391 void reset() { 392 mData = 0; 393 if (next != null) { 394 next.reset(); 395 } 396 } 397 398 void insert(int index, boolean value) { 399 if (index >= BITS_PER_WORD) { 400 ensureNext(); 401 next.insert(index - BITS_PER_WORD, value); 402 } else { 403 final boolean lastBit = (mData & LAST_BIT) != 0; 404 long mask = (1L << index) - 1; 405 final long before = mData & mask; 406 final long after = ((mData & ~mask)) << 1; 407 mData = before | after; 408 if (value) { 409 set(index); 410 } else { 411 clear(index); 412 } 413 if (lastBit || next != null) { 414 ensureNext(); 415 next.insert(0, lastBit); 416 } 417 } 418 } 419 420 boolean remove(int index) { 421 if (index >= BITS_PER_WORD) { 422 ensureNext(); 423 return next.remove(index - BITS_PER_WORD); 424 } else { 425 long mask = (1L << index); 426 final boolean value = (mData & mask) != 0; 427 mData &= ~mask; 428 mask = mask - 1; 429 final long before = mData & mask; 430 // cannot use >> because it adds one. 431 final long after = Long.rotateRight(mData & ~mask, 1); 432 mData = before | after; 433 if (next != null) { 434 if (next.get(0)) { 435 set(BITS_PER_WORD - 1); 436 } 437 next.remove(0); 438 } 439 return value; 440 } 441 } 442 443 int countOnesBefore(int index) { 444 if (next == null) { 445 if (index >= BITS_PER_WORD) { 446 return Long.bitCount(mData); 447 } 448 return Long.bitCount(mData & ((1L << index) - 1)); 449 } 450 if (index < BITS_PER_WORD) { 451 return Long.bitCount(mData & ((1L << index) - 1)); 452 } else { 453 return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); 454 } 455 } 456 457 @Override 458 public String toString() { 459 return next == null ? Long.toBinaryString(mData) 460 : next.toString() + "xx" + Long.toBinaryString(mData); 461 } 462 } 463 464 static interface Callback { 465 466 int getChildCount(); 467 468 void addView(View child, int index); 469 470 int indexOfChild(View view); 471 472 void removeViewAt(int index); 473 474 View getChildAt(int offset); 475 476 void removeAllViews(); 477 478 RecyclerView.ViewHolder getChildViewHolder(View view); 479 480 void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); 481 482 void detachViewFromParent(int offset); 483 } 484} 485