GridLayoutManager.java revision 5db6d414ed0329b4f5464a92d1a7f8e6d8bb1678
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 // position could be NO_POSITION for the views off the screen that were cached. For 1886 // these views markKnownViewsInvalid invalidates them. 1887 if (location == null || (position == NO_POSITION)) { 1888 if (DEBUG) Log.w(getTag(), "fastRelayout(): no Location at " + position); 1889 invalidateAfter = true; 1890 break; 1891 } 1892 1893 int startSecondary = getRowStartSecondary(location.row) 1894 + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary; 1895 int primarySize, end; 1896 int start = getViewMin(view); 1897 int oldPrimarySize = getViewPrimarySize(view); 1898 1899 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 1900 if (lp.viewNeedsUpdate()) { 1901 int viewIndex = mBaseGridView.indexOfChild(view); 1902 detachAndScrapView(view, mRecycler); 1903 view = getViewForPosition(position); 1904 addView(view, viewIndex); 1905 } 1906 1907 measureChild(view); 1908 if (mOrientation == HORIZONTAL) { 1909 primarySize = getDecoratedMeasuredWidthWithMargin(view); 1910 end = start + primarySize; 1911 } else { 1912 primarySize = getDecoratedMeasuredHeightWithMargin(view); 1913 end = start + primarySize; 1914 } 1915 layoutChild(location.row, view, start, end, startSecondary); 1916 if (oldPrimarySize != primarySize) { 1917 // size changed invalidate remaining Locations 1918 if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position); 1919 invalidateAfter = true; 1920 break; 1921 } 1922 } 1923 if (invalidateAfter) { 1924 final int savedLastPos = mGrid.getLastVisibleIndex(); 1925 mGrid.invalidateItemsAfter(position); 1926 if (mPruneChild) { 1927 // in regular prune child mode, we just append items up to edge limit 1928 appendVisibleItems(); 1929 if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) { 1930 // make sure add focus view back: the view might be outside edge limit 1931 // when there is delta in onLayoutChildren(). 1932 while (mGrid.getLastVisibleIndex() < mFocusPosition) { 1933 mGrid.appendOneColumnVisibleItems(); 1934 } 1935 } 1936 } else { 1937 // prune disabled(e.g. in RowsFragment transition): append all removed items 1938 while (mGrid.appendOneColumnVisibleItems() 1939 && mGrid.getLastVisibleIndex() < savedLastPos); 1940 } 1941 } 1942 updateScrollLimits(); 1943 updateSecondaryScrollLimits(); 1944 } 1945 1946 @Override 1947 public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) { 1948 if (TRACE) TraceCompat.beginSection("removeAndRecycleAllViews"); 1949 if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount()); 1950 for (int i = getChildCount() - 1; i >= 0; i--) { 1951 removeAndRecycleViewAt(i, recycler); 1952 } 1953 if (TRACE) TraceCompat.endSection(); 1954 } 1955 1956 // called by onLayoutChildren, either focus to FocusPosition or declare focusViewAvailable 1957 // and scroll to the view if framework focus on it. 1958 private void focusToViewInLayout(boolean hadFocus, boolean alignToView) { 1959 View focusView = findViewByPosition(mFocusPosition); 1960 if (focusView != null && alignToView) { 1961 scrollToView(focusView, false); 1962 } 1963 if (focusView != null && hadFocus && !focusView.hasFocus()) { 1964 focusView.requestFocus(); 1965 } else if (!hadFocus && !mBaseGridView.hasFocus()) { 1966 if (focusView != null && focusView.hasFocusable()) { 1967 mBaseGridView.focusableViewAvailable(focusView); 1968 } else { 1969 for (int i = 0, count = getChildCount(); i < count; i++) { 1970 focusView = getChildAt(i); 1971 if (focusView != null && focusView.hasFocusable()) { 1972 mBaseGridView.focusableViewAvailable(focusView); 1973 break; 1974 } 1975 } 1976 } 1977 // focusViewAvailable() might focus to the view, scroll to it if that is the case. 1978 if (alignToView && focusView != null && focusView.hasFocus()) { 1979 scrollToView(focusView, false); 1980 } 1981 } 1982 } 1983 1984 @VisibleForTesting 1985 public static class OnLayoutCompleteListener { 1986 public void onLayoutCompleted(RecyclerView.State state) { 1987 } 1988 } 1989 1990 @VisibleForTesting 1991 OnLayoutCompleteListener mLayoutCompleteListener; 1992 1993 @Override 1994 public void onLayoutCompleted(State state) { 1995 if (mLayoutCompleteListener != null) { 1996 mLayoutCompleteListener.onLayoutCompleted(state); 1997 } 1998 } 1999 2000 @Override 2001 public boolean supportsPredictiveItemAnimations() { 2002 return true; 2003 } 2004 2005 void updatePositionToRowMapInPostLayout() { 2006 mPositionToRowInPostLayout.clear(); 2007 final int childCount = getChildCount(); 2008 for (int i = 0; i < childCount; i++) { 2009 // Grid still maps to old positions at this point, use old position to get row infor 2010 int position = mBaseGridView.getChildViewHolder(getChildAt(i)).getOldPosition(); 2011 if (position >= 0) { 2012 Grid.Location loc = mGrid.getLocation(position); 2013 if (loc != null) { 2014 mPositionToRowInPostLayout.put(position, loc.row); 2015 } 2016 } 2017 } 2018 } 2019 2020 void fillScrapViewsInPostLayout() { 2021 List<RecyclerView.ViewHolder> scrapList = mRecycler.getScrapList(); 2022 final int scrapSize = scrapList.size(); 2023 if (scrapSize == 0) { 2024 return; 2025 } 2026 // initialize the int array or re-allocate the array. 2027 if (mDisappearingPositions == null || scrapSize > mDisappearingPositions.length) { 2028 int length = mDisappearingPositions == null ? 16 : mDisappearingPositions.length; 2029 while (length < scrapSize) { 2030 length = length << 1; 2031 } 2032 mDisappearingPositions = new int[length]; 2033 } 2034 int totalItems = 0; 2035 for (int i = 0; i < scrapSize; i++) { 2036 int pos = scrapList.get(i).getAdapterPosition(); 2037 if (pos >= 0) { 2038 mDisappearingPositions[totalItems++] = pos; 2039 } 2040 } 2041 // totalItems now has the length of disappearing items 2042 if (totalItems > 0) { 2043 Arrays.sort(mDisappearingPositions, 0, totalItems); 2044 mGrid.fillDisappearingItems(mDisappearingPositions, totalItems, 2045 mPositionToRowInPostLayout); 2046 } 2047 mPositionToRowInPostLayout.clear(); 2048 } 2049 2050 // Lays out items based on the current scroll position 2051 @Override 2052 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 2053 if (DEBUG) { 2054 Log.v(getTag(), "layoutChildren start numRows " + mNumRows 2055 + " inPreLayout " + state.isPreLayout() 2056 + " didStructureChange " + state.didStructureChange() 2057 + " mForceFullLayout " + mForceFullLayout); 2058 Log.v(getTag(), "width " + getWidth() + " height " + getHeight()); 2059 } 2060 2061 if (mNumRows == 0) { 2062 // haven't done measure yet 2063 return; 2064 } 2065 final int itemCount = state.getItemCount(); 2066 if (itemCount < 0) { 2067 return; 2068 } 2069 2070 if (mIsSlidingChildViews) { 2071 // if there is already children, delay the layout process until slideIn(), if it's 2072 // first time layout children: scroll them offscreen at end of onLayoutChildren() 2073 if (getChildCount() > 0) { 2074 mLayoutEatenInSliding = true; 2075 return; 2076 } 2077 } 2078 if (!mLayoutEnabled) { 2079 discardLayoutInfo(); 2080 removeAndRecycleAllViews(recycler); 2081 return; 2082 } 2083 mInLayout = true; 2084 2085 saveContext(recycler, state); 2086 if (state.isPreLayout()) { 2087 int childCount = getChildCount(); 2088 if (mGrid != null && childCount > 0) { 2089 int minChangedEdge = Integer.MAX_VALUE; 2090 int maxChangeEdge = Integer.MIN_VALUE; 2091 int minOldAdapterPosition = mBaseGridView.getChildViewHolder( 2092 getChildAt(0)).getOldPosition(); 2093 int maxOldAdapterPosition = mBaseGridView.getChildViewHolder( 2094 getChildAt(childCount - 1)).getOldPosition(); 2095 for (int i = 0; i < childCount; i++) { 2096 View view = getChildAt(i); 2097 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2098 if (i == 0) { 2099 // first child's layout position can be smaller than index if there were 2100 // items removed before first visible index. 2101 mPositionDeltaInPreLayout = mGrid.getFirstVisibleIndex() 2102 - lp.getViewLayoutPosition(); 2103 } 2104 int newAdapterPosition = mBaseGridView.getChildAdapterPosition(view); 2105 // if either of following happening 2106 // 1. item itself has changed or layout parameter changed 2107 // 2. item is losing focus 2108 // 3. item is gaining focus 2109 // 4. item is moved out of old adapter position range. 2110 if (lp.isItemChanged() || lp.isItemRemoved() || view.isLayoutRequested() 2111 || (!view.hasFocus() && mFocusPosition == lp.getViewAdapterPosition()) 2112 || (view.hasFocus() && mFocusPosition != lp.getViewAdapterPosition()) 2113 || newAdapterPosition < minOldAdapterPosition 2114 || newAdapterPosition > maxOldAdapterPosition) { 2115 minChangedEdge = Math.min(minChangedEdge, getViewMin(view)); 2116 maxChangeEdge = Math.max(maxChangeEdge, getViewMax(view)); 2117 } 2118 } 2119 if (maxChangeEdge > minChangedEdge) { 2120 mExtraLayoutSpaceInPreLayout = maxChangeEdge - minChangedEdge; 2121 } 2122 // append items for mExtraLayoutSpaceInPreLayout 2123 appendVisibleItems(); 2124 prependVisibleItems(); 2125 } 2126 mInLayout = false; 2127 leaveContext(); 2128 if (DEBUG) Log.v(getTag(), "layoutChildren end"); 2129 return; 2130 } 2131 2132 // save all view's row information before detach all views 2133 if (state.willRunPredictiveAnimations()) { 2134 updatePositionToRowMapInPostLayout(); 2135 } 2136 // check if we need align to mFocusPosition, this is usually true unless in smoothScrolling 2137 final boolean scrollToFocus = !isSmoothScrolling() 2138 && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED; 2139 if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) { 2140 mFocusPosition = mFocusPosition + mFocusPositionOffset; 2141 mSubFocusPosition = 0; 2142 } 2143 mFocusPositionOffset = 0; 2144 2145 View savedFocusView = findViewByPosition(mFocusPosition); 2146 int savedFocusPos = mFocusPosition; 2147 int savedSubFocusPos = mSubFocusPosition; 2148 boolean hadFocus = mBaseGridView.hasFocus(); 2149 final int firstVisibleIndex = mGrid != null ? mGrid.getFirstVisibleIndex() : NO_POSITION; 2150 final int lastVisibleIndex = mGrid != null ? mGrid.getLastVisibleIndex() : NO_POSITION; 2151 int savedViewCenter = savedFocusView == null ? 0 : getAdjustedViewCenter(savedFocusView); 2152 int savedViewCenterSecondary = savedFocusView == null ? 0 : 2153 getViewCenterSecondary(savedFocusView); 2154 2155 if (mInFastRelayout = layoutInit()) { 2156 // If grid view is empty, we will start from mFocusPosition 2157 mGrid.setStart(mFocusPosition); 2158 fastRelayout(); 2159 } else { 2160 // layoutInit() has detached all views, so start from scratch 2161 mInLayoutSearchFocus = hadFocus; 2162 int startFromPosition, endPos; 2163 if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex 2164 || mFocusPosition < firstVisibleIndex)) { 2165 startFromPosition = endPos = mFocusPosition; 2166 } else { 2167 startFromPosition = firstVisibleIndex; 2168 endPos = lastVisibleIndex; 2169 } 2170 2171 mGrid.setStart(startFromPosition); 2172 if (endPos != NO_POSITION) { 2173 while (appendOneColumnVisibleItems() && findViewByPosition(endPos) == null) { 2174 // continuously append items until endPos 2175 } 2176 } 2177 } 2178 // multiple rounds: scrollToView of first round may drag first/last child into 2179 // "visible window" and we update scrollMin/scrollMax then run second scrollToView 2180 // we must do this for fastRelayout() for the append item case 2181 int oldFirstVisible; 2182 int oldLastVisible; 2183 do { 2184 updateScrollLimits(); 2185 oldFirstVisible = mGrid.getFirstVisibleIndex(); 2186 oldLastVisible = mGrid.getLastVisibleIndex(); 2187 if (scrollToFocus && savedFocusView != null) { 2188 // Make previous focused view stay at the original place: 2189 focusToViewInLayout(hadFocus, false); 2190 int newViewCenter = getAdjustedViewCenter(savedFocusView); 2191 int newViewCenterSecondary = getViewCenterSecondary(savedFocusView); 2192 scrollDirectionPrimary(newViewCenter - savedViewCenter); 2193 scrollDirectionSecondary(newViewCenterSecondary - savedViewCenterSecondary); 2194 } else { 2195 // focus and scroll to the view 2196 focusToViewInLayout(hadFocus, scrollToFocus); 2197 } 2198 appendVisibleItems(); 2199 prependVisibleItems(); 2200 removeInvisibleViewsAtFront(); 2201 removeInvisibleViewsAtEnd(); 2202 } while (mGrid.getFirstVisibleIndex() != oldFirstVisible 2203 || mGrid.getLastVisibleIndex() != oldLastVisible); 2204 2205 if (scrollToFocus) { 2206 // we need scroll to the new focus view 2207 View newFocusView = findViewByPosition(mFocusPosition); // must not be null 2208 View newChildFocusView = newFocusView != null && newFocusView.hasFocus() 2209 ? newFocusView.findFocus() : null; 2210 if (newFocusView != null) { 2211 // get scroll delta of primary / secondary to the new focus view 2212 // Note that we need to multiple rounds to updateScrollLimits() 2213 int newFocusViewCenter = getAdjustedViewCenter(newFocusView); 2214 int newFocusViewCenterSecondary = getViewCenterSecondary(newFocusView); 2215 do { 2216 updateScrollLimits(); 2217 oldFirstVisible = mGrid.getFirstVisibleIndex(); 2218 oldLastVisible = mGrid.getLastVisibleIndex(); 2219 scrollToView(newFocusView, newChildFocusView, false); 2220 appendVisibleItems(); 2221 prependVisibleItems(); 2222 removeInvisibleViewsAtFront(); 2223 removeInvisibleViewsAtEnd(); 2224 } while (mGrid.getFirstVisibleIndex() != oldFirstVisible 2225 || mGrid.getLastVisibleIndex() != oldLastVisible); 2226 int primary = newFocusViewCenter - getAdjustedViewCenter(newFocusView); 2227 int secondary = newFocusViewCenterSecondary - getViewCenterSecondary(newFocusView); 2228 final int scrollX, scrollY; 2229 if (mOrientation == HORIZONTAL) { 2230 scrollX = primary; 2231 scrollY = secondary; 2232 } else { 2233 scrollY = primary; 2234 scrollX = secondary; 2235 } 2236 final int remainingScrollX = state.getRemainingScrollHorizontal(); 2237 final int remainingScrollY = state.getRemainingScrollVertical(); 2238 // check if the remaining scroll will stop at the new focus view 2239 if (remainingScrollX != scrollX || remainingScrollY != scrollY) { 2240 if (remainingScrollX == 0 && remainingScrollY == 0) { 2241 // if there wasnt scroll animation, we dont start animation, let 2242 // ItemAnimation to do the move animation. 2243 } else { 2244 // if there was scroll animation, we will start a new scroll animation. 2245 // after move back to current position 2246 scrollAndAppendPrepend(-primary, -secondary); 2247 if (scrollX != 0 || scrollY != 0) { 2248 mBaseGridView.smoothScrollBy(scrollX, scrollY); 2249 } else { 2250 mBaseGridView.stopScroll(); 2251 } 2252 } 2253 } else { 2254 // move back to current position and let scroll animation continue 2255 scrollAndAppendPrepend(-primary, -secondary); 2256 } 2257 } 2258 } 2259 if (state.willRunPredictiveAnimations()) { 2260 fillScrapViewsInPostLayout(); 2261 } 2262 2263 if (DEBUG) { 2264 StringWriter sw = new StringWriter(); 2265 PrintWriter pw = new PrintWriter(sw); 2266 mGrid.debugPrint(pw); 2267 Log.d(getTag(), sw.toString()); 2268 } 2269 2270 if (mRowSecondarySizeRefresh) { 2271 mRowSecondarySizeRefresh = false; 2272 } else { 2273 updateRowSecondarySizeRefresh(); 2274 } 2275 2276 // For fastRelayout, only dispatch event when focus position changes. 2277 if (mInFastRelayout && (mFocusPosition != savedFocusPos || mSubFocusPosition 2278 != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView)) { 2279 dispatchChildSelected(); 2280 } else if (!mInFastRelayout && mInLayoutSearchFocus) { 2281 // For full layout we dispatchChildSelected() in createItem() unless searched all 2282 // children and found none is focusable then dispatchChildSelected() here. 2283 dispatchChildSelected(); 2284 } 2285 dispatchChildSelectedAndPositioned(); 2286 if (mIsSlidingChildViews) { 2287 scrollDirectionPrimary(getSlideOutDistance()); 2288 } 2289 2290 mInLayout = false; 2291 leaveContext(); 2292 if (DEBUG) Log.v(getTag(), "layoutChildren end"); 2293 } 2294 2295 void scrollAndAppendPrepend(int primary, int secondary) { 2296 scrollDirectionPrimary(primary); 2297 scrollDirectionSecondary(secondary); 2298 appendVisibleItems(); 2299 prependVisibleItems(); 2300 removeInvisibleViewsAtFront(); 2301 removeInvisibleViewsAtEnd(); 2302 } 2303 2304 private void offsetChildrenSecondary(int increment) { 2305 final int childCount = getChildCount(); 2306 if (mOrientation == HORIZONTAL) { 2307 for (int i = 0; i < childCount; i++) { 2308 getChildAt(i).offsetTopAndBottom(increment); 2309 } 2310 } else { 2311 for (int i = 0; i < childCount; i++) { 2312 getChildAt(i).offsetLeftAndRight(increment); 2313 } 2314 } 2315 } 2316 2317 private void offsetChildrenPrimary(int increment) { 2318 final int childCount = getChildCount(); 2319 if (mOrientation == VERTICAL) { 2320 for (int i = 0; i < childCount; i++) { 2321 getChildAt(i).offsetTopAndBottom(increment); 2322 } 2323 } else { 2324 for (int i = 0; i < childCount; i++) { 2325 getChildAt(i).offsetLeftAndRight(increment); 2326 } 2327 } 2328 } 2329 2330 @Override 2331 public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) { 2332 if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx); 2333 if (!mLayoutEnabled || !hasDoneFirstLayout()) { 2334 return 0; 2335 } 2336 saveContext(recycler, state); 2337 mInScroll = true; 2338 int result; 2339 if (mOrientation == HORIZONTAL) { 2340 result = scrollDirectionPrimary(dx); 2341 } else { 2342 result = scrollDirectionSecondary(dx); 2343 } 2344 leaveContext(); 2345 mInScroll = false; 2346 return result; 2347 } 2348 2349 @Override 2350 public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) { 2351 if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy); 2352 if (!mLayoutEnabled || !hasDoneFirstLayout()) { 2353 return 0; 2354 } 2355 mInScroll = true; 2356 saveContext(recycler, state); 2357 int result; 2358 if (mOrientation == VERTICAL) { 2359 result = scrollDirectionPrimary(dy); 2360 } else { 2361 result = scrollDirectionSecondary(dy); 2362 } 2363 leaveContext(); 2364 mInScroll = false; 2365 return result; 2366 } 2367 2368 // scroll in main direction may add/prune views 2369 private int scrollDirectionPrimary(int da) { 2370 if (TRACE) TraceCompat.beginSection("scrollPrimary"); 2371 boolean isMaxUnknown = false, isMinUnknown = false; 2372 int minScroll = 0, maxScroll = 0; 2373 if (!mIsSlidingChildViews) { 2374 if (da > 0) { 2375 isMaxUnknown = mWindowAlignment.mainAxis().isMaxUnknown(); 2376 if (!isMaxUnknown) { 2377 maxScroll = mWindowAlignment.mainAxis().getMaxScroll(); 2378 if (da > maxScroll) { 2379 da = maxScroll; 2380 } 2381 } 2382 } else if (da < 0) { 2383 isMinUnknown = mWindowAlignment.mainAxis().isMinUnknown(); 2384 if (!isMinUnknown) { 2385 minScroll = mWindowAlignment.mainAxis().getMinScroll(); 2386 if (da < minScroll) { 2387 da = minScroll; 2388 } 2389 } 2390 } 2391 } 2392 if (da == 0) { 2393 if (TRACE) TraceCompat.endSection(); 2394 return 0; 2395 } 2396 offsetChildrenPrimary(-da); 2397 if (mInLayout) { 2398 updateScrollLimits(); 2399 if (TRACE) TraceCompat.endSection(); 2400 return da; 2401 } 2402 2403 int childCount = getChildCount(); 2404 boolean updated; 2405 2406 if (mReverseFlowPrimary ? da > 0 : da < 0) { 2407 prependVisibleItems(); 2408 } else { 2409 appendVisibleItems(); 2410 } 2411 updated = getChildCount() > childCount; 2412 childCount = getChildCount(); 2413 2414 if (TRACE) TraceCompat.beginSection("remove"); 2415 if (mReverseFlowPrimary ? da > 0 : da < 0) { 2416 removeInvisibleViewsAtEnd(); 2417 } else { 2418 removeInvisibleViewsAtFront(); 2419 } 2420 if (TRACE) TraceCompat.endSection(); 2421 updated |= getChildCount() < childCount; 2422 if (updated) { 2423 updateRowSecondarySizeRefresh(); 2424 } 2425 2426 mBaseGridView.invalidate(); 2427 updateScrollLimits(); 2428 if (TRACE) TraceCompat.endSection(); 2429 return da; 2430 } 2431 2432 // scroll in second direction will not add/prune views 2433 private int scrollDirectionSecondary(int dy) { 2434 if (dy == 0) { 2435 return 0; 2436 } 2437 offsetChildrenSecondary(-dy); 2438 mScrollOffsetSecondary += dy; 2439 updateSecondaryScrollLimits(); 2440 mBaseGridView.invalidate(); 2441 return dy; 2442 } 2443 2444 @Override 2445 public void collectAdjacentPrefetchPositions(int dx, int dy, State state, 2446 LayoutPrefetchRegistry layoutPrefetchRegistry) { 2447 try { 2448 saveContext(null, state); 2449 int da = (mOrientation == HORIZONTAL) ? dx : dy; 2450 if (getChildCount() == 0 || da == 0) { 2451 // can't support this scroll, so don't bother prefetching 2452 return; 2453 } 2454 2455 int fromLimit = da < 0 2456 ? -mExtraLayoutSpace 2457 : mSizePrimary + mExtraLayoutSpace; 2458 mGrid.collectAdjacentPrefetchPositions(fromLimit, da, layoutPrefetchRegistry); 2459 } finally { 2460 leaveContext(); 2461 } 2462 } 2463 2464 @Override 2465 public void collectInitialPrefetchPositions(int adapterItemCount, 2466 LayoutPrefetchRegistry layoutPrefetchRegistry) { 2467 int numToPrefetch = mBaseGridView.mInitialPrefetchItemCount; 2468 if (adapterItemCount != 0 && numToPrefetch != 0) { 2469 // prefetch items centered around mFocusPosition 2470 int initialPos = Math.max(0, Math.min(mFocusPosition - (numToPrefetch - 1)/ 2, 2471 adapterItemCount - numToPrefetch)); 2472 for (int i = initialPos; i < adapterItemCount && i < initialPos + numToPrefetch; i++) { 2473 layoutPrefetchRegistry.addPosition(i, 0); 2474 } 2475 } 2476 } 2477 2478 void updateScrollLimits() { 2479 if (mState.getItemCount() == 0) { 2480 return; 2481 } 2482 int highVisiblePos, lowVisiblePos; 2483 int highMaxPos, lowMinPos; 2484 if (!mReverseFlowPrimary) { 2485 highVisiblePos = mGrid.getLastVisibleIndex(); 2486 highMaxPos = mState.getItemCount() - 1; 2487 lowVisiblePos = mGrid.getFirstVisibleIndex(); 2488 lowMinPos = 0; 2489 } else { 2490 highVisiblePos = mGrid.getFirstVisibleIndex(); 2491 highMaxPos = 0; 2492 lowVisiblePos = mGrid.getLastVisibleIndex(); 2493 lowMinPos = mState.getItemCount() - 1; 2494 } 2495 if (highVisiblePos < 0 || lowVisiblePos < 0) { 2496 return; 2497 } 2498 final boolean highAvailable = highVisiblePos == highMaxPos; 2499 final boolean lowAvailable = lowVisiblePos == lowMinPos; 2500 if (!highAvailable && mWindowAlignment.mainAxis().isMaxUnknown() 2501 && !lowAvailable && mWindowAlignment.mainAxis().isMinUnknown()) { 2502 return; 2503 } 2504 int maxEdge, maxViewCenter; 2505 if (highAvailable) { 2506 maxEdge = mGrid.findRowMax(true, sTwoInts); 2507 View maxChild = findViewByPosition(sTwoInts[1]); 2508 maxViewCenter = getViewCenter(maxChild); 2509 final LayoutParams lp = (LayoutParams) maxChild.getLayoutParams(); 2510 int[] multipleAligns = lp.getAlignMultiple(); 2511 if (multipleAligns != null && multipleAligns.length > 0) { 2512 maxViewCenter += multipleAligns[multipleAligns.length - 1] - multipleAligns[0]; 2513 } 2514 } else { 2515 maxEdge = Integer.MAX_VALUE; 2516 maxViewCenter = Integer.MAX_VALUE; 2517 } 2518 int minEdge, minViewCenter; 2519 if (lowAvailable) { 2520 minEdge = mGrid.findRowMin(false, sTwoInts); 2521 View minChild = findViewByPosition(sTwoInts[1]); 2522 minViewCenter = getViewCenter(minChild); 2523 } else { 2524 minEdge = Integer.MIN_VALUE; 2525 minViewCenter = Integer.MIN_VALUE; 2526 } 2527 mWindowAlignment.mainAxis().updateMinMax(minEdge, maxEdge, minViewCenter, maxViewCenter); 2528 } 2529 2530 /** 2531 * Update secondary axis's scroll min/max, should be updated in 2532 * {@link #scrollDirectionSecondary(int)}. 2533 */ 2534 private void updateSecondaryScrollLimits() { 2535 WindowAlignment.Axis secondAxis = mWindowAlignment.secondAxis(); 2536 int minEdge = secondAxis.getPaddingMin() - mScrollOffsetSecondary; 2537 int maxEdge = minEdge + getSizeSecondary(); 2538 secondAxis.updateMinMax(minEdge, maxEdge, minEdge, maxEdge); 2539 } 2540 2541 private void initScrollController() { 2542 mWindowAlignment.reset(); 2543 mWindowAlignment.horizontal.setSize(getWidth()); 2544 mWindowAlignment.vertical.setSize(getHeight()); 2545 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 2546 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 2547 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 2548 mScrollOffsetSecondary = 0; 2549 2550 if (DEBUG) { 2551 Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary 2552 + " mWindowAlignment " + mWindowAlignment); 2553 } 2554 } 2555 2556 private void updateScrollController() { 2557 mWindowAlignment.horizontal.setSize(getWidth()); 2558 mWindowAlignment.vertical.setSize(getHeight()); 2559 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 2560 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 2561 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 2562 2563 if (DEBUG) { 2564 Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary 2565 + " mWindowAlignment " + mWindowAlignment); 2566 } 2567 } 2568 2569 @Override 2570 public void scrollToPosition(int position) { 2571 setSelection(position, 0, false, 0); 2572 } 2573 2574 @Override 2575 public void smoothScrollToPosition(RecyclerView recyclerView, State state, 2576 int position) { 2577 setSelection(position, 0, true, 0); 2578 } 2579 2580 public void setSelection(int position, 2581 int primaryScrollExtra) { 2582 setSelection(position, 0, false, primaryScrollExtra); 2583 } 2584 2585 public void setSelectionSmooth(int position) { 2586 setSelection(position, 0, true, 0); 2587 } 2588 2589 public void setSelectionWithSub(int position, int subposition, 2590 int primaryScrollExtra) { 2591 setSelection(position, subposition, false, primaryScrollExtra); 2592 } 2593 2594 public void setSelectionSmoothWithSub(int position, int subposition) { 2595 setSelection(position, subposition, true, 0); 2596 } 2597 2598 public int getSelection() { 2599 return mFocusPosition; 2600 } 2601 2602 public int getSubSelection() { 2603 return mSubFocusPosition; 2604 } 2605 2606 public void setSelection(int position, int subposition, boolean smooth, 2607 int primaryScrollExtra) { 2608 if ((mFocusPosition != position && position != NO_POSITION) 2609 || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) { 2610 scrollToSelection(position, subposition, smooth, primaryScrollExtra); 2611 } 2612 } 2613 2614 void scrollToSelection(int position, int subposition, 2615 boolean smooth, int primaryScrollExtra) { 2616 if (TRACE) TraceCompat.beginSection("scrollToSelection"); 2617 mPrimaryScrollExtra = primaryScrollExtra; 2618 View view = findViewByPosition(position); 2619 if (view != null) { 2620 mInSelection = true; 2621 scrollToView(view, smooth); 2622 mInSelection = false; 2623 } else { 2624 mFocusPosition = position; 2625 mSubFocusPosition = subposition; 2626 mFocusPositionOffset = Integer.MIN_VALUE; 2627 if (!mLayoutEnabled || mIsSlidingChildViews) { 2628 return; 2629 } 2630 if (smooth) { 2631 if (!hasDoneFirstLayout()) { 2632 Log.w(getTag(), "setSelectionSmooth should " 2633 + "not be called before first layout pass"); 2634 return; 2635 } 2636 position = startPositionSmoothScroller(position); 2637 if (position != mFocusPosition) { 2638 // gets cropped by adapter size 2639 mFocusPosition = position; 2640 mSubFocusPosition = 0; 2641 } 2642 } else { 2643 mForceFullLayout = true; 2644 requestLayout(); 2645 } 2646 } 2647 if (TRACE) TraceCompat.endSection(); 2648 } 2649 2650 int startPositionSmoothScroller(int position) { 2651 LinearSmoothScroller linearSmoothScroller = new GridLinearSmoothScroller() { 2652 @Override 2653 public PointF computeScrollVectorForPosition(int targetPosition) { 2654 if (getChildCount() == 0) { 2655 return null; 2656 } 2657 final int firstChildPos = getPosition(getChildAt(0)); 2658 // TODO We should be able to deduce direction from bounds of current and target 2659 // focus, rather than making assumptions about positions and directionality 2660 final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos 2661 : targetPosition < firstChildPos; 2662 final int direction = isStart ? -1 : 1; 2663 if (mOrientation == HORIZONTAL) { 2664 return new PointF(direction, 0); 2665 } else { 2666 return new PointF(0, direction); 2667 } 2668 } 2669 2670 }; 2671 linearSmoothScroller.setTargetPosition(position); 2672 startSmoothScroll(linearSmoothScroller); 2673 return linearSmoothScroller.getTargetPosition(); 2674 } 2675 2676 private void processPendingMovement(boolean forward) { 2677 if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) { 2678 return; 2679 } 2680 if (mPendingMoveSmoothScroller == null) { 2681 // Stop existing scroller and create a new PendingMoveSmoothScroller. 2682 mBaseGridView.stopScroll(); 2683 PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller( 2684 forward ? 1 : -1, mNumRows > 1); 2685 mFocusPositionOffset = 0; 2686 startSmoothScroll(linearSmoothScroller); 2687 if (linearSmoothScroller.isRunning()) { 2688 mPendingMoveSmoothScroller = linearSmoothScroller; 2689 } 2690 } else { 2691 if (forward) { 2692 mPendingMoveSmoothScroller.increasePendingMoves(); 2693 } else { 2694 mPendingMoveSmoothScroller.decreasePendingMoves(); 2695 } 2696 } 2697 } 2698 2699 @Override 2700 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 2701 if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart " 2702 + positionStart + " itemCount " + itemCount); 2703 if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0 2704 && mFocusPositionOffset != Integer.MIN_VALUE) { 2705 int pos = mFocusPosition + mFocusPositionOffset; 2706 if (positionStart <= pos) { 2707 mFocusPositionOffset += itemCount; 2708 } 2709 } 2710 mChildrenStates.clear(); 2711 } 2712 2713 @Override 2714 public void onItemsChanged(RecyclerView recyclerView) { 2715 if (DEBUG) Log.v(getTag(), "onItemsChanged"); 2716 mFocusPositionOffset = 0; 2717 mChildrenStates.clear(); 2718 } 2719 2720 @Override 2721 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 2722 if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart " 2723 + positionStart + " itemCount " + itemCount); 2724 if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0 2725 && mFocusPositionOffset != Integer.MIN_VALUE) { 2726 int pos = mFocusPosition + mFocusPositionOffset; 2727 if (positionStart <= pos) { 2728 if (positionStart + itemCount > pos) { 2729 // the focus item was removed 2730 mFocusPositionOffset += positionStart - pos; 2731 } else { 2732 mFocusPositionOffset -= itemCount; 2733 } 2734 } 2735 } 2736 mChildrenStates.clear(); 2737 } 2738 2739 @Override 2740 public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition, 2741 int itemCount) { 2742 if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition " 2743 + fromPosition + " toPosition " + toPosition); 2744 if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) { 2745 int pos = mFocusPosition + mFocusPositionOffset; 2746 if (fromPosition <= pos && pos < fromPosition + itemCount) { 2747 // moved items include focused position 2748 mFocusPositionOffset += toPosition - fromPosition; 2749 } else if (fromPosition < pos && toPosition > pos - itemCount) { 2750 // move items before focus position to after focused position 2751 mFocusPositionOffset -= itemCount; 2752 } else if (fromPosition > pos && toPosition < pos) { 2753 // move items after focus position to before focused position 2754 mFocusPositionOffset += itemCount; 2755 } 2756 } 2757 mChildrenStates.clear(); 2758 } 2759 2760 @Override 2761 public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { 2762 if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart " 2763 + positionStart + " itemCount " + itemCount); 2764 for (int i = positionStart, end = positionStart + itemCount; i < end; i++) { 2765 mChildrenStates.remove(i); 2766 } 2767 } 2768 2769 @Override 2770 public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) { 2771 if (mFocusSearchDisabled) { 2772 return true; 2773 } 2774 if (getAdapterPositionByView(child) == NO_POSITION) { 2775 // This is could be the last view in DISAPPEARING animation. 2776 return true; 2777 } 2778 if (!mInLayout && !mInSelection && !mInScroll) { 2779 scrollToView(child, focused, true); 2780 } 2781 return true; 2782 } 2783 2784 @Override 2785 public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect, 2786 boolean immediate) { 2787 if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect); 2788 return false; 2789 } 2790 2791 public void getViewSelectedOffsets(View view, int[] offsets) { 2792 if (mOrientation == HORIZONTAL) { 2793 offsets[0] = getPrimaryAlignedScrollDistance(view); 2794 offsets[1] = getSecondaryScrollDistance(view); 2795 } else { 2796 offsets[1] = getPrimaryAlignedScrollDistance(view); 2797 offsets[0] = getSecondaryScrollDistance(view); 2798 } 2799 } 2800 2801 /** 2802 * Return the scroll delta on primary direction to make the view selected. If the return value 2803 * is 0, there is no need to scroll. 2804 */ 2805 private int getPrimaryAlignedScrollDistance(View view) { 2806 return mWindowAlignment.mainAxis().getScroll(getViewCenter(view)); 2807 } 2808 2809 /** 2810 * Get adjusted primary position for a given childView (if there is multiple ItemAlignment 2811 * defined on the view). 2812 */ 2813 private int getAdjustedPrimaryAlignedScrollDistance(int scrollPrimary, View view, 2814 View childView) { 2815 int subindex = getSubPositionByView(view, childView); 2816 if (subindex != 0) { 2817 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2818 scrollPrimary += lp.getAlignMultiple()[subindex] - lp.getAlignMultiple()[0]; 2819 } 2820 return scrollPrimary; 2821 } 2822 2823 private int getSecondaryScrollDistance(View view) { 2824 int viewCenterSecondary = getViewCenterSecondary(view); 2825 return mWindowAlignment.secondAxis().getScroll(viewCenterSecondary); 2826 } 2827 2828 /** 2829 * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state. 2830 */ 2831 void scrollToView(View view, boolean smooth) { 2832 scrollToView(view, view == null ? null : view.findFocus(), smooth); 2833 } 2834 2835 /** 2836 * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state. 2837 */ 2838 private void scrollToView(View view, View childView, boolean smooth) { 2839 if (mIsSlidingChildViews) { 2840 return; 2841 } 2842 int newFocusPosition = getAdapterPositionByView(view); 2843 int newSubFocusPosition = getSubPositionByView(view, childView); 2844 if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) { 2845 mFocusPosition = newFocusPosition; 2846 mSubFocusPosition = newSubFocusPosition; 2847 mFocusPositionOffset = 0; 2848 if (!mInLayout) { 2849 dispatchChildSelected(); 2850 } 2851 if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) { 2852 mBaseGridView.invalidate(); 2853 } 2854 } 2855 if (view == null) { 2856 return; 2857 } 2858 if (!view.hasFocus() && mBaseGridView.hasFocus()) { 2859 // transfer focus to the child if it does not have focus yet (e.g. triggered 2860 // by setSelection()) 2861 view.requestFocus(); 2862 } 2863 if (!mScrollEnabled && smooth) { 2864 return; 2865 } 2866 if (getScrollPosition(view, childView, sTwoInts)) { 2867 scrollGrid(sTwoInts[0], sTwoInts[1], smooth); 2868 } 2869 } 2870 2871 boolean getScrollPosition(View view, View childView, int[] deltas) { 2872 switch (mFocusScrollStrategy) { 2873 case BaseGridView.FOCUS_SCROLL_ALIGNED: 2874 default: 2875 return getAlignedPosition(view, childView, deltas); 2876 case BaseGridView.FOCUS_SCROLL_ITEM: 2877 case BaseGridView.FOCUS_SCROLL_PAGE: 2878 return getNoneAlignedPosition(view, deltas); 2879 } 2880 } 2881 2882 private boolean getNoneAlignedPosition(View view, int[] deltas) { 2883 int pos = getAdapterPositionByView(view); 2884 int viewMin = getViewMin(view); 2885 int viewMax = getViewMax(view); 2886 // we either align "firstView" to left/top padding edge 2887 // or align "lastView" to right/bottom padding edge 2888 View firstView = null; 2889 View lastView = null; 2890 int paddingMin = mWindowAlignment.mainAxis().getPaddingMin(); 2891 int clientSize = mWindowAlignment.mainAxis().getClientSize(); 2892 final int row = mGrid.getRowIndex(pos); 2893 if (viewMin < paddingMin) { 2894 // view enters low padding area: 2895 firstView = view; 2896 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 2897 // scroll one "page" left/top, 2898 // align first visible item of the "page" at the low padding edge. 2899 while (prependOneColumnVisibleItems()) { 2900 CircularIntArray positions = 2901 mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row]; 2902 firstView = findViewByPosition(positions.get(0)); 2903 if (viewMax - getViewMin(firstView) > clientSize) { 2904 if (positions.size() > 2) { 2905 firstView = findViewByPosition(positions.get(2)); 2906 } 2907 break; 2908 } 2909 } 2910 } 2911 } else if (viewMax > clientSize + paddingMin) { 2912 // view enters high padding area: 2913 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 2914 // scroll whole one page right/bottom, align view at the low padding edge. 2915 firstView = view; 2916 do { 2917 CircularIntArray positions = 2918 mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row]; 2919 lastView = findViewByPosition(positions.get(positions.size() - 1)); 2920 if (getViewMax(lastView) - viewMin > clientSize) { 2921 lastView = null; 2922 break; 2923 } 2924 } while (appendOneColumnVisibleItems()); 2925 if (lastView != null) { 2926 // however if we reached end, we should align last view. 2927 firstView = null; 2928 } 2929 } else { 2930 lastView = view; 2931 } 2932 } 2933 int scrollPrimary = 0; 2934 int scrollSecondary = 0; 2935 if (firstView != null) { 2936 scrollPrimary = getViewMin(firstView) - paddingMin; 2937 } else if (lastView != null) { 2938 scrollPrimary = getViewMax(lastView) - (paddingMin + clientSize); 2939 } 2940 View secondaryAlignedView; 2941 if (firstView != null) { 2942 secondaryAlignedView = firstView; 2943 } else if (lastView != null) { 2944 secondaryAlignedView = lastView; 2945 } else { 2946 secondaryAlignedView = view; 2947 } 2948 scrollSecondary = getSecondaryScrollDistance(secondaryAlignedView); 2949 if (scrollPrimary != 0 || scrollSecondary != 0) { 2950 deltas[0] = scrollPrimary; 2951 deltas[1] = scrollSecondary; 2952 return true; 2953 } 2954 return false; 2955 } 2956 2957 private boolean getAlignedPosition(View view, View childView, int[] deltas) { 2958 int scrollPrimary = getPrimaryAlignedScrollDistance(view); 2959 if (childView != null) { 2960 scrollPrimary = getAdjustedPrimaryAlignedScrollDistance(scrollPrimary, view, childView); 2961 } 2962 int scrollSecondary = getSecondaryScrollDistance(view); 2963 if (DEBUG) { 2964 Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary 2965 + " " + mPrimaryScrollExtra + " " + mWindowAlignment); 2966 } 2967 scrollPrimary += mPrimaryScrollExtra; 2968 if (scrollPrimary != 0 || scrollSecondary != 0) { 2969 deltas[0] = scrollPrimary; 2970 deltas[1] = scrollSecondary; 2971 return true; 2972 } else { 2973 deltas[0] = 0; 2974 deltas[1] = 0; 2975 } 2976 return false; 2977 } 2978 2979 private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) { 2980 if (mInLayout) { 2981 scrollDirectionPrimary(scrollPrimary); 2982 scrollDirectionSecondary(scrollSecondary); 2983 } else { 2984 int scrollX; 2985 int scrollY; 2986 if (mOrientation == HORIZONTAL) { 2987 scrollX = scrollPrimary; 2988 scrollY = scrollSecondary; 2989 } else { 2990 scrollX = scrollSecondary; 2991 scrollY = scrollPrimary; 2992 } 2993 if (smooth) { 2994 mBaseGridView.smoothScrollBy(scrollX, scrollY); 2995 } else { 2996 mBaseGridView.scrollBy(scrollX, scrollY); 2997 dispatchChildSelectedAndPositioned(); 2998 } 2999 } 3000 } 3001 3002 public void setPruneChild(boolean pruneChild) { 3003 if (mPruneChild != pruneChild) { 3004 mPruneChild = pruneChild; 3005 if (mPruneChild) { 3006 requestLayout(); 3007 } 3008 } 3009 } 3010 3011 public boolean getPruneChild() { 3012 return mPruneChild; 3013 } 3014 3015 public void setScrollEnabled(boolean scrollEnabled) { 3016 if (mScrollEnabled != scrollEnabled) { 3017 mScrollEnabled = scrollEnabled; 3018 if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED 3019 && mFocusPosition != NO_POSITION) { 3020 scrollToSelection(mFocusPosition, mSubFocusPosition, 3021 true, mPrimaryScrollExtra); 3022 } 3023 } 3024 } 3025 3026 public boolean isScrollEnabled() { 3027 return mScrollEnabled; 3028 } 3029 3030 private int findImmediateChildIndex(View view) { 3031 if (mBaseGridView != null && view != mBaseGridView) { 3032 view = findContainingItemView(view); 3033 if (view != null) { 3034 for (int i = 0, count = getChildCount(); i < count; i++) { 3035 if (getChildAt(i) == view) { 3036 return i; 3037 } 3038 } 3039 } 3040 } 3041 return NO_POSITION; 3042 } 3043 3044 void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 3045 if (gainFocus) { 3046 // if gridview.requestFocus() is called, select first focusable child. 3047 for (int i = mFocusPosition; ;i++) { 3048 View view = findViewByPosition(i); 3049 if (view == null) { 3050 break; 3051 } 3052 if (view.getVisibility() == View.VISIBLE && view.hasFocusable()) { 3053 view.requestFocus(); 3054 break; 3055 } 3056 } 3057 } 3058 } 3059 3060 void setFocusSearchDisabled(boolean disabled) { 3061 mFocusSearchDisabled = disabled; 3062 } 3063 3064 boolean isFocusSearchDisabled() { 3065 return mFocusSearchDisabled; 3066 } 3067 3068 @Override 3069 public View onInterceptFocusSearch(View focused, int direction) { 3070 if (mFocusSearchDisabled) { 3071 return focused; 3072 } 3073 3074 final FocusFinder ff = FocusFinder.getInstance(); 3075 View result = null; 3076 if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) { 3077 // convert direction to absolute direction and see if we have a view there and if not 3078 // tell LayoutManager to add if it can. 3079 if (canScrollVertically()) { 3080 final int absDir = 3081 direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP; 3082 result = ff.findNextFocus(mBaseGridView, focused, absDir); 3083 } 3084 if (canScrollHorizontally()) { 3085 boolean rtl = getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; 3086 final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl 3087 ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 3088 result = ff.findNextFocus(mBaseGridView, focused, absDir); 3089 } 3090 } else { 3091 result = ff.findNextFocus(mBaseGridView, focused, direction); 3092 } 3093 if (result != null) { 3094 return result; 3095 } 3096 3097 if (mBaseGridView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) { 3098 return mBaseGridView.getParent().focusSearch(focused, direction); 3099 } 3100 3101 if (DEBUG) Log.v(getTag(), "regular focusSearch failed direction " + direction); 3102 int movement = getMovement(direction); 3103 final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE; 3104 if (movement == NEXT_ITEM) { 3105 if (isScroll || !mFocusOutEnd) { 3106 result = focused; 3107 } 3108 if (mScrollEnabled && !hasCreatedLastItem()) { 3109 processPendingMovement(true); 3110 result = focused; 3111 } 3112 } else if (movement == PREV_ITEM) { 3113 if (isScroll || !mFocusOutFront) { 3114 result = focused; 3115 } 3116 if (mScrollEnabled && !hasCreatedFirstItem()) { 3117 processPendingMovement(false); 3118 result = focused; 3119 } 3120 } else if (movement == NEXT_ROW) { 3121 if (isScroll || !mFocusOutSideEnd) { 3122 result = focused; 3123 } 3124 } else if (movement == PREV_ROW) { 3125 if (isScroll || !mFocusOutSideStart) { 3126 result = focused; 3127 } 3128 } 3129 if (result != null) { 3130 return result; 3131 } 3132 3133 if (DEBUG) Log.v(getTag(), "now focusSearch in parent"); 3134 result = mBaseGridView.getParent().focusSearch(focused, direction); 3135 if (result != null) { 3136 return result; 3137 } 3138 return focused != null ? focused : mBaseGridView; 3139 } 3140 3141 boolean hasPreviousViewInSameRow(int pos) { 3142 if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) { 3143 return false; 3144 } 3145 if (mGrid.getFirstVisibleIndex() > 0) { 3146 return true; 3147 } 3148 final int focusedRow = mGrid.getLocation(pos).row; 3149 for (int i = getChildCount() - 1; i >= 0; i--) { 3150 int position = getAdapterPositionByIndex(i); 3151 Grid.Location loc = mGrid.getLocation(position); 3152 if (loc != null && loc.row == focusedRow) { 3153 if (position < pos) { 3154 return true; 3155 } 3156 } 3157 } 3158 return false; 3159 } 3160 3161 @Override 3162 public boolean onAddFocusables(RecyclerView recyclerView, 3163 ArrayList<View> views, int direction, int focusableMode) { 3164 if (mFocusSearchDisabled) { 3165 return true; 3166 } 3167 // If this viewgroup or one of its children currently has focus then we 3168 // consider our children for focus searching in main direction on the same row. 3169 // If this viewgroup has no focus and using focus align, we want the system 3170 // to ignore our children and pass focus to the viewgroup, which will pass 3171 // focus on to its children appropriately. 3172 // If this viewgroup has no focus and not using focus align, we want to 3173 // consider the child that does not overlap with padding area. 3174 if (recyclerView.hasFocus()) { 3175 if (mPendingMoveSmoothScroller != null) { 3176 // don't find next focusable if has pending movement. 3177 return true; 3178 } 3179 final int movement = getMovement(direction); 3180 final View focused = recyclerView.findFocus(); 3181 final int focusedIndex = findImmediateChildIndex(focused); 3182 final int focusedPos = getAdapterPositionByIndex(focusedIndex); 3183 // Add focusables of focused item. 3184 if (focusedPos != NO_POSITION) { 3185 findViewByPosition(focusedPos).addFocusables(views, direction, focusableMode); 3186 } 3187 if (mGrid == null || getChildCount() == 0) { 3188 // no grid information, or no child, bail out. 3189 return true; 3190 } 3191 if ((movement == NEXT_ROW || movement == PREV_ROW) && mGrid.getNumRows() <= 1) { 3192 // For single row, cannot navigate to previous/next row. 3193 return true; 3194 } 3195 // Add focusables of neighbor depending on the focus search direction. 3196 final int focusedRow = mGrid != null && focusedPos != NO_POSITION 3197 ? mGrid.getLocation(focusedPos).row : NO_POSITION; 3198 final int focusableCount = views.size(); 3199 int inc = movement == NEXT_ITEM || movement == NEXT_ROW ? 1 : -1; 3200 int loop_end = inc > 0 ? getChildCount() - 1 : 0; 3201 int loop_start; 3202 if (focusedIndex == NO_POSITION) { 3203 loop_start = inc > 0 ? 0 : getChildCount() - 1; 3204 } else { 3205 loop_start = focusedIndex + inc; 3206 } 3207 for (int i = loop_start; inc > 0 ? i <= loop_end : i >= loop_end; i += inc) { 3208 final View child = getChildAt(i); 3209 if (child.getVisibility() != View.VISIBLE || !child.hasFocusable()) { 3210 continue; 3211 } 3212 // if there wasn't any focusing item, add the very first focusable 3213 // items and stop. 3214 if (focusedPos == NO_POSITION) { 3215 child.addFocusables(views, direction, focusableMode); 3216 if (views.size() > focusableCount) { 3217 break; 3218 } 3219 continue; 3220 } 3221 int position = getAdapterPositionByIndex(i); 3222 Grid.Location loc = mGrid.getLocation(position); 3223 if (loc == null) { 3224 continue; 3225 } 3226 if (movement == NEXT_ITEM) { 3227 // Add first focusable item on the same row 3228 if (loc.row == focusedRow && position > focusedPos) { 3229 child.addFocusables(views, direction, focusableMode); 3230 if (views.size() > focusableCount) { 3231 break; 3232 } 3233 } 3234 } else if (movement == PREV_ITEM) { 3235 // Add first focusable item on the same row 3236 if (loc.row == focusedRow && position < focusedPos) { 3237 child.addFocusables(views, direction, focusableMode); 3238 if (views.size() > focusableCount) { 3239 break; 3240 } 3241 } 3242 } else if (movement == NEXT_ROW) { 3243 // Add all focusable items after this item whose row index is bigger 3244 if (loc.row == focusedRow) { 3245 continue; 3246 } else if (loc.row < focusedRow) { 3247 break; 3248 } 3249 child.addFocusables(views, direction, focusableMode); 3250 } else if (movement == PREV_ROW) { 3251 // Add all focusable items before this item whose row index is smaller 3252 if (loc.row == focusedRow) { 3253 continue; 3254 } else if (loc.row > focusedRow) { 3255 break; 3256 } 3257 child.addFocusables(views, direction, focusableMode); 3258 } 3259 } 3260 } else { 3261 int focusableCount = views.size(); 3262 if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) { 3263 // adding views not overlapping padding area to avoid scrolling in gaining focus 3264 int left = mWindowAlignment.mainAxis().getPaddingMin(); 3265 int right = mWindowAlignment.mainAxis().getClientSize() + left; 3266 for (int i = 0, count = getChildCount(); i < count; i++) { 3267 View child = getChildAt(i); 3268 if (child.getVisibility() == View.VISIBLE) { 3269 if (getViewMin(child) >= left && getViewMax(child) <= right) { 3270 child.addFocusables(views, direction, focusableMode); 3271 } 3272 } 3273 } 3274 // if we cannot find any, then just add all children. 3275 if (views.size() == focusableCount) { 3276 for (int i = 0, count = getChildCount(); i < count; i++) { 3277 View child = getChildAt(i); 3278 if (child.getVisibility() == View.VISIBLE) { 3279 child.addFocusables(views, direction, focusableMode); 3280 } 3281 } 3282 } 3283 } else { 3284 View view = findViewByPosition(mFocusPosition); 3285 if (view != null) { 3286 view.addFocusables(views, direction, focusableMode); 3287 } 3288 } 3289 // if still cannot find any, fall through and add itself 3290 if (views.size() != focusableCount) { 3291 return true; 3292 } 3293 if (recyclerView.isFocusable()) { 3294 views.add(recyclerView); 3295 } 3296 } 3297 return true; 3298 } 3299 3300 boolean hasCreatedLastItem() { 3301 int count = getItemCount(); 3302 return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(count - 1) != null; 3303 } 3304 3305 boolean hasCreatedFirstItem() { 3306 int count = getItemCount(); 3307 return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(0) != null; 3308 } 3309 3310 boolean canScrollTo(View view) { 3311 return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable()); 3312 } 3313 3314 boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction, 3315 Rect previouslyFocusedRect) { 3316 switch (mFocusScrollStrategy) { 3317 case BaseGridView.FOCUS_SCROLL_ALIGNED: 3318 default: 3319 return gridOnRequestFocusInDescendantsAligned(recyclerView, 3320 direction, previouslyFocusedRect); 3321 case BaseGridView.FOCUS_SCROLL_PAGE: 3322 case BaseGridView.FOCUS_SCROLL_ITEM: 3323 return gridOnRequestFocusInDescendantsUnaligned(recyclerView, 3324 direction, previouslyFocusedRect); 3325 } 3326 } 3327 3328 private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView, 3329 int direction, Rect previouslyFocusedRect) { 3330 View view = findViewByPosition(mFocusPosition); 3331 if (view != null) { 3332 boolean result = view.requestFocus(direction, previouslyFocusedRect); 3333 if (!result && DEBUG) { 3334 Log.w(getTag(), "failed to request focus on " + view); 3335 } 3336 return result; 3337 } 3338 return false; 3339 } 3340 3341 private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView, 3342 int direction, Rect previouslyFocusedRect) { 3343 // focus to view not overlapping padding area to avoid scrolling in gaining focus 3344 int index; 3345 int increment; 3346 int end; 3347 int count = getChildCount(); 3348 if ((direction & View.FOCUS_FORWARD) != 0) { 3349 index = 0; 3350 increment = 1; 3351 end = count; 3352 } else { 3353 index = count - 1; 3354 increment = -1; 3355 end = -1; 3356 } 3357 int left = mWindowAlignment.mainAxis().getPaddingMin(); 3358 int right = mWindowAlignment.mainAxis().getClientSize() + left; 3359 for (int i = index; i != end; i += increment) { 3360 View child = getChildAt(i); 3361 if (child.getVisibility() == View.VISIBLE) { 3362 if (getViewMin(child) >= left && getViewMax(child) <= right) { 3363 if (child.requestFocus(direction, previouslyFocusedRect)) { 3364 return true; 3365 } 3366 } 3367 } 3368 } 3369 return false; 3370 } 3371 3372 private final static int PREV_ITEM = 0; 3373 private final static int NEXT_ITEM = 1; 3374 private final static int PREV_ROW = 2; 3375 private final static int NEXT_ROW = 3; 3376 3377 private int getMovement(int direction) { 3378 int movement = View.FOCUS_LEFT; 3379 3380 if (mOrientation == HORIZONTAL) { 3381 switch(direction) { 3382 case View.FOCUS_LEFT: 3383 movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM; 3384 break; 3385 case View.FOCUS_RIGHT: 3386 movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM; 3387 break; 3388 case View.FOCUS_UP: 3389 movement = PREV_ROW; 3390 break; 3391 case View.FOCUS_DOWN: 3392 movement = NEXT_ROW; 3393 break; 3394 } 3395 } else if (mOrientation == VERTICAL) { 3396 switch(direction) { 3397 case View.FOCUS_LEFT: 3398 movement = (!mReverseFlowSecondary) ? PREV_ROW : NEXT_ROW; 3399 break; 3400 case View.FOCUS_RIGHT: 3401 movement = (!mReverseFlowSecondary) ? NEXT_ROW : PREV_ROW; 3402 break; 3403 case View.FOCUS_UP: 3404 movement = PREV_ITEM; 3405 break; 3406 case View.FOCUS_DOWN: 3407 movement = NEXT_ITEM; 3408 break; 3409 } 3410 } 3411 3412 return movement; 3413 } 3414 3415 int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) { 3416 View view = findViewByPosition(mFocusPosition); 3417 if (view == null) { 3418 return i; 3419 } 3420 int focusIndex = recyclerView.indexOfChild(view); 3421 // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item 3422 // drawing order is 0 1 2 3 9 8 7 6 5 4 3423 if (i < focusIndex) { 3424 return i; 3425 } else if (i < childCount - 1) { 3426 return focusIndex + childCount - 1 - i; 3427 } else { 3428 return focusIndex; 3429 } 3430 } 3431 3432 @Override 3433 public void onAdapterChanged(RecyclerView.Adapter oldAdapter, 3434 RecyclerView.Adapter newAdapter) { 3435 if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter); 3436 if (oldAdapter != null) { 3437 discardLayoutInfo(); 3438 mFocusPosition = NO_POSITION; 3439 mFocusPositionOffset = 0; 3440 mChildrenStates.clear(); 3441 } 3442 if (newAdapter instanceof FacetProviderAdapter) { 3443 mFacetProviderAdapter = (FacetProviderAdapter) newAdapter; 3444 } else { 3445 mFacetProviderAdapter = null; 3446 } 3447 super.onAdapterChanged(oldAdapter, newAdapter); 3448 } 3449 3450 private void discardLayoutInfo() { 3451 mGrid = null; 3452 mRowSizeSecondary = null; 3453 mRowSecondarySizeRefresh = false; 3454 } 3455 3456 public void setLayoutEnabled(boolean layoutEnabled) { 3457 if (mLayoutEnabled != layoutEnabled) { 3458 mLayoutEnabled = layoutEnabled; 3459 requestLayout(); 3460 } 3461 } 3462 3463 void setChildrenVisibility(int visibility) { 3464 mChildVisibility = visibility; 3465 if (mChildVisibility != -1) { 3466 int count = getChildCount(); 3467 for (int i= 0; i < count; i++) { 3468 getChildAt(i).setVisibility(mChildVisibility); 3469 } 3470 } 3471 } 3472 3473 final static class SavedState implements Parcelable { 3474 3475 int index; // index inside adapter of the current view 3476 Bundle childStates = Bundle.EMPTY; 3477 3478 @Override 3479 public void writeToParcel(Parcel out, int flags) { 3480 out.writeInt(index); 3481 out.writeBundle(childStates); 3482 } 3483 3484 @SuppressWarnings("hiding") 3485 public static final Parcelable.Creator<SavedState> CREATOR = 3486 new Parcelable.Creator<SavedState>() { 3487 @Override 3488 public SavedState createFromParcel(Parcel in) { 3489 return new SavedState(in); 3490 } 3491 3492 @Override 3493 public SavedState[] newArray(int size) { 3494 return new SavedState[size]; 3495 } 3496 }; 3497 3498 @Override 3499 public int describeContents() { 3500 return 0; 3501 } 3502 3503 SavedState(Parcel in) { 3504 index = in.readInt(); 3505 childStates = in.readBundle(GridLayoutManager.class.getClassLoader()); 3506 } 3507 3508 SavedState() { 3509 } 3510 } 3511 3512 @Override 3513 public Parcelable onSaveInstanceState() { 3514 if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection()); 3515 SavedState ss = new SavedState(); 3516 // save selected index 3517 ss.index = getSelection(); 3518 // save offscreen child (state when they are recycled) 3519 Bundle bundle = mChildrenStates.saveAsBundle(); 3520 // save views currently is on screen (TODO save cached views) 3521 for (int i = 0, count = getChildCount(); i < count; i++) { 3522 View view = getChildAt(i); 3523 int position = getAdapterPositionByView(view); 3524 if (position != NO_POSITION) { 3525 bundle = mChildrenStates.saveOnScreenView(bundle, view, position); 3526 } 3527 } 3528 ss.childStates = bundle; 3529 return ss; 3530 } 3531 3532 void onChildRecycled(RecyclerView.ViewHolder holder) { 3533 final int position = holder.getAdapterPosition(); 3534 if (position != NO_POSITION) { 3535 mChildrenStates.saveOffscreenView(holder.itemView, position); 3536 } 3537 } 3538 3539 @Override 3540 public void onRestoreInstanceState(Parcelable state) { 3541 if (!(state instanceof SavedState)) { 3542 return; 3543 } 3544 SavedState loadingState = (SavedState)state; 3545 mFocusPosition = loadingState.index; 3546 mFocusPositionOffset = 0; 3547 mChildrenStates.loadFromBundle(loadingState.childStates); 3548 mForceFullLayout = true; 3549 requestLayout(); 3550 if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition); 3551 } 3552 3553 @Override 3554 public int getRowCountForAccessibility(RecyclerView.Recycler recycler, 3555 RecyclerView.State state) { 3556 if (mOrientation == HORIZONTAL && mGrid != null) { 3557 return mGrid.getNumRows(); 3558 } 3559 return super.getRowCountForAccessibility(recycler, state); 3560 } 3561 3562 @Override 3563 public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, 3564 RecyclerView.State state) { 3565 if (mOrientation == VERTICAL && mGrid != null) { 3566 return mGrid.getNumRows(); 3567 } 3568 return super.getColumnCountForAccessibility(recycler, state); 3569 } 3570 3571 @Override 3572 public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, 3573 RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { 3574 ViewGroup.LayoutParams lp = host.getLayoutParams(); 3575 if (mGrid == null || !(lp instanceof LayoutParams)) { 3576 return; 3577 } 3578 LayoutParams glp = (LayoutParams) lp; 3579 int position = glp.getViewAdapterPosition(); 3580 int rowIndex = position >= 0 ? mGrid.getRowIndex(position) : -1; 3581 if (rowIndex < 0) { 3582 return; 3583 } 3584 int guessSpanIndex = position / mGrid.getNumRows(); 3585 if (mOrientation == HORIZONTAL) { 3586 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 3587 rowIndex, 1, guessSpanIndex, 1, false, false)); 3588 } else { 3589 info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( 3590 guessSpanIndex, 1, rowIndex, 1, false, false)); 3591 } 3592 } 3593 3594 /* 3595 * Leanback widget is different than the default implementation because the "scroll" is driven 3596 * by selection change. 3597 */ 3598 @Override 3599 public boolean performAccessibilityAction(Recycler recycler, State state, int action, 3600 Bundle args) { 3601 saveContext(recycler, state); 3602 switch (action) { 3603 case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: 3604 // try to focus all the way to the last visible item on the same row. 3605 processSelectionMoves(false, -mState.getItemCount()); 3606 break; 3607 case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: 3608 processSelectionMoves(false, mState.getItemCount()); 3609 break; 3610 } 3611 leaveContext(); 3612 return true; 3613 } 3614 3615 /* 3616 * Move mFocusPosition multiple steps on the same row in main direction. 3617 * Stops when moves are all consumed or reach first/last visible item. 3618 * Returning remaining moves. 3619 */ 3620 int processSelectionMoves(boolean preventScroll, int moves) { 3621 if (mGrid == null) { 3622 return moves; 3623 } 3624 int focusPosition = mFocusPosition; 3625 int focusedRow = focusPosition != NO_POSITION 3626 ? mGrid.getRowIndex(focusPosition) : NO_POSITION; 3627 View newSelected = null; 3628 for (int i = 0, count = getChildCount(); i < count && moves != 0; i++) { 3629 int index = moves > 0 ? i : count - 1 - i; 3630 final View child = getChildAt(index); 3631 if (!canScrollTo(child)) { 3632 continue; 3633 } 3634 int position = getAdapterPositionByIndex(index); 3635 int rowIndex = mGrid.getRowIndex(position); 3636 if (focusedRow == NO_POSITION) { 3637 focusPosition = position; 3638 newSelected = child; 3639 focusedRow = rowIndex; 3640 } else if (rowIndex == focusedRow) { 3641 if ((moves > 0 && position > focusPosition) 3642 || (moves < 0 && position < focusPosition)) { 3643 focusPosition = position; 3644 newSelected = child; 3645 if (moves > 0) { 3646 moves--; 3647 } else { 3648 moves++; 3649 } 3650 } 3651 } 3652 } 3653 if (newSelected != null) { 3654 if (preventScroll) { 3655 if (hasFocus()) { 3656 mInSelection = true; 3657 newSelected.requestFocus(); 3658 mInSelection = false; 3659 } 3660 mFocusPosition = focusPosition; 3661 mSubFocusPosition = 0; 3662 } else { 3663 scrollToView(newSelected, true); 3664 } 3665 } 3666 return moves; 3667 } 3668 3669 @Override 3670 public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state, 3671 AccessibilityNodeInfoCompat info) { 3672 saveContext(recycler, state); 3673 if (mScrollEnabled && !hasCreatedFirstItem()) { 3674 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); 3675 info.setScrollable(true); 3676 } 3677 if (mScrollEnabled && !hasCreatedLastItem()) { 3678 info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); 3679 info.setScrollable(true); 3680 } 3681 final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = 3682 AccessibilityNodeInfoCompat.CollectionInfoCompat 3683 .obtain(getRowCountForAccessibility(recycler, state), 3684 getColumnCountForAccessibility(recycler, state), 3685 isLayoutHierarchical(recycler, state), 3686 getSelectionModeForAccessibility(recycler, state)); 3687 info.setCollectionInfo(collectionInfo); 3688 leaveContext(); 3689 } 3690} 3691