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