ChildHelper.java revision 504c54ea52c1b2aae6f8f4ae128f1dcaac7e3f6a
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 * Marks a child view as hidden. 284 * 285 * @param view The view to hide. 286 */ 287 void hide(View view) { 288 final int offset = mCallback.indexOfChild(view); 289 if (offset < 0) { 290 throw new IllegalArgumentException("view is not a child, cannot hide " + view); 291 } 292 mBucket.set(offset); 293 mHiddenViews.add(view); 294 if (DEBUG) { 295 Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this); 296 } 297 } 298 299 @Override 300 public String toString() { 301 return mBucket.toString(); 302 } 303 304 /** 305 * Removes a view from the ViewGroup if it is hidden. 306 * 307 * @param view The view to remove. 308 * @return True if the View is found and it is hidden. False otherwise. 309 */ 310 boolean removeViewIfHidden(View view) { 311 final int index = mCallback.indexOfChild(view); 312 if (index == -1) { 313 if (mHiddenViews.remove(view) && DEBUG) { 314 throw new IllegalStateException("view is in hidden list but not in view group"); 315 } 316 return true; 317 } 318 if (mBucket.get(index)) { 319 mBucket.remove(index); 320 mCallback.removeViewAt(index); 321 if (!mHiddenViews.remove(view) && DEBUG) { 322 throw new IllegalStateException( 323 "removed a hidden view but it is not in hidden views list"); 324 } 325 return true; 326 } 327 return false; 328 } 329 330 /** 331 * Bitset implementation that provides methods to offset indices. 332 */ 333 static class Bucket { 334 335 final static int BITS_PER_WORD = Long.SIZE; 336 337 final static long LAST_BIT = 1L << (Long.SIZE - 1); 338 339 long mData = 0; 340 341 Bucket next; 342 343 void set(int index) { 344 if (index >= BITS_PER_WORD) { 345 ensureNext(); 346 next.set(index - BITS_PER_WORD); 347 } else { 348 mData |= 1L << index; 349 } 350 } 351 352 private void ensureNext() { 353 if (next == null) { 354 next = new Bucket(); 355 } 356 } 357 358 void clear(int index) { 359 if (index >= BITS_PER_WORD) { 360 if (next != null) { 361 next.clear(index - BITS_PER_WORD); 362 } 363 } else { 364 mData &= ~(1L << index); 365 } 366 367 } 368 369 boolean get(int index) { 370 if (index >= BITS_PER_WORD) { 371 ensureNext(); 372 return next.get(index - BITS_PER_WORD); 373 } else { 374 return (mData & (1L << index)) != 0; 375 } 376 } 377 378 void reset() { 379 mData = 0; 380 if (next != null) { 381 next.reset(); 382 } 383 } 384 385 void insert(int index, boolean value) { 386 if (index >= BITS_PER_WORD) { 387 ensureNext(); 388 next.insert(index - BITS_PER_WORD, value); 389 } else { 390 final boolean lastBit = (mData & LAST_BIT) != 0; 391 long mask = (1L << index) - 1; 392 final long before = mData & mask; 393 final long after = ((mData & ~mask)) << 1; 394 mData = before | after; 395 if (value) { 396 set(index); 397 } else { 398 clear(index); 399 } 400 if (lastBit || next != null) { 401 ensureNext(); 402 next.insert(0, lastBit); 403 } 404 } 405 } 406 407 boolean remove(int index) { 408 if (index >= BITS_PER_WORD) { 409 ensureNext(); 410 return next.remove(index - BITS_PER_WORD); 411 } else { 412 long mask = (1L << index); 413 final boolean value = (mData & mask) != 0; 414 mData &= ~mask; 415 mask = mask - 1; 416 final long before = mData & mask; 417 // cannot use >> because it adds one. 418 final long after = Long.rotateRight(mData & ~mask, 1); 419 mData = before | after; 420 if (next != null) { 421 if (next.get(0)) { 422 set(BITS_PER_WORD - 1); 423 } 424 next.remove(0); 425 } 426 return value; 427 } 428 } 429 430 int countOnesBefore(int index) { 431 if (next == null) { 432 if (index >= BITS_PER_WORD) { 433 return Long.bitCount(mData); 434 } 435 return Long.bitCount(mData & ((1L << index) - 1)); 436 } 437 if (index < BITS_PER_WORD) { 438 return Long.bitCount(mData & ((1L << index) - 1)); 439 } else { 440 return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); 441 } 442 } 443 444 @Override 445 public String toString() { 446 return next == null ? Long.toBinaryString(mData) 447 : next.toString() + "xx" + Long.toBinaryString(mData); 448 } 449 } 450 451 static interface Callback { 452 453 int getChildCount(); 454 455 void addView(View child, int index); 456 457 int indexOfChild(View view); 458 459 void removeViewAt(int index); 460 461 View getChildAt(int offset); 462 463 void removeAllViews(); 464 465 RecyclerView.ViewHolder getChildViewHolder(View view); 466 467 void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); 468 469 void detachViewFromParent(int offset); 470 } 471} 472