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