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 package androidx.recyclerview.widget; 17 18 import android.content.Context; 19 import android.graphics.Rect; 20 import android.os.Build; 21 import android.os.Bundle; 22 import android.util.AttributeSet; 23 import android.util.Log; 24 import android.util.SparseIntArray; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.view.accessibility.AccessibilityEvent; 28 import android.view.accessibility.AccessibilityNodeInfo; 29 import android.widget.GridView; 30 31 import androidx.annotation.RequiresApi; 32 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 33 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; 34 35 import org.jspecify.annotations.NonNull; 36 import org.jspecify.annotations.Nullable; 37 38 import java.util.Arrays; 39 import java.util.Collections; 40 import java.util.HashSet; 41 import java.util.Map; 42 import java.util.Objects; 43 import java.util.Set; 44 import java.util.TreeMap; 45 46 /** 47 * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid. 48 * <p> 49 * By default, each item occupies 1 span. You can change it by providing a custom 50 * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}. 51 */ 52 public class GridLayoutManager extends LinearLayoutManager { 53 54 private static final boolean DEBUG = false; 55 private static final String TAG = "GridLayoutManager"; 56 public static final int DEFAULT_SPAN_COUNT = -1; 57 private static final int INVALID_POSITION = -1; 58 59 private static final Set<Integer> sSupportedDirectionsForActionScrollInDirection = 60 Collections.unmodifiableSet(new HashSet<>(Arrays.asList( 61 View.FOCUS_LEFT, 62 View.FOCUS_RIGHT, 63 View.FOCUS_UP, 64 View.FOCUS_DOWN))); 65 66 /** 67 * Span size have been changed but we've not done a new layout calculation. 68 */ 69 boolean mPendingSpanCountChange = false; 70 int mSpanCount = DEFAULT_SPAN_COUNT; 71 /** 72 * Right borders for each span. 73 * <p>For <b>i-th</b> item start is {@link #mCachedBorders}[i-1] + 1 74 * and end is {@link #mCachedBorders}[i]. 75 */ 76 int [] mCachedBorders; 77 /** 78 * Temporary array to keep views in layoutChunk method 79 */ 80 View[] mSet; 81 final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray(); 82 final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray(); 83 SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); 84 // re-used variable to acquire decor insets from RecyclerView 85 final Rect mDecorInsets = new Rect(); 86 87 private boolean mUsingSpansToEstimateScrollBarDimensions; 88 89 /** 90 * Used to track the position of the target node brought on screen by 91 * {@code ACTIONS_SCROLL_IN_DIRECTION} so that a {@code TYPE_VIEW_TARGETED_BY_SCROLL} event can 92 * be emitted. 93 */ 94 private int mPositionTargetedByScrollInDirection = INVALID_POSITION; 95 96 /** 97 * Stores the index of the row with accessibility focus for use with 98 * {@link AccessibilityNodeInfoCompat.AccessibilityActionCompat#ACTION_SCROLL_IN_DIRECTION}. 99 * This may include a position that is spanned by a grid child. For example, in the following 100 * grid... 101 * 0 3 4 102 * 1 3 5 103 * 2 3 6 104 * ...the child at adapter position 3 (which spans three rows) could have a row index of either 105 * 0, 1, or 2, and the choice may depend on which row of the grid previously had 106 * accessibility focus. Note that for single span cells, the row index stored here should be 107 * the same as the value returned by {@code getRowIndex()}. 108 */ 109 int mRowWithAccessibilityFocus = INVALID_POSITION; 110 111 /** 112 * Stores the index of the column with accessibility focus for use with 113 * {@link AccessibilityNodeInfoCompat.AccessibilityActionCompat#ACTION_SCROLL_IN_DIRECTION}. 114 * This may include a position that is spanned by a grid child. For example, in the following 115 * grid... 116 * 0 1 2 117 * 3 3 3 118 * 4 5 6 119 * ... the child at adapter position 3 (which spans three columns) could have a column index 120 * of either 0, 1, or 2, and the choice may depend on which column of the grid previously had 121 * accessibility focus. Note that for single span cells, the column index stored here should be 122 * the same as the value returned by {@code getColumnIndex()}. 123 */ 124 int mColumnWithAccessibilityFocus = INVALID_POSITION; 125 126 /** 127 * Constructor used when layout manager is set in XML by RecyclerView attribute 128 * "layoutManager". If spanCount is not specified in the XML, it defaults to a 129 * single column. 130 * 131 * {@link androidx.recyclerview.R.attr#spanCount} 132 */ GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)133 public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, 134 int defStyleRes) { 135 super(context, attrs, defStyleAttr, defStyleRes); 136 Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); 137 setSpanCount(properties.spanCount); 138 } 139 140 /** 141 * Creates a vertical GridLayoutManager 142 * 143 * @param context Current context, will be used to access resources. 144 * @param spanCount The number of columns in the grid 145 */ GridLayoutManager(Context context, int spanCount)146 public GridLayoutManager(Context context, int spanCount) { 147 super(context); 148 setSpanCount(spanCount); 149 } 150 151 /** 152 * @param context Current context, will be used to access resources. 153 * @param spanCount The number of columns or rows in the grid 154 * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link 155 * #VERTICAL}. 156 * @param reverseLayout When set to true, layouts from end to start. 157 */ GridLayoutManager(Context context, int spanCount, @RecyclerView.Orientation int orientation, boolean reverseLayout)158 public GridLayoutManager(Context context, int spanCount, 159 @RecyclerView.Orientation int orientation, boolean reverseLayout) { 160 super(context, orientation, reverseLayout); 161 setSpanCount(spanCount); 162 } 163 164 /** 165 * stackFromEnd is not supported by GridLayoutManager. Consider using 166 * {@link #setReverseLayout(boolean)}. 167 */ 168 @Override setStackFromEnd(boolean stackFromEnd)169 public void setStackFromEnd(boolean stackFromEnd) { 170 if (stackFromEnd) { 171 throw new UnsupportedOperationException( 172 "GridLayoutManager does not support stack from end." 173 + " Consider using reverse layout"); 174 } 175 super.setStackFromEnd(false); 176 } 177 178 @Override getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)179 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 180 RecyclerView.State state) { 181 if (mOrientation == HORIZONTAL) { 182 return Math.min(mSpanCount, getItemCount()); 183 } 184 if (state.getItemCount() < 1) { 185 return 0; 186 } 187 188 // Row count is one more than the last item's row index. 189 return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1; 190 } 191 192 @Override getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)193 public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, 194 RecyclerView.State state) { 195 if (mOrientation == VERTICAL) { 196 return Math.min(mSpanCount, getItemCount()); 197 } 198 if (state.getItemCount() < 1) { 199 return 0; 200 } 201 202 // Column count is one more than the last item's column index. 203 return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1; 204 } 205 206 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)207 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 208 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 209 ViewGroup.LayoutParams lp = host.getLayoutParams(); 210 if (!(lp instanceof LayoutParams)) { 211 super.onInitializeAccessibilityNodeInfoForItem(host, info); 212 return; 213 } 214 LayoutParams glp = (LayoutParams) lp; 215 int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition()); 216 if (mOrientation == HORIZONTAL) { 217 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 218 glp.getSpanIndex(), glp.getSpanSize(), 219 spanGroupIndex, 1, false, false)); 220 } else { // VERTICAL 221 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 222 spanGroupIndex , 1, 223 glp.getSpanIndex(), glp.getSpanSize(), false, false)); 224 } 225 } 226 227 @Override onInitializeAccessibilityNodeInfo(RecyclerView.@onNull Recycler recycler, RecyclerView.@NonNull State state, @NonNull AccessibilityNodeInfoCompat info)228 public void onInitializeAccessibilityNodeInfo(RecyclerView.@NonNull Recycler recycler, 229 RecyclerView.@NonNull State state, @NonNull AccessibilityNodeInfoCompat info) { 230 super.onInitializeAccessibilityNodeInfo(recycler, state, info); 231 // Set the class name so this is treated as a grid. A11y services should identify grids 232 // and list via CollectionInfos, but an almost empty grid may be incorrectly identified 233 // as a list. 234 info.setClassName(GridView.class.getName()); 235 236 if (mRecyclerView.mAdapter != null && mRecyclerView.mAdapter.getItemCount() > 1) { 237 info.addAction(AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION); 238 } 239 } 240 241 @Override performAccessibilityAction(int action, @Nullable Bundle args)242 boolean performAccessibilityAction(int action, @Nullable Bundle args) { 243 // TODO (267511848): when U constants are finalized: 244 // - convert if/else blocks to switch statement 245 // - remove SDK check 246 // - remove the -1 check (this check makes accessibilityActionScrollInDirection 247 // no-op for < 34; see action definition in AccessibilityNodeInfoCompat.java). 248 if (action == AccessibilityActionCompat.ACTION_SCROLL_IN_DIRECTION.getId() 249 && action != -1) { 250 final View viewWithAccessibilityFocus = findChildWithAccessibilityFocus(); 251 if (viewWithAccessibilityFocus == null) { 252 // TODO(b/268487724#comment2): handle rare cases when the requesting service does 253 // not place accessibility focus on a child. Consider scrolling forward/backward? 254 return false; 255 } 256 257 // Direction must be specified. 258 if (args == null) { 259 return false; 260 } 261 262 final int direction = args.getInt( 263 AccessibilityNodeInfo.ACTION_ARGUMENT_DIRECTION_INT, INVALID_POSITION); 264 265 if (!sSupportedDirectionsForActionScrollInDirection.contains(direction)) { 266 if (DEBUG) { 267 Log.w(TAG, "Direction equals " + direction 268 + "which is unsupported when using ACTION_SCROLL_IN_DIRECTION"); 269 } 270 return false; 271 } 272 273 RecyclerView.ViewHolder vh = 274 mRecyclerView.getChildViewHolder(viewWithAccessibilityFocus); 275 if (vh == null) { 276 if (DEBUG) { 277 throw new RuntimeException( 278 "viewHolder is null for " + viewWithAccessibilityFocus); 279 } 280 return false; 281 } 282 283 int startingAdapterPosition = vh.getAbsoluteAdapterPosition(); 284 int startingRow = getRowIndex(startingAdapterPosition); 285 int startingColumn = getColumnIndex(startingAdapterPosition); 286 287 if (startingRow < 0 || startingColumn < 0) { 288 if (DEBUG) { 289 throw new RuntimeException("startingRow equals " + startingRow + ", and " 290 + "startingColumn equals " + startingColumn + ", and neither can be " 291 + "less than 0."); 292 } 293 return false; 294 } 295 296 if (hasAccessibilityFocusChanged(startingAdapterPosition)) { 297 mRowWithAccessibilityFocus = startingRow; 298 mColumnWithAccessibilityFocus = startingColumn; 299 } 300 301 int scrollTargetPosition; 302 303 int row = (mRowWithAccessibilityFocus == INVALID_POSITION) ? startingRow 304 : mRowWithAccessibilityFocus; 305 int column = (mColumnWithAccessibilityFocus == INVALID_POSITION) 306 ? startingColumn : mColumnWithAccessibilityFocus; 307 308 switch (direction) { 309 case View.FOCUS_LEFT: 310 scrollTargetPosition = findScrollTargetPositionOnTheLeft(row, column, 311 startingAdapterPosition); 312 break; 313 case View.FOCUS_RIGHT: 314 scrollTargetPosition = 315 findScrollTargetPositionOnTheRight(row, column, 316 startingAdapterPosition); 317 break; 318 case View.FOCUS_UP: 319 scrollTargetPosition = findScrollTargetPositionAbove(row, column, 320 startingAdapterPosition); 321 break; 322 case View.FOCUS_DOWN: 323 scrollTargetPosition = findScrollTargetPositionBelow(row, column, 324 startingAdapterPosition); 325 break; 326 default: 327 return false; 328 } 329 330 if (scrollTargetPosition == INVALID_POSITION 331 && mOrientation == RecyclerView.HORIZONTAL) { 332 // TODO (b/268487724): handle RTL. 333 // Handle case in grids with horizontal orientation where the scroll target is on 334 // a different row. 335 if (direction == View.FOCUS_LEFT) { 336 scrollTargetPosition = findPositionOfLastItemOnARowAboveForHorizontalGrid( 337 startingRow); 338 } else if (direction == View.FOCUS_RIGHT) { 339 scrollTargetPosition = findPositionOfFirstItemOnARowBelowForHorizontalGrid( 340 startingRow); 341 } 342 } 343 344 if (scrollTargetPosition != INVALID_POSITION) { 345 scrollToPosition(scrollTargetPosition); 346 mPositionTargetedByScrollInDirection = scrollTargetPosition; 347 return true; 348 } 349 350 return false; 351 } else if (action == android.R.id.accessibilityActionScrollToPosition) { 352 final int noRow = -1; 353 final int noColumn = -1; 354 if (args != null) { 355 int rowArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT, 356 noRow); 357 int columnArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT, 358 noColumn); 359 360 if (rowArg == noRow || columnArg == noColumn) { 361 return false; 362 } 363 364 int itemCount = mRecyclerView.mAdapter.getItemCount(); 365 366 int position = -1; 367 for (int i = 0; i < itemCount; i++) { 368 // Corresponds to a column value if the orientation is VERTICAL and a row value 369 // if the orientation is HORIZONTAL 370 int spanIndex = getSpanIndex(mRecyclerView.mRecycler, mRecyclerView.mState, i); 371 372 // Corresponds to a row value if the orientation is VERTICAL and a column value 373 // if the orientation is HORIZONTAL 374 int spanGroupIndex = getSpanGroupIndex(mRecyclerView.mRecycler, 375 mRecyclerView.mState, i); 376 377 if (mOrientation == VERTICAL) { 378 if (spanIndex == columnArg && spanGroupIndex == rowArg) { 379 position = i; 380 break; 381 } 382 } else { // horizontal 383 if (spanIndex == rowArg && spanGroupIndex == columnArg) { 384 position = i; 385 break; 386 } 387 } 388 } 389 390 if (position > -1) { 391 scrollToPositionWithOffset(position, 0); 392 return true; 393 } 394 return false; 395 } 396 } 397 return super.performAccessibilityAction(action, args); 398 } 399 findScrollTargetPositionOnTheRight(int startingRow, int startingColumn, int startingAdapterPosition)400 private int findScrollTargetPositionOnTheRight(int startingRow, int startingColumn, 401 int startingAdapterPosition) { 402 int scrollTargetPosition = INVALID_POSITION; 403 for (int i = startingAdapterPosition + 1; i < getItemCount(); i++) { 404 int currentRow = getRowIndex(i); 405 int currentColumn = getColumnIndex(i); 406 407 if (currentRow < 0 || currentColumn < 0) { 408 if (DEBUG) { 409 throw new RuntimeException("currentRow equals " + currentRow + ", and " 410 + "currentColumn equals " + currentColumn + ", and neither can be " 411 + "less than 0."); 412 } 413 return INVALID_POSITION; 414 } 415 416 if (mOrientation == VERTICAL) { 417 /* 418 * For grids with vertical orientation... 419 * 1 2 3 420 * 4 5 5 421 * 6 7 422 * ... the scroll target may lie on the same or a following row. 423 */ 424 // TODO (b/268487724): handle RTL. 425 if ((currentRow == startingRow && currentColumn > startingColumn) 426 || (currentRow > startingRow)) { 427 mRowWithAccessibilityFocus = currentRow; 428 mColumnWithAccessibilityFocus = currentColumn; 429 return i; 430 } 431 } else { // HORIZONTAL 432 /* 433 * For grids with horizontal orientation, the scroll target may span multiple 434 * rows. For example, in this grid... 435 * 1 4 6 436 * 2 5 7 437 * 3 5 8 438 * ... moving from 3 to 5 is considered staying on the "same row" because 5 spans 439 * multiple rows and the row indices for 5 include 3's row. 440 */ 441 if (currentColumn > startingColumn && getRowIndices(i).contains(startingRow)) { 442 // Note: mRowWithAccessibilityFocus not updated since the scroll target is on 443 // the same row. 444 mColumnWithAccessibilityFocus = currentColumn; 445 return i; 446 } 447 } 448 } 449 450 return scrollTargetPosition; 451 } 452 findScrollTargetPositionOnTheLeft(int startingRow, int startingColumn, int startingAdapterPosition)453 private int findScrollTargetPositionOnTheLeft(int startingRow, int startingColumn, 454 int startingAdapterPosition) { 455 int scrollTargetPosition = INVALID_POSITION; 456 for (int i = startingAdapterPosition - 1; i >= 0; i--) { 457 int currentRow = getRowIndex(i); 458 int currentColumn = getColumnIndex(i); 459 460 if (currentRow < 0 || currentColumn < 0) { 461 if (DEBUG) { 462 throw new RuntimeException("currentRow equals " + currentRow + ", and " 463 + "currentColumn equals " + currentColumn + ", and neither can be " 464 + "less than 0."); 465 } 466 return INVALID_POSITION; 467 } 468 469 if (mOrientation == VERTICAL) { 470 /* 471 * For grids with vertical orientation... 472 * 1 2 3 473 * 4 5 5 474 * 6 7 475 * ... the scroll target may lie on the same or a preceding row. 476 */ 477 // TODO (b/268487724): handle RTL. 478 if ((currentRow == startingRow && currentColumn < startingColumn) 479 || (currentRow < startingRow)) { 480 scrollTargetPosition = i; 481 mRowWithAccessibilityFocus = currentRow; 482 mColumnWithAccessibilityFocus = currentColumn; 483 break; 484 } 485 } else { // HORIZONTAL 486 /* 487 * For grids with horizontal orientation, the scroll target may span multiple 488 * rows. For example, in this grid... 489 * 1 4 6 490 * 2 5 7 491 * 3 5 8 492 * ... moving from 8 to 5 or from 7 to 5 is considered staying on the "same row" 493 * because the row indices for 5 include 8's and 7's row. 494 */ 495 if (getRowIndices(i).contains(startingRow) && currentColumn < startingColumn) { 496 // Note: mRowWithAccessibilityFocus not updated since the scroll target is on 497 // the same row. 498 mColumnWithAccessibilityFocus = currentColumn; 499 return i; 500 } 501 } 502 } 503 return scrollTargetPosition; 504 } 505 findScrollTargetPositionAbove(int startingRow, int startingColumn, int startingAdapterPosition)506 private int findScrollTargetPositionAbove(int startingRow, int startingColumn, 507 int startingAdapterPosition) { 508 int scrollTargetPosition = INVALID_POSITION; 509 for (int i = startingAdapterPosition - 1; i >= 0; i--) { 510 int currentRow = getRowIndex(i); 511 int currentColumn = getColumnIndex(i); 512 513 if (currentRow < 0 || currentColumn < 0) { 514 if (DEBUG) { 515 throw new RuntimeException("currentRow equals " + currentRow + ", and " 516 + "currentColumn equals " + currentColumn + ", and neither can be " 517 + "less than 0."); 518 } 519 return INVALID_POSITION; 520 } 521 522 if (mOrientation == VERTICAL) { 523 /* 524 * The scroll target may span multiple columns. For example, in this grid... 525 * 1 2 3 526 * 4 4 5 527 * 6 7 528 * ... moving from 7 to 4 interprets as staying in second column, and moving from 529 * 6 to 4 interprets as staying in the first column. 530 */ 531 if (currentRow < startingRow && getColumnIndices(i).contains(startingColumn)) { 532 scrollTargetPosition = i; 533 mRowWithAccessibilityFocus = currentRow; 534 // Note: mColumnWithAccessibilityFocus not updated since the scroll target is on 535 // the same column. 536 break; 537 } 538 } else { // HORIZONTAL 539 /* 540 * The scroll target may span multiple rows. In this grid... 541 * 1 4 542 * 2 5 543 * 2 544 * 3 545 * ... 2 spans two rows and moving up from 3 to 2 interprets moving to the third 546 * row. 547 */ 548 if (currentRow < startingRow && currentColumn == startingColumn) { 549 Set<Integer> rowIndices = getRowIndices(i); 550 scrollTargetPosition = i; 551 mRowWithAccessibilityFocus = Collections.max(rowIndices); 552 // Note: mColumnWithAccessibilityFocus not updated since the scroll target is on 553 // the same column. 554 break; 555 } 556 } 557 } 558 return scrollTargetPosition; 559 } 560 findScrollTargetPositionBelow(int startingRow, int startingColumn, int startingAdapterPosition)561 private int findScrollTargetPositionBelow(int startingRow, int startingColumn, 562 int startingAdapterPosition) { 563 int scrollTargetPosition = INVALID_POSITION; 564 for (int i = startingAdapterPosition + 1; i < getItemCount(); i++) { 565 int currentRow = getRowIndex(i); 566 int currentColumn = getColumnIndex(i); 567 568 if (currentRow < 0 || currentColumn < 0) { 569 if (DEBUG) { 570 throw new RuntimeException("currentRow equals " + currentRow + ", and " 571 + "currentColumn equals " + currentColumn + ", and neither can be " 572 + "less than 0."); 573 } 574 return INVALID_POSITION; 575 } 576 577 if (mOrientation == VERTICAL) { 578 /* 579 * The scroll target may span multiple columns. For example, in this grid... 580 * 1 2 3 581 * 4 4 5 582 * 6 7 583 * ... moving from 2 to 4 interprets as staying in second column, and moving from 584 * 1 to 4 interprets as staying in the first column. 585 */ 586 if ((currentRow > startingRow) && (currentColumn == startingColumn 587 || getColumnIndices(i).contains(startingColumn))) { 588 scrollTargetPosition = i; 589 mRowWithAccessibilityFocus = currentRow; 590 break; 591 } 592 } else { // HORIZONTAL 593 /* 594 * The scroll target may span multiple rows. In this grid... 595 * 1 4 596 * 2 5 597 * 2 598 * 3 599 * ... 2 spans two rows and moving down from 1 to 2 interprets moving to the second 600 * row. 601 */ 602 if (currentRow > startingRow && currentColumn == startingColumn) { 603 scrollTargetPosition = i; 604 mRowWithAccessibilityFocus = getRowIndex(i); 605 break; 606 } 607 } 608 } 609 return scrollTargetPosition; 610 } 611 612 @SuppressWarnings("ConstantConditions") // For the spurious NPE warning related to getting a 613 // value from a map using one of the map keys. findPositionOfLastItemOnARowAboveForHorizontalGrid(int startingRow)614 int findPositionOfLastItemOnARowAboveForHorizontalGrid(int startingRow) { 615 if (startingRow < 0) { 616 if (DEBUG) { 617 throw new RuntimeException( 618 "startingRow equals " + startingRow + ". It cannot be less than zero"); 619 } 620 return INVALID_POSITION; 621 } 622 623 if (mOrientation == VERTICAL) { 624 // This only handles cases of grids with horizontal orientation. 625 if (DEBUG) { 626 Log.w(TAG, "You should not " 627 + "use findPositionOfLastItemOnARowAboveForHorizontalGrid(...) with grids " 628 + "with VERTICAL orientation"); 629 } 630 return INVALID_POSITION; 631 } 632 633 // Map where the keys are row numbers and values are the adapter positions of the last 634 // item in each row. This map is used to locate a scroll target on a previous row in grids 635 // with horizontal orientation. In this example... 636 // 1 4 7 637 // 2 5 8 638 // 3 6 639 // ... the generated map - {2 -> 5, 1 -> 7, 0 -> 6} - can be used to scroll from, 640 // say, "2" (adapter position 1) in the second row to "7" (adapter position 6) in the 641 // preceding row. 642 // 643 // Sometimes cells span multiple rows. In this example: 644 // 1 4 7 645 // 2 5 7 646 // 3 6 8 647 // ... the generated map - {0 -> 6, 1 -> 6, 2 -> 7} - can be used to scroll left from, 648 // say, "3" (adapter position 2) in the third row to "7" (adapter position 6) on the 649 // second row, and then to "5" (adapter position 4). 650 Map<Integer, Integer> rowToLastItemPositionMap = new TreeMap<>(Collections.reverseOrder()); 651 for (int position = 0; position < getItemCount(); position++) { 652 Set<Integer> rows = getRowIndices(position); 653 for (int row: rows) { 654 if (row < 0) { 655 if (DEBUG) { 656 throw new RuntimeException( 657 "row equals " + row + ". It cannot be less than zero"); 658 } 659 return INVALID_POSITION; 660 } 661 rowToLastItemPositionMap.put(row, position); 662 } 663 } 664 665 for (int row : rowToLastItemPositionMap.keySet()) { 666 if (row < startingRow) { 667 int scrollTargetPosition = rowToLastItemPositionMap.get(row); 668 mRowWithAccessibilityFocus = row; 669 mColumnWithAccessibilityFocus = getColumnIndex(scrollTargetPosition); 670 return scrollTargetPosition; 671 } 672 } 673 return INVALID_POSITION; 674 } 675 676 @SuppressWarnings("ConstantConditions") // For the spurious NPE warning related to getting a 677 // value from a map using one of the map keys. findPositionOfFirstItemOnARowBelowForHorizontalGrid(int startingRow)678 int findPositionOfFirstItemOnARowBelowForHorizontalGrid(int startingRow) { 679 if (startingRow < 0) { 680 if (DEBUG) { 681 throw new RuntimeException( 682 "startingRow equals " + startingRow + ". It cannot be less than zero"); 683 } 684 return INVALID_POSITION; 685 } 686 687 if (mOrientation == VERTICAL) { 688 // This only handles cases of grids with horizontal orientation. 689 if (DEBUG) { 690 Log.w(TAG, "You should not " 691 + "use findPositionOfFirstItemOnARowBelowForHorizontalGrid(...) with grids " 692 + "with VERTICAL orientation"); 693 } 694 return INVALID_POSITION; 695 } 696 697 // Map where the keys are row numbers and values are the adapter positions of the first 698 // item in each row. This map is used to locate a scroll target on a following row in grids 699 // with horizontal orientation. In this example: 700 // 1 4 7 701 // 2 5 8 702 // 3 6 703 // ... the generated map - {0 -> 0, 1 -> 1, 2 -> 2} - can be used to scroll from, say, 704 // "7" (adapter position 6) in the first row to "2" (adapter position 1) in the next row. 705 // Sometimes cells span multiple rows. In this example: 706 // 1 3 6 707 // 1 4 7 708 // 2 5 8 709 // ... the generated map - {0 -> 0, 1 -> 0, 2 -> 1} - can be used to scroll right from, 710 // say, "6" (adapter position 5) in the first row to "1" (adapter position 0) on the 711 // second row, and then to "4" (adapter position 3). 712 Map<Integer, Integer> rowToFirstItemPositionMap = new TreeMap<>(); 713 for (int position = 0; position < getItemCount(); position++) { 714 Set<Integer> rows = getRowIndices(position); 715 for (int row : rows) { 716 if (row < 0) { 717 if (DEBUG) { 718 throw new RuntimeException( 719 "row equals " + row + ". It cannot be less than zero"); 720 } 721 return INVALID_POSITION; 722 } 723 // We only care about the first item on each row. 724 if (!rowToFirstItemPositionMap.containsKey(row)) { 725 rowToFirstItemPositionMap.put(row, position); 726 } 727 } 728 } 729 730 for (int row : rowToFirstItemPositionMap.keySet()) { 731 if (row > startingRow) { 732 int scrollTargetPosition = rowToFirstItemPositionMap.get(row); 733 mRowWithAccessibilityFocus = row; 734 mColumnWithAccessibilityFocus = 0; 735 return scrollTargetPosition; 736 } 737 } 738 return INVALID_POSITION; 739 } 740 741 /** 742 * Returns the row index associated with a position. If the item at this position spans multiple 743 * rows, it returns the first row index. To get all row indices for a position, use 744 * {@link #getRowIndices(int)}. 745 */ getRowIndex(int position)746 private int getRowIndex(int position) { 747 return mOrientation == VERTICAL ? getSpanGroupIndex(mRecyclerView.mRecycler, 748 mRecyclerView.mState, position) : getSpanIndex(mRecyclerView.mRecycler, 749 mRecyclerView.mState, position); 750 } 751 752 /** 753 * Returns the column index associated with a position. If the item at this position spans 754 * multiple columns, it returns the first column index. To get all column indices, use 755 * {@link #getColumnIndices(int)}. 756 */ getColumnIndex(int position)757 private int getColumnIndex(int position) { 758 return mOrientation == HORIZONTAL ? getSpanGroupIndex(mRecyclerView.mRecycler, 759 mRecyclerView.mState, position) : getSpanIndex(mRecyclerView.mRecycler, 760 mRecyclerView.mState, position); 761 } 762 763 /** 764 * Returns the row indices for a cell associated with {@code position}. For example, in this 765 * grid... 766 * 0 2 3 767 * 1 2 4 768 * ... the rows for the view at position 2 will be [0, 1] and the rows for position 3 will be 769 * [0]. 770 */ getRowIndices(int position)771 private Set<Integer> getRowIndices(int position) { 772 return getRowOrColumnIndices(getRowIndex(position), position); 773 } 774 775 /** 776 * Returns the column indices for a cell associated with {@code position}. For example, in this 777 * grid... 778 * 0 1 779 * 2 2 780 * 3 4 781 * ... the columns for the view at position 2 will be [0, 1] and the columns for position 3 782 * will be [0]. 783 */ getColumnIndices(int position)784 private Set<Integer> getColumnIndices(int position) { 785 return getRowOrColumnIndices(getColumnIndex(position), position); 786 } 787 getRowOrColumnIndices(int rowOrColumnIndex, int position)788 private Set<Integer> getRowOrColumnIndices(int rowOrColumnIndex, int position) { 789 Set<Integer> indices = new HashSet<>(); 790 int spanSize = getSpanSize(mRecyclerView.mRecycler, mRecyclerView.mState, position); 791 for (int i = rowOrColumnIndex; i < rowOrColumnIndex + spanSize; i++) { 792 indices.add(i); 793 } 794 return indices; 795 } 796 findChildWithAccessibilityFocus()797 private @Nullable View findChildWithAccessibilityFocus() { 798 View child = null; 799 // SDK check needed for View#isAccessibilityFocused() 800 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 801 boolean childFound = false; 802 int i; 803 for (i = 0; i < getChildCount(); i++) { 804 if (Api21Impl.isAccessibilityFocused(Objects.requireNonNull(getChildAt(i)))) { 805 childFound = true; 806 break; 807 } 808 } 809 if (childFound) { 810 child = getChildAt(i); 811 } 812 } 813 return child; 814 } 815 816 /** 817 * Returns true if the values stored in {@link #mRowWithAccessibilityFocus} and 818 * {@link #mColumnWithAccessibilityFocus} are not correct for the view at 819 * {@code adapterPosition}. 820 * 821 * Note that for cells that span multiple rows or multiple columns, {@link 822 * #mRowWithAccessibilityFocus} and {@link #mColumnWithAccessibilityFocus} can be set to more 823 * than one of several values. Accessibility focus is considered unchanged if any of the 824 * possible row values for a cell are the same as {@link #mRowWithAccessibilityFocus} and any 825 * of the possible column values are the same as {@link #mColumnWithAccessibilityFocus}. 826 */ hasAccessibilityFocusChanged(int adapterPosition)827 private boolean hasAccessibilityFocusChanged(int adapterPosition) { 828 return !getRowIndices(adapterPosition).contains(mRowWithAccessibilityFocus) 829 || !getColumnIndices(adapterPosition).contains(mColumnWithAccessibilityFocus); 830 } 831 832 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)833 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 834 if (state.isPreLayout()) { 835 cachePreLayoutSpanMapping(); 836 } 837 super.onLayoutChildren(recycler, state); 838 if (DEBUG) { 839 validateChildOrder(); 840 } 841 clearPreLayoutSpanMappingCache(); 842 } 843 844 @Override onLayoutCompleted(RecyclerView.State state)845 public void onLayoutCompleted(RecyclerView.State state) { 846 super.onLayoutCompleted(state); 847 mPendingSpanCountChange = false; 848 if (mPositionTargetedByScrollInDirection != INVALID_POSITION) { 849 View viewTargetedByScrollInDirection = findViewByPosition( 850 mPositionTargetedByScrollInDirection); 851 if (viewTargetedByScrollInDirection != null) { 852 // Send event after the scroll associated with ACTION_SCROLL_IN_DIRECTION (see 853 // performAccessibilityAction()) concludes and layout completes. Accessibility 854 // services can listen for this event and change UI state as needed. 855 viewTargetedByScrollInDirection.sendAccessibilityEvent( 856 AccessibilityEvent.TYPE_VIEW_TARGETED_BY_SCROLL); 857 mPositionTargetedByScrollInDirection = INVALID_POSITION; 858 } 859 } 860 } 861 clearPreLayoutSpanMappingCache()862 private void clearPreLayoutSpanMappingCache() { 863 mPreLayoutSpanSizeCache.clear(); 864 mPreLayoutSpanIndexCache.clear(); 865 } 866 cachePreLayoutSpanMapping()867 private void cachePreLayoutSpanMapping() { 868 final int childCount = getChildCount(); 869 for (int i = 0; i < childCount; i++) { 870 final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 871 final int viewPosition = lp.getViewLayoutPosition(); 872 mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize()); 873 mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex()); 874 } 875 } 876 877 @Override onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)878 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 879 mSpanSizeLookup.invalidateSpanIndexCache(); 880 mSpanSizeLookup.invalidateSpanGroupIndexCache(); 881 } 882 883 @Override onItemsChanged(RecyclerView recyclerView)884 public void onItemsChanged(RecyclerView recyclerView) { 885 mSpanSizeLookup.invalidateSpanIndexCache(); 886 mSpanSizeLookup.invalidateSpanGroupIndexCache(); 887 } 888 889 @Override onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)890 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 891 mSpanSizeLookup.invalidateSpanIndexCache(); 892 mSpanSizeLookup.invalidateSpanGroupIndexCache(); 893 } 894 895 @Override onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, Object payload)896 public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, 897 Object payload) { 898 mSpanSizeLookup.invalidateSpanIndexCache(); 899 mSpanSizeLookup.invalidateSpanGroupIndexCache(); 900 } 901 902 @Override onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)903 public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { 904 mSpanSizeLookup.invalidateSpanIndexCache(); 905 mSpanSizeLookup.invalidateSpanGroupIndexCache(); 906 } 907 908 @Override generateDefaultLayoutParams()909 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 910 if (mOrientation == HORIZONTAL) { 911 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 912 ViewGroup.LayoutParams.MATCH_PARENT); 913 } else { 914 return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 915 ViewGroup.LayoutParams.WRAP_CONTENT); 916 } 917 } 918 919 @Override generateLayoutParams(Context c, AttributeSet attrs)920 public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { 921 return new LayoutParams(c, attrs); 922 } 923 924 @Override generateLayoutParams(ViewGroup.LayoutParams lp)925 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 926 if (lp instanceof ViewGroup.MarginLayoutParams) { 927 return new LayoutParams((ViewGroup.MarginLayoutParams) lp); 928 } else { 929 return new LayoutParams(lp); 930 } 931 } 932 933 @Override checkLayoutParams(RecyclerView.LayoutParams lp)934 public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { 935 return lp instanceof LayoutParams; 936 } 937 938 /** 939 * Sets the source to get the number of spans occupied by each item in the adapter. 940 * 941 * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans 942 * occupied by each item 943 */ setSpanSizeLookup(SpanSizeLookup spanSizeLookup)944 public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) { 945 mSpanSizeLookup = spanSizeLookup; 946 } 947 948 /** 949 * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager. 950 * 951 * @return The current {@link SpanSizeLookup} used by the GridLayoutManager. 952 */ getSpanSizeLookup()953 public SpanSizeLookup getSpanSizeLookup() { 954 return mSpanSizeLookup; 955 } 956 updateMeasurements()957 private void updateMeasurements() { 958 int totalSpace; 959 if (getOrientation() == VERTICAL) { 960 totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); 961 } else { 962 totalSpace = getHeight() - getPaddingBottom() - getPaddingTop(); 963 } 964 calculateItemBorders(totalSpace); 965 } 966 967 @Override setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)968 public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { 969 if (mCachedBorders == null) { 970 super.setMeasuredDimension(childrenBounds, wSpec, hSpec); 971 } 972 final int width, height; 973 final int horizontalPadding = getPaddingLeft() + getPaddingRight(); 974 final int verticalPadding = getPaddingTop() + getPaddingBottom(); 975 if (mOrientation == VERTICAL) { 976 final int usedHeight = childrenBounds.height() + verticalPadding; 977 height = chooseSize(hSpec, usedHeight, getMinimumHeight()); 978 width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1] + horizontalPadding, 979 getMinimumWidth()); 980 } else { 981 final int usedWidth = childrenBounds.width() + horizontalPadding; 982 width = chooseSize(wSpec, usedWidth, getMinimumWidth()); 983 height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1] + verticalPadding, 984 getMinimumHeight()); 985 } 986 setMeasuredDimension(width, height); 987 } 988 989 /** 990 * @param totalSpace Total available space after padding is removed 991 */ calculateItemBorders(int totalSpace)992 private void calculateItemBorders(int totalSpace) { 993 mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace); 994 } 995 996 /** 997 * @param cachedBorders The out array 998 * @param spanCount number of spans 999 * @param totalSpace total available space after padding is removed 1000 * @return The updated array. Might be the same instance as the provided array if its size 1001 * has not changed. 1002 */ calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace)1003 static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) { 1004 if (cachedBorders == null || cachedBorders.length != spanCount + 1 1005 || cachedBorders[cachedBorders.length - 1] != totalSpace) { 1006 cachedBorders = new int[spanCount + 1]; 1007 } 1008 cachedBorders[0] = 0; 1009 int sizePerSpan = totalSpace / spanCount; 1010 int sizePerSpanRemainder = totalSpace % spanCount; 1011 int consumedPixels = 0; 1012 int additionalSize = 0; 1013 for (int i = 1; i <= spanCount; i++) { 1014 int itemSize = sizePerSpan; 1015 additionalSize += sizePerSpanRemainder; 1016 if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) { 1017 itemSize += 1; 1018 additionalSize -= spanCount; 1019 } 1020 consumedPixels += itemSize; 1021 cachedBorders[i] = consumedPixels; 1022 } 1023 return cachedBorders; 1024 } 1025 getSpaceForSpanRange(int startSpan, int spanSize)1026 int getSpaceForSpanRange(int startSpan, int spanSize) { 1027 if (mOrientation == VERTICAL && isLayoutRTL()) { 1028 return mCachedBorders[mSpanCount - startSpan] 1029 - mCachedBorders[mSpanCount - startSpan - spanSize]; 1030 } else { 1031 return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan]; 1032 } 1033 } 1034 1035 @Override onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)1036 void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, 1037 AnchorInfo anchorInfo, int itemDirection) { 1038 super.onAnchorReady(recycler, state, anchorInfo, itemDirection); 1039 updateMeasurements(); 1040 if (state.getItemCount() > 0 && !state.isPreLayout()) { 1041 ensureAnchorIsInCorrectSpan(recycler, state, anchorInfo, itemDirection); 1042 } 1043 ensureViewSet(); 1044 } 1045 ensureViewSet()1046 private void ensureViewSet() { 1047 if (mSet == null || mSet.length != mSpanCount) { 1048 mSet = new View[mSpanCount]; 1049 } 1050 } 1051 1052 @Override scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)1053 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 1054 RecyclerView.State state) { 1055 updateMeasurements(); 1056 ensureViewSet(); 1057 return super.scrollHorizontallyBy(dx, recycler, state); 1058 } 1059 1060 @Override scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)1061 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 1062 RecyclerView.State state) { 1063 updateMeasurements(); 1064 ensureViewSet(); 1065 return super.scrollVerticallyBy(dy, recycler, state); 1066 } 1067 ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection)1068 private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, 1069 RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) { 1070 final boolean layingOutInPrimaryDirection = 1071 itemDirection == LayoutState.ITEM_DIRECTION_TAIL; 1072 int span = getSpanIndex(recycler, state, anchorInfo.mPosition); 1073 if (layingOutInPrimaryDirection) { 1074 // choose span 0 1075 while (span > 0 && anchorInfo.mPosition > 0) { 1076 anchorInfo.mPosition--; 1077 span = getSpanIndex(recycler, state, anchorInfo.mPosition); 1078 } 1079 } else { 1080 // choose the max span we can get. hopefully last one 1081 final int indexLimit = state.getItemCount() - 1; 1082 int pos = anchorInfo.mPosition; 1083 int bestSpan = span; 1084 while (pos < indexLimit) { 1085 int next = getSpanIndex(recycler, state, pos + 1); 1086 if (next > bestSpan) { 1087 pos += 1; 1088 bestSpan = next; 1089 } else { 1090 break; 1091 } 1092 } 1093 anchorInfo.mPosition = pos; 1094 } 1095 } 1096 1097 @Override findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, boolean layoutFromEnd, boolean traverseChildrenInReverseOrder)1098 View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, 1099 boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) { 1100 1101 int start = 0; 1102 int end = getChildCount(); 1103 int diff = 1; 1104 if (traverseChildrenInReverseOrder) { 1105 start = getChildCount() - 1; 1106 end = -1; 1107 diff = -1; 1108 } 1109 1110 int itemCount = state.getItemCount(); 1111 1112 ensureLayoutState(); 1113 View invalidMatch = null; 1114 View outOfBoundsMatch = null; 1115 1116 final int boundsStart = mOrientationHelper.getStartAfterPadding(); 1117 final int boundsEnd = mOrientationHelper.getEndAfterPadding(); 1118 1119 for (int i = start; i != end; i += diff) { 1120 final View view = getChildAt(i); 1121 final int position = getPosition(view); 1122 if (position >= 0 && position < itemCount) { 1123 final int span = getSpanIndex(recycler, state, position); 1124 if (span != 0) { 1125 continue; 1126 } 1127 if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) { 1128 if (invalidMatch == null) { 1129 invalidMatch = view; // removed item, least preferred 1130 } 1131 } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd 1132 || mOrientationHelper.getDecoratedEnd(view) < boundsStart) { 1133 if (outOfBoundsMatch == null) { 1134 outOfBoundsMatch = view; // item is not visible, less preferred 1135 } 1136 } else { 1137 return view; 1138 } 1139 } 1140 } 1141 return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch; 1142 } 1143 getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int viewPosition)1144 private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, 1145 int viewPosition) { 1146 if (!state.isPreLayout()) { 1147 return mSpanSizeLookup.getCachedSpanGroupIndex(viewPosition, mSpanCount); 1148 } 1149 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition); 1150 if (adapterPosition == -1) { 1151 if (DEBUG) { 1152 throw new RuntimeException("Cannot find span group index for position " 1153 + viewPosition); 1154 } 1155 Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition); 1156 return 0; 1157 } 1158 return mSpanSizeLookup.getCachedSpanGroupIndex(adapterPosition, mSpanCount); 1159 } 1160 getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)1161 private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { 1162 if (!state.isPreLayout()) { 1163 return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount); 1164 } 1165 final int cached = mPreLayoutSpanIndexCache.get(pos, -1); 1166 if (cached != -1) { 1167 return cached; 1168 } 1169 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); 1170 if (adapterPosition == -1) { 1171 if (DEBUG) { 1172 throw new RuntimeException("Cannot find span index for pre layout position. It is" 1173 + " not cached, not in the adapter. Pos:" + pos); 1174 } 1175 Log.w(TAG, "Cannot find span size for pre layout position. It is" 1176 + " not cached, not in the adapter. Pos:" + pos); 1177 return 0; 1178 } 1179 return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount); 1180 } 1181 getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos)1182 private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { 1183 if (!state.isPreLayout()) { 1184 return mSpanSizeLookup.getSpanSize(pos); 1185 } 1186 final int cached = mPreLayoutSpanSizeCache.get(pos, -1); 1187 if (cached != -1) { 1188 return cached; 1189 } 1190 final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); 1191 if (adapterPosition == -1) { 1192 if (DEBUG) { 1193 throw new RuntimeException("Cannot find span size for pre layout position. It is" 1194 + " not cached, not in the adapter. Pos:" + pos); 1195 } 1196 Log.w(TAG, "Cannot find span size for pre layout position. It is" 1197 + " not cached, not in the adapter. Pos:" + pos); 1198 return 1; 1199 } 1200 return mSpanSizeLookup.getSpanSize(adapterPosition); 1201 } 1202 1203 @Override collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, LayoutPrefetchRegistry layoutPrefetchRegistry)1204 void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, 1205 LayoutPrefetchRegistry layoutPrefetchRegistry) { 1206 int remainingSpan = mSpanCount; 1207 int count = 0; 1208 while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { 1209 final int pos = layoutState.mCurrentPosition; 1210 layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); 1211 final int spanSize = mSpanSizeLookup.getSpanSize(pos); 1212 remainingSpan -= spanSize; 1213 layoutState.mCurrentPosition += layoutState.mItemDirection; 1214 count++; 1215 } 1216 } 1217 1218 @Override layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result)1219 void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, 1220 LayoutState layoutState, LayoutChunkResult result) { 1221 final int otherDirSpecMode = mOrientationHelper.getModeInOther(); 1222 final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY; 1223 final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0; 1224 // if grid layout's dimensions are not specified, let the new row change the measurements 1225 // This is not perfect since we not covering all rows but still solves an important case 1226 // where they may have a header row which should be laid out according to children. 1227 if (flexibleInOtherDir) { 1228 updateMeasurements(); // reset measurements 1229 } 1230 final boolean layingOutInPrimaryDirection = 1231 layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL; 1232 int count = 0; 1233 int remainingSpan = mSpanCount; 1234 if (!layingOutInPrimaryDirection) { 1235 int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition); 1236 int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition); 1237 remainingSpan = itemSpanIndex + itemSpanSize; 1238 } 1239 while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { 1240 int pos = layoutState.mCurrentPosition; 1241 final int spanSize = getSpanSize(recycler, state, pos); 1242 if (spanSize > mSpanCount) { 1243 throw new IllegalArgumentException("Item at position " + pos + " requires " 1244 + spanSize + " spans but GridLayoutManager has only " + mSpanCount 1245 + " spans."); 1246 } 1247 remainingSpan -= spanSize; 1248 if (remainingSpan < 0) { 1249 break; // item did not fit into this row or column 1250 } 1251 View view = layoutState.next(recycler); 1252 if (view == null) { 1253 break; 1254 } 1255 mSet[count] = view; 1256 count++; 1257 } 1258 1259 if (count == 0) { 1260 result.mFinished = true; 1261 return; 1262 } 1263 1264 int maxSize = 0; 1265 float maxSizeInOther = 0; // use a float to get size per span 1266 1267 // we should assign spans before item decor offsets are calculated 1268 assignSpans(recycler, state, count, layingOutInPrimaryDirection); 1269 for (int i = 0; i < count; i++) { 1270 View view = mSet[i]; 1271 if (layoutState.mScrapList == null) { 1272 if (layingOutInPrimaryDirection) { 1273 addView(view); 1274 } else { 1275 addView(view, 0); 1276 } 1277 } else { 1278 if (layingOutInPrimaryDirection) { 1279 addDisappearingView(view); 1280 } else { 1281 addDisappearingView(view, 0); 1282 } 1283 } 1284 calculateItemDecorationsForChild(view, mDecorInsets); 1285 1286 measureChild(view, otherDirSpecMode, false); 1287 final int size = mOrientationHelper.getDecoratedMeasurement(view); 1288 if (size > maxSize) { 1289 maxSize = size; 1290 } 1291 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1292 final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) 1293 / lp.mSpanSize; 1294 if (otherSize > maxSizeInOther) { 1295 maxSizeInOther = otherSize; 1296 } 1297 } 1298 if (flexibleInOtherDir) { 1299 // re-distribute columns 1300 guessMeasurement(maxSizeInOther, currentOtherDirSize); 1301 // now we should re-measure any item that was match parent. 1302 maxSize = 0; 1303 for (int i = 0; i < count; i++) { 1304 View view = mSet[i]; 1305 measureChild(view, View.MeasureSpec.EXACTLY, true); 1306 final int size = mOrientationHelper.getDecoratedMeasurement(view); 1307 if (size > maxSize) { 1308 maxSize = size; 1309 } 1310 } 1311 } 1312 1313 // Views that did not measure the maxSize has to be re-measured 1314 // We will stop doing this once we introduce Gravity in the GLM layout params 1315 for (int i = 0; i < count; i++) { 1316 final View view = mSet[i]; 1317 if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) { 1318 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1319 final Rect decorInsets = lp.mDecorInsets; 1320 final int verticalInsets = decorInsets.top + decorInsets.bottom 1321 + lp.topMargin + lp.bottomMargin; 1322 final int horizontalInsets = decorInsets.left + decorInsets.right 1323 + lp.leftMargin + lp.rightMargin; 1324 final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); 1325 final int wSpec; 1326 final int hSpec; 1327 if (mOrientation == VERTICAL) { 1328 wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, 1329 horizontalInsets, lp.width, false); 1330 hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets, 1331 View.MeasureSpec.EXACTLY); 1332 } else { 1333 wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets, 1334 View.MeasureSpec.EXACTLY); 1335 hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, 1336 verticalInsets, lp.height, false); 1337 } 1338 measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true); 1339 } 1340 } 1341 1342 result.mConsumed = maxSize; 1343 1344 int left = 0, right = 0, top = 0, bottom = 0; 1345 if (mOrientation == VERTICAL) { 1346 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1347 bottom = layoutState.mOffset; 1348 top = bottom - maxSize; 1349 } else { 1350 top = layoutState.mOffset; 1351 bottom = top + maxSize; 1352 } 1353 } else { 1354 if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { 1355 right = layoutState.mOffset; 1356 left = right - maxSize; 1357 } else { 1358 left = layoutState.mOffset; 1359 right = left + maxSize; 1360 } 1361 } 1362 for (int i = 0; i < count; i++) { 1363 View view = mSet[i]; 1364 LayoutParams params = (LayoutParams) view.getLayoutParams(); 1365 if (mOrientation == VERTICAL) { 1366 if (isLayoutRTL()) { 1367 right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex]; 1368 left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); 1369 } else { 1370 left = getPaddingLeft() + mCachedBorders[params.mSpanIndex]; 1371 right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); 1372 } 1373 } else { 1374 top = getPaddingTop() + mCachedBorders[params.mSpanIndex]; 1375 bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); 1376 } 1377 // We calculate everything with View's bounding box (which includes decor and margins) 1378 // To calculate correct layout position, we subtract margins. 1379 layoutDecoratedWithMargins(view, left, top, right, bottom); 1380 if (DEBUG) { 1381 Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" 1382 + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" 1383 + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin) 1384 + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize); 1385 } 1386 // Consume the available space if the view is not removed OR changed 1387 if (params.isItemRemoved() || params.isItemChanged()) { 1388 result.mIgnoreConsumed = true; 1389 } 1390 result.mFocusable |= view.hasFocusable(); 1391 } 1392 Arrays.fill(mSet, null); 1393 } 1394 1395 /** 1396 * Measures a child with currently known information. This is not necessarily the child's final 1397 * measurement. (see fillChunk for details). 1398 * 1399 * @param view The child view to be measured 1400 * @param otherDirParentSpecMode The RV measure spec that should be used in the secondary 1401 * orientation 1402 * @param alreadyMeasured True if we've already measured this view once 1403 */ measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured)1404 private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) { 1405 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1406 final Rect decorInsets = lp.mDecorInsets; 1407 final int verticalInsets = decorInsets.top + decorInsets.bottom 1408 + lp.topMargin + lp.bottomMargin; 1409 final int horizontalInsets = decorInsets.left + decorInsets.right 1410 + lp.leftMargin + lp.rightMargin; 1411 final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); 1412 final int wSpec; 1413 final int hSpec; 1414 if (mOrientation == VERTICAL) { 1415 wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, 1416 horizontalInsets, lp.width, false); 1417 hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(), 1418 verticalInsets, lp.height, true); 1419 } else { 1420 hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, 1421 verticalInsets, lp.height, false); 1422 wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(), 1423 horizontalInsets, lp.width, true); 1424 } 1425 measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured); 1426 } 1427 1428 /** 1429 * This is called after laying out a row (if vertical) or a column (if horizontal) when the 1430 * RecyclerView does not have exact measurement specs. 1431 * <p> 1432 * Here we try to assign a best guess width or height and re-do the layout to update other 1433 * views that wanted to MATCH_PARENT in the non-scroll orientation. 1434 * 1435 * @param maxSizeInOther The maximum size per span ratio from the measurement of the children. 1436 * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below. 1437 */ guessMeasurement(float maxSizeInOther, int currentOtherDirSize)1438 private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) { 1439 final int contentSize = Math.round(maxSizeInOther * mSpanCount); 1440 // always re-calculate because borders were stretched during the fill 1441 calculateItemBorders(Math.max(contentSize, currentOtherDirSize)); 1442 } 1443 measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, boolean alreadyMeasured)1444 private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, 1445 boolean alreadyMeasured) { 1446 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); 1447 final boolean measure; 1448 if (alreadyMeasured) { 1449 measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp); 1450 } else { 1451 measure = shouldMeasureChild(child, widthSpec, heightSpec, lp); 1452 } 1453 if (measure) { 1454 child.measure(widthSpec, heightSpec); 1455 } 1456 } 1457 assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, boolean layingOutInPrimaryDirection)1458 private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, 1459 boolean layingOutInPrimaryDirection) { 1460 // spans are always assigned from 0 to N no matter if it is RTL or not. 1461 // RTL is used only when positioning the view. 1462 int span, start, end, diff; 1463 // make sure we traverse from min position to max position 1464 if (layingOutInPrimaryDirection) { 1465 start = 0; 1466 end = count; 1467 diff = 1; 1468 } else { 1469 start = count - 1; 1470 end = -1; 1471 diff = -1; 1472 } 1473 span = 0; 1474 for (int i = start; i != end; i += diff) { 1475 View view = mSet[i]; 1476 LayoutParams params = (LayoutParams) view.getLayoutParams(); 1477 params.mSpanSize = getSpanSize(recycler, state, getPosition(view)); 1478 params.mSpanIndex = span; 1479 span += params.mSpanSize; 1480 } 1481 } 1482 1483 /** 1484 * Returns the number of spans laid out by this grid. 1485 * 1486 * @return The number of spans 1487 * @see #setSpanCount(int) 1488 */ getSpanCount()1489 public int getSpanCount() { 1490 return mSpanCount; 1491 } 1492 1493 /** 1494 * Sets the number of spans to be laid out. 1495 * <p> 1496 * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns. 1497 * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows. 1498 * 1499 * @param spanCount The total number of spans in the grid 1500 * @see #getSpanCount() 1501 */ setSpanCount(int spanCount)1502 public void setSpanCount(int spanCount) { 1503 if (spanCount == mSpanCount) { 1504 return; 1505 } 1506 mPendingSpanCountChange = true; 1507 if (spanCount < 1) { 1508 throw new IllegalArgumentException("Span count should be at least 1. Provided " 1509 + spanCount); 1510 } 1511 mSpanCount = spanCount; 1512 mSpanSizeLookup.invalidateSpanIndexCache(); 1513 requestLayout(); 1514 } 1515 1516 /** 1517 * A helper class to provide the number of spans each item occupies. 1518 * <p> 1519 * Default implementation sets each item to occupy exactly 1 span. 1520 * 1521 * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) 1522 */ 1523 public abstract static class SpanSizeLookup { 1524 1525 final SparseIntArray mSpanIndexCache = new SparseIntArray(); 1526 final SparseIntArray mSpanGroupIndexCache = new SparseIntArray(); 1527 1528 private boolean mCacheSpanIndices = false; 1529 private boolean mCacheSpanGroupIndices = false; 1530 1531 /** 1532 * Returns the number of span occupied by the item at <code>position</code>. 1533 * 1534 * @param position The adapter position of the item 1535 * @return The number of spans occupied by the item at the provided position 1536 */ getSpanSize(int position)1537 public abstract int getSpanSize(int position); 1538 1539 /** 1540 * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or 1541 * not. By default these values are not cached. If you are not overriding 1542 * {@link #getSpanIndex(int, int)} with something highly performant, you should set this 1543 * to true for better performance. 1544 * 1545 * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not. 1546 */ setSpanIndexCacheEnabled(boolean cacheSpanIndices)1547 public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) { 1548 if (!cacheSpanIndices) { 1549 mSpanGroupIndexCache.clear(); 1550 } 1551 mCacheSpanIndices = cacheSpanIndices; 1552 } 1553 1554 /** 1555 * Sets whether the results of {@link #getSpanGroupIndex(int, int)} method should be cached 1556 * or not. By default these values are not cached. If you are not overriding 1557 * {@link #getSpanGroupIndex(int, int)} with something highly performant, and you are using 1558 * spans to calculate scrollbar offset and range, you should set this to true for better 1559 * performance. 1560 * 1561 * @param cacheSpanGroupIndices Whether results of getGroupSpanIndex should be cached or 1562 * not. 1563 */ setSpanGroupIndexCacheEnabled(boolean cacheSpanGroupIndices)1564 public void setSpanGroupIndexCacheEnabled(boolean cacheSpanGroupIndices) { 1565 if (!cacheSpanGroupIndices) { 1566 mSpanGroupIndexCache.clear(); 1567 } 1568 mCacheSpanGroupIndices = cacheSpanGroupIndices; 1569 } 1570 1571 /** 1572 * Clears the span index cache. GridLayoutManager automatically calls this method when 1573 * adapter changes occur. 1574 */ invalidateSpanIndexCache()1575 public void invalidateSpanIndexCache() { 1576 mSpanIndexCache.clear(); 1577 } 1578 1579 /** 1580 * Clears the span group index cache. GridLayoutManager automatically calls this method 1581 * when adapter changes occur. 1582 */ invalidateSpanGroupIndexCache()1583 public void invalidateSpanGroupIndexCache() { 1584 mSpanGroupIndexCache.clear(); 1585 } 1586 1587 /** 1588 * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not. 1589 * 1590 * @return True if results of {@link #getSpanIndex(int, int)} are cached. 1591 */ isSpanIndexCacheEnabled()1592 public boolean isSpanIndexCacheEnabled() { 1593 return mCacheSpanIndices; 1594 } 1595 1596 /** 1597 * Returns whether results of {@link #getSpanGroupIndex(int, int)} method are cached or not. 1598 * 1599 * @return True if results of {@link #getSpanGroupIndex(int, int)} are cached. 1600 */ isSpanGroupIndexCacheEnabled()1601 public boolean isSpanGroupIndexCacheEnabled() { 1602 return mCacheSpanGroupIndices; 1603 } 1604 getCachedSpanIndex(int position, int spanCount)1605 int getCachedSpanIndex(int position, int spanCount) { 1606 if (!mCacheSpanIndices) { 1607 return getSpanIndex(position, spanCount); 1608 } 1609 final int existing = mSpanIndexCache.get(position, -1); 1610 if (existing != -1) { 1611 return existing; 1612 } 1613 final int value = getSpanIndex(position, spanCount); 1614 mSpanIndexCache.put(position, value); 1615 return value; 1616 } 1617 getCachedSpanGroupIndex(int position, int spanCount)1618 int getCachedSpanGroupIndex(int position, int spanCount) { 1619 if (!mCacheSpanGroupIndices) { 1620 return getSpanGroupIndex(position, spanCount); 1621 } 1622 final int existing = mSpanGroupIndexCache.get(position, -1); 1623 if (existing != -1) { 1624 return existing; 1625 } 1626 final int value = getSpanGroupIndex(position, spanCount); 1627 mSpanGroupIndexCache.put(position, value); 1628 return value; 1629 } 1630 1631 /** 1632 * Returns the final span index of the provided position. 1633 * <p> 1634 * If {@link #getOrientation()} is {@link #VERTICAL}, this is a column value. 1635 * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is a row value. 1636 * <p> 1637 * If you have a faster way to calculate span index for your items, you should override 1638 * this method. Otherwise, you should enable span index cache 1639 * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is 1640 * disabled, default implementation traverses all items from 0 to 1641 * <code>position</code>. When caching is enabled, it calculates from the closest cached 1642 * value before the <code>position</code>. 1643 * <p> 1644 * If you override this method, you need to make sure it is consistent with 1645 * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for 1646 * each item. It is called only for the reference item and rest of the items 1647 * are assigned to spans based on the reference item. For example, you cannot assign a 1648 * position to span 2 while span 1 is empty. 1649 * <p> 1650 * Note that span offsets always start with 0 and are not affected by RTL. 1651 * 1652 * @param position The position of the item 1653 * @param spanCount The total number of spans in the grid 1654 * @return The final span position of the item. Should be between 0 (inclusive) and 1655 * <code>spanCount</code>(exclusive) 1656 */ getSpanIndex(int position, int spanCount)1657 public int getSpanIndex(int position, int spanCount) { 1658 int positionSpanSize = getSpanSize(position); 1659 if (positionSpanSize == spanCount) { 1660 return 0; // quick return for full-span items 1661 } 1662 int span = 0; 1663 int startPos = 0; 1664 // If caching is enabled, try to jump 1665 if (mCacheSpanIndices) { 1666 int prevKey = findFirstKeyLessThan(mSpanIndexCache, position); 1667 if (prevKey >= 0) { 1668 span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey); 1669 startPos = prevKey + 1; 1670 } 1671 } 1672 for (int i = startPos; i < position; i++) { 1673 int size = getSpanSize(i); 1674 span += size; 1675 if (span == spanCount) { 1676 span = 0; 1677 } else if (span > spanCount) { 1678 // did not fit, moving to next row / column 1679 span = size; 1680 } 1681 } 1682 if (span + positionSpanSize <= spanCount) { 1683 return span; 1684 } 1685 return 0; 1686 } 1687 findFirstKeyLessThan(SparseIntArray cache, int position)1688 static int findFirstKeyLessThan(SparseIntArray cache, int position) { 1689 int lo = 0; 1690 int hi = cache.size() - 1; 1691 1692 while (lo <= hi) { 1693 // Using unsigned shift here to divide by two because it is guaranteed to not 1694 // overflow. 1695 final int mid = (lo + hi) >>> 1; 1696 final int midVal = cache.keyAt(mid); 1697 if (midVal < position) { 1698 lo = mid + 1; 1699 } else { 1700 hi = mid - 1; 1701 } 1702 } 1703 int index = lo - 1; 1704 if (index >= 0 && index < cache.size()) { 1705 return cache.keyAt(index); 1706 } 1707 return -1; 1708 } 1709 1710 /** 1711 * Returns the index of the group this position belongs. 1712 * <p> 1713 * If {@link #getOrientation()} is {@link #VERTICAL}, this is a row value. 1714 * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is a column value. 1715 * <p> 1716 * For example, if grid has 3 columns and each item occupies 1 span, span group index 1717 * for item 1 will be 0, item 5 will be 1. 1718 * 1719 * @param adapterPosition The position in adapter 1720 * @param spanCount The total number of spans in the grid 1721 * @return The index of the span group including the item at the given adapter position 1722 */ getSpanGroupIndex(int adapterPosition, int spanCount)1723 public int getSpanGroupIndex(int adapterPosition, int spanCount) { 1724 int span = 0; 1725 int group = 0; 1726 int start = 0; 1727 if (mCacheSpanGroupIndices) { 1728 // This finds the first non empty cached group cache key. 1729 int prevKey = findFirstKeyLessThan(mSpanGroupIndexCache, adapterPosition); 1730 if (prevKey != -1) { 1731 group = mSpanGroupIndexCache.get(prevKey); 1732 start = prevKey + 1; 1733 span = getCachedSpanIndex(prevKey, spanCount) + getSpanSize(prevKey); 1734 if (span == spanCount) { 1735 span = 0; 1736 group++; 1737 } 1738 } 1739 } 1740 int positionSpanSize = getSpanSize(adapterPosition); 1741 for (int i = start; i < adapterPosition; i++) { 1742 int size = getSpanSize(i); 1743 span += size; 1744 if (span == spanCount) { 1745 span = 0; 1746 group++; 1747 } else if (span > spanCount) { 1748 // did not fit, moving to next row / column 1749 span = size; 1750 group++; 1751 } 1752 } 1753 if (span + positionSpanSize > spanCount) { 1754 group++; 1755 } 1756 return group; 1757 } 1758 } 1759 1760 @Override onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)1761 public View onFocusSearchFailed(View focused, int direction, 1762 RecyclerView.Recycler recycler, RecyclerView.State state) { 1763 View prevFocusedChild = findContainingItemView(focused); 1764 if (prevFocusedChild == null) { 1765 return null; 1766 } 1767 LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams(); 1768 final int prevSpanStart = lp.mSpanIndex; 1769 final int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize; 1770 View view = super.onFocusSearchFailed(focused, direction, recycler, state); 1771 if (view == null) { 1772 return null; 1773 } 1774 // LinearLayoutManager finds the last child. What we want is the child which has the same 1775 // spanIndex. 1776 final int layoutDir = convertFocusDirectionToLayoutDirection(direction); 1777 final boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout; 1778 final int start, inc, limit; 1779 if (ascend) { 1780 start = getChildCount() - 1; 1781 inc = -1; 1782 limit = -1; 1783 } else { 1784 start = 0; 1785 inc = 1; 1786 limit = getChildCount(); 1787 } 1788 final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); 1789 1790 // The focusable candidate to be picked if no perfect focusable candidate is found. 1791 // The best focusable candidate is the one with the highest amount of span overlap with 1792 // the currently focused view. 1793 View focusableWeakCandidate = null; // somewhat matches but not strong 1794 int focusableWeakCandidateSpanIndex = -1; 1795 int focusableWeakCandidateOverlap = 0; // how many spans overlap 1796 1797 // The unfocusable candidate to become visible on the screen next, if no perfect or 1798 // weak focusable candidates are found to receive focus next. 1799 // We are only interested in partially visible unfocusable views. These are views that are 1800 // not fully visible, that is either partially overlapping, or out-of-bounds and right below 1801 // or above RV's padded bounded area. The best unfocusable candidate is the one with the 1802 // highest amount of span overlap with the currently focused view. 1803 View unfocusableWeakCandidate = null; // somewhat matches but not strong 1804 int unfocusableWeakCandidateSpanIndex = -1; 1805 int unfocusableWeakCandidateOverlap = 0; // how many spans overlap 1806 1807 // The span group index of the start child. This indicates the span group index of the 1808 // next focusable item to receive focus, if a focusable item within the same span group 1809 // exists. Any focusable item beyond this group index are not relevant since they 1810 // were already stored in the layout before onFocusSearchFailed call and were not picked 1811 // by the focusSearch algorithm. 1812 int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start); 1813 for (int i = start; i != limit; i += inc) { 1814 int spanGroupIndex = getSpanGroupIndex(recycler, state, i); 1815 View candidate = getChildAt(i); 1816 if (candidate == prevFocusedChild) { 1817 break; 1818 } 1819 1820 if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) { 1821 // We are past the allowable span group index for the next focusable item. 1822 // The search only continues if no focusable weak candidates have been found up 1823 // until this point, in order to find the best unfocusable candidate to become 1824 // visible on the screen next. 1825 if (focusableWeakCandidate != null) { 1826 break; 1827 } 1828 continue; 1829 } 1830 1831 final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams(); 1832 final int candidateStart = candidateLp.mSpanIndex; 1833 final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize; 1834 if (candidate.hasFocusable() && candidateStart == prevSpanStart 1835 && candidateEnd == prevSpanEnd) { 1836 return candidate; // perfect match 1837 } 1838 boolean assignAsWeek = false; 1839 if ((candidate.hasFocusable() && focusableWeakCandidate == null) 1840 || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) { 1841 assignAsWeek = true; 1842 } else { 1843 int maxStart = Math.max(candidateStart, prevSpanStart); 1844 int minEnd = Math.min(candidateEnd, prevSpanEnd); 1845 int overlap = minEnd - maxStart; 1846 if (candidate.hasFocusable()) { 1847 if (overlap > focusableWeakCandidateOverlap) { 1848 assignAsWeek = true; 1849 } else if (overlap == focusableWeakCandidateOverlap 1850 && preferLastSpan == (candidateStart 1851 > focusableWeakCandidateSpanIndex)) { 1852 assignAsWeek = true; 1853 } 1854 } else if (focusableWeakCandidate == null 1855 && isViewPartiallyVisible(candidate, false, true)) { 1856 if (overlap > unfocusableWeakCandidateOverlap) { 1857 assignAsWeek = true; 1858 } else if (overlap == unfocusableWeakCandidateOverlap 1859 && preferLastSpan == (candidateStart 1860 > unfocusableWeakCandidateSpanIndex)) { 1861 assignAsWeek = true; 1862 } 1863 } 1864 } 1865 1866 if (assignAsWeek) { 1867 if (candidate.hasFocusable()) { 1868 focusableWeakCandidate = candidate; 1869 focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; 1870 focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) 1871 - Math.max(candidateStart, prevSpanStart); 1872 } else { 1873 unfocusableWeakCandidate = candidate; 1874 unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; 1875 unfocusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) 1876 - Math.max(candidateStart, prevSpanStart); 1877 } 1878 } 1879 } 1880 return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate; 1881 } 1882 1883 @Override supportsPredictiveItemAnimations()1884 public boolean supportsPredictiveItemAnimations() { 1885 return mPendingSavedState == null && !mPendingSpanCountChange; 1886 } 1887 1888 @Override computeHorizontalScrollRange(RecyclerView.State state)1889 public int computeHorizontalScrollRange(RecyclerView.State state) { 1890 if (mUsingSpansToEstimateScrollBarDimensions) { 1891 return computeScrollRangeWithSpanInfo(state); 1892 } else { 1893 return super.computeHorizontalScrollRange(state); 1894 } 1895 } 1896 1897 @Override computeVerticalScrollRange(RecyclerView.State state)1898 public int computeVerticalScrollRange(RecyclerView.State state) { 1899 if (mUsingSpansToEstimateScrollBarDimensions) { 1900 return computeScrollRangeWithSpanInfo(state); 1901 } else { 1902 return super.computeVerticalScrollRange(state); 1903 } 1904 } 1905 1906 @Override computeHorizontalScrollOffset(RecyclerView.State state)1907 public int computeHorizontalScrollOffset(RecyclerView.State state) { 1908 if (mUsingSpansToEstimateScrollBarDimensions) { 1909 return computeScrollOffsetWithSpanInfo(state); 1910 } else { 1911 return super.computeHorizontalScrollOffset(state); 1912 } 1913 } 1914 1915 @Override computeVerticalScrollOffset(RecyclerView.State state)1916 public int computeVerticalScrollOffset(RecyclerView.State state) { 1917 if (mUsingSpansToEstimateScrollBarDimensions) { 1918 return computeScrollOffsetWithSpanInfo(state); 1919 } else { 1920 return super.computeVerticalScrollOffset(state); 1921 } 1922 } 1923 1924 /** 1925 * When this flag is set, the scroll offset and scroll range calculations will take account 1926 * of span information. 1927 * 1928 * <p>This is will increase the accuracy of the scroll bar's size and offset but will require 1929 * more calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)}". 1930 * 1931 * <p>This additional accuracy may or may not be needed, depending on the characteristics of 1932 * your layout. You will likely benefit from this accuracy when: 1933 * 1934 * <ul> 1935 * <li>The variation in item span sizes is large. 1936 * <li>The size of your data set is small (if your data set is large, the scrollbar will 1937 * likely be very small anyway, and thus the increased accuracy has less impact). 1938 * <li>Calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast. 1939 * </ul> 1940 * 1941 * <p>If you decide to enable this feature, you should be sure that calls to 1942 * {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast, that set span group index 1943 * caching is set to true via a call to 1944 * {@link SpanSizeLookup#setSpanGroupIndexCacheEnabled(boolean), 1945 * and span index caching is also enabled via a call to 1946 * {@link SpanSizeLookup#setSpanIndexCacheEnabled(boolean)}}. 1947 */ setUsingSpansToEstimateScrollbarDimensions( boolean useSpansToEstimateScrollBarDimensions)1948 public void setUsingSpansToEstimateScrollbarDimensions( 1949 boolean useSpansToEstimateScrollBarDimensions) { 1950 mUsingSpansToEstimateScrollBarDimensions = useSpansToEstimateScrollBarDimensions; 1951 } 1952 1953 /** 1954 * Returns true if the scroll offset and scroll range calculations take account of span 1955 * information. See {@link #setUsingSpansToEstimateScrollbarDimensions(boolean)} for more 1956 * information on this topic. Defaults to {@code false}. 1957 * 1958 * @return true if the scroll offset and scroll range calculations take account of span 1959 * information. 1960 */ isUsingSpansToEstimateScrollbarDimensions()1961 public boolean isUsingSpansToEstimateScrollbarDimensions() { 1962 return mUsingSpansToEstimateScrollBarDimensions; 1963 } 1964 computeScrollRangeWithSpanInfo(RecyclerView.State state)1965 private int computeScrollRangeWithSpanInfo(RecyclerView.State state) { 1966 if (getChildCount() == 0 || state.getItemCount() == 0) { 1967 return 0; 1968 } 1969 ensureLayoutState(); 1970 1971 View startChild = findFirstVisibleChildClosestToStart(!isSmoothScrollbarEnabled(), true); 1972 View endChild = findFirstVisibleChildClosestToEnd(!isSmoothScrollbarEnabled(), true); 1973 1974 if (startChild == null || endChild == null) { 1975 return 0; 1976 } 1977 if (!isSmoothScrollbarEnabled()) { 1978 return mSpanSizeLookup.getCachedSpanGroupIndex( 1979 state.getItemCount() - 1, mSpanCount) + 1; 1980 } 1981 1982 // smooth scrollbar enabled. try to estimate better. 1983 final int laidOutArea = mOrientationHelper.getDecoratedEnd(endChild) 1984 - mOrientationHelper.getDecoratedStart(startChild); 1985 1986 final int firstVisibleSpan = 1987 mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); 1988 final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), 1989 mSpanCount); 1990 final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, 1991 mSpanCount) + 1; 1992 final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; 1993 1994 // estimate a size for full list. 1995 return (int) (((float) laidOutArea / laidOutSpans) * totalSpans); 1996 } 1997 computeScrollOffsetWithSpanInfo(RecyclerView.State state)1998 private int computeScrollOffsetWithSpanInfo(RecyclerView.State state) { 1999 if (getChildCount() == 0 || state.getItemCount() == 0) { 2000 return 0; 2001 } 2002 ensureLayoutState(); 2003 2004 boolean smoothScrollEnabled = isSmoothScrollbarEnabled(); 2005 View startChild = findFirstVisibleChildClosestToStart(!smoothScrollEnabled, true); 2006 View endChild = findFirstVisibleChildClosestToEnd(!smoothScrollEnabled, true); 2007 if (startChild == null || endChild == null) { 2008 return 0; 2009 } 2010 int startChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), 2011 mSpanCount); 2012 int endChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), 2013 mSpanCount); 2014 2015 final int minSpan = Math.min(startChildSpan, endChildSpan); 2016 final int maxSpan = Math.max(startChildSpan, endChildSpan); 2017 final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, 2018 mSpanCount) + 1; 2019 2020 final int spansBefore = mShouldReverseLayout 2021 ? Math.max(0, totalSpans - maxSpan - 1) 2022 : Math.max(0, minSpan); 2023 if (!smoothScrollEnabled) { 2024 return spansBefore; 2025 } 2026 final int laidOutArea = Math.abs(mOrientationHelper.getDecoratedEnd(endChild) 2027 - mOrientationHelper.getDecoratedStart(startChild)); 2028 2029 final int firstVisibleSpan = 2030 mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); 2031 final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), 2032 mSpanCount); 2033 final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; 2034 final float avgSizePerSpan = (float) laidOutArea / laidOutSpans; 2035 2036 return Math.round(spansBefore * avgSizePerSpan + (mOrientationHelper.getStartAfterPadding() 2037 - mOrientationHelper.getDecoratedStart(startChild))); 2038 } 2039 2040 /** 2041 * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span. 2042 */ 2043 public static final class DefaultSpanSizeLookup extends SpanSizeLookup { 2044 2045 @Override getSpanSize(int position)2046 public int getSpanSize(int position) { 2047 return 1; 2048 } 2049 2050 @Override getSpanIndex(int position, int spanCount)2051 public int getSpanIndex(int position, int spanCount) { 2052 return position % spanCount; 2053 } 2054 } 2055 2056 /** 2057 * LayoutParams used by GridLayoutManager. 2058 * <p> 2059 * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the 2060 * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is 2061 * expected to fill all of the space given to it. 2062 */ 2063 public static class LayoutParams extends RecyclerView.LayoutParams { 2064 2065 /** 2066 * Span Id for Views that are not laid out yet. 2067 */ 2068 public static final int INVALID_SPAN_ID = -1; 2069 2070 int mSpanIndex = INVALID_SPAN_ID; 2071 2072 int mSpanSize = 0; 2073 LayoutParams(Context c, AttributeSet attrs)2074 public LayoutParams(Context c, AttributeSet attrs) { 2075 super(c, attrs); 2076 } 2077 LayoutParams(int width, int height)2078 public LayoutParams(int width, int height) { 2079 super(width, height); 2080 } 2081 LayoutParams(ViewGroup.MarginLayoutParams source)2082 public LayoutParams(ViewGroup.MarginLayoutParams source) { 2083 super(source); 2084 } 2085 LayoutParams(ViewGroup.LayoutParams source)2086 public LayoutParams(ViewGroup.LayoutParams source) { 2087 super(source); 2088 } 2089 LayoutParams(RecyclerView.LayoutParams source)2090 public LayoutParams(RecyclerView.LayoutParams source) { 2091 super(source); 2092 } 2093 2094 /** 2095 * Returns the current span index of this View. If the View is not laid out yet, the return 2096 * value is <code>undefined</code>. 2097 * <p> 2098 * Starting with RecyclerView <b>24.2.0</b>, span indices are always indexed from position 0 2099 * even if the layout is RTL. In a vertical GridLayoutManager, <b>leftmost</b> span is span 2100 * 0 if the layout is <b>LTR</b> and <b>rightmost</b> span is span 0 if the layout is 2101 * <b>RTL</b>. Prior to 24.2.0, it was the opposite which was conflicting with 2102 * {@link SpanSizeLookup#getSpanIndex(int, int)}. 2103 * <p> 2104 * If the View occupies multiple spans, span with the minimum index is returned. 2105 * 2106 * @return The span index of the View. 2107 */ getSpanIndex()2108 public int getSpanIndex() { 2109 return mSpanIndex; 2110 } 2111 2112 /** 2113 * Returns the number of spans occupied by this View. If the View not laid out yet, the 2114 * return value is <code>undefined</code>. 2115 * 2116 * @return The number of spans occupied by this View. 2117 */ getSpanSize()2118 public int getSpanSize() { 2119 return mSpanSize; 2120 } 2121 } 2122 2123 2124 @RequiresApi(21) 2125 private static class Api21Impl { Api21Impl()2126 private Api21Impl() { 2127 // This class is not instantiable. 2128 } 2129 isAccessibilityFocused(@onNull View view)2130 static boolean isAccessibilityFocused(@NonNull View view) { 2131 return view.isAccessibilityFocused(); 2132 } 2133 } 2134 }