ChildHelper.java revision 668e774379c036a5d53d07ec69ed9ebee13a1fd9
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 final int limit = mCallback.getChildCount(); 91 int offset = index; 92 while (offset < limit) { 93 final int removedBefore = mBucket.countOnesBefore(offset); 94 final int diff = index - (offset - removedBefore); 95 if (diff == 0) { 96 while (mBucket.get(offset)) { // ensure this offset is not hidden 97 offset ++; 98 } 99 return offset; 100 } else { 101 offset += diff; 102 } 103 } 104 return -1; 105 } 106 107 /** 108 * Removes the provided View from underlying RecyclerView. 109 * 110 * @param view The view to remove. 111 */ 112 void removeView(View view) { 113 int index = mCallback.indexOfChild(view); 114 if (index < 0) { 115 return; 116 } 117 mCallback.removeViewAt(index); 118 if (mBucket.remove(index)) { 119 mHiddenViews.remove(view); 120 } 121 if (DEBUG) { 122 Log.d(TAG, "remove View off:" + index + "," + this); 123 } 124 } 125 126 /** 127 * Removes the view at the provided index from RecyclerView. 128 * 129 * @param index Index of the child from the regular perspective (excluding hidden views). 130 * ChildHelper offsets this index to actual ViewGroup index. 131 */ 132 void removeViewAt(int index) { 133 final int offset = getOffset(index); 134 final View view = mCallback.getChildAt(offset); 135 if (view == null) { 136 return; 137 } 138 mCallback.removeViewAt(offset); 139 if (mBucket.remove(offset)) { 140 mHiddenViews.remove(view); 141 } 142 if (DEBUG) { 143 Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this); 144 } 145 } 146 147 /** 148 * Returns the child at provided index. 149 * 150 * @param index Index of the child to return in regular perspective. 151 */ 152 View getChildAt(int index) { 153 final int offset = getOffset(index); 154 return mCallback.getChildAt(offset); 155 } 156 157 /** 158 * Removes all views from the ViewGroup including the hidden ones. 159 */ 160 void removeAllViewsUnfiltered() { 161 mCallback.removeAllViews(); 162 mBucket.reset(); 163 mHiddenViews.clear(); 164 if (DEBUG) { 165 Log.d(TAG, "removeAllViewsUnfiltered"); 166 } 167 } 168 169 /** 170 * This can be used to find a disappearing view by position. 171 * 172 * @param position The adapter position of the item. 173 * @param type View type, can be {@link RecyclerView#INVALID_TYPE}. 174 * @return A hidden view with a valid ViewHolder that matches the position and type. 175 */ 176 View findHiddenNonRemovedView(int position, int type) { 177 final int count = mHiddenViews.size(); 178 for (int i = 0; i < count; i++) { 179 final View view = mHiddenViews.get(i); 180 RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view); 181 if (holder.getPosition() == position && !holder.isInvalid() && 182 (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) { 183 return view; 184 } 185 } 186 return null; 187 } 188 189 /** 190 * Attaches the provided view to the underlying ViewGroup. 191 * 192 * @param child Child to attach. 193 * @param index Index of the child to attach in regular perspective. 194 * @param layoutParams LayoutParams for the child. 195 * @param hidden If set to true, this item will be invisible to the regular methods. 196 */ 197 void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, 198 boolean hidden) { 199 final int offset; 200 if (index < 0) { 201 offset = mCallback.getChildCount(); 202 } else { 203 offset = getOffset(index); 204 } 205 mCallback.attachViewToParent(child, offset, layoutParams); 206 mBucket.insert(offset, hidden); 207 if (DEBUG) { 208 Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," + 209 "h:" + hidden + ", " + this); 210 } 211 } 212 213 /** 214 * Returns the number of children that are not hidden. 215 * 216 * @return Number of children that are not hidden. 217 * @see #getChildAt(int) 218 */ 219 int getChildCount() { 220 return mCallback.getChildCount() - mHiddenViews.size(); 221 } 222 223 /** 224 * Returns the total number of children. 225 * 226 * @return The total number of children including the hidden views. 227 * @see #getUnfilteredChildAt(int) 228 */ 229 int getUnfilteredChildCount() { 230 return mCallback.getChildCount(); 231 } 232 233 /** 234 * Returns a child by ViewGroup offset. ChildHelper won't offset this index. 235 * 236 * @param index ViewGroup index of the child to return. 237 * @return The view in the provided index. 238 */ 239 View getUnfilteredChildAt(int index) { 240 return mCallback.getChildAt(index); 241 } 242 243 /** 244 * Detaches the view at the provided index. 245 * 246 * @param index Index of the child to return in regular perspective. 247 */ 248 void detachViewFromParent(int index) { 249 final int offset = getOffset(index); 250 mCallback.detachViewFromParent(offset); 251 mBucket.remove(offset); 252 if (DEBUG) { 253 Log.d(TAG, "detach view from parent " + index + ", off:" + offset); 254 } 255 } 256 257 /** 258 * Returns the index of the child in regular perspective. 259 * 260 * @param child The child whose index will be returned. 261 * @return The regular perspective index of the child or -1 if it does not exists. 262 */ 263 int indexOfChild(View child) { 264 final int index = mCallback.indexOfChild(child); 265 if (index == -1) { 266 return -1; 267 } 268 if (mBucket.get(index)) { 269 if (DEBUG) { 270 throw new IllegalArgumentException("cannot get index of a hidden child"); 271 } else { 272 return -1; 273 } 274 } 275 // reverse the index 276 return index - mBucket.countOnesBefore(index); 277 } 278 279 /** 280 * Marks a child view as hidden. 281 * 282 * @param view The view to hide. 283 */ 284 void hide(View view) { 285 final int offset = mCallback.indexOfChild(view); 286 if (offset < 0) { 287 throw new IllegalArgumentException("view is not a child, cannot hide " + view); 288 } 289 mBucket.set(offset); 290 mHiddenViews.add(view); 291 if (DEBUG) { 292 Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this); 293 } 294 } 295 296 @Override 297 public String toString() { 298 return mBucket.toString(); 299 } 300 301 /** 302 * Removes a view from the ViewGroup if it is hidden. 303 * 304 * @param view The view to remove. 305 * @return True if the View is found and it is hidden. False otherwise. 306 */ 307 boolean removeViewIfHidden(View view) { 308 final int index = mCallback.indexOfChild(view); 309 if (index < -1) { 310 if (DEBUG) { 311 if (mHiddenViews.contains(view)) { 312 throw new IllegalStateException("view is in hidden list but not in view group"); 313 } 314 } 315 return true; 316 } 317 if (mBucket.get(index)) { 318 mBucket.remove(index); 319 mCallback.removeViewAt(index); 320 if (!mHiddenViews.remove(view) && DEBUG) { 321 throw new IllegalStateException( 322 "removed a hidden view but it is not in hidden views list"); 323 } 324 return true; 325 } 326 return false; 327 } 328 329 /** 330 * Bitset implementation that provides methods to offset indices. 331 */ 332 static class Bucket { 333 334 final static int BITS_PER_WORD = Long.SIZE; 335 336 final static long LAST_BIT = 1L << (Long.SIZE - 1); 337 338 long mData = 0; 339 340 Bucket next; 341 342 void set(int index) { 343 if (index >= BITS_PER_WORD) { 344 ensureNext(); 345 next.set(index - BITS_PER_WORD); 346 } else { 347 mData |= 1L << index; 348 } 349 } 350 351 private void ensureNext() { 352 if (next == null) { 353 next = new Bucket(); 354 } 355 } 356 357 void clear(int index) { 358 if (index >= BITS_PER_WORD) { 359 if (next != null) { 360 next.clear(index - BITS_PER_WORD); 361 } 362 } else { 363 mData &= ~(1L << index); 364 } 365 366 } 367 368 boolean get(int index) { 369 if (index >= BITS_PER_WORD) { 370 ensureNext(); 371 return next.get(index - BITS_PER_WORD); 372 } else { 373 return (mData & (1L << index)) != 0; 374 } 375 } 376 377 void reset() { 378 mData = 0; 379 if (next != null) { 380 next.reset(); 381 } 382 } 383 384 void insert(int index, boolean value) { 385 if (index >= BITS_PER_WORD) { 386 ensureNext(); 387 next.insert(index - BITS_PER_WORD, value); 388 } else { 389 final boolean lastBit = (mData & LAST_BIT) != 0; 390 long mask = (1L << index) - 1; 391 final long before = mData & mask; 392 final long after = ((mData & ~mask)) << 1; 393 mData = before | after; 394 if (value) { 395 set(index); 396 } else { 397 clear(index); 398 } 399 if (lastBit || next != null) { 400 ensureNext(); 401 next.insert(0, lastBit); 402 } 403 } 404 } 405 406 boolean remove(int index) { 407 if (index >= BITS_PER_WORD) { 408 ensureNext(); 409 return next.remove(index - BITS_PER_WORD); 410 } else { 411 long mask = (1L << index); 412 final boolean value = (mData & mask) != 0; 413 mData &= ~mask; 414 mask = mask - 1; 415 final long before = mData & mask; 416 // cannot use >> because it adds one. 417 final long after = Long.rotateRight(mData & ~mask, 1); 418 mData = before | after; 419 if (next != null) { 420 if (next.get(0)) { 421 set(BITS_PER_WORD - 1); 422 } 423 next.remove(0); 424 } 425 return value; 426 } 427 } 428 429 int countOnesBefore(int index) { 430 if (next == null) { 431 if (index >= BITS_PER_WORD) { 432 return Long.bitCount(mData); 433 } 434 return Long.bitCount(mData & ((1L << index) - 1)); 435 } 436 if (index < BITS_PER_WORD) { 437 return Long.bitCount(mData & ((1L << index) - 1)); 438 } else { 439 return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); 440 } 441 } 442 443 @Override 444 public String toString() { 445 return next == null ? Long.toBinaryString(mData) 446 : next.toString() + "xx" + Long.toBinaryString(mData); 447 } 448 } 449 450 static interface Callback { 451 452 int getChildCount(); 453 454 void addView(View child, int index); 455 456 int indexOfChild(View view); 457 458 void removeViewAt(int index); 459 460 View getChildAt(int offset); 461 462 void removeAllViews(); 463 464 RecyclerView.ViewHolder getChildViewHolder(View view); 465 466 void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); 467 468 void detachViewFromParent(int offset); 469 } 470} 471