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