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