1 /* 2 * Copyright 2021 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.leanback.widget; 17 18 import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL; 19 import static androidx.recyclerview.widget.RecyclerView.NO_ID; 20 import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; 21 import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; 22 import static androidx.recyclerview.widget.RecyclerView.VERTICAL; 23 24 import android.annotation.SuppressLint; 25 import android.content.Context; 26 import android.graphics.PointF; 27 import android.graphics.Rect; 28 import android.media.AudioManager; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.os.Parcel; 32 import android.os.Parcelable; 33 import android.util.AttributeSet; 34 import android.util.DisplayMetrics; 35 import android.util.Log; 36 import android.util.SparseIntArray; 37 import android.view.FocusFinder; 38 import android.view.Gravity; 39 import android.view.View; 40 import android.view.View.MeasureSpec; 41 import android.view.ViewGroup; 42 import android.view.ViewGroup.MarginLayoutParams; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.view.animation.AccelerateDecelerateInterpolator; 45 import android.widget.GridView; 46 47 import androidx.annotation.VisibleForTesting; 48 import androidx.collection.CircularIntArray; 49 import androidx.core.view.ViewCompat; 50 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; 51 import androidx.recyclerview.widget.LinearSmoothScroller; 52 import androidx.recyclerview.widget.OrientationHelper; 53 import androidx.recyclerview.widget.RecyclerView; 54 import androidx.recyclerview.widget.RecyclerView.Recycler; 55 import androidx.recyclerview.widget.RecyclerView.State; 56 57 import org.jspecify.annotations.NonNull; 58 import org.jspecify.annotations.Nullable; 59 60 import java.io.PrintWriter; 61 import java.io.StringWriter; 62 import java.util.ArrayList; 63 import java.util.Arrays; 64 import java.util.List; 65 66 /** 67 * A {@link RecyclerView.LayoutManager} implementation that lays out items in a grid for leanback 68 * {@link VerticalGridView} and {@link HorizontalGridView}. 69 */ 70 public final class GridLayoutManager extends RecyclerView.LayoutManager { 71 72 /* 73 * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}. 74 * The class currently does two internal jobs: 75 * - Saves optical bounds insets. 76 * - Caches focus align view center. 77 */ 78 static final class LayoutParams extends RecyclerView.LayoutParams { 79 80 // For placement 81 int mLeftInset; 82 int mTopInset; 83 int mRightInset; 84 int mBottomInset; 85 86 // For alignment 87 private int mAlignX; 88 private int mAlignY; 89 private int[] mAlignMultiple; 90 private ItemAlignmentFacet mAlignmentFacet; 91 LayoutParams(Context c, AttributeSet attrs)92 LayoutParams(Context c, AttributeSet attrs) { 93 super(c, attrs); 94 } 95 LayoutParams(int width, int height)96 LayoutParams(int width, int height) { 97 super(width, height); 98 } 99 LayoutParams(MarginLayoutParams source)100 LayoutParams(MarginLayoutParams source) { 101 super(source); 102 } 103 LayoutParams(ViewGroup.LayoutParams source)104 LayoutParams(ViewGroup.LayoutParams source) { 105 super(source); 106 } 107 LayoutParams(RecyclerView.LayoutParams source)108 LayoutParams(RecyclerView.LayoutParams source) { 109 super(source); 110 } 111 LayoutParams(LayoutParams source)112 LayoutParams(LayoutParams source) { 113 super(source); 114 } 115 getAlignX()116 int getAlignX() { 117 return mAlignX; 118 } 119 getAlignY()120 int getAlignY() { 121 return mAlignY; 122 } 123 getOpticalLeft(View view)124 int getOpticalLeft(View view) { 125 return view.getLeft() + mLeftInset; 126 } 127 getOpticalTop(View view)128 int getOpticalTop(View view) { 129 return view.getTop() + mTopInset; 130 } 131 getOpticalRight(View view)132 int getOpticalRight(View view) { 133 return view.getRight() - mRightInset; 134 } 135 getOpticalBottom(View view)136 int getOpticalBottom(View view) { 137 return view.getBottom() - mBottomInset; 138 } 139 getOpticalWidth(View view)140 int getOpticalWidth(View view) { 141 return view.getWidth() - mLeftInset - mRightInset; 142 } 143 getOpticalHeight(View view)144 int getOpticalHeight(View view) { 145 return view.getHeight() - mTopInset - mBottomInset; 146 } 147 getOpticalLeftInset()148 int getOpticalLeftInset() { 149 return mLeftInset; 150 } 151 getOpticalRightInset()152 int getOpticalRightInset() { 153 return mRightInset; 154 } 155 getOpticalTopInset()156 int getOpticalTopInset() { 157 return mTopInset; 158 } 159 getOpticalBottomInset()160 int getOpticalBottomInset() { 161 return mBottomInset; 162 } 163 setAlignX(int alignX)164 void setAlignX(int alignX) { 165 mAlignX = alignX; 166 } 167 setAlignY(int alignY)168 void setAlignY(int alignY) { 169 mAlignY = alignY; 170 } 171 setItemAlignmentFacet(ItemAlignmentFacet facet)172 void setItemAlignmentFacet(ItemAlignmentFacet facet) { 173 mAlignmentFacet = facet; 174 } 175 getItemAlignmentFacet()176 ItemAlignmentFacet getItemAlignmentFacet() { 177 return mAlignmentFacet; 178 } 179 calculateItemAlignments(int orientation, View view)180 void calculateItemAlignments(int orientation, View view) { 181 ItemAlignmentFacet.ItemAlignmentDef[] defs = mAlignmentFacet.getAlignmentDefs(); 182 if (mAlignMultiple == null || mAlignMultiple.length != defs.length) { 183 mAlignMultiple = new int[defs.length]; 184 } 185 for (int i = 0; i < defs.length; i++) { 186 mAlignMultiple[i] = ItemAlignmentFacetHelper 187 .getAlignmentPosition(view, defs[i], orientation); 188 } 189 if (orientation == HORIZONTAL) { 190 mAlignX = mAlignMultiple[0]; 191 } else { 192 mAlignY = mAlignMultiple[0]; 193 } 194 } 195 getAlignMultiple()196 int[] getAlignMultiple() { 197 return mAlignMultiple; 198 } 199 setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset)200 void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) { 201 mLeftInset = leftInset; 202 mTopInset = topInset; 203 mRightInset = rightInset; 204 mBottomInset = bottomInset; 205 } 206 } 207 208 /** 209 * Base class which scrolls to selected view in onStop(). 210 */ 211 abstract class GridLinearSmoothScroller extends LinearSmoothScroller { 212 boolean mSkipOnStopInternal; 213 GridLinearSmoothScroller()214 GridLinearSmoothScroller() { 215 super(mBaseGridView.getContext()); 216 } 217 218 @Override onStop()219 protected void onStop() { 220 super.onStop(); 221 if (!mSkipOnStopInternal) { 222 onStopInternal(); 223 } 224 if (mCurrentSmoothScroller == this) { 225 mCurrentSmoothScroller = null; 226 } 227 if (mPendingMoveSmoothScroller == this) { 228 mPendingMoveSmoothScroller = null; 229 } 230 } 231 onStopInternal()232 protected void onStopInternal() { 233 // onTargetFound() may not be called if we hit the "wall" first or get cancelled. 234 View targetView = findViewByPosition(getTargetPosition()); 235 if (targetView == null) { 236 if (getTargetPosition() >= 0) { 237 // if smooth scroller is stopped without target, immediately jumps 238 // to the target position. 239 scrollToSelection(getTargetPosition(), 0, false, 0); 240 } 241 return; 242 } 243 if (mFocusPosition != getTargetPosition()) { 244 // This should not happen since we cropped value in startPositionSmoothScroller() 245 mFocusPosition = getTargetPosition(); 246 } 247 if (hasFocus()) { 248 mFlag |= PF_IN_SELECTION; 249 targetView.requestFocus(); 250 mFlag &= ~PF_IN_SELECTION; 251 } 252 dispatchChildSelected(); 253 dispatchChildSelectedAndPositioned(); 254 } 255 256 @Override calculateSpeedPerPixel(DisplayMetrics displayMetrics)257 protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { 258 return super.calculateSpeedPerPixel(displayMetrics) * mSmoothScrollSpeedFactor; 259 } 260 261 @Override calculateTimeForScrolling(int dx)262 protected int calculateTimeForScrolling(int dx) { 263 int ms = super.calculateTimeForScrolling(dx); 264 if (mWindowAlignment.mainAxis().getSize() > 0) { 265 float minMs = (float) MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN 266 / mWindowAlignment.mainAxis().getSize() * dx; 267 if (ms < minMs) { 268 ms = (int) minMs; 269 } 270 } 271 return ms; 272 } 273 274 @Override onTargetFound(View targetView, RecyclerView.State state, Action action)275 protected void onTargetFound(View targetView, 276 RecyclerView.State state, Action action) { 277 if (getScrollPosition(targetView, null, sTwoInts)) { 278 int dx, dy; 279 if (mOrientation == HORIZONTAL) { 280 dx = sTwoInts[0]; 281 dy = sTwoInts[1]; 282 } else { 283 dx = sTwoInts[1]; 284 dy = sTwoInts[0]; 285 } 286 final int distance = (int) Math.sqrt(dx * dx + dy * dy); 287 final int time = calculateTimeForDeceleration(distance); 288 action.update(dx, dy, time, mDecelerateInterpolator); 289 } 290 } 291 } 292 293 /** 294 * The SmoothScroller that remembers pending DPAD keys and consume pending keys 295 * during scroll. 296 */ 297 final class PendingMoveSmoothScroller extends GridLinearSmoothScroller { 298 // -2 is a target position that LinearSmoothScroller can never find until 299 // consumePendingMovesXXX() sets real targetPosition. 300 static final int TARGET_UNDEFINED = -2; 301 // whether the grid is staggered. 302 private final boolean mStaggeredGrid; 303 // Number of pending movements on primary direction, negative if PREV_ITEM. 304 private int mPendingMoves; 305 PendingMoveSmoothScroller(int initialPendingMoves, boolean staggeredGrid)306 PendingMoveSmoothScroller(int initialPendingMoves, boolean staggeredGrid) { 307 mPendingMoves = initialPendingMoves; 308 mStaggeredGrid = staggeredGrid; 309 setTargetPosition(TARGET_UNDEFINED); 310 } 311 increasePendingMoves()312 void increasePendingMoves() { 313 if (mPendingMoves < mMaxPendingMoves) { 314 mPendingMoves++; 315 } 316 } 317 decreasePendingMoves()318 void decreasePendingMoves() { 319 if (mPendingMoves > -mMaxPendingMoves) { 320 mPendingMoves--; 321 } 322 } 323 324 /** 325 * Called before laid out an item when non-staggered grid can handle pending movements 326 * by skipping "mNumRows" per movement; staggered grid will have to wait the item 327 * has been laid out in consumePendingMovesAfterLayout(). 328 */ consumePendingMovesBeforeLayout()329 void consumePendingMovesBeforeLayout() { 330 if (mStaggeredGrid || mPendingMoves == 0) { 331 return; 332 } 333 View newSelected = null; 334 int startPos = mPendingMoves > 0 ? mFocusPosition + mNumRows : 335 mFocusPosition - mNumRows; 336 for (int pos = startPos; mPendingMoves != 0; 337 pos = mPendingMoves > 0 ? pos + mNumRows : pos - mNumRows) { 338 View v = findViewByPosition(pos); 339 if (v == null) { 340 break; 341 } 342 if (!canScrollTo(v)) { 343 continue; 344 } 345 newSelected = v; 346 mFocusPosition = pos; 347 mSubFocusPosition = 0; 348 if (mPendingMoves > 0) { 349 mPendingMoves--; 350 } else { 351 mPendingMoves++; 352 } 353 } 354 if (newSelected != null && hasFocus()) { 355 mFlag |= PF_IN_SELECTION; 356 newSelected.requestFocus(); 357 mFlag &= ~PF_IN_SELECTION; 358 } 359 } 360 361 /** 362 * Called after laid out an item. Staggered grid should find view on same 363 * Row and consume pending movements. 364 */ consumePendingMovesAfterLayout()365 void consumePendingMovesAfterLayout() { 366 if (mStaggeredGrid && mPendingMoves != 0) { 367 // consume pending moves, focus to item on the same row. 368 mPendingMoves = processSelectionMoves(true, mPendingMoves); 369 } 370 if (mPendingMoves == 0 || (mPendingMoves > 0 && hasCreatedLastItem()) 371 || (mPendingMoves < 0 && hasCreatedFirstItem())) { 372 setTargetPosition(mFocusPosition); 373 stop(); 374 } 375 } 376 377 @Override computeScrollVectorForPosition(int targetPosition)378 public PointF computeScrollVectorForPosition(int targetPosition) { 379 if (mPendingMoves == 0) { 380 return null; 381 } 382 int direction = ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 383 ? mPendingMoves > 0 : mPendingMoves < 0) 384 ? -1 : 1; 385 if (mOrientation == HORIZONTAL) { 386 return new PointF(direction, 0); 387 } else { 388 return new PointF(0, direction); 389 } 390 } 391 392 @Override onStopInternal()393 protected void onStopInternal() { 394 super.onStopInternal(); 395 // if we hit wall, need clear the remaining pending moves. 396 mPendingMoves = 0; 397 View v = findViewByPosition(getTargetPosition()); 398 if (v != null) scrollToView(v, true); 399 } 400 } 401 402 private static final String TAG = "GridLayoutManager"; 403 static final boolean DEBUG = false; 404 405 // maximum pending movement in one direction. 406 static final int DEFAULT_MAX_PENDING_MOVES = 10; 407 float mSmoothScrollSpeedFactor = 1f; 408 int mMaxPendingMoves = DEFAULT_MAX_PENDING_MOVES; 409 // minimal milliseconds to scroll window size in major direction, we put a cap to prevent the 410 // effect smooth scrolling too over to bind an item view then drag the item view back. 411 static final int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30; 412 getTag()413 String getTag() { 414 return TAG + ":" + mBaseGridView.getId(); 415 } 416 417 BaseGridView mBaseGridView; 418 419 /** 420 * Note on conventions in the presence of RTL layout directions: 421 * Many properties and method names reference entities related to the 422 * beginnings and ends of things. In the presence of RTL flows, 423 * it may not be clear whether this is intended to reference a 424 * quantity that changes direction in RTL cases, or a quantity that 425 * does not. Here are the conventions in use: 426 * 427 * start/end: coordinate quantities - do reverse 428 * (optical) left/right: coordinate quantities - do not reverse 429 * low/high: coordinate quantities - do not reverse 430 * min/max: coordinate quantities - do not reverse 431 * scroll offset - coordinate quantities - do not reverse 432 * first/last: positional indices - do not reverse 433 * front/end: positional indices - do not reverse 434 * prepend/append: related to positional indices - do not reverse 435 * 436 * Note that although quantities do not reverse in RTL flows, their 437 * relationship does. In LTR flows, the first positional index is 438 * leftmost; in RTL flows, it is rightmost. Thus, anywhere that 439 * positional quantities are mapped onto coordinate quantities, 440 * the flow must be checked and the logic reversed. 441 */ 442 443 /** 444 * The orientation of a "row". 445 */ 446 @RecyclerView.Orientation 447 int mOrientation = HORIZONTAL; 448 private OrientationHelper mOrientationHelper = OrientationHelper.createHorizontalHelper(this); 449 450 private int mSaveContextLevel; 451 RecyclerView.State mState; 452 // Suppose currently showing 4, 5, 6, 7; removing 2,3,4 will make the layoutPosition to be 453 // 2(deleted), 3, 4, 5 in prelayout pass. So when we add item in prelayout, we must subtract 2 454 // from index of Grid.createItem. 455 int mPositionDeltaInPreLayout; 456 // Extra layout space needs to fill in prelayout pass. Note we apply the extra space to both 457 // appends and prepends due to the fact leanback is doing mario scrolling: removing items to 458 // the left of focused item might need extra layout on the right. 459 int mExtraLayoutSpaceInPreLayout; 460 // mPositionToRowInPostLayout and mDisappearingPositions are temp variables in post layout. 461 final SparseIntArray mPositionToRowInPostLayout = new SparseIntArray(); 462 int[] mDisappearingPositions; 463 464 AudioManager mAudioManager; 465 466 RecyclerView.Recycler mRecycler; 467 468 private static final Rect sTempRect = new Rect(); 469 470 // 2 bits mask is for 3 STAGEs: 0, PF_STAGE_LAYOUT or PF_STAGE_SCROLL. 471 static final int PF_STAGE_MASK = 0x3; 472 static final int PF_STAGE_LAYOUT = 0x1; 473 static final int PF_STAGE_SCROLL = 0x2; 474 475 // Flag for "in fast relayout", determined by layoutInit() result. 476 static final int PF_FAST_RELAYOUT = 1 << 2; 477 478 // Flag for the selected item being updated in fast relayout. 479 static final int PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION = 1 << 3; 480 /** 481 * During full layout pass, when GridView had focus: onLayoutChildren will 482 * skip non-focusable child and adjust mFocusPosition. 483 */ 484 static final int PF_IN_LAYOUT_SEARCH_FOCUS = 1 << 4; 485 486 // flag to prevent reentry if it's already processing selection request. 487 static final int PF_IN_SELECTION = 1 << 5; 488 489 // Represents whether child views are temporarily sliding out 490 static final int PF_SLIDING = 1 << 6; 491 static final int PF_LAYOUT_EATEN_IN_SLIDING = 1 << 7; 492 493 /** 494 * Force a full layout under certain situations. E.g. Rows change, jump to invisible child. 495 */ 496 static final int PF_FORCE_FULL_LAYOUT = 1 << 8; 497 498 /** 499 * True if layout is enabled. 500 */ 501 static final int PF_LAYOUT_ENABLED = 1 << 9; 502 503 /** 504 * Flag controlling whether the current/next layout should 505 * be updating the secondary size of rows. 506 */ 507 static final int PF_ROW_SECONDARY_SIZE_REFRESH = 1 << 10; 508 509 /** 510 * Allow DPAD key to navigate out at the front of the View (where position = 0), 511 * default is false. 512 */ 513 static final int PF_FOCUS_OUT_FRONT = 1 << 11; 514 515 /** 516 * Allow DPAD key to navigate out at the back of the view, default is false. 517 */ 518 static final int PF_FOCUS_OUT_BACK = 1 << 12; 519 520 static final int PF_FOCUS_OUT_MASKS = PF_FOCUS_OUT_FRONT | PF_FOCUS_OUT_BACK; 521 522 /** 523 * Allow DPAD key to navigate out of second axis. 524 * default is true. 525 */ 526 static final int PF_FOCUS_OUT_SIDE_START = 1 << 13; 527 528 /** 529 * Allow DPAD key to navigate out of second axis. 530 */ 531 static final int PF_FOCUS_OUT_SIDE_END = 1 << 14; 532 533 static final int PF_FOCUS_OUT_SIDE_MASKS = PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END; 534 535 /** 536 * True if focus search is disabled. 537 */ 538 static final int PF_FOCUS_SEARCH_DISABLED = 1 << 15; 539 540 /** 541 * True if prune child, might be disabled during transition. 542 */ 543 static final int PF_PRUNE_CHILD = 1 << 16; 544 545 /** 546 * True if scroll content, might be disabled during transition. 547 */ 548 static final int PF_SCROLL_ENABLED = 1 << 17; 549 550 /** 551 * Set to true for RTL layout in horizontal orientation 552 */ 553 static final int PF_REVERSE_FLOW_PRIMARY = 1 << 18; 554 555 /** 556 * Set to true for RTL layout in vertical orientation 557 */ 558 static final int PF_REVERSE_FLOW_SECONDARY = 1 << 19; 559 560 static final int PF_REVERSE_FLOW_MASK = PF_REVERSE_FLOW_PRIMARY | PF_REVERSE_FLOW_SECONDARY; 561 562 int mFlag = PF_LAYOUT_ENABLED 563 | PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END 564 | PF_PRUNE_CHILD | PF_SCROLL_ENABLED; 565 566 @SuppressWarnings("deprecation") 567 private OnChildSelectedListener mChildSelectedListener = null; 568 569 private ArrayList<OnChildViewHolderSelectedListener> mChildViewHolderSelectedListeners = null; 570 571 @VisibleForTesting 572 ArrayList<BaseGridView.OnLayoutCompletedListener> mOnLayoutCompletedListeners = null; 573 574 OnChildLaidOutListener mChildLaidOutListener = null; 575 576 /** 577 * The focused position, it's not the currently visually aligned position 578 * but it is the final position that we intend to focus on. If there are 579 * multiple setSelection() called, mFocusPosition saves last value. 580 */ 581 int mFocusPosition = NO_POSITION; 582 583 /** 584 * A view can have multiple alignment position, this is the index of which 585 * alignment is used, by default is 0. 586 */ 587 int mSubFocusPosition = 0; 588 589 /** 590 * Current running SmoothScroller. 591 */ 592 GridLinearSmoothScroller mCurrentSmoothScroller; 593 594 /** 595 * LinearSmoothScroller that consume pending DPAD movements. Can be same object as 596 * mCurrentSmoothScroller when mCurrentSmoothScroller is PendingMoveSmoothScroller. 597 */ 598 PendingMoveSmoothScroller mPendingMoveSmoothScroller; 599 600 /** 601 * The offset to be applied to mFocusPosition, due to adapter change, on the next 602 * layout. Set to Integer.MIN_VALUE means we should stop adding delta to mFocusPosition 603 * until next layout cycle. 604 * TODO: This is somewhat duplication of RecyclerView getOldPosition() which is 605 * unfortunately cleared after prelayout. 606 */ 607 private int mFocusPositionOffset = 0; 608 609 /** 610 * Extra pixels applied on primary direction. 611 */ 612 private int mPrimaryScrollExtra; 613 614 /** 615 * override child visibility 616 */ 617 @Visibility 618 int mChildVisibility; 619 620 /** 621 * Pixels that scrolled in secondary forward direction. Negative value means backward. 622 * Note that we treat secondary differently than main. For the main axis, update scroll min/max 623 * based on first/last item's view location. For second axis, we don't use item's view location. 624 * We are using the {@link #getRowSizeSecondary(int)} plus mScrollOffsetSecondary. see 625 * details in {@link #updateSecondaryScrollLimits()}. 626 */ 627 int mScrollOffsetSecondary; 628 629 /** 630 * User-specified row height/column width. Can be WRAP_CONTENT. 631 */ 632 private int mRowSizeSecondaryRequested; 633 634 /** 635 * The fixed size of each grid item in the secondary direction. This corresponds to 636 * the row height, equal for all rows. Grid items may have variable length 637 * in the primary direction. 638 */ 639 private int mFixedRowSizeSecondary; 640 641 /** 642 * Tracks the secondary size of each row. 643 */ 644 private int[] mRowSizeSecondary; 645 646 /** 647 * The maximum measured size of the view. 648 */ 649 private int mMaxSizeSecondary; 650 651 /** 652 * Margin between items. 653 */ 654 private int mHorizontalSpacing; 655 /** 656 * Margin between items vertically. 657 */ 658 private int mVerticalSpacing; 659 /** 660 * Margin in main direction. 661 */ 662 private int mSpacingPrimary; 663 /** 664 * Margin in second direction. 665 */ 666 private int mSpacingSecondary; 667 /** 668 * How to position child in secondary direction. 669 */ 670 private int mGravity = Gravity.START | Gravity.TOP; 671 /** 672 * The number of rows in the grid. 673 */ 674 int mNumRows; 675 /** 676 * Number of rows requested, can be 0 to be determined by parent size and 677 * rowHeight. 678 */ 679 private int mNumRowsRequested = 1; 680 681 /** 682 * Saves grid information of each view. 683 */ 684 Grid mGrid; 685 686 /** 687 * Focus Scroll strategy. 688 */ 689 private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED; 690 /** 691 * Defines how item view is aligned in the window. 692 */ 693 final WindowAlignment mWindowAlignment = new WindowAlignment(); 694 695 /** 696 * Defines how item view is aligned. 697 */ 698 private final ItemAlignment mItemAlignment = new ItemAlignment(); 699 700 /** 701 * Dimensions of the view, width or height depending on orientation. 702 */ 703 private int mSizePrimary; 704 705 /** 706 * Pixels of extra space for layout item (outside the widget) 707 */ 708 private int mExtraLayoutSpace; 709 710 /** 711 * Temporary variable: an int array of length=2. 712 */ 713 static int[] sTwoInts = new int[2]; 714 715 /** 716 * Temporaries used for measuring. 717 */ 718 private final int[] mMeasuredDimension = new int[2]; 719 720 final ViewsStateBundle mChildrenStates = new ViewsStateBundle(); 721 722 /** 723 * Optional interface implemented by Adapter. 724 */ 725 private FacetProviderAdapter mFacetProviderAdapter; 726 GridLayoutManager()727 public GridLayoutManager() { 728 this(null); 729 } 730 731 @SuppressLint("WrongConstant") GridLayoutManager(@ullable BaseGridView baseGridView)732 GridLayoutManager(@Nullable BaseGridView baseGridView) { 733 mBaseGridView = baseGridView; 734 mChildVisibility = -1; 735 // disable prefetch by default, prefetch causes regression on low power chipset 736 setItemPrefetchEnabled(false); 737 } 738 setGridView(BaseGridView baseGridView)739 void setGridView(BaseGridView baseGridView) { 740 mBaseGridView = baseGridView; 741 mGrid = null; 742 } 743 744 /** 745 * Sets grid view orientation. 746 */ setOrientation(@ecyclerView.Orientation int orientation)747 public void setOrientation(@RecyclerView.Orientation int orientation) { 748 if (orientation != HORIZONTAL && orientation != VERTICAL) { 749 if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation); 750 return; 751 } 752 753 mOrientation = orientation; 754 mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); 755 mWindowAlignment.setOrientation(orientation); 756 mItemAlignment.setOrientation(orientation); 757 mFlag |= PF_FORCE_FULL_LAYOUT; 758 } 759 760 /** 761 * Sets whether focus can move out from the front and/or back of the grid view. 762 * 763 * @param throughFront For the vertical orientation, this controls whether focus can move out 764 * from the top of the grid. For the horizontal orientation, this controls whether focus can 765 * move out the front side of the grid. 766 * 767 * @param throughBack For the vertical orientation, this controls whether focus can move out 768 * from the bottom of the grid. For the horizontal orientation, this controls whether focus can 769 * move out the back side of the grid. 770 */ setFocusOutAllowed(boolean throughFront, boolean throughBack)771 public void setFocusOutAllowed(boolean throughFront, boolean throughBack) { 772 mFlag = (mFlag & ~PF_FOCUS_OUT_MASKS) 773 | (throughFront ? PF_FOCUS_OUT_FRONT : 0) 774 | (throughBack ? PF_FOCUS_OUT_BACK : 0); 775 } 776 onRtlPropertiesChanged(int layoutDirection)777 void onRtlPropertiesChanged(int layoutDirection) { 778 final int flags; 779 if (mOrientation == HORIZONTAL) { 780 flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_PRIMARY : 0; 781 } else { 782 flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_SECONDARY : 0; 783 } 784 if ((mFlag & PF_REVERSE_FLOW_MASK) == flags) { 785 return; 786 } 787 mFlag = (mFlag & ~PF_REVERSE_FLOW_MASK) | flags; 788 mFlag |= PF_FORCE_FULL_LAYOUT; 789 mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL); 790 } 791 getFocusScrollStrategy()792 int getFocusScrollStrategy() { 793 return mFocusScrollStrategy; 794 } 795 setFocusScrollStrategy(int focusScrollStrategy)796 void setFocusScrollStrategy(int focusScrollStrategy) { 797 mFocusScrollStrategy = focusScrollStrategy; 798 } 799 setWindowAlignment(int windowAlignment)800 void setWindowAlignment(int windowAlignment) { 801 mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment); 802 } 803 getWindowAlignment()804 int getWindowAlignment() { 805 return mWindowAlignment.mainAxis().getWindowAlignment(); 806 } 807 setWindowAlignmentOffset(int alignmentOffset)808 void setWindowAlignmentOffset(int alignmentOffset) { 809 mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset); 810 } 811 getWindowAlignmentOffset()812 int getWindowAlignmentOffset() { 813 return mWindowAlignment.mainAxis().getWindowAlignmentOffset(); 814 } 815 setWindowAlignmentOffsetPercent(float offsetPercent)816 void setWindowAlignmentOffsetPercent(float offsetPercent) { 817 mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent); 818 } 819 getWindowAlignmentOffsetPercent()820 float getWindowAlignmentOffsetPercent() { 821 return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent(); 822 } 823 setItemAlignmentOffset(int alignmentOffset)824 void setItemAlignmentOffset(int alignmentOffset) { 825 mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset); 826 updateChildAlignments(); 827 } 828 getItemAlignmentOffset()829 int getItemAlignmentOffset() { 830 return mItemAlignment.mainAxis().getItemAlignmentOffset(); 831 } 832 setItemAlignmentOffsetWithPadding(boolean withPadding)833 void setItemAlignmentOffsetWithPadding(boolean withPadding) { 834 mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding); 835 updateChildAlignments(); 836 } 837 isItemAlignmentOffsetWithPadding()838 boolean isItemAlignmentOffsetWithPadding() { 839 return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding(); 840 } 841 setItemAlignmentOffsetPercent(float offsetPercent)842 void setItemAlignmentOffsetPercent(float offsetPercent) { 843 mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent); 844 updateChildAlignments(); 845 } 846 getItemAlignmentOffsetPercent()847 float getItemAlignmentOffsetPercent() { 848 return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent(); 849 } 850 setItemAlignmentViewId(int viewId)851 void setItemAlignmentViewId(int viewId) { 852 mItemAlignment.mainAxis().setItemAlignmentViewId(viewId); 853 updateChildAlignments(); 854 } 855 getItemAlignmentViewId()856 int getItemAlignmentViewId() { 857 return mItemAlignment.mainAxis().getItemAlignmentViewId(); 858 } 859 setFocusOutSideAllowed(boolean throughStart, boolean throughEnd)860 void setFocusOutSideAllowed(boolean throughStart, boolean throughEnd) { 861 mFlag = (mFlag & ~PF_FOCUS_OUT_SIDE_MASKS) 862 | (throughStart ? PF_FOCUS_OUT_SIDE_START : 0) 863 | (throughEnd ? PF_FOCUS_OUT_SIDE_END : 0); 864 } 865 setNumRows(int numRows)866 void setNumRows(int numRows) { 867 if (numRows < 0) throw new IllegalArgumentException(); 868 mNumRowsRequested = numRows; 869 } 870 871 /** 872 * Set the row height. May be WRAP_CONTENT, or a size in pixels. 873 */ setRowHeight(int height)874 void setRowHeight(int height) { 875 if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) { 876 mRowSizeSecondaryRequested = height; 877 } else { 878 throw new IllegalArgumentException("Invalid row height: " + height); 879 } 880 } 881 setItemSpacing(int space)882 void setItemSpacing(int space) { 883 mVerticalSpacing = mHorizontalSpacing = space; 884 mSpacingPrimary = mSpacingSecondary = space; 885 } 886 setVerticalSpacing(int space)887 void setVerticalSpacing(int space) { 888 if (mOrientation == VERTICAL) { 889 mSpacingPrimary = mVerticalSpacing = space; 890 } else { 891 mSpacingSecondary = mVerticalSpacing = space; 892 } 893 } 894 setHorizontalSpacing(int space)895 void setHorizontalSpacing(int space) { 896 if (mOrientation == HORIZONTAL) { 897 mSpacingPrimary = mHorizontalSpacing = space; 898 } else { 899 mSpacingSecondary = mHorizontalSpacing = space; 900 } 901 } 902 getVerticalSpacing()903 int getVerticalSpacing() { 904 return mVerticalSpacing; 905 } 906 getHorizontalSpacing()907 int getHorizontalSpacing() { 908 return mHorizontalSpacing; 909 } 910 setGravity(int gravity)911 void setGravity(int gravity) { 912 mGravity = gravity; 913 } 914 hasDoneFirstLayout()915 boolean hasDoneFirstLayout() { 916 return mGrid != null; 917 } 918 919 @SuppressWarnings("deprecation") setOnChildSelectedListener(OnChildSelectedListener listener)920 void setOnChildSelectedListener(OnChildSelectedListener listener) { 921 mChildSelectedListener = listener; 922 } 923 setOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener)924 void setOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) { 925 if (listener == null) { 926 mChildViewHolderSelectedListeners = null; 927 return; 928 } 929 if (mChildViewHolderSelectedListeners == null) { 930 mChildViewHolderSelectedListeners = new ArrayList<>(); 931 } else { 932 mChildViewHolderSelectedListeners.clear(); 933 } 934 mChildViewHolderSelectedListeners.add(listener); 935 } 936 addOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener)937 void addOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) { 938 if (mChildViewHolderSelectedListeners == null) { 939 mChildViewHolderSelectedListeners = new ArrayList<OnChildViewHolderSelectedListener>(); 940 } 941 mChildViewHolderSelectedListeners.add(listener); 942 } 943 removeOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener)944 void removeOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener 945 listener) { 946 if (mChildViewHolderSelectedListeners != null) { 947 mChildViewHolderSelectedListeners.remove(listener); 948 } 949 } 950 hasOnChildViewHolderSelectedListener()951 boolean hasOnChildViewHolderSelectedListener() { 952 return mChildViewHolderSelectedListeners != null 953 && mChildViewHolderSelectedListeners.size() > 0; 954 } 955 fireOnChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, int position, int subposition)956 void fireOnChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, 957 int position, int subposition) { 958 if (mChildViewHolderSelectedListeners == null) { 959 return; 960 } 961 for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0; i--) { 962 mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelected(parent, child, 963 position, subposition); 964 } 965 } 966 fireOnChildViewHolderSelectedAndPositioned(RecyclerView parent, RecyclerView.ViewHolder child, int position, int subposition)967 void fireOnChildViewHolderSelectedAndPositioned(RecyclerView parent, RecyclerView.ViewHolder 968 child, int position, int subposition) { 969 if (mChildViewHolderSelectedListeners == null) { 970 return; 971 } 972 for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0; i--) { 973 mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelectedAndPositioned(parent, 974 child, position, subposition); 975 } 976 } 977 addOnLayoutCompletedListener(BaseGridView.OnLayoutCompletedListener listener)978 void addOnLayoutCompletedListener(BaseGridView.OnLayoutCompletedListener listener) { 979 if (mOnLayoutCompletedListeners == null) { 980 mOnLayoutCompletedListeners = new ArrayList<>(); 981 } 982 mOnLayoutCompletedListeners.add(listener); 983 } 984 removeOnLayoutCompletedListener(BaseGridView.OnLayoutCompletedListener listener)985 void removeOnLayoutCompletedListener(BaseGridView.OnLayoutCompletedListener listener) { 986 if (mOnLayoutCompletedListeners != null) { 987 mOnLayoutCompletedListeners.remove(listener); 988 } 989 } 990 setOnChildLaidOutListener(OnChildLaidOutListener listener)991 void setOnChildLaidOutListener(OnChildLaidOutListener listener) { 992 mChildLaidOutListener = listener; 993 } 994 getAdapterPositionByView(View view)995 private int getAdapterPositionByView(View view) { 996 if (view == null) { 997 return NO_POSITION; 998 } 999 LayoutParams params = (LayoutParams) view.getLayoutParams(); 1000 if (params == null || params.isItemRemoved()) { 1001 // when item is removed, the position value can be any value. 1002 return NO_POSITION; 1003 } 1004 return params.getAbsoluteAdapterPosition(); 1005 } 1006 getSubPositionByView(View view, View childView)1007 int getSubPositionByView(View view, View childView) { 1008 if (view == null || childView == null) { 1009 return 0; 1010 } 1011 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1012 final ItemAlignmentFacet facet = lp.getItemAlignmentFacet(); 1013 if (facet != null) { 1014 final ItemAlignmentFacet.ItemAlignmentDef[] defs = facet.getAlignmentDefs(); 1015 if (defs.length > 1) { 1016 while (childView != view) { 1017 int id = childView.getId(); 1018 if (id != View.NO_ID) { 1019 for (int i = 1; i < defs.length; i++) { 1020 if (defs[i].getItemAlignmentFocusViewId() == id) { 1021 return i; 1022 } 1023 } 1024 } 1025 childView = (View) childView.getParent(); 1026 } 1027 } 1028 } 1029 return 0; 1030 } 1031 getAdapterPositionByIndex(int index)1032 private int getAdapterPositionByIndex(int index) { 1033 return getAdapterPositionByView(getChildAt(index)); 1034 } 1035 dispatchChildSelected()1036 void dispatchChildSelected() { 1037 if (mChildSelectedListener == null && !hasOnChildViewHolderSelectedListener()) { 1038 return; 1039 } 1040 1041 View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition); 1042 if (view != null) { 1043 RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view); 1044 if (mChildSelectedListener != null) { 1045 mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition, 1046 vh == null ? NO_ID : vh.getItemId()); 1047 } 1048 fireOnChildViewHolderSelected(mBaseGridView, vh, mFocusPosition, mSubFocusPosition); 1049 } else { 1050 if (mChildSelectedListener != null) { 1051 mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID); 1052 } 1053 fireOnChildViewHolderSelected(mBaseGridView, null, NO_POSITION, 0); 1054 } 1055 1056 // Children may request layout when a child selection event occurs (such as a change of 1057 // padding on the current and previously selected rows). 1058 // If in layout, a child requesting layout may have been laid out before the selection 1059 // callback. 1060 // If it was not, the child will be laid out after the selection callback. 1061 // If so, the layout request will be honoured though the view system will emit a double- 1062 // layout warning. 1063 // If not in layout, we may be scrolling in which case the child layout request will be 1064 // eaten by recyclerview. Post a requestLayout. 1065 if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && !mBaseGridView.isLayoutRequested()) { 1066 int childCount = getChildCount(); 1067 for (int i = 0; i < childCount; i++) { 1068 if (getChildAt(i).isLayoutRequested()) { 1069 forceRequestLayout(); 1070 break; 1071 } 1072 } 1073 } 1074 } 1075 1076 @SuppressWarnings("WeakerAccess") /* synthetic access */ dispatchChildSelectedAndPositioned()1077 void dispatchChildSelectedAndPositioned() { 1078 if (!hasOnChildViewHolderSelectedListener()) { 1079 return; 1080 } 1081 1082 View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition); 1083 if (view != null) { 1084 RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view); 1085 fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, vh, mFocusPosition, 1086 mSubFocusPosition); 1087 } else { 1088 if (mChildSelectedListener != null) { 1089 mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID); 1090 } 1091 fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, null, NO_POSITION, 0); 1092 } 1093 1094 } 1095 1096 @Override checkLayoutParams(RecyclerView.@ullable LayoutParams lp)1097 public boolean checkLayoutParams(RecyclerView.@Nullable LayoutParams lp) { 1098 return lp instanceof LayoutParams; 1099 } 1100 1101 @Override canScrollHorizontally()1102 public boolean canScrollHorizontally() { 1103 // We can scroll horizontally if we have horizontal orientation, or if 1104 // we are vertical and have more than one column. 1105 return mOrientation == HORIZONTAL || mNumRows > 1; 1106 } 1107 1108 @Override canScrollVertically()1109 public boolean canScrollVertically() { 1110 // We can scroll vertically if we have vertical orientation, or if we 1111 // are horizontal and have more than one row. 1112 return mOrientation == VERTICAL || mNumRows > 1; 1113 } 1114 1115 /** 1116 * {@inheritDoc} 1117 */ 1118 @Override generateDefaultLayoutParams()1119 public RecyclerView.@NonNull LayoutParams generateDefaultLayoutParams() { 1120 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 1121 ViewGroup.LayoutParams.WRAP_CONTENT); 1122 } 1123 1124 /** 1125 * {@inheritDoc} 1126 */ 1127 @Override generateLayoutParams(@onNull Context context, @NonNull AttributeSet attrs)1128 public RecyclerView.@NonNull LayoutParams generateLayoutParams(@NonNull Context context, 1129 @NonNull AttributeSet attrs) { 1130 return new LayoutParams(context, attrs); 1131 } 1132 1133 /** 1134 * {@inheritDoc} 1135 */ 1136 @Override generateLayoutParams( ViewGroup.@onNull LayoutParams lp)1137 public RecyclerView.@NonNull LayoutParams generateLayoutParams( 1138 ViewGroup.@NonNull LayoutParams lp) { 1139 if (lp instanceof LayoutParams) { 1140 return new LayoutParams((LayoutParams) lp); 1141 } else if (lp instanceof RecyclerView.LayoutParams) { 1142 return new LayoutParams((RecyclerView.LayoutParams) lp); 1143 } else if (lp instanceof MarginLayoutParams) { 1144 return new LayoutParams((MarginLayoutParams) lp); 1145 } else { 1146 return new LayoutParams(lp); 1147 } 1148 } 1149 getViewForPosition(int position)1150 View getViewForPosition(int position) { 1151 View v = mRecycler.getViewForPosition(position); 1152 LayoutParams lp = (LayoutParams) v.getLayoutParams(); 1153 RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v); 1154 lp.setItemAlignmentFacet((ItemAlignmentFacet) getFacet(vh, ItemAlignmentFacet.class)); 1155 return v; 1156 } 1157 getOpticalLeft(View v)1158 int getOpticalLeft(View v) { 1159 return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v); 1160 } 1161 getOpticalRight(View v)1162 int getOpticalRight(View v) { 1163 return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v); 1164 } 1165 getOpticalTop(View v)1166 int getOpticalTop(View v) { 1167 return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v); 1168 } 1169 getOpticalBottom(View v)1170 int getOpticalBottom(View v) { 1171 return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v); 1172 } 1173 1174 @Override getDecoratedLeft(@onNull View child)1175 public int getDecoratedLeft(@NonNull View child) { 1176 return super.getDecoratedLeft(child) + ((LayoutParams) child.getLayoutParams()).mLeftInset; 1177 } 1178 1179 @Override getDecoratedTop(@onNull View child)1180 public int getDecoratedTop(@NonNull View child) { 1181 return super.getDecoratedTop(child) + ((LayoutParams) child.getLayoutParams()).mTopInset; 1182 } 1183 1184 @Override getDecoratedRight(@onNull View child)1185 public int getDecoratedRight(@NonNull View child) { 1186 return super.getDecoratedRight(child) 1187 - ((LayoutParams) child.getLayoutParams()).mRightInset; 1188 } 1189 1190 @Override getDecoratedBottom(@onNull View child)1191 public int getDecoratedBottom(@NonNull View child) { 1192 return super.getDecoratedBottom(child) 1193 - ((LayoutParams) child.getLayoutParams()).mBottomInset; 1194 } 1195 1196 @Override getDecoratedBoundsWithMargins(@onNull View view, @NonNull Rect outBounds)1197 public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outBounds) { 1198 super.getDecoratedBoundsWithMargins(view, outBounds); 1199 LayoutParams params = ((LayoutParams) view.getLayoutParams()); 1200 outBounds.left += params.mLeftInset; 1201 outBounds.top += params.mTopInset; 1202 outBounds.right -= params.mRightInset; 1203 outBounds.bottom -= params.mBottomInset; 1204 } 1205 getViewMin(View v)1206 int getViewMin(View v) { 1207 return mOrientationHelper.getDecoratedStart(v); 1208 } 1209 getViewMax(View v)1210 int getViewMax(View v) { 1211 return mOrientationHelper.getDecoratedEnd(v); 1212 } 1213 getViewPrimarySize(View view)1214 int getViewPrimarySize(View view) { 1215 getDecoratedBoundsWithMargins(view, sTempRect); 1216 return mOrientation == HORIZONTAL ? sTempRect.width() : sTempRect.height(); 1217 } 1218 getViewCenter(View view)1219 private int getViewCenter(View view) { 1220 return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view); 1221 } 1222 getViewCenterSecondary(View view)1223 private int getViewCenterSecondary(View view) { 1224 return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view); 1225 } 1226 getViewCenterX(View v)1227 private int getViewCenterX(View v) { 1228 LayoutParams p = (LayoutParams) v.getLayoutParams(); 1229 return p.getOpticalLeft(v) + p.getAlignX(); 1230 } 1231 getViewCenterY(View v)1232 private int getViewCenterY(View v) { 1233 LayoutParams p = (LayoutParams) v.getLayoutParams(); 1234 return p.getOpticalTop(v) + p.getAlignY(); 1235 } 1236 getAudioManager()1237 AudioManager getAudioManager() { 1238 if (mAudioManager == null) { 1239 mAudioManager = (AudioManager) mBaseGridView.getContext() 1240 .getSystemService(Context.AUDIO_SERVICE); 1241 } 1242 return mAudioManager; 1243 } 1244 1245 /** 1246 * Save Recycler and State for convenience. Must be paired with leaveContext(). 1247 */ saveContext(Recycler recycler, State state)1248 private void saveContext(Recycler recycler, State state) { 1249 if (mSaveContextLevel == 0) { 1250 mRecycler = recycler; 1251 mState = state; 1252 mPositionDeltaInPreLayout = 0; 1253 mExtraLayoutSpaceInPreLayout = 0; 1254 } 1255 mSaveContextLevel++; 1256 } 1257 1258 /** 1259 * Discard saved Recycler and State. 1260 */ leaveContext()1261 private void leaveContext() { 1262 mSaveContextLevel--; 1263 if (mSaveContextLevel == 0) { 1264 mRecycler = null; 1265 mState = null; 1266 mPositionDeltaInPreLayout = 0; 1267 mExtraLayoutSpaceInPreLayout = 0; 1268 } 1269 } 1270 1271 /** 1272 * Re-initialize data structures for a data change or handling invisible 1273 * selection. The method tries its best to preserve position information so 1274 * that staggered grid looks same before and after re-initialize. 1275 * 1276 * @return true if can fastRelayout() 1277 */ layoutInit()1278 private boolean layoutInit() { 1279 final int newItemCount = mState.getItemCount(); 1280 if (newItemCount == 0) { 1281 mFocusPosition = NO_POSITION; 1282 mSubFocusPosition = 0; 1283 } else if (mFocusPosition >= newItemCount) { 1284 mFocusPosition = newItemCount - 1; 1285 mSubFocusPosition = 0; 1286 } else if (mFocusPosition == NO_POSITION && newItemCount > 0) { 1287 // if focus position is never set before, initialize it to 0 1288 mFocusPosition = 0; 1289 mSubFocusPosition = 0; 1290 } 1291 if (!mState.didStructureChange() && mGrid != null && mGrid.getFirstVisibleIndex() >= 0 1292 && (mFlag & PF_FORCE_FULL_LAYOUT) == 0 && mGrid.getNumRows() == mNumRows) { 1293 updateScrollController(); 1294 updateSecondaryScrollLimits(); 1295 mGrid.setSpacing(mSpacingPrimary); 1296 return true; 1297 } else { 1298 mFlag &= ~PF_FORCE_FULL_LAYOUT; 1299 1300 if (mGrid == null || mNumRows != mGrid.getNumRows() 1301 || ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) != mGrid.isReversedFlow()) { 1302 mGrid = Grid.createGrid(mNumRows); 1303 mGrid.setProvider(mGridProvider); 1304 mGrid.setReversedFlow((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0); 1305 } 1306 initScrollController(); 1307 updateSecondaryScrollLimits(); 1308 mGrid.setSpacing(mSpacingPrimary); 1309 detachAndScrapAttachedViews(mRecycler); 1310 mGrid.resetVisibleIndex(); 1311 mWindowAlignment.mainAxis().invalidateScrollMin(); 1312 mWindowAlignment.mainAxis().invalidateScrollMax(); 1313 return false; 1314 } 1315 } 1316 getRowSizeSecondary(int rowIndex)1317 private int getRowSizeSecondary(int rowIndex) { 1318 if (mFixedRowSizeSecondary != 0) { 1319 return mFixedRowSizeSecondary; 1320 } 1321 if (mRowSizeSecondary == null) { 1322 return 0; 1323 } 1324 return mRowSizeSecondary[rowIndex]; 1325 } 1326 getRowStartSecondary(int rowIndex)1327 int getRowStartSecondary(int rowIndex) { 1328 int start = 0; 1329 // Iterate from left to right, which is a different index traversal 1330 // in RTL flow 1331 if ((mFlag & PF_REVERSE_FLOW_SECONDARY) != 0) { 1332 for (int i = mNumRows - 1; i > rowIndex; i--) { 1333 start += getRowSizeSecondary(i) + mSpacingSecondary; 1334 } 1335 } else { 1336 for (int i = 0; i < rowIndex; i++) { 1337 start += getRowSizeSecondary(i) + mSpacingSecondary; 1338 } 1339 } 1340 return start; 1341 } 1342 getSizeSecondary()1343 private int getSizeSecondary() { 1344 int rightmostIndex = (mFlag & PF_REVERSE_FLOW_SECONDARY) != 0 ? 0 : mNumRows - 1; 1345 return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex); 1346 } 1347 getDecoratedMeasuredWidthWithMargin(View v)1348 int getDecoratedMeasuredWidthWithMargin(View v) { 1349 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 1350 return getDecoratedMeasuredWidth(v) + lp.leftMargin + lp.rightMargin; 1351 } 1352 getDecoratedMeasuredHeightWithMargin(View v)1353 int getDecoratedMeasuredHeightWithMargin(View v) { 1354 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 1355 return getDecoratedMeasuredHeight(v) + lp.topMargin + lp.bottomMargin; 1356 } 1357 measureScrapChild(int position, int widthSpec, int heightSpec, int[] measuredDimension)1358 private void measureScrapChild(int position, int widthSpec, int heightSpec, 1359 int[] measuredDimension) { 1360 View view = mRecycler.getViewForPosition(position); 1361 if (view != null) { 1362 final LayoutParams p = (LayoutParams) view.getLayoutParams(); 1363 calculateItemDecorationsForChild(view, sTempRect); 1364 int widthUsed = p.leftMargin + p.rightMargin + sTempRect.left + sTempRect.right; 1365 int heightUsed = p.topMargin + p.bottomMargin + sTempRect.top + sTempRect.bottom; 1366 1367 int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, 1368 getPaddingLeft() + getPaddingRight() + widthUsed, p.width); 1369 int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, 1370 getPaddingTop() + getPaddingBottom() + heightUsed, p.height); 1371 view.measure(childWidthSpec, childHeightSpec); 1372 1373 measuredDimension[0] = getDecoratedMeasuredWidthWithMargin(view); 1374 measuredDimension[1] = getDecoratedMeasuredHeightWithMargin(view); 1375 mRecycler.recycleView(view); 1376 } 1377 } 1378 processRowSizeSecondary(boolean measure)1379 private boolean processRowSizeSecondary(boolean measure) { 1380 if (mFixedRowSizeSecondary != 0 || mRowSizeSecondary == null) { 1381 return false; 1382 } 1383 1384 CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows(); 1385 boolean changed = false; 1386 int scrapeChildSize = -1; 1387 1388 for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) { 1389 CircularIntArray row = rows == null ? null : rows[rowIndex]; 1390 final int rowItemsPairCount = row == null ? 0 : row.size(); 1391 int rowSize = -1; 1392 for (int rowItemPairIndex = 0; rowItemPairIndex < rowItemsPairCount; 1393 rowItemPairIndex += 2) { 1394 final int rowIndexStart = row.get(rowItemPairIndex); 1395 final int rowIndexEnd = row.get(rowItemPairIndex + 1); 1396 for (int i = rowIndexStart; i <= rowIndexEnd; i++) { 1397 final View view = findViewByPosition(i - mPositionDeltaInPreLayout); 1398 if (view == null) { 1399 continue; 1400 } 1401 if (measure) { 1402 measureChild(view); 1403 } 1404 final int secondarySize = mOrientation == HORIZONTAL 1405 ? getDecoratedMeasuredHeightWithMargin(view) 1406 : getDecoratedMeasuredWidthWithMargin(view); 1407 if (secondarySize > rowSize) { 1408 rowSize = secondarySize; 1409 } 1410 } 1411 } 1412 1413 final int itemCount = mState.getItemCount(); 1414 if (!mBaseGridView.hasFixedSize() && measure && rowSize < 0 && itemCount > 0) { 1415 if (scrapeChildSize < 0) { 1416 // measure a child that is close to mFocusPosition but not currently visible 1417 int position = mFocusPosition; 1418 if (position < 0) { 1419 position = 0; 1420 } else if (position >= itemCount) { 1421 position = itemCount - 1; 1422 } 1423 if (getChildCount() > 0) { 1424 int firstPos = mBaseGridView.getChildViewHolder( 1425 getChildAt(0)).getLayoutPosition(); 1426 int lastPos = mBaseGridView.getChildViewHolder( 1427 getChildAt(getChildCount() - 1)).getLayoutPosition(); 1428 // if mFocusPosition is between first and last, choose either 1429 // first - 1 or last + 1 1430 if (position >= firstPos && position <= lastPos) { 1431 position = (position - firstPos <= lastPos - position) 1432 ? (firstPos - 1) : (lastPos + 1); 1433 // try the other value if the position is invalid. if both values are 1434 // invalid, skip measureScrapChild below. 1435 if (position < 0 && lastPos < itemCount - 1) { 1436 position = lastPos + 1; 1437 } else if (position >= itemCount && firstPos > 0) { 1438 position = firstPos - 1; 1439 } 1440 } 1441 } 1442 if (position >= 0 && position < itemCount) { 1443 measureScrapChild(position, 1444 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 1445 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 1446 mMeasuredDimension); 1447 scrapeChildSize = mOrientation == HORIZONTAL ? mMeasuredDimension[1] : 1448 mMeasuredDimension[0]; 1449 if (DEBUG) { 1450 Log.v(TAG, "measured scrap child: " + mMeasuredDimension[0] + " " 1451 + mMeasuredDimension[1]); 1452 } 1453 } 1454 } 1455 if (scrapeChildSize >= 0) { 1456 rowSize = scrapeChildSize; 1457 } 1458 } 1459 if (rowSize < 0) { 1460 rowSize = 0; 1461 } 1462 if (mRowSizeSecondary[rowIndex] != rowSize) { 1463 if (DEBUG) { 1464 Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex] 1465 + ", " + rowSize); 1466 } 1467 mRowSizeSecondary[rowIndex] = rowSize; 1468 changed = true; 1469 } 1470 } 1471 1472 return changed; 1473 } 1474 1475 /** 1476 * Checks if we need to update row secondary sizes. 1477 */ updateRowSecondarySizeRefresh()1478 private void updateRowSecondarySizeRefresh() { 1479 mFlag = (mFlag & ~PF_ROW_SECONDARY_SIZE_REFRESH) 1480 | (processRowSizeSecondary(false) ? PF_ROW_SECONDARY_SIZE_REFRESH : 0); 1481 if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) { 1482 if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set"); 1483 forceRequestLayout(); 1484 } 1485 } 1486 forceRequestLayout()1487 private void forceRequestLayout() { 1488 if (DEBUG) Log.v(getTag(), "forceRequestLayout"); 1489 // RecyclerView prevents us from requesting layout in many cases 1490 // (during layout, during scroll, etc.) 1491 // For secondary row size wrap_content support we currently need a 1492 // second layout pass to update the measured size after having measured 1493 // and added child views in layoutChildren. 1494 // Force the second layout by posting a delayed runnable. 1495 // TODO: investigate allowing a second layout pass, 1496 // or move child add/measure logic to the measure phase. 1497 ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable); 1498 } 1499 1500 private final Runnable mRequestLayoutRunnable = new Runnable() { 1501 @Override 1502 public void run() { 1503 if (DEBUG) Log.v(getTag(), "request Layout from runnable"); 1504 requestLayout(); 1505 } 1506 }; 1507 1508 @Override 1509 @SuppressWarnings("ObjectToString") onMeasure(@onNull Recycler recycler, @NonNull State state, int widthSpec, int heightSpec)1510 public void onMeasure(@NonNull Recycler recycler, @NonNull State state, 1511 int widthSpec, int heightSpec) { 1512 saveContext(recycler, state); 1513 1514 int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary; 1515 int measuredSizeSecondary; 1516 if (mOrientation == HORIZONTAL) { 1517 sizePrimary = MeasureSpec.getSize(widthSpec); 1518 sizeSecondary = MeasureSpec.getSize(heightSpec); 1519 modeSecondary = MeasureSpec.getMode(heightSpec); 1520 paddingSecondary = getPaddingTop() + getPaddingBottom(); 1521 } else { 1522 sizeSecondary = MeasureSpec.getSize(widthSpec); 1523 sizePrimary = MeasureSpec.getSize(heightSpec); 1524 modeSecondary = MeasureSpec.getMode(widthSpec); 1525 paddingSecondary = getPaddingLeft() + getPaddingRight(); 1526 } 1527 if (DEBUG) { 1528 Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) 1529 + " heightSpec " + Integer.toHexString(heightSpec) 1530 + " modeSecondary " + Integer.toHexString(modeSecondary) 1531 + " sizeSecondary " + sizeSecondary + " " + this); 1532 } 1533 1534 mMaxSizeSecondary = sizeSecondary; 1535 1536 if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) { 1537 mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested; 1538 mFixedRowSizeSecondary = 0; 1539 1540 if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) { 1541 mRowSizeSecondary = new int[mNumRows]; 1542 } 1543 1544 if (mState.isPreLayout()) { 1545 updatePositionDeltaInPreLayout(); 1546 } 1547 // Measure all current children and update cached row height or column width 1548 processRowSizeSecondary(true); 1549 1550 switch (modeSecondary) { 1551 case MeasureSpec.UNSPECIFIED: 1552 measuredSizeSecondary = getSizeSecondary() + paddingSecondary; 1553 break; 1554 case MeasureSpec.AT_MOST: 1555 measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary, 1556 mMaxSizeSecondary); 1557 break; 1558 case MeasureSpec.EXACTLY: 1559 measuredSizeSecondary = mMaxSizeSecondary; 1560 break; 1561 default: 1562 throw new IllegalStateException("wrong spec"); 1563 } 1564 1565 } else { 1566 switch (modeSecondary) { 1567 case MeasureSpec.UNSPECIFIED: 1568 mFixedRowSizeSecondary = mRowSizeSecondaryRequested == 0 1569 ? sizeSecondary - paddingSecondary : mRowSizeSecondaryRequested; 1570 mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested; 1571 measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary 1572 * (mNumRows - 1) + paddingSecondary; 1573 break; 1574 case MeasureSpec.AT_MOST: 1575 case MeasureSpec.EXACTLY: 1576 if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) { 1577 mNumRows = 1; 1578 mFixedRowSizeSecondary = sizeSecondary - paddingSecondary; 1579 } else if (mNumRowsRequested == 0) { 1580 mFixedRowSizeSecondary = mRowSizeSecondaryRequested; 1581 mNumRows = (sizeSecondary + mSpacingSecondary) 1582 / (mRowSizeSecondaryRequested + mSpacingSecondary); 1583 } else if (mRowSizeSecondaryRequested == 0) { 1584 mNumRows = mNumRowsRequested; 1585 mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary 1586 - mSpacingSecondary * (mNumRows - 1)) / mNumRows; 1587 } else { 1588 mNumRows = mNumRowsRequested; 1589 mFixedRowSizeSecondary = mRowSizeSecondaryRequested; 1590 } 1591 measuredSizeSecondary = sizeSecondary; 1592 if (modeSecondary == MeasureSpec.AT_MOST) { 1593 int childrenSize = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary 1594 * (mNumRows - 1) + paddingSecondary; 1595 if (childrenSize < measuredSizeSecondary) { 1596 measuredSizeSecondary = childrenSize; 1597 } 1598 } 1599 break; 1600 default: 1601 throw new IllegalStateException("wrong spec"); 1602 } 1603 } 1604 if (mOrientation == HORIZONTAL) { 1605 setMeasuredDimension(sizePrimary, measuredSizeSecondary); 1606 } else { 1607 setMeasuredDimension(measuredSizeSecondary, sizePrimary); 1608 } 1609 if (DEBUG) { 1610 Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary 1611 + " measuredSizeSecondary " + measuredSizeSecondary 1612 + " mFixedRowSizeSecondary " + mFixedRowSizeSecondary 1613 + " mNumRows " + mNumRows); 1614 } 1615 leaveContext(); 1616 } 1617 measureChild(View child)1618 void measureChild(View child) { 1619 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1620 calculateItemDecorationsForChild(child, sTempRect); 1621 int widthUsed = lp.leftMargin + lp.rightMargin + sTempRect.left + sTempRect.right; 1622 int heightUsed = lp.topMargin + lp.bottomMargin + sTempRect.top + sTempRect.bottom; 1623 1624 final int secondarySpec = 1625 (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) 1626 ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) 1627 : MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY); 1628 int widthSpec, heightSpec; 1629 1630 if (mOrientation == HORIZONTAL) { 1631 widthSpec = ViewGroup.getChildMeasureSpec( 1632 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), widthUsed, lp.width); 1633 heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, heightUsed, lp.height); 1634 } else { 1635 heightSpec = ViewGroup.getChildMeasureSpec( 1636 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed, lp.height); 1637 widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, widthUsed, lp.width); 1638 } 1639 child.measure(widthSpec, heightSpec); 1640 if (DEBUG) { 1641 Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) 1642 + " widthSpec " + Integer.toHexString(widthSpec) 1643 + " heightSpec " + Integer.toHexString(heightSpec) 1644 + " measuredWidth " + child.getMeasuredWidth() 1645 + " measuredHeight " + child.getMeasuredHeight()); 1646 } 1647 if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height); 1648 } 1649 1650 /** 1651 * Get facet from the ViewHolder or the viewType. 1652 */ 1653 @SuppressWarnings("unchecked") getFacet(RecyclerView.ViewHolder vh, Class<? extends E> facetClass)1654 <E> E getFacet(RecyclerView.ViewHolder vh, Class<? extends E> facetClass) { 1655 E facet = null; 1656 if (vh instanceof FacetProvider) { 1657 facet = (E) ((FacetProvider) vh).getFacet(facetClass); 1658 } 1659 if (facet == null && mFacetProviderAdapter != null) { 1660 FacetProvider p = mFacetProviderAdapter.getFacetProvider(vh.getItemViewType()); 1661 if (p != null) { 1662 facet = (E) p.getFacet(facetClass); 1663 } 1664 } 1665 return facet; 1666 } 1667 1668 private final Grid.Provider mGridProvider = new Grid.Provider() { 1669 1670 @Override 1671 public int getMinIndex() { 1672 return mPositionDeltaInPreLayout; 1673 } 1674 1675 @Override 1676 public int getCount() { 1677 return mState.getItemCount() + mPositionDeltaInPreLayout; 1678 } 1679 1680 @Override 1681 public int createItem(int index, boolean append, Object[] item, boolean disappearingItem) { 1682 View v = getViewForPosition(index - mPositionDeltaInPreLayout); 1683 LayoutParams lp = (LayoutParams) v.getLayoutParams(); 1684 // See recyclerView docs: we don't need re-add scraped view if it was removed. 1685 if (!lp.isItemRemoved()) { 1686 if (disappearingItem) { 1687 if (append) { 1688 addDisappearingView(v); 1689 } else { 1690 addDisappearingView(v, 0); 1691 } 1692 } else { 1693 if (append) { 1694 addView(v); 1695 } else { 1696 addView(v, 0); 1697 } 1698 } 1699 if (mChildVisibility != -1) { 1700 v.setVisibility(mChildVisibility); 1701 } 1702 1703 if (mPendingMoveSmoothScroller != null) { 1704 mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout(); 1705 } 1706 int subindex = getSubPositionByView(v, v.findFocus()); 1707 if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) { 1708 // when we are appending item during scroll pass and the item's position 1709 // matches the mFocusPosition, we should signal a childSelected event. 1710 // However if we are still running PendingMoveSmoothScroller, we defer and 1711 // signal the event in PendingMoveSmoothScroller.onStop(). This can 1712 // avoid lots of childSelected events during a long smooth scrolling and 1713 // increase performance. 1714 if (index == mFocusPosition && subindex == mSubFocusPosition 1715 && mPendingMoveSmoothScroller == null) { 1716 dispatchChildSelected(); 1717 } 1718 } else if ((mFlag & PF_FAST_RELAYOUT) == 0) { 1719 // fastRelayout will dispatch event at end of onLayoutChildren(). 1720 // For full layout, two situations here: 1721 // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition. 1722 // 2. mInLayoutSearchFocus is true: dispatchChildSelected() on first child 1723 // equal to or after mFocusPosition that can take focus. 1724 if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) == 0 && index == mFocusPosition 1725 && subindex == mSubFocusPosition) { 1726 dispatchChildSelected(); 1727 } else if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) != 0 && index >= mFocusPosition 1728 && v.hasFocusable()) { 1729 mFocusPosition = index; 1730 mSubFocusPosition = subindex; 1731 mFlag &= ~PF_IN_LAYOUT_SEARCH_FOCUS; 1732 dispatchChildSelected(); 1733 } 1734 } 1735 measureChild(v); 1736 } 1737 item[0] = v; 1738 return mOrientation == HORIZONTAL ? getDecoratedMeasuredWidthWithMargin(v) 1739 : getDecoratedMeasuredHeightWithMargin(v); 1740 } 1741 1742 @Override 1743 public void addItem(Object item, int index, int length, int rowIndex, int edge) { 1744 View v = (View) item; 1745 int start, end; 1746 if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) { 1747 edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingMin() 1748 : mWindowAlignment.mainAxis().getSize() 1749 - mWindowAlignment.mainAxis().getPaddingMax(); 1750 } 1751 boolean edgeIsMin = !mGrid.isReversedFlow(); 1752 if (edgeIsMin) { 1753 start = edge; 1754 end = edge + length; 1755 } else { 1756 start = edge - length; 1757 end = edge; 1758 } 1759 int startSecondary = getRowStartSecondary(rowIndex) 1760 + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary; 1761 mChildrenStates.loadView(v, index); 1762 layoutChild(rowIndex, v, start, end, startSecondary); 1763 if (DEBUG) { 1764 Log.d(getTag(), "addView " + index + " " + v); 1765 } 1766 1767 if (!mState.isPreLayout()) { 1768 updateScrollLimits(); 1769 } 1770 if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && mPendingMoveSmoothScroller != null) { 1771 mPendingMoveSmoothScroller.consumePendingMovesAfterLayout(); 1772 } 1773 if (mChildLaidOutListener != null) { 1774 RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v); 1775 mChildLaidOutListener.onChildLaidOut(mBaseGridView, v, index, 1776 vh == null ? NO_ID : vh.getItemId()); 1777 } 1778 } 1779 1780 @Override 1781 public void removeItem(int index) { 1782 View v = findViewByPosition(index - mPositionDeltaInPreLayout); 1783 if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) { 1784 detachAndScrapView(v, mRecycler); 1785 } else { 1786 removeAndRecycleView(v, mRecycler); 1787 } 1788 } 1789 1790 @Override 1791 public int getEdge(int index) { 1792 View v = findViewByPosition(index - mPositionDeltaInPreLayout); 1793 return (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? getViewMax(v) : getViewMin(v); 1794 } 1795 1796 @Override 1797 public int getSize(int index) { 1798 return getViewPrimarySize(findViewByPosition(index - mPositionDeltaInPreLayout)); 1799 } 1800 }; 1801 layoutChild(int rowIndex, View v, int start, int end, int startSecondary)1802 void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) { 1803 int sizeSecondary = mOrientation == HORIZONTAL ? getDecoratedMeasuredHeightWithMargin(v) 1804 : getDecoratedMeasuredWidthWithMargin(v); 1805 if (mFixedRowSizeSecondary > 0) { 1806 sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary); 1807 } 1808 final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 1809 final int horizontalGravity = (mFlag & PF_REVERSE_FLOW_MASK) != 0 1810 ? Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, 1811 View.LAYOUT_DIRECTION_RTL) 1812 : mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1813 if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP) 1814 || (mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT)) { 1815 // do nothing 1816 } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM) 1817 || (mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT)) { 1818 startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary; 1819 } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL) 1820 || (mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL)) { 1821 startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2; 1822 } 1823 int left, top, right, bottom; 1824 if (mOrientation == HORIZONTAL) { 1825 left = start; 1826 top = startSecondary; 1827 right = end; 1828 bottom = startSecondary + sizeSecondary; 1829 } else { 1830 top = start; 1831 left = startSecondary; 1832 bottom = end; 1833 right = startSecondary + sizeSecondary; 1834 } 1835 LayoutParams params = (LayoutParams) v.getLayoutParams(); 1836 layoutDecoratedWithMargins(v, left, top, right, bottom); 1837 // Now super.getDecoratedBoundsWithMargins() includes the extra space for optical bounds, 1838 // subtracting it from value passed in layoutDecoratedWithMargins(), we can get the optical 1839 // bounds insets. 1840 super.getDecoratedBoundsWithMargins(v, sTempRect); 1841 params.setOpticalInsets(left - sTempRect.left, top - sTempRect.top, 1842 sTempRect.right - right, sTempRect.bottom - bottom); 1843 updateChildAlignments(v); 1844 } 1845 updateChildAlignments(View v)1846 private void updateChildAlignments(View v) { 1847 final LayoutParams p = (LayoutParams) v.getLayoutParams(); 1848 if (p.getItemAlignmentFacet() == null) { 1849 // Fallback to global settings on grid view 1850 p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v)); 1851 p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v)); 1852 } else { 1853 // Use ItemAlignmentFacet defined on specific ViewHolder 1854 p.calculateItemAlignments(mOrientation, v); 1855 if (mOrientation == HORIZONTAL) { 1856 p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v)); 1857 } else { 1858 p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v)); 1859 } 1860 } 1861 } 1862 updateChildAlignments()1863 private void updateChildAlignments() { 1864 for (int i = 0, c = getChildCount(); i < c; i++) { 1865 updateChildAlignments(getChildAt(i)); 1866 } 1867 } 1868 setExtraLayoutSpace(int extraLayoutSpace)1869 void setExtraLayoutSpace(int extraLayoutSpace) { 1870 if (mExtraLayoutSpace == extraLayoutSpace) { 1871 return; 1872 } else if (mExtraLayoutSpace < 0) { 1873 throw new IllegalArgumentException("ExtraLayoutSpace must >= 0"); 1874 } 1875 mExtraLayoutSpace = extraLayoutSpace; 1876 requestLayout(); 1877 } 1878 getExtraLayoutSpace()1879 int getExtraLayoutSpace() { 1880 return mExtraLayoutSpace; 1881 } 1882 removeInvisibleViewsAtEnd()1883 private void removeInvisibleViewsAtEnd() { 1884 if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) { 1885 mGrid.removeInvisibleItemsAtEnd(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 1886 ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace); 1887 } 1888 } 1889 removeInvisibleViewsAtFront()1890 private void removeInvisibleViewsAtFront() { 1891 if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) { 1892 mGrid.removeInvisibleItemsAtFront(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 1893 ? mSizePrimary + mExtraLayoutSpace : -mExtraLayoutSpace); 1894 } 1895 } 1896 appendOneColumnVisibleItems()1897 private boolean appendOneColumnVisibleItems() { 1898 return mGrid.appendOneColumnVisibleItems(); 1899 } 1900 slideIn()1901 void slideIn() { 1902 if ((mFlag & PF_SLIDING) != 0) { 1903 mFlag &= ~PF_SLIDING; 1904 if (mFocusPosition >= 0) { 1905 scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra); 1906 } else { 1907 mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING; 1908 requestLayout(); 1909 } 1910 if ((mFlag & PF_LAYOUT_EATEN_IN_SLIDING) != 0) { 1911 mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING; 1912 if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) { 1913 mBaseGridView.addOnScrollListener(new RecyclerView.OnScrollListener() { 1914 @Override 1915 public void onScrollStateChanged(@NonNull RecyclerView recyclerView, 1916 int newState) { 1917 if (newState == SCROLL_STATE_IDLE) { 1918 mBaseGridView.removeOnScrollListener(this); 1919 requestLayout(); 1920 } 1921 } 1922 }); 1923 } else { 1924 requestLayout(); 1925 } 1926 } 1927 } 1928 } 1929 getSlideOutDistance()1930 int getSlideOutDistance() { 1931 int distance; 1932 if (mOrientation == VERTICAL) { 1933 distance = -getHeight(); 1934 if (getChildCount() > 0) { 1935 int top = getChildAt(0).getTop(); 1936 if (top < 0) { 1937 // scroll more if first child is above top edge 1938 distance = distance + top; 1939 } 1940 } 1941 } else { 1942 if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) { 1943 distance = getWidth(); 1944 if (getChildCount() > 0) { 1945 int start = getChildAt(0).getRight(); 1946 if (start > distance) { 1947 // scroll more if first child is outside right edge 1948 distance = start; 1949 } 1950 } 1951 } else { 1952 distance = -getWidth(); 1953 if (getChildCount() > 0) { 1954 int start = getChildAt(0).getLeft(); 1955 if (start < 0) { 1956 // scroll more if first child is out side left edge 1957 distance = distance + start; 1958 } 1959 } 1960 } 1961 } 1962 return distance; 1963 } 1964 isSlidingChildViews()1965 boolean isSlidingChildViews() { 1966 return (mFlag & PF_SLIDING) != 0; 1967 } 1968 1969 /** 1970 * Temporarily slide out child and block layout and scroll requests. 1971 */ slideOut()1972 void slideOut() { 1973 if ((mFlag & PF_SLIDING) != 0) { 1974 return; 1975 } 1976 mFlag |= PF_SLIDING; 1977 if (getChildCount() == 0) { 1978 return; 1979 } 1980 if (mOrientation == VERTICAL) { 1981 mBaseGridView.smoothScrollBy(0, getSlideOutDistance(), 1982 new AccelerateDecelerateInterpolator()); 1983 } else { 1984 mBaseGridView.smoothScrollBy(getSlideOutDistance(), 0, 1985 new AccelerateDecelerateInterpolator()); 1986 } 1987 } 1988 prependOneColumnVisibleItems()1989 private boolean prependOneColumnVisibleItems() { 1990 return mGrid.prependOneColumnVisibleItems(); 1991 } 1992 appendVisibleItems()1993 private void appendVisibleItems() { 1994 mGrid.appendVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 1995 ? -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout 1996 : mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout); 1997 } 1998 prependVisibleItems()1999 private void prependVisibleItems() { 2000 mGrid.prependVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 2001 ? mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout 2002 : -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout); 2003 } 2004 2005 /** 2006 * Fast layout when there is no structure change, adapter change, etc. 2007 * It will layout all views was layout requested or updated, until hit a view 2008 * with different size, then it break and detachAndScrap all views after that. 2009 */ fastRelayout()2010 private void fastRelayout() { 2011 boolean invalidateAfter = false; 2012 final int childCount = getChildCount(); 2013 int position = mGrid.getFirstVisibleIndex(); 2014 int index = 0; 2015 mFlag &= ~PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION; 2016 for (; index < childCount; index++, position++) { 2017 View view = getChildAt(index); 2018 // We don't hit fastRelayout() if State.didStructure() is true, but prelayout may add 2019 // extra views and invalidate existing Grid position. Also the prelayout calling 2020 // getViewForPosotion() may retrieve item from cache with FLAG_INVALID. The adapter 2021 // postion will be -1 for this case. Either case, we should invalidate after this item 2022 // and call getViewForPosition() again to rebind. 2023 if (position != getAdapterPositionByView(view)) { 2024 invalidateAfter = true; 2025 break; 2026 } 2027 Grid.Location location = mGrid.getLocation(position); 2028 if (location == null) { 2029 invalidateAfter = true; 2030 break; 2031 } 2032 2033 int startSecondary = getRowStartSecondary(location.mRow) 2034 + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary; 2035 int primarySize, end; 2036 int start = getViewMin(view); 2037 int oldPrimarySize = getViewPrimarySize(view); 2038 2039 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2040 if (lp.viewNeedsUpdate()) { 2041 mFlag |= PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION; 2042 detachAndScrapView(view, mRecycler); 2043 view = getViewForPosition(position); 2044 addView(view, index); 2045 } 2046 2047 measureChild(view); 2048 if (mOrientation == HORIZONTAL) { 2049 primarySize = getDecoratedMeasuredWidthWithMargin(view); 2050 end = start + primarySize; 2051 } else { 2052 primarySize = getDecoratedMeasuredHeightWithMargin(view); 2053 end = start + primarySize; 2054 } 2055 layoutChild(location.mRow, view, start, end, startSecondary); 2056 if (oldPrimarySize != primarySize) { 2057 // size changed invalidate remaining Locations 2058 if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position); 2059 invalidateAfter = true; 2060 break; 2061 } 2062 } 2063 if (invalidateAfter) { 2064 final int savedLastPos = mGrid.getLastVisibleIndex(); 2065 for (int i = childCount - 1; i >= index; i--) { 2066 View v = getChildAt(i); 2067 detachAndScrapView(v, mRecycler); 2068 } 2069 mGrid.invalidateItemsAfter(position); 2070 if ((mFlag & PF_PRUNE_CHILD) != 0) { 2071 // in regular prune child mode, we just append items up to edge limit 2072 appendVisibleItems(); 2073 if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) { 2074 // make sure add focus view back: the view might be outside edge limit 2075 // when there is delta in onLayoutChildren(). 2076 while (mGrid.getLastVisibleIndex() < mFocusPosition) { 2077 mGrid.appendOneColumnVisibleItems(); 2078 } 2079 } 2080 } else { 2081 // prune disabled(e.g. in RowsFragment transition): append all removed items 2082 while (mGrid.appendOneColumnVisibleItems() 2083 && mGrid.getLastVisibleIndex() < savedLastPos) { 2084 // Do nothing. 2085 } 2086 } 2087 } 2088 updateScrollLimits(); 2089 updateSecondaryScrollLimits(); 2090 } 2091 2092 @Override removeAndRecycleAllViews(RecyclerView.@onNull Recycler recycler)2093 public void removeAndRecycleAllViews(RecyclerView.@NonNull Recycler recycler) { 2094 if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount()); 2095 for (int i = getChildCount() - 1; i >= 0; i--) { 2096 removeAndRecycleViewAt(i, recycler); 2097 } 2098 } 2099 2100 // called by onLayoutChildren, either focus to FocusPosition or declare focusViewAvailable 2101 // and scroll to the view if framework focus on it. focusToViewInLayout(boolean hadFocus, boolean alignToView, int extraDelta, int extraDeltaSecondary)2102 private void focusToViewInLayout(boolean hadFocus, boolean alignToView, int extraDelta, 2103 int extraDeltaSecondary) { 2104 View focusView = findViewByPosition(mFocusPosition); 2105 if (focusView != null && alignToView) { 2106 scrollToView(focusView, false, extraDelta, extraDeltaSecondary); 2107 } 2108 if (focusView != null && hadFocus && !focusView.hasFocus()) { 2109 focusView.requestFocus(); 2110 } else if (!hadFocus && !mBaseGridView.hasFocus()) { 2111 if (focusView != null && focusView.hasFocusable()) { 2112 mBaseGridView.focusableViewAvailable(focusView); 2113 } else { 2114 for (int i = 0, count = getChildCount(); i < count; i++) { 2115 focusView = getChildAt(i); 2116 if (focusView != null && focusView.hasFocusable()) { 2117 mBaseGridView.focusableViewAvailable(focusView); 2118 break; 2119 } 2120 } 2121 } 2122 // focusViewAvailable() might focus to the view, scroll to it if that is the case. 2123 if (alignToView && focusView != null && focusView.hasFocus()) { 2124 scrollToView(focusView, false, extraDelta, extraDeltaSecondary); 2125 } 2126 } 2127 } 2128 2129 @Override onLayoutCompleted(@onNull State state)2130 public void onLayoutCompleted(@NonNull State state) { 2131 if (mOnLayoutCompletedListeners != null) { 2132 for (int i = mOnLayoutCompletedListeners.size() - 1; i >= 0; i--) { 2133 mOnLayoutCompletedListeners.get(i).onLayoutCompleted(state); 2134 } 2135 } 2136 } 2137 2138 @Override supportsPredictiveItemAnimations()2139 public boolean supportsPredictiveItemAnimations() { 2140 return true; 2141 } 2142 updatePositionToRowMapInPostLayout()2143 void updatePositionToRowMapInPostLayout() { 2144 mPositionToRowInPostLayout.clear(); 2145 final int childCount = getChildCount(); 2146 for (int i = 0; i < childCount; i++) { 2147 // Grid still maps to old positions at this point, use old position to get row infor 2148 int position = mBaseGridView.getChildViewHolder(getChildAt(i)).getOldPosition(); 2149 if (position >= 0) { 2150 Grid.Location loc = mGrid.getLocation(position); 2151 if (loc != null) { 2152 mPositionToRowInPostLayout.put(position, loc.mRow); 2153 } 2154 } 2155 } 2156 } 2157 fillScrapViewsInPostLayout()2158 void fillScrapViewsInPostLayout() { 2159 List<RecyclerView.ViewHolder> scrapList = mRecycler.getScrapList(); 2160 final int scrapSize = scrapList.size(); 2161 if (scrapSize == 0) { 2162 return; 2163 } 2164 // initialize the int array or re-allocate the array. 2165 if (mDisappearingPositions == null || scrapSize > mDisappearingPositions.length) { 2166 int length = mDisappearingPositions == null ? 16 : mDisappearingPositions.length; 2167 while (length < scrapSize) { 2168 length = length << 1; 2169 } 2170 mDisappearingPositions = new int[length]; 2171 } 2172 int totalItems = 0; 2173 for (int i = 0; i < scrapSize; i++) { 2174 int pos = scrapList.get(i).getAbsoluteAdapterPosition(); 2175 if (pos >= 0) { 2176 mDisappearingPositions[totalItems++] = pos; 2177 } 2178 } 2179 // totalItems now has the length of disappearing items 2180 if (totalItems > 0) { 2181 Arrays.sort(mDisappearingPositions, 0, totalItems); 2182 mGrid.fillDisappearingItems(mDisappearingPositions, totalItems, 2183 mPositionToRowInPostLayout); 2184 } 2185 mPositionToRowInPostLayout.clear(); 2186 } 2187 2188 // in prelayout, first child's getViewPosition can be smaller than old adapter position 2189 // if there were items removed before first visible index. For example: 2190 // visible items are 3, 4, 5, 6, deleting 1, 2, 3 from adapter; the view position in 2191 // prelayout are not 3(deleted), 4, 5, 6. Instead it's 1(deleted), 2, 3, 4. 2192 // So there is a delta (2 in this case) between last cached position and prelayout position. updatePositionDeltaInPreLayout()2193 void updatePositionDeltaInPreLayout() { 2194 if (getChildCount() > 0) { 2195 View view = getChildAt(0); 2196 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2197 mPositionDeltaInPreLayout = mGrid.getFirstVisibleIndex() 2198 - lp.getViewLayoutPosition(); 2199 } else { 2200 mPositionDeltaInPreLayout = 0; 2201 } 2202 } 2203 2204 // Lays out items based on the current scroll position 2205 @Override onLayoutChildren(RecyclerView.@onNull Recycler recycler, RecyclerView.@NonNull State state)2206 public void onLayoutChildren(RecyclerView.@NonNull Recycler recycler, 2207 RecyclerView.@NonNull State state) { 2208 if (DEBUG) { 2209 Log.v(getTag(), "layoutChildren start numRows " + mNumRows 2210 + " inPreLayout " + state.isPreLayout() 2211 + " didStructureChange " + state.didStructureChange() 2212 + " mForceFullLayout " + ((mFlag & PF_FORCE_FULL_LAYOUT) != 0)); 2213 Log.v(getTag(), "width " + getWidth() + " height " + getHeight()); 2214 } 2215 2216 if (mNumRows == 0) { 2217 // haven't done measure yet 2218 return; 2219 } 2220 final int itemCount = state.getItemCount(); 2221 if (itemCount < 0) { 2222 return; 2223 } 2224 2225 if ((mFlag & PF_SLIDING) != 0) { 2226 // if there is already children, delay the layout process until slideIn(), if it's 2227 // first time layout children: scroll them offscreen at end of onLayoutChildren() 2228 if (getChildCount() > 0) { 2229 mFlag |= PF_LAYOUT_EATEN_IN_SLIDING; 2230 return; 2231 } 2232 } 2233 if ((mFlag & PF_LAYOUT_ENABLED) == 0) { 2234 discardLayoutInfo(); 2235 removeAndRecycleAllViews(recycler); 2236 return; 2237 } 2238 mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_LAYOUT; 2239 2240 saveContext(recycler, state); 2241 if (state.isPreLayout()) { 2242 updatePositionDeltaInPreLayout(); 2243 int childCount = getChildCount(); 2244 if (mGrid != null && childCount > 0) { 2245 int minChangedEdge = Integer.MAX_VALUE; 2246 int maxChangeEdge = Integer.MIN_VALUE; 2247 int minOldAdapterPosition = mBaseGridView.getChildViewHolder( 2248 getChildAt(0)).getOldPosition(); 2249 int maxOldAdapterPosition = mBaseGridView.getChildViewHolder( 2250 getChildAt(childCount - 1)).getOldPosition(); 2251 for (int i = 0; i < childCount; i++) { 2252 View view = getChildAt(i); 2253 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2254 int newAdapterPosition = mBaseGridView.getChildAdapterPosition(view); 2255 // if either of following happening 2256 // 1. item itself has changed or layout parameter changed 2257 // 2. item is losing focus 2258 // 3. item is gaining focus 2259 // 4. item is moved out of old adapter position range. 2260 if (lp.isItemChanged() || lp.isItemRemoved() || view.isLayoutRequested() 2261 || (!view.hasFocus() 2262 && mFocusPosition == lp.getAbsoluteAdapterPosition()) 2263 || (view.hasFocus() 2264 && mFocusPosition != lp.getAbsoluteAdapterPosition()) 2265 || newAdapterPosition < minOldAdapterPosition 2266 || newAdapterPosition > maxOldAdapterPosition) { 2267 minChangedEdge = Math.min(minChangedEdge, getViewMin(view)); 2268 maxChangeEdge = Math.max(maxChangeEdge, getViewMax(view)); 2269 } 2270 } 2271 if (maxChangeEdge > minChangedEdge) { 2272 mExtraLayoutSpaceInPreLayout = maxChangeEdge - minChangedEdge; 2273 } 2274 // append items for mExtraLayoutSpaceInPreLayout 2275 appendVisibleItems(); 2276 prependVisibleItems(); 2277 } 2278 mFlag &= ~PF_STAGE_MASK; 2279 leaveContext(); 2280 if (DEBUG) Log.v(getTag(), "layoutChildren end"); 2281 return; 2282 } 2283 2284 // save all view's row information before detach all views 2285 if (state.willRunPredictiveAnimations()) { 2286 updatePositionToRowMapInPostLayout(); 2287 } 2288 // check if we need align to mFocusPosition, this is usually true unless in smoothScrolling 2289 final boolean scrollToFocus = !isSmoothScrolling() 2290 && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED; 2291 if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) { 2292 mFocusPosition = mFocusPosition + mFocusPositionOffset; 2293 mSubFocusPosition = 0; 2294 } 2295 mFocusPositionOffset = 0; 2296 2297 View savedFocusView = findViewByPosition(mFocusPosition); 2298 int savedFocusPos = mFocusPosition; 2299 int savedSubFocusPos = mSubFocusPosition; 2300 boolean hadFocus = mBaseGridView.hasFocus(); 2301 final int firstVisibleIndex = mGrid != null ? mGrid.getFirstVisibleIndex() : NO_POSITION; 2302 final int lastVisibleIndex = mGrid != null ? mGrid.getLastVisibleIndex() : NO_POSITION; 2303 final int deltaPrimary; 2304 final int deltaSecondary; 2305 if (mOrientation == HORIZONTAL) { 2306 deltaPrimary = state.getRemainingScrollHorizontal(); 2307 deltaSecondary = state.getRemainingScrollVertical(); 2308 } else { 2309 deltaSecondary = state.getRemainingScrollHorizontal(); 2310 deltaPrimary = state.getRemainingScrollVertical(); 2311 } 2312 if (layoutInit()) { 2313 mFlag |= PF_FAST_RELAYOUT; 2314 // If grid view is empty, we will start from mFocusPosition 2315 mGrid.setStart(mFocusPosition); 2316 fastRelayout(); 2317 } else { 2318 mFlag &= ~PF_FAST_RELAYOUT; 2319 // layoutInit() has detached all views, so start from scratch 2320 mFlag = (mFlag & ~PF_IN_LAYOUT_SEARCH_FOCUS) 2321 | (scrollToFocus ? PF_IN_LAYOUT_SEARCH_FOCUS : 0); 2322 int startFromPosition, endPos; 2323 if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex 2324 || mFocusPosition < firstVisibleIndex)) { 2325 startFromPosition = endPos = mFocusPosition; 2326 } else { 2327 startFromPosition = firstVisibleIndex; 2328 endPos = lastVisibleIndex; 2329 } 2330 mGrid.setStart(startFromPosition); 2331 if (endPos != NO_POSITION) { 2332 while (appendOneColumnVisibleItems() && findViewByPosition(endPos) == null) { 2333 // continuously append items until endPos 2334 } 2335 } 2336 } 2337 // multiple rounds: scrollToView of first round may drag first/last child into 2338 // "visible window" and we update scrollMin/scrollMax then run second scrollToView 2339 // we must do this for fastRelayout() for the append item case 2340 int oldFirstVisible; 2341 int oldLastVisible; 2342 do { 2343 updateScrollLimits(); 2344 oldFirstVisible = mGrid.getFirstVisibleIndex(); 2345 oldLastVisible = mGrid.getLastVisibleIndex(); 2346 focusToViewInLayout(hadFocus, scrollToFocus, -deltaPrimary, -deltaSecondary); 2347 appendVisibleItems(); 2348 prependVisibleItems(); 2349 // b/67370222: do not removeInvisibleViewsAtFront/End() in the loop, otherwise 2350 // loop may bounce between scroll forward and scroll backward forever. Example: 2351 // Assuming there are 19 items, child#18 and child#19 are both in RV, we are 2352 // trying to focus to child#18 and there are 200px remaining scroll distance. 2353 // 1 focusToViewInLayout() tries scroll forward 50 px to align focused child#18 on 2354 // right edge, but there to compensate remaining scroll 200px, also scroll 2355 // backward 200px, 150px pushes last child#19 out side of right edge. 2356 // 2 removeInvisibleViewsAtEnd() remove last child#19, updateScrollLimits() 2357 // invalidates scroll max 2358 // 3 In next iteration, when scroll max/min is unknown, focusToViewInLayout() will 2359 // align focused child#18 at center of screen. 2360 // 4 Because #18 is aligned at center, appendVisibleItems() will fill child#19 to 2361 // the right. 2362 // 5 (back to 1 and loop forever) 2363 } while (mGrid.getFirstVisibleIndex() != oldFirstVisible 2364 || mGrid.getLastVisibleIndex() != oldLastVisible); 2365 removeInvisibleViewsAtFront(); 2366 removeInvisibleViewsAtEnd(); 2367 2368 if (state.willRunPredictiveAnimations()) { 2369 fillScrapViewsInPostLayout(); 2370 } 2371 2372 if (DEBUG) { 2373 StringWriter sw = new StringWriter(); 2374 PrintWriter pw = new PrintWriter(sw); 2375 mGrid.debugPrint(pw); 2376 Log.d(getTag(), sw.toString()); 2377 } 2378 2379 if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) { 2380 mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH; 2381 } else { 2382 updateRowSecondarySizeRefresh(); 2383 } 2384 2385 // For fastRelayout, only dispatch event when focus position changes or selected item 2386 // being updated. 2387 if ((mFlag & PF_FAST_RELAYOUT) != 0 && (mFocusPosition != savedFocusPos || mSubFocusPosition 2388 != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView 2389 || (mFlag & PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION) != 0)) { 2390 dispatchChildSelected(); 2391 } else if ((mFlag & (PF_FAST_RELAYOUT | PF_IN_LAYOUT_SEARCH_FOCUS)) 2392 == PF_IN_LAYOUT_SEARCH_FOCUS) { 2393 // For full layout we dispatchChildSelected() in createItem() unless searched all 2394 // children and found none is focusable then dispatchChildSelected() here. 2395 dispatchChildSelected(); 2396 } 2397 dispatchChildSelectedAndPositioned(); 2398 if ((mFlag & PF_SLIDING) != 0) { 2399 scrollDirectionPrimary(getSlideOutDistance()); 2400 } 2401 2402 mFlag &= ~PF_STAGE_MASK; 2403 leaveContext(); 2404 if (DEBUG) Log.v(getTag(), "layoutChildren end"); 2405 } 2406 offsetChildrenSecondary(int increment)2407 private void offsetChildrenSecondary(int increment) { 2408 final int childCount = getChildCount(); 2409 if (mOrientation == HORIZONTAL) { 2410 for (int i = 0; i < childCount; i++) { 2411 getChildAt(i).offsetTopAndBottom(increment); 2412 } 2413 } else { 2414 for (int i = 0; i < childCount; i++) { 2415 getChildAt(i).offsetLeftAndRight(increment); 2416 } 2417 } 2418 } 2419 offsetChildrenPrimary(int increment)2420 private void offsetChildrenPrimary(int increment) { 2421 final int childCount = getChildCount(); 2422 if (mOrientation == VERTICAL) { 2423 for (int i = 0; i < childCount; i++) { 2424 getChildAt(i).offsetTopAndBottom(increment); 2425 } 2426 } else { 2427 for (int i = 0; i < childCount; i++) { 2428 getChildAt(i).offsetLeftAndRight(increment); 2429 } 2430 } 2431 } 2432 2433 @Override scrollHorizontallyBy(int dx, @NonNull Recycler recycler, RecyclerView.@NonNull State state)2434 public int scrollHorizontallyBy(int dx, @NonNull Recycler recycler, 2435 RecyclerView.@NonNull State state) { 2436 if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx); 2437 if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) { 2438 return 0; 2439 } 2440 saveContext(recycler, state); 2441 mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL; 2442 int result; 2443 if (mOrientation == HORIZONTAL) { 2444 result = scrollDirectionPrimary(dx); 2445 } else { 2446 result = scrollDirectionSecondary(dx); 2447 } 2448 leaveContext(); 2449 mFlag &= ~PF_STAGE_MASK; 2450 return result; 2451 } 2452 2453 @Override scrollVerticallyBy(int dy, @NonNull Recycler recycler, RecyclerView.@NonNull State state)2454 public int scrollVerticallyBy(int dy, @NonNull Recycler recycler, 2455 RecyclerView.@NonNull State state) { 2456 if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy); 2457 if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) { 2458 return 0; 2459 } 2460 mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL; 2461 saveContext(recycler, state); 2462 int result; 2463 if (mOrientation == VERTICAL) { 2464 result = scrollDirectionPrimary(dy); 2465 } else { 2466 result = scrollDirectionSecondary(dy); 2467 } 2468 leaveContext(); 2469 mFlag &= ~PF_STAGE_MASK; 2470 return result; 2471 } 2472 2473 // scroll in main direction may add/prune views scrollDirectionPrimary(int da)2474 private int scrollDirectionPrimary(int da) { 2475 // We apply the cap of maxScroll/minScroll to the delta, except for two cases: 2476 // 1. when children are in sliding out mode 2477 // 2. During onLayoutChildren(), it may compensate the remaining scroll delta, 2478 // we should honor the request regardless if it goes over minScroll / maxScroll. 2479 // (see b/64931938 testScrollAndRemove and testScrollAndRemoveSample1) 2480 if ((mFlag & PF_SLIDING) == 0 && (mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) { 2481 if (da > 0) { 2482 if (!mWindowAlignment.mainAxis().isMaxUnknown()) { 2483 int maxScroll = mWindowAlignment.mainAxis().getMaxScroll(); 2484 if (da > maxScroll) { 2485 da = maxScroll; 2486 } 2487 } 2488 } else if (da < 0) { 2489 if (!mWindowAlignment.mainAxis().isMinUnknown()) { 2490 int minScroll = mWindowAlignment.mainAxis().getMinScroll(); 2491 if (da < minScroll) { 2492 da = minScroll; 2493 } 2494 } 2495 } 2496 } 2497 if (da == 0) { 2498 return 0; 2499 } 2500 offsetChildrenPrimary(-da); 2501 if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) { 2502 updateScrollLimits(); 2503 return da; 2504 } 2505 2506 int childCount = getChildCount(); 2507 boolean updated; 2508 2509 if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) { 2510 prependVisibleItems(); 2511 } else { 2512 appendVisibleItems(); 2513 } 2514 updated = getChildCount() > childCount; 2515 childCount = getChildCount(); 2516 2517 if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) { 2518 removeInvisibleViewsAtEnd(); 2519 } else { 2520 removeInvisibleViewsAtFront(); 2521 } 2522 updated |= getChildCount() < childCount; 2523 if (updated) { 2524 updateRowSecondarySizeRefresh(); 2525 } 2526 2527 mBaseGridView.invalidate(); 2528 updateScrollLimits(); 2529 return da; 2530 } 2531 2532 // scroll in second direction will not add/prune views 2533 private int scrollDirectionSecondary(int dy) { 2534 if (dy == 0) { 2535 return 0; 2536 } 2537 offsetChildrenSecondary(-dy); 2538 mScrollOffsetSecondary += dy; 2539 updateSecondaryScrollLimits(); 2540 mBaseGridView.invalidate(); 2541 return dy; 2542 } 2543 2544 @Override 2545 public void collectAdjacentPrefetchPositions(int dx, int dy, @NonNull State state, 2546 @NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) { 2547 try { 2548 saveContext(null, state); 2549 int da = (mOrientation == HORIZONTAL) ? dx : dy; 2550 if (getChildCount() == 0 || da == 0) { 2551 // can't support this scroll, so don't bother prefetching 2552 return; 2553 } 2554 2555 int fromLimit = da < 0 2556 ? -mExtraLayoutSpace 2557 : mSizePrimary + mExtraLayoutSpace; 2558 mGrid.collectAdjacentPrefetchPositions(fromLimit, da, layoutPrefetchRegistry); 2559 } finally { 2560 leaveContext(); 2561 } 2562 } 2563 2564 @Override 2565 public void collectInitialPrefetchPositions(int adapterItemCount, 2566 @NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) { 2567 int numToPrefetch = mBaseGridView.mInitialPrefetchItemCount; 2568 if (adapterItemCount != 0 && numToPrefetch != 0) { 2569 // prefetch items centered around mFocusPosition 2570 int initialPos = Math.max(0, Math.min(mFocusPosition - (numToPrefetch - 1) / 2, 2571 adapterItemCount - numToPrefetch)); 2572 for (int i = initialPos; i < adapterItemCount && i < initialPos + numToPrefetch; i++) { 2573 layoutPrefetchRegistry.addPosition(i, 0); 2574 } 2575 } 2576 } 2577 2578 void updateScrollLimits() { 2579 if (mState.getItemCount() == 0) { 2580 return; 2581 } 2582 int highVisiblePos, lowVisiblePos; 2583 int highMaxPos, lowMinPos; 2584 if ((mFlag & PF_REVERSE_FLOW_PRIMARY) == 0) { 2585 highVisiblePos = mGrid.getLastVisibleIndex(); 2586 highMaxPos = mState.getItemCount() - 1; 2587 lowVisiblePos = mGrid.getFirstVisibleIndex(); 2588 lowMinPos = 0; 2589 } else { 2590 highVisiblePos = mGrid.getFirstVisibleIndex(); 2591 highMaxPos = 0; 2592 lowVisiblePos = mGrid.getLastVisibleIndex(); 2593 lowMinPos = mState.getItemCount() - 1; 2594 } 2595 if (highVisiblePos < 0 || lowVisiblePos < 0) { 2596 return; 2597 } 2598 final boolean highAvailable = highVisiblePos == highMaxPos; 2599 final boolean lowAvailable = lowVisiblePos == lowMinPos; 2600 if (!highAvailable && mWindowAlignment.mainAxis().isMaxUnknown() 2601 && !lowAvailable && mWindowAlignment.mainAxis().isMinUnknown()) { 2602 return; 2603 } 2604 int maxEdge, maxViewCenter; 2605 if (highAvailable) { 2606 maxEdge = mGrid.findRowMax(true, sTwoInts); 2607 View maxChild = findViewByPosition(sTwoInts[1]); 2608 maxViewCenter = getViewCenter(maxChild); 2609 final LayoutParams lp = (LayoutParams) maxChild.getLayoutParams(); 2610 int[] multipleAligns = lp.getAlignMultiple(); 2611 if (multipleAligns != null && multipleAligns.length > 0) { 2612 maxViewCenter += multipleAligns[multipleAligns.length - 1] - multipleAligns[0]; 2613 } 2614 } else { 2615 maxEdge = Integer.MAX_VALUE; 2616 maxViewCenter = Integer.MAX_VALUE; 2617 } 2618 int minEdge, minViewCenter; 2619 if (lowAvailable) { 2620 minEdge = mGrid.findRowMin(false, sTwoInts); 2621 View minChild = findViewByPosition(sTwoInts[1]); 2622 minViewCenter = getViewCenter(minChild); 2623 } else { 2624 minEdge = Integer.MIN_VALUE; 2625 minViewCenter = Integer.MIN_VALUE; 2626 } 2627 mWindowAlignment.mainAxis().updateMinMax(minEdge, maxEdge, minViewCenter, maxViewCenter); 2628 } 2629 2630 /** 2631 * Update secondary axis's scroll min/max, should be updated in 2632 * {@link #scrollDirectionSecondary(int)}. 2633 */ 2634 private void updateSecondaryScrollLimits() { 2635 WindowAlignment.Axis secondAxis = mWindowAlignment.secondAxis(); 2636 int minEdge = secondAxis.getPaddingMin() - mScrollOffsetSecondary; 2637 int maxEdge = minEdge + getSizeSecondary(); 2638 secondAxis.updateMinMax(minEdge, maxEdge, minEdge, maxEdge); 2639 } 2640 2641 private void initScrollController() { 2642 mWindowAlignment.reset(); 2643 mWindowAlignment.horizontal.setSize(getWidth()); 2644 mWindowAlignment.vertical.setSize(getHeight()); 2645 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 2646 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 2647 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 2648 mScrollOffsetSecondary = 0; 2649 2650 if (DEBUG) { 2651 Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary 2652 + " mWindowAlignment " + mWindowAlignment); 2653 } 2654 } 2655 2656 private void updateScrollController() { 2657 mWindowAlignment.horizontal.setSize(getWidth()); 2658 mWindowAlignment.vertical.setSize(getHeight()); 2659 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 2660 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 2661 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 2662 2663 if (DEBUG) { 2664 Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary 2665 + " mWindowAlignment " + mWindowAlignment); 2666 } 2667 } 2668 2669 @Override 2670 public void scrollToPosition(int position) { 2671 setSelection(position, 0, false, 0); 2672 } 2673 2674 @Override 2675 public void smoothScrollToPosition(@NonNull RecyclerView recyclerView, @NonNull State state, 2676 int position) { 2677 setSelection(position, 0, true, 0); 2678 } 2679 2680 void setSelection(int position, 2681 int primaryScrollExtra) { 2682 setSelection(position, 0, false, primaryScrollExtra); 2683 } 2684 2685 void setSelectionSmooth(int position) { 2686 setSelection(position, 0, true, 0); 2687 } 2688 2689 void setSelectionWithSub(int position, int subposition, 2690 int primaryScrollExtra) { 2691 setSelection(position, subposition, false, primaryScrollExtra); 2692 } 2693 2694 void setSelectionSmoothWithSub(int position, int subposition) { 2695 setSelection(position, subposition, true, 0); 2696 } 2697 2698 int getSelection() { 2699 return mFocusPosition; 2700 } 2701 2702 int getSubSelection() { 2703 return mSubFocusPosition; 2704 } 2705 2706 void setSelection(int position, int subposition, boolean smooth, 2707 int primaryScrollExtra) { 2708 if ((mFocusPosition != position && position != NO_POSITION) 2709 || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) { 2710 scrollToSelection(position, subposition, smooth, primaryScrollExtra); 2711 } 2712 } 2713 2714 void scrollToSelection(int position, int subposition, 2715 boolean smooth, int primaryScrollExtra) { 2716 mPrimaryScrollExtra = primaryScrollExtra; 2717 2718 View view = findViewByPosition(position); 2719 // scrollToView() is based on Adapter position. Only call scrollToView() when item 2720 // is still valid and no layout is requested, otherwise defer to next layout pass. 2721 // If it is still in smoothScrolling, we should either update smoothScroller or initiate 2722 // a layout. 2723 final boolean notSmoothScrolling = !isSmoothScrolling(); 2724 if (notSmoothScrolling && !mBaseGridView.isLayoutRequested() 2725 && view != null && getAdapterPositionByView(view) == position) { 2726 mFlag |= PF_IN_SELECTION; 2727 scrollToView(view, smooth); 2728 mFlag &= ~PF_IN_SELECTION; 2729 } else { 2730 if ((mFlag & PF_LAYOUT_ENABLED) == 0 || (mFlag & PF_SLIDING) != 0) { 2731 mFocusPosition = position; 2732 mSubFocusPosition = subposition; 2733 mFocusPositionOffset = Integer.MIN_VALUE; 2734 return; 2735 } 2736 if (smooth && !mBaseGridView.isLayoutRequested()) { 2737 mFocusPosition = position; 2738 mSubFocusPosition = subposition; 2739 mFocusPositionOffset = Integer.MIN_VALUE; 2740 if (!hasDoneFirstLayout()) { 2741 Log.w(getTag(), "setSelectionSmooth should " 2742 + "not be called before first layout pass"); 2743 return; 2744 } 2745 position = startPositionSmoothScroller(position); 2746 if (position != mFocusPosition) { 2747 // gets cropped by adapter size 2748 mFocusPosition = position; 2749 mSubFocusPosition = 0; 2750 } 2751 } else { 2752 // stopScroll might change mFocusPosition, so call it before assign value to 2753 // mFocusPosition 2754 if (!notSmoothScrolling) { 2755 skipSmoothScrollerOnStopInternal(); 2756 mBaseGridView.stopScroll(); 2757 } 2758 if (!mBaseGridView.isLayoutRequested() 2759 && view != null && getAdapterPositionByView(view) == position) { 2760 mFlag |= PF_IN_SELECTION; 2761 scrollToView(view, smooth); 2762 mFlag &= ~PF_IN_SELECTION; 2763 } else { 2764 mFocusPosition = position; 2765 mSubFocusPosition = subposition; 2766 mFocusPositionOffset = Integer.MIN_VALUE; 2767 mFlag |= PF_FORCE_FULL_LAYOUT; 2768 requestLayout(); 2769 } 2770 } 2771 } 2772 } 2773 2774 int startPositionSmoothScroller(int position) { 2775 LinearSmoothScroller linearSmoothScroller = new GridLinearSmoothScroller() { 2776 @Override 2777 public PointF computeScrollVectorForPosition(int targetPosition) { 2778 if (getChildCount() == 0) { 2779 return null; 2780 } 2781 final int firstChildPos = getPosition(getChildAt(0)); 2782 // TODO We should be able to deduce direction from bounds of current and target 2783 // focus, rather than making assumptions about positions and directionality 2784 final boolean isStart = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 2785 ? targetPosition > firstChildPos 2786 : targetPosition < firstChildPos; 2787 final int direction = isStart ? -1 : 1; 2788 if (mOrientation == HORIZONTAL) { 2789 return new PointF(direction, 0); 2790 } else { 2791 return new PointF(0, direction); 2792 } 2793 } 2794 2795 }; 2796 linearSmoothScroller.setTargetPosition(position); 2797 startSmoothScroll(linearSmoothScroller); 2798 return linearSmoothScroller.getTargetPosition(); 2799 } 2800 2801 /** 2802 * when start a new SmoothScroller or scroll to a different location, dont need 2803 * current SmoothScroller.onStopInternal() doing the scroll work. 2804 */ 2805 void skipSmoothScrollerOnStopInternal() { 2806 if (mCurrentSmoothScroller != null) { 2807 mCurrentSmoothScroller.mSkipOnStopInternal = true; 2808 } 2809 } 2810 2811 @Override 2812 public void startSmoothScroll(RecyclerView.@NonNull SmoothScroller smoothScroller) { 2813 skipSmoothScrollerOnStopInternal(); 2814 super.startSmoothScroll(smoothScroller); 2815 if (smoothScroller.isRunning() && smoothScroller instanceof GridLinearSmoothScroller) { 2816 mCurrentSmoothScroller = (GridLinearSmoothScroller) smoothScroller; 2817 if (mCurrentSmoothScroller instanceof PendingMoveSmoothScroller) { 2818 mPendingMoveSmoothScroller = (PendingMoveSmoothScroller) mCurrentSmoothScroller; 2819 } else { 2820 mPendingMoveSmoothScroller = null; 2821 } 2822 } else { 2823 mCurrentSmoothScroller = null; 2824 mPendingMoveSmoothScroller = null; 2825 } 2826 } 2827 2828 void processPendingMovement(boolean forward) { 2829 if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) { 2830 return; 2831 } 2832 if (mPendingMoveSmoothScroller == null) { 2833 PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller( 2834 forward ? 1 : -1, mNumRows > 1); 2835 mFocusPositionOffset = 0; 2836 startSmoothScroll(linearSmoothScroller); 2837 } else { 2838 if (forward) { 2839 mPendingMoveSmoothScroller.increasePendingMoves(); 2840 } else { 2841 mPendingMoveSmoothScroller.decreasePendingMoves(); 2842 } 2843 } 2844 int soundEffect; 2845 if (mOrientation == HORIZONTAL) { 2846 boolean rtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 2847 if (rtl) { 2848 soundEffect = forward ? AudioManager.FX_FOCUS_NAVIGATION_LEFT : 2849 AudioManager.FX_FOCUS_NAVIGATION_RIGHT; 2850 } else { 2851 soundEffect = forward ? AudioManager.FX_FOCUS_NAVIGATION_RIGHT : 2852 AudioManager.FX_FOCUS_NAVIGATION_LEFT; 2853 } 2854 } else { 2855 soundEffect = forward ? AudioManager.FX_FOCUS_NAVIGATION_DOWN : 2856 AudioManager.FX_FOCUS_NAVIGATION_UP; 2857 } 2858 getAudioManager().playSoundEffect(soundEffect); 2859 } 2860 2861 @Override 2862 public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, 2863 int itemCount) { 2864 if (DEBUG) { 2865 Log.v(getTag(), "onItemsAdded positionStart " 2866 + positionStart + " itemCount " + itemCount); 2867 } 2868 if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0 2869 && mFocusPositionOffset != Integer.MIN_VALUE) { 2870 int pos = mFocusPosition + mFocusPositionOffset; 2871 if (positionStart <= pos) { 2872 mFocusPositionOffset += itemCount; 2873 } 2874 } 2875 mChildrenStates.clear(); 2876 } 2877 2878 @Override 2879 public void onItemsChanged(@NonNull RecyclerView recyclerView) { 2880 if (DEBUG) Log.v(getTag(), "onItemsChanged"); 2881 mFocusPositionOffset = 0; 2882 mChildrenStates.clear(); 2883 } 2884 2885 @Override 2886 public void onItemsRemoved(@NonNull RecyclerView recyclerView, 2887 int positionStart, int itemCount) { 2888 if (DEBUG) { 2889 Log.v(getTag(), "onItemsRemoved positionStart " 2890 + positionStart + " itemCount " + itemCount); 2891 } 2892 if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0 2893 && mFocusPositionOffset != Integer.MIN_VALUE) { 2894 int pos = mFocusPosition + mFocusPositionOffset; 2895 if (positionStart <= pos) { 2896 if (positionStart + itemCount > pos) { 2897 // stop updating offset after the focus item was removed 2898 mFocusPositionOffset += positionStart - pos; 2899 mFocusPosition += mFocusPositionOffset; 2900 mFocusPositionOffset = Integer.MIN_VALUE; 2901 } else { 2902 mFocusPositionOffset -= itemCount; 2903 } 2904 } 2905 } 2906 mChildrenStates.clear(); 2907 } 2908 2909 @Override 2910 public void onItemsMoved(@NonNull RecyclerView recyclerView, 2911 int fromPosition, int toPosition, int itemCount) { 2912 if (DEBUG) { 2913 Log.v(getTag(), "onItemsMoved fromPosition " 2914 + fromPosition + " toPosition " + toPosition); 2915 } 2916 if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) { 2917 int pos = mFocusPosition + mFocusPositionOffset; 2918 if (fromPosition <= pos && pos < fromPosition + itemCount) { 2919 // moved items include focused position 2920 mFocusPositionOffset += toPosition - fromPosition; 2921 } else if (fromPosition < pos && toPosition > pos - itemCount) { 2922 // move items before focus position to after focused position 2923 mFocusPositionOffset -= itemCount; 2924 } else if (fromPosition > pos && toPosition < pos) { 2925 // move items after focus position to before focused position 2926 mFocusPositionOffset += itemCount; 2927 } 2928 } 2929 mChildrenStates.clear(); 2930 } 2931 2932 @Override 2933 public void onItemsUpdated(@NonNull RecyclerView recyclerView, 2934 int positionStart, int itemCount) { 2935 if (DEBUG) { 2936 Log.v(getTag(), "onItemsUpdated positionStart " 2937 + positionStart + " itemCount " + itemCount); 2938 } 2939 for (int i = positionStart, end = positionStart + itemCount; i < end; i++) { 2940 mChildrenStates.remove(i); 2941 } 2942 } 2943 2944 @Override 2945 public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull State state, 2946 @NonNull View child, @Nullable View focused) { 2947 if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) { 2948 return true; 2949 } 2950 if (getAdapterPositionByView(child) == NO_POSITION) { 2951 // This is could be the last view in DISAPPEARING animation. 2952 return true; 2953 } 2954 if ((mFlag & (PF_STAGE_MASK | PF_IN_SELECTION)) == 0) { 2955 scrollToView(child, focused, true); 2956 } 2957 return true; 2958 } 2959 2960 @Override 2961 public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, 2962 @NonNull View child, @NonNull Rect rect, boolean immediate) { 2963 if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + child + " " + rect); 2964 return false; 2965 } 2966 2967 void getViewSelectedOffsets(View view, int[] offsets) { 2968 if (mOrientation == HORIZONTAL) { 2969 offsets[0] = getPrimaryAlignedScrollDistance(view); 2970 offsets[1] = getSecondaryScrollDistance(view); 2971 } else { 2972 offsets[1] = getPrimaryAlignedScrollDistance(view); 2973 offsets[0] = getSecondaryScrollDistance(view); 2974 } 2975 } 2976 2977 /** 2978 * Return the scroll delta on primary direction to make the view selected. If the return value 2979 * is 0, there is no need to scroll. 2980 */ 2981 private int getPrimaryAlignedScrollDistance(View view) { 2982 return mWindowAlignment.mainAxis().getScroll(getViewCenter(view)); 2983 } 2984 2985 /** 2986 * Get adjusted primary position for a given childView (if there is multiple ItemAlignment 2987 * defined on the view). 2988 */ 2989 private int getAdjustedPrimaryAlignedScrollDistance(int scrollPrimary, View view, 2990 View childView) { 2991 int subindex = getSubPositionByView(view, childView); 2992 if (subindex != 0) { 2993 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2994 scrollPrimary += lp.getAlignMultiple()[subindex] - lp.getAlignMultiple()[0]; 2995 } 2996 return scrollPrimary; 2997 } 2998 2999 private int getSecondaryScrollDistance(View view) { 3000 int viewCenterSecondary = getViewCenterSecondary(view); 3001 return mWindowAlignment.secondAxis().getScroll(viewCenterSecondary); 3002 } 3003 3004 /** 3005 * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state. 3006 */ 3007 void scrollToView(View view, boolean smooth) { 3008 scrollToView(view, view == null ? null : view.findFocus(), smooth); 3009 } 3010 3011 void scrollToView(View view, boolean smooth, int extraDelta, int extraDeltaSecondary) { 3012 scrollToView(view, view == null ? null : view.findFocus(), smooth, extraDelta, 3013 extraDeltaSecondary); 3014 } 3015 3016 private void scrollToView(View view, View childView, boolean smooth) { 3017 scrollToView(view, childView, smooth, 0, 0); 3018 } 3019 3020 /** 3021 * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state. 3022 */ 3023 private void scrollToView(View view, View childView, boolean smooth, int extraDelta, 3024 int extraDeltaSecondary) { 3025 if ((mFlag & PF_SLIDING) != 0) { 3026 return; 3027 } 3028 int newFocusPosition = getAdapterPositionByView(view); 3029 int newSubFocusPosition = getSubPositionByView(view, childView); 3030 if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) { 3031 mFocusPosition = newFocusPosition; 3032 mSubFocusPosition = newSubFocusPosition; 3033 mFocusPositionOffset = 0; 3034 if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) { 3035 dispatchChildSelected(); 3036 } 3037 if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) { 3038 mBaseGridView.invalidate(); 3039 } 3040 } 3041 if (view == null) { 3042 return; 3043 } 3044 if (!view.hasFocus() && mBaseGridView.hasFocus()) { 3045 // transfer focus to the child if it does not have focus yet (e.g. triggered 3046 // by setSelection()) 3047 view.requestFocus(); 3048 } 3049 if ((mFlag & PF_SCROLL_ENABLED) == 0 && smooth) { 3050 return; 3051 } 3052 if (getScrollPosition(view, childView, sTwoInts) 3053 || extraDelta != 0 || extraDeltaSecondary != 0) { 3054 scrollGrid(sTwoInts[0] + extraDelta, sTwoInts[1] + extraDeltaSecondary, smooth); 3055 } 3056 } 3057 3058 boolean getScrollPosition(View view, View childView, int[] deltas) { 3059 switch (mFocusScrollStrategy) { 3060 case BaseGridView.FOCUS_SCROLL_ALIGNED: 3061 default: 3062 return getAlignedPosition(view, childView, deltas); 3063 case BaseGridView.FOCUS_SCROLL_ITEM: 3064 case BaseGridView.FOCUS_SCROLL_PAGE: 3065 return getNoneAlignedPosition(view, deltas); 3066 } 3067 } 3068 3069 private boolean getNoneAlignedPosition(View view, int[] deltas) { 3070 int pos = getAdapterPositionByView(view); 3071 int viewMin = getViewMin(view); 3072 int viewMax = getViewMax(view); 3073 // we either align "firstView" to left/top padding edge 3074 // or align "lastView" to right/bottom padding edge 3075 View firstView = null; 3076 View lastView = null; 3077 int paddingMin = mWindowAlignment.mainAxis().getPaddingMin(); 3078 int clientSize = mWindowAlignment.mainAxis().getClientSize(); 3079 final int row = mGrid.getRowIndex(pos); 3080 if (viewMin < paddingMin) { 3081 // view enters low padding area: 3082 firstView = view; 3083 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 3084 // scroll one "page" left/top, 3085 // align first visible item of the "page" at the low padding edge. 3086 while (prependOneColumnVisibleItems()) { 3087 CircularIntArray positions = 3088 mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row]; 3089 firstView = findViewByPosition(positions.get(0)); 3090 if (viewMax - getViewMin(firstView) > clientSize) { 3091 if (positions.size() > 2) { 3092 firstView = findViewByPosition(positions.get(2)); 3093 } 3094 break; 3095 } 3096 } 3097 } 3098 } else if (viewMax > clientSize + paddingMin) { 3099 // view enters high padding area: 3100 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 3101 // scroll whole one page right/bottom, align view at the low padding edge. 3102 firstView = view; 3103 do { 3104 CircularIntArray positions = 3105 mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row]; 3106 lastView = findViewByPosition(positions.get(positions.size() - 1)); 3107 if (getViewMax(lastView) - viewMin > clientSize) { 3108 lastView = null; 3109 break; 3110 } 3111 } while (appendOneColumnVisibleItems()); 3112 if (lastView != null) { 3113 // however if we reached end, we should align last view. 3114 firstView = null; 3115 } 3116 } else { 3117 lastView = view; 3118 } 3119 } 3120 int scrollPrimary = 0; 3121 if (firstView != null) { 3122 scrollPrimary = getViewMin(firstView) - paddingMin; 3123 } else if (lastView != null) { 3124 scrollPrimary = getViewMax(lastView) - (paddingMin + clientSize); 3125 } 3126 View secondaryAlignedView; 3127 if (firstView != null) { 3128 secondaryAlignedView = firstView; 3129 } else if (lastView != null) { 3130 secondaryAlignedView = lastView; 3131 } else { 3132 secondaryAlignedView = view; 3133 } 3134 int scrollSecondary = getSecondaryScrollDistance(secondaryAlignedView); 3135 if (scrollPrimary != 0 || scrollSecondary != 0) { 3136 deltas[0] = scrollPrimary; 3137 deltas[1] = scrollSecondary; 3138 return true; 3139 } 3140 return false; 3141 } 3142 3143 private boolean getAlignedPosition(View view, View childView, int[] deltas) { 3144 int scrollPrimary = getPrimaryAlignedScrollDistance(view); 3145 if (childView != null) { 3146 scrollPrimary = getAdjustedPrimaryAlignedScrollDistance(scrollPrimary, view, childView); 3147 } 3148 int scrollSecondary = getSecondaryScrollDistance(view); 3149 if (DEBUG) { 3150 Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary 3151 + " " + mPrimaryScrollExtra + " " + mWindowAlignment); 3152 } 3153 scrollPrimary += mPrimaryScrollExtra; 3154 if (scrollPrimary != 0 || scrollSecondary != 0) { 3155 deltas[0] = scrollPrimary; 3156 deltas[1] = scrollSecondary; 3157 return true; 3158 } else { 3159 deltas[0] = 0; 3160 deltas[1] = 0; 3161 } 3162 return false; 3163 } 3164 3165 private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) { 3166 if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) { 3167 scrollDirectionPrimary(scrollPrimary); 3168 scrollDirectionSecondary(scrollSecondary); 3169 } else { 3170 int scrollX; 3171 int scrollY; 3172 if (mOrientation == HORIZONTAL) { 3173 scrollX = scrollPrimary; 3174 scrollY = scrollSecondary; 3175 } else { 3176 scrollX = scrollSecondary; 3177 scrollY = scrollPrimary; 3178 } 3179 if (smooth) { 3180 mBaseGridView.smoothScrollBy(scrollX, scrollY); 3181 } else { 3182 mBaseGridView.scrollBy(scrollX, scrollY); 3183 dispatchChildSelectedAndPositioned(); 3184 } 3185 } 3186 } 3187 3188 void setPruneChild(boolean pruneChild) { 3189 if (((mFlag & PF_PRUNE_CHILD) != 0) != pruneChild) { 3190 mFlag = (mFlag & ~PF_PRUNE_CHILD) | (pruneChild ? PF_PRUNE_CHILD : 0); 3191 if (pruneChild) { 3192 requestLayout(); 3193 } 3194 } 3195 } 3196 3197 boolean getPruneChild() { 3198 return (mFlag & PF_PRUNE_CHILD) != 0; 3199 } 3200 3201 void setScrollEnabled(boolean scrollEnabled) { 3202 if (((mFlag & PF_SCROLL_ENABLED) != 0) != scrollEnabled) { 3203 mFlag = (mFlag & ~PF_SCROLL_ENABLED) | (scrollEnabled ? PF_SCROLL_ENABLED : 0); 3204 if (((mFlag & PF_SCROLL_ENABLED) != 0) 3205 && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED 3206 && mFocusPosition != NO_POSITION) { 3207 scrollToSelection(mFocusPosition, mSubFocusPosition, 3208 true, mPrimaryScrollExtra); 3209 } 3210 } 3211 } 3212 3213 boolean isScrollEnabled() { 3214 return (mFlag & PF_SCROLL_ENABLED) != 0; 3215 } 3216 3217 private int findImmediateChildIndex(View view) { 3218 if (view != null && mBaseGridView != null && view != mBaseGridView) { 3219 view = findContainingItemView(view); 3220 if (view != null) { 3221 for (int i = 0, count = getChildCount(); i < count; i++) { 3222 if (getChildAt(i) == view) { 3223 return i; 3224 } 3225 } 3226 } 3227 } 3228 return NO_POSITION; 3229 } 3230 3231 void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 3232 if (gainFocus) { 3233 // if gridview.requestFocus() is called, select first focusable child. 3234 int i = mFocusPosition; 3235 while (true) { 3236 View view = findViewByPosition(i); 3237 if (view == null) { 3238 break; 3239 } 3240 if (view.getVisibility() == View.VISIBLE && view.hasFocusable()) { 3241 view.requestFocus(); 3242 break; 3243 } 3244 i++; 3245 } 3246 } 3247 } 3248 3249 void setFocusSearchDisabled(boolean disabled) { 3250 mFlag = (mFlag & ~PF_FOCUS_SEARCH_DISABLED) | (disabled ? PF_FOCUS_SEARCH_DISABLED : 0); 3251 } 3252 3253 boolean isFocusSearchDisabled() { 3254 return (mFlag & PF_FOCUS_SEARCH_DISABLED) != 0; 3255 } 3256 3257 @Override 3258 public @Nullable View onInterceptFocusSearch(@Nullable View focused, int direction) { 3259 if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) { 3260 return focused; 3261 } 3262 3263 final FocusFinder ff = FocusFinder.getInstance(); 3264 View result = null; 3265 if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) { 3266 // convert direction to absolute direction and see if we have a view there and if not 3267 // tell LayoutManager to add if it can. 3268 if (canScrollVertically()) { 3269 final int absDir = 3270 direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP; 3271 result = ff.findNextFocus(mBaseGridView, focused, absDir); 3272 } 3273 if (canScrollHorizontally()) { 3274 boolean rtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 3275 final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl 3276 ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 3277 result = ff.findNextFocus(mBaseGridView, focused, absDir); 3278 } 3279 } else { 3280 result = ff.findNextFocus(mBaseGridView, focused, direction); 3281 } 3282 if (result != null) { 3283 return result; 3284 } 3285 3286 if (mBaseGridView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) { 3287 return mBaseGridView.getParent().focusSearch(focused, direction); 3288 } 3289 3290 if (DEBUG) Log.v(getTag(), "regular focusSearch failed direction " + direction); 3291 int movement = getMovement(direction); 3292 final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE; 3293 if (movement == NEXT_ITEM) { 3294 if (isScroll || (mFlag & PF_FOCUS_OUT_BACK) == 0) { 3295 result = focused; 3296 } 3297 if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedLastItem()) { 3298 processPendingMovement(true); 3299 result = focused; 3300 } 3301 } else if (movement == PREV_ITEM) { 3302 if (isScroll || (mFlag & PF_FOCUS_OUT_FRONT) == 0) { 3303 result = focused; 3304 } 3305 if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedFirstItem()) { 3306 processPendingMovement(false); 3307 result = focused; 3308 } 3309 } else if (movement == NEXT_ROW) { 3310 if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_END) == 0) { 3311 result = focused; 3312 } 3313 } else if (movement == PREV_ROW) { 3314 if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_START) == 0) { 3315 result = focused; 3316 } 3317 } 3318 if (result != null) { 3319 return result; 3320 } 3321 3322 if (DEBUG) Log.v(getTag(), "now focusSearch in parent"); 3323 result = mBaseGridView.getParent().focusSearch(focused, direction); 3324 if (result != null) { 3325 return result; 3326 } 3327 return focused != null ? focused : mBaseGridView; 3328 } 3329 3330 boolean hasPreviousViewInSameRow(int pos) { 3331 if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) { 3332 return false; 3333 } 3334 if (mGrid.getFirstVisibleIndex() > 0) { 3335 return true; 3336 } 3337 final int focusedRow = mGrid.getLocation(pos).mRow; 3338 for (int i = getChildCount() - 1; i >= 0; i--) { 3339 int position = getAdapterPositionByIndex(i); 3340 Grid.Location loc = mGrid.getLocation(position); 3341 if (loc != null && loc.mRow == focusedRow) { 3342 if (position < pos) { 3343 return true; 3344 } 3345 } 3346 } 3347 return false; 3348 } 3349 3350 @Override 3351 public boolean onAddFocusables(@NonNull RecyclerView recyclerView, 3352 @SuppressLint("ConcreteCollection") @NonNull ArrayList<View> views, int direction, 3353 int focusableMode) { 3354 if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) { 3355 return true; 3356 } 3357 // If this viewgroup or one of its children currently has focus then we 3358 // consider our children for focus searching in main direction on the same row. 3359 // If this viewgroup has no focus and using focus align, we want the system 3360 // to ignore our children and pass focus to the viewgroup, which will pass 3361 // focus on to its children appropriately. 3362 // If this viewgroup has no focus and not using focus align, we want to 3363 // consider the child that does not overlap with padding area. 3364 if (recyclerView.hasFocus()) { 3365 if (mPendingMoveSmoothScroller != null) { 3366 // don't find next focusable if has pending movement. 3367 return true; 3368 } 3369 final int movement = getMovement(direction); 3370 final View focused = recyclerView.findFocus(); 3371 final int focusedIndex = findImmediateChildIndex(focused); 3372 final int focusedPos = getAdapterPositionByIndex(focusedIndex); 3373 // Even if focusedPos != NO_POSITION, findViewByPosition could return null if the view 3374 // is ignored or getLayoutPosition does not match the adapter position of focused view. 3375 final View immediateFocusedChild = (focusedPos == NO_POSITION) ? null 3376 : findViewByPosition(focusedPos); 3377 // Add focusables of focused item. 3378 if (immediateFocusedChild != null) { 3379 immediateFocusedChild.addFocusables(views, direction, focusableMode); 3380 } 3381 if (mGrid == null || getChildCount() == 0) { 3382 // no grid information, or no child, bail out. 3383 return true; 3384 } 3385 if ((movement == NEXT_ROW || movement == PREV_ROW) && mGrid.getNumRows() <= 1) { 3386 // For single row, cannot navigate to previous/next row. 3387 return true; 3388 } 3389 // Add focusables of neighbor depending on the focus search direction. 3390 final int focusedRow = mGrid != null && immediateFocusedChild != null 3391 ? mGrid.getLocation(focusedPos).mRow : NO_POSITION; 3392 final int focusableCount = views.size(); 3393 int inc = movement == NEXT_ITEM || movement == NEXT_ROW ? 1 : -1; 3394 int loop_end = inc > 0 ? getChildCount() - 1 : 0; 3395 int loop_start; 3396 if (focusedIndex == NO_POSITION) { 3397 loop_start = inc > 0 ? 0 : getChildCount() - 1; 3398 } else { 3399 loop_start = focusedIndex + inc; 3400 } 3401 for (int i = loop_start; inc > 0 ? i <= loop_end : i >= loop_end; i += inc) { 3402 final View child = getChildAt(i); 3403 if (child.getVisibility() != View.VISIBLE || !child.hasFocusable()) { 3404 continue; 3405 } 3406 // if there wasn't any focused item, add the very first focusable 3407 // items and stop. 3408 if (immediateFocusedChild == null) { 3409 child.addFocusables(views, direction, focusableMode); 3410 if (views.size() > focusableCount) { 3411 break; 3412 } 3413 continue; 3414 } 3415 int position = getAdapterPositionByIndex(i); 3416 Grid.Location loc = mGrid.getLocation(position); 3417 if (loc == null) { 3418 continue; 3419 } 3420 if (movement == NEXT_ITEM) { 3421 // Add first focusable item on the same row 3422 if (loc.mRow == focusedRow && position > focusedPos) { 3423 child.addFocusables(views, direction, focusableMode); 3424 if (views.size() > focusableCount) { 3425 break; 3426 } 3427 } 3428 } else if (movement == PREV_ITEM) { 3429 // Add first focusable item on the same row 3430 if (loc.mRow == focusedRow && position < focusedPos) { 3431 child.addFocusables(views, direction, focusableMode); 3432 if (views.size() > focusableCount) { 3433 break; 3434 } 3435 } 3436 } else if (movement == NEXT_ROW) { 3437 // Add all focusable items after this item whose row index is bigger 3438 if (loc.mRow == focusedRow) { 3439 continue; 3440 } else if (loc.mRow < focusedRow) { 3441 break; 3442 } 3443 child.addFocusables(views, direction, focusableMode); 3444 } else if (movement == PREV_ROW) { 3445 // Add all focusable items before this item whose row index is smaller 3446 if (loc.mRow == focusedRow) { 3447 continue; 3448 } else if (loc.mRow > focusedRow) { 3449 break; 3450 } 3451 child.addFocusables(views, direction, focusableMode); 3452 } 3453 } 3454 } else { 3455 int focusableCount = views.size(); 3456 if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) { 3457 // adding views not overlapping padding area to avoid scrolling in gaining focus 3458 int left = mWindowAlignment.mainAxis().getPaddingMin(); 3459 int right = mWindowAlignment.mainAxis().getClientSize() + left; 3460 for (int i = 0, count = getChildCount(); i < count; i++) { 3461 View child = getChildAt(i); 3462 if (child.getVisibility() == View.VISIBLE) { 3463 if (getViewMin(child) >= left && getViewMax(child) <= right) { 3464 child.addFocusables(views, direction, focusableMode); 3465 } 3466 } 3467 } 3468 // if we cannot find any, then just add all children. 3469 if (views.size() == focusableCount) { 3470 for (int i = 0, count = getChildCount(); i < count; i++) { 3471 View child = getChildAt(i); 3472 if (child.getVisibility() == View.VISIBLE) { 3473 child.addFocusables(views, direction, focusableMode); 3474 } 3475 } 3476 } 3477 } else { 3478 View view = findViewByPosition(mFocusPosition); 3479 if (view != null) { 3480 view.addFocusables(views, direction, focusableMode); 3481 } 3482 } 3483 // if still cannot find any, fall through and add itself 3484 if (views.size() != focusableCount) { 3485 return true; 3486 } 3487 if (recyclerView.isFocusable()) { 3488 views.add(recyclerView); 3489 } 3490 } 3491 return true; 3492 } 3493 hasCreatedLastItem()3494 boolean hasCreatedLastItem() { 3495 int count = getItemCount(); 3496 return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(count - 1) != null; 3497 } 3498 hasCreatedFirstItem()3499 boolean hasCreatedFirstItem() { 3500 int count = getItemCount(); 3501 return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(0) != null; 3502 } 3503 isItemFullyVisible(int pos)3504 boolean isItemFullyVisible(int pos) { 3505 RecyclerView.ViewHolder vh = mBaseGridView.findViewHolderForAdapterPosition(pos); 3506 if (vh == null) { 3507 return false; 3508 } 3509 return vh.itemView.getLeft() >= 0 && vh.itemView.getRight() <= mBaseGridView.getWidth() 3510 && vh.itemView.getTop() >= 0 && vh.itemView.getBottom() 3511 <= mBaseGridView.getHeight(); 3512 } 3513 canScrollTo(View view)3514 boolean canScrollTo(View view) { 3515 return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable()); 3516 } 3517 gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction, Rect previouslyFocusedRect)3518 boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction, 3519 Rect previouslyFocusedRect) { 3520 switch (mFocusScrollStrategy) { 3521 case BaseGridView.FOCUS_SCROLL_ALIGNED: 3522 default: 3523 return gridOnRequestFocusInDescendantsAligned( 3524 direction, previouslyFocusedRect); 3525 case BaseGridView.FOCUS_SCROLL_PAGE: 3526 case BaseGridView.FOCUS_SCROLL_ITEM: 3527 return gridOnRequestFocusInDescendantsUnaligned( 3528 direction, previouslyFocusedRect); 3529 } 3530 } 3531 gridOnRequestFocusInDescendantsAligned(int direction, Rect previouslyFocusedRect)3532 private boolean gridOnRequestFocusInDescendantsAligned(int direction, 3533 Rect previouslyFocusedRect) { 3534 View view = findViewByPosition(mFocusPosition); 3535 if (view != null) { 3536 boolean result = view.requestFocus(direction, previouslyFocusedRect); 3537 if (!result && DEBUG) { 3538 Log.w(getTag(), "failed to request focus on " + view); 3539 } 3540 return result; 3541 } 3542 return false; 3543 } 3544 gridOnRequestFocusInDescendantsUnaligned(int direction, Rect previouslyFocusedRect)3545 private boolean gridOnRequestFocusInDescendantsUnaligned(int direction, 3546 Rect previouslyFocusedRect) { 3547 // focus to view not overlapping padding area to avoid scrolling in gaining focus 3548 int index; 3549 int increment; 3550 int end; 3551 int count = getChildCount(); 3552 if ((direction & View.FOCUS_FORWARD) != 0) { 3553 index = 0; 3554 increment = 1; 3555 end = count; 3556 } else { 3557 index = count - 1; 3558 increment = -1; 3559 end = -1; 3560 } 3561 int left = mWindowAlignment.mainAxis().getPaddingMin(); 3562 int right = mWindowAlignment.mainAxis().getClientSize() + left; 3563 for (int i = index; i != end; i += increment) { 3564 View child = getChildAt(i); 3565 if (child.getVisibility() == View.VISIBLE) { 3566 if (getViewMin(child) >= left && getViewMax(child) <= right) { 3567 if (child.requestFocus(direction, previouslyFocusedRect)) { 3568 return true; 3569 } 3570 } 3571 } 3572 } 3573 return false; 3574 } 3575 3576 private static final int PREV_ITEM = 0; 3577 private static final int NEXT_ITEM = 1; 3578 private static final int PREV_ROW = 2; 3579 private static final int NEXT_ROW = 3; 3580 getMovement(int direction)3581 private int getMovement(int direction) { 3582 int movement = View.FOCUS_LEFT; 3583 3584 if (mOrientation == HORIZONTAL) { 3585 switch (direction) { 3586 case View.FOCUS_LEFT: 3587 movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? PREV_ITEM : NEXT_ITEM; 3588 break; 3589 case View.FOCUS_RIGHT: 3590 movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? NEXT_ITEM : PREV_ITEM; 3591 break; 3592 case View.FOCUS_UP: 3593 movement = PREV_ROW; 3594 break; 3595 case View.FOCUS_DOWN: 3596 movement = NEXT_ROW; 3597 break; 3598 } 3599 } else if (mOrientation == VERTICAL) { 3600 switch (direction) { 3601 case View.FOCUS_LEFT: 3602 movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? PREV_ROW : NEXT_ROW; 3603 break; 3604 case View.FOCUS_RIGHT: 3605 movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? NEXT_ROW : PREV_ROW; 3606 break; 3607 case View.FOCUS_UP: 3608 movement = PREV_ITEM; 3609 break; 3610 case View.FOCUS_DOWN: 3611 movement = NEXT_ITEM; 3612 break; 3613 } 3614 } 3615 3616 return movement; 3617 } 3618 getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i)3619 int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) { 3620 View view = findViewByPosition(mFocusPosition); 3621 if (view == null) { 3622 return i; 3623 } 3624 int focusIndex = recyclerView.indexOfChild(view); 3625 // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item 3626 // drawing order is 0 1 2 3 9 8 7 6 5 4 3627 if (i < focusIndex) { 3628 return i; 3629 } else if (i < childCount - 1) { 3630 return focusIndex + childCount - 1 - i; 3631 } else { 3632 return focusIndex; 3633 } 3634 } 3635 3636 @Override onAdapterChanged(RecyclerView.@ullable Adapter oldAdapter, RecyclerView.@Nullable Adapter newAdapter)3637 public void onAdapterChanged(RecyclerView.@Nullable Adapter oldAdapter, 3638 RecyclerView.@Nullable Adapter newAdapter) { 3639 if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter); 3640 if (oldAdapter != null) { 3641 discardLayoutInfo(); 3642 mFocusPosition = NO_POSITION; 3643 mFocusPositionOffset = 0; 3644 mChildrenStates.clear(); 3645 } 3646 if (newAdapter instanceof FacetProviderAdapter) { 3647 mFacetProviderAdapter = (FacetProviderAdapter) newAdapter; 3648 } else { 3649 mFacetProviderAdapter = null; 3650 } 3651 super.onAdapterChanged(oldAdapter, newAdapter); 3652 } 3653 discardLayoutInfo()3654 private void discardLayoutInfo() { 3655 mGrid = null; 3656 mRowSizeSecondary = null; 3657 mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH; 3658 } 3659 setLayoutEnabled(boolean layoutEnabled)3660 void setLayoutEnabled(boolean layoutEnabled) { 3661 if (((mFlag & PF_LAYOUT_ENABLED) != 0) != layoutEnabled) { 3662 mFlag = (mFlag & ~PF_LAYOUT_ENABLED) | (layoutEnabled ? PF_LAYOUT_ENABLED : 0); 3663 requestLayout(); 3664 } 3665 } 3666 setChildrenVisibility(int visibility)3667 void setChildrenVisibility(int visibility) { 3668 mChildVisibility = visibility; 3669 if (mChildVisibility != -1) { 3670 int count = getChildCount(); 3671 for (int i = 0; i < count; i++) { 3672 getChildAt(i).setVisibility(mChildVisibility); 3673 } 3674 } 3675 } 3676 3677 @SuppressLint("BanParcelableUsage") 3678 static final class SavedState implements Parcelable { 3679 3680 int mIndex; // index inside adapter of the current view 3681 Bundle mChildStates = Bundle.EMPTY; 3682 3683 @Override writeToParcel(Parcel out, int flags)3684 public void writeToParcel(Parcel out, int flags) { 3685 out.writeInt(mIndex); 3686 out.writeBundle(mChildStates); 3687 } 3688 3689 @SuppressWarnings("hiding") 3690 public static final Parcelable.Creator<SavedState> CREATOR = 3691 new Parcelable.Creator<SavedState>() { 3692 @Override 3693 public SavedState createFromParcel(Parcel in) { 3694 return new SavedState(in); 3695 } 3696 3697 @Override 3698 public SavedState[] newArray(int size) { 3699 return new SavedState[size]; 3700 } 3701 }; 3702 3703 @Override describeContents()3704 public int describeContents() { 3705 return 0; 3706 } 3707 SavedState(Parcel in)3708 SavedState(Parcel in) { 3709 mIndex = in.readInt(); 3710 mChildStates = in.readBundle(GridLayoutManager.class.getClassLoader()); 3711 } 3712 SavedState()3713 SavedState() { 3714 } 3715 } 3716 3717 @Override onSaveInstanceState()3718 public @NonNull Parcelable onSaveInstanceState() { 3719 if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection()); 3720 SavedState ss = new SavedState(); 3721 // save selected index 3722 ss.mIndex = getSelection(); 3723 // save offscreen child (state when they are recycled) 3724 Bundle bundle = mChildrenStates.saveAsBundle(); 3725 // save views currently is on screen (TODO save cached views) 3726 for (int i = 0, count = getChildCount(); i < count; i++) { 3727 View view = getChildAt(i); 3728 int position = getAdapterPositionByView(view); 3729 if (position != NO_POSITION) { 3730 bundle = mChildrenStates.saveOnScreenView(bundle, view, position); 3731 } 3732 } 3733 ss.mChildStates = bundle; 3734 return ss; 3735 } 3736 onChildRecycled(RecyclerView.ViewHolder holder)3737 void onChildRecycled(RecyclerView.ViewHolder holder) { 3738 final int position = holder.getAbsoluteAdapterPosition(); 3739 if (position != NO_POSITION) { 3740 mChildrenStates.saveOffscreenView(holder.itemView, position); 3741 } 3742 } 3743 3744 @Override onRestoreInstanceState(@ullable Parcelable state)3745 public void onRestoreInstanceState(@Nullable Parcelable state) { 3746 if (!(state instanceof SavedState)) { 3747 return; 3748 } 3749 SavedState loadingState = (SavedState) state; 3750 mFocusPosition = loadingState.mIndex; 3751 mFocusPositionOffset = 0; 3752 mChildrenStates.loadFromBundle(loadingState.mChildStates); 3753 mFlag |= PF_FORCE_FULL_LAYOUT; 3754 requestLayout(); 3755 if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition); 3756 } 3757 3758 @Override getRowCountForAccessibility(RecyclerView.@onNull Recycler recycler, RecyclerView.@NonNull State state)3759 public int getRowCountForAccessibility(RecyclerView.@NonNull Recycler recycler, 3760 RecyclerView.@NonNull State state) { 3761 if (mOrientation == HORIZONTAL && mGrid != null) { 3762 return mGrid.getNumRows(); 3763 } 3764 return super.getRowCountForAccessibility(recycler, state); 3765 } 3766 3767 @Override getColumnCountForAccessibility(RecyclerView.@onNull Recycler recycler, RecyclerView.@NonNull State state)3768 public int getColumnCountForAccessibility(RecyclerView.@NonNull Recycler recycler, 3769 RecyclerView.@NonNull State state) { 3770 if (mOrientation == VERTICAL && mGrid != null) { 3771 return mGrid.getNumRows(); 3772 } 3773 return super.getColumnCountForAccessibility(recycler, state); 3774 } 3775 3776 @Override onInitializeAccessibilityNodeInfoForItem(RecyclerView.@onNull Recycler recycler, RecyclerView.@NonNull State state, @NonNull View host, @NonNull AccessibilityNodeInfoCompat info)3777 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.@NonNull Recycler recycler, 3778 RecyclerView.@NonNull State state, @NonNull View host, 3779 @NonNull AccessibilityNodeInfoCompat info) { 3780 ViewGroup.LayoutParams lp = host.getLayoutParams(); 3781 if (mGrid == null || !(lp instanceof LayoutParams)) { 3782 return; 3783 } 3784 LayoutParams glp = (LayoutParams) lp; 3785 int position = glp.getAbsoluteAdapterPosition(); 3786 int rowIndex = position >= 0 ? mGrid.getRowIndex(position) : -1; 3787 if (rowIndex < 0) { 3788 return; 3789 } 3790 int guessSpanIndex = position / mGrid.getNumRows(); 3791 if (mOrientation == HORIZONTAL) { 3792 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 3793 rowIndex, 1, guessSpanIndex, 1, false, false)); 3794 } else { 3795 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 3796 guessSpanIndex, 1, rowIndex, 1, false, false)); 3797 } 3798 } 3799 3800 /* 3801 * Leanback widget is different than the default implementation because the "scroll" is driven 3802 * by selection change. 3803 */ 3804 @Override performAccessibilityAction(@onNull Recycler recycler, @NonNull State state, int action, @Nullable Bundle args)3805 public boolean performAccessibilityAction(@NonNull Recycler recycler, @NonNull State state, 3806 int action, @Nullable Bundle args) { 3807 if (!isScrollEnabled()) { 3808 // eat action request so that talkback wont focus out of RV 3809 return true; 3810 } 3811 saveContext(recycler, state); 3812 int translatedAction = action; 3813 boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0; 3814 if (Build.VERSION.SDK_INT >= 23) { 3815 if (mOrientation == HORIZONTAL) { 3816 if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat 3817 .ACTION_SCROLL_LEFT.getId()) { 3818 translatedAction = reverseFlowPrimary 3819 ? AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD : 3820 AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD; 3821 } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat 3822 .ACTION_SCROLL_RIGHT.getId()) { 3823 translatedAction = reverseFlowPrimary 3824 ? AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD : 3825 AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD; 3826 } 3827 } else { // VERTICAL layout 3828 if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP 3829 .getId()) { 3830 translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD; 3831 } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat 3832 .ACTION_SCROLL_DOWN.getId()) { 3833 translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD; 3834 } 3835 } 3836 } 3837 boolean scrollingReachedBeginning = (mFocusPosition == 0 3838 && translatedAction == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 3839 boolean scrollingReachedEnd = (mFocusPosition == state.getItemCount() - 1 3840 && translatedAction == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 3841 if (scrollingReachedBeginning || scrollingReachedEnd) { 3842 // Send a fake scroll completion event to notify Talkback that the scroll event was 3843 // successful. Hence, Talkback will only look for next focus within the RecyclerView. 3844 // Not sending this will result in Talkback classifying it as a failed scroll event, and 3845 // will try to jump focus out of the RecyclerView. 3846 // We know at this point that either focusOutFront or focusOutEnd is true (or both), 3847 // because otherwise, we never hit ACTION_SCROLL_BACKWARD/FORWARD here. 3848 sendTypeViewScrolledAccessibilityEvent(); 3849 } else { 3850 switch (translatedAction) { 3851 case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: 3852 processPendingMovement(false); 3853 processSelectionMoves(false, -1); 3854 break; 3855 case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: 3856 processPendingMovement(true); 3857 processSelectionMoves(false, 1); 3858 break; 3859 } 3860 } 3861 leaveContext(); 3862 return true; 3863 } 3864 3865 @SuppressWarnings("deprecation") sendTypeViewScrolledAccessibilityEvent()3866 private void sendTypeViewScrolledAccessibilityEvent() { 3867 AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED); 3868 mBaseGridView.onInitializeAccessibilityEvent(event); 3869 mBaseGridView.requestSendAccessibilityEvent(mBaseGridView, event); 3870 } 3871 3872 /* 3873 * Move mFocusPosition multiple steps on the same row in main direction. 3874 * Stops when moves are all consumed or reach first/last visible item. 3875 * Returning remaining moves. 3876 */ processSelectionMoves(boolean preventScroll, int moves)3877 int processSelectionMoves(boolean preventScroll, int moves) { 3878 if (mGrid == null) { 3879 return moves; 3880 } 3881 int focusPosition = mFocusPosition; 3882 int focusedRow = focusPosition != NO_POSITION 3883 ? mGrid.getRowIndex(focusPosition) : NO_POSITION; 3884 View newSelected = null; 3885 for (int i = 0, count = getChildCount(); i < count && moves != 0; i++) { 3886 int index = moves > 0 ? i : count - 1 - i; 3887 final View child = getChildAt(index); 3888 if (!canScrollTo(child)) { 3889 continue; 3890 } 3891 int position = getAdapterPositionByIndex(index); 3892 int rowIndex = mGrid.getRowIndex(position); 3893 if (focusedRow == NO_POSITION) { 3894 focusPosition = position; 3895 newSelected = child; 3896 focusedRow = rowIndex; 3897 } else if (rowIndex == focusedRow) { 3898 if ((moves > 0 && position > focusPosition) 3899 || (moves < 0 && position < focusPosition)) { 3900 focusPosition = position; 3901 newSelected = child; 3902 if (moves > 0) { 3903 moves--; 3904 } else { 3905 moves++; 3906 } 3907 } 3908 } 3909 } 3910 if (newSelected != null) { 3911 if (preventScroll) { 3912 if (hasFocus()) { 3913 mFlag |= PF_IN_SELECTION; 3914 newSelected.requestFocus(); 3915 mFlag &= ~PF_IN_SELECTION; 3916 } 3917 mFocusPosition = focusPosition; 3918 mSubFocusPosition = 0; 3919 } else { 3920 scrollToView(newSelected, true); 3921 } 3922 } 3923 return moves; 3924 } 3925 addA11yActionMovingBackward(AccessibilityNodeInfoCompat info, boolean reverseFlowPrimary)3926 private void addA11yActionMovingBackward(AccessibilityNodeInfoCompat info, 3927 boolean reverseFlowPrimary) { 3928 if (Build.VERSION.SDK_INT >= 23) { 3929 if (mOrientation == HORIZONTAL) { 3930 info.addAction(reverseFlowPrimary 3931 ? AccessibilityNodeInfoCompat.AccessibilityActionCompat 3932 .ACTION_SCROLL_RIGHT : 3933 AccessibilityNodeInfoCompat.AccessibilityActionCompat 3934 .ACTION_SCROLL_LEFT); 3935 } else { 3936 info.addAction( 3937 AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP); 3938 } 3939 } else { 3940 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 3941 } 3942 info.setScrollable(true); 3943 } 3944 addA11yActionMovingForward(AccessibilityNodeInfoCompat info, boolean reverseFlowPrimary)3945 private void addA11yActionMovingForward(AccessibilityNodeInfoCompat info, 3946 boolean reverseFlowPrimary) { 3947 if (Build.VERSION.SDK_INT >= 23) { 3948 if (mOrientation == HORIZONTAL) { 3949 info.addAction(reverseFlowPrimary 3950 ? AccessibilityNodeInfoCompat.AccessibilityActionCompat 3951 .ACTION_SCROLL_LEFT : 3952 AccessibilityNodeInfoCompat.AccessibilityActionCompat 3953 .ACTION_SCROLL_RIGHT); 3954 } else { 3955 info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat 3956 .ACTION_SCROLL_DOWN); 3957 } 3958 } else { 3959 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 3960 } 3961 info.setScrollable(true); 3962 } 3963 3964 @Override onInitializeAccessibilityNodeInfo(@onNull Recycler recycler, @NonNull State state, @NonNull AccessibilityNodeInfoCompat info)3965 public void onInitializeAccessibilityNodeInfo(@NonNull Recycler recycler, 3966 @NonNull State state, @NonNull AccessibilityNodeInfoCompat info) { 3967 saveContext(recycler, state); 3968 int count = state.getItemCount(); 3969 // reverseFlowPrimary is whether we are in LTR/RTL mode. 3970 boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0; 3971 // If focusOutFront/focusOutEnd is false, override Talkback in handling 3972 // backward/forward actions by adding such actions to supported action list. 3973 if ((mFlag & PF_FOCUS_OUT_FRONT) == 0 || (count > 1 && !isItemFullyVisible(0))) { 3974 addA11yActionMovingBackward(info, reverseFlowPrimary); 3975 } 3976 if ((mFlag & PF_FOCUS_OUT_BACK) == 0 || (count > 1 && !isItemFullyVisible(count - 1))) { 3977 addA11yActionMovingForward(info, reverseFlowPrimary); 3978 } 3979 final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = 3980 AccessibilityNodeInfoCompat.CollectionInfoCompat 3981 .obtain(getRowCountForAccessibility(recycler, state), 3982 getColumnCountForAccessibility(recycler, state), 3983 isLayoutHierarchical(recycler, state), 3984 getSelectionModeForAccessibility(recycler, state)); 3985 info.setCollectionInfo(collectionInfo); 3986 // Set the class name so this is treated as a grid. A11y services should identify grids 3987 // and list via CollectionInfos, but an almost empty grid may be incorrectly identified 3988 // as a list. 3989 info.setClassName(GridView.class.getName()); 3990 leaveContext(); 3991 } 3992 } 3993