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