GridLayoutManager.java revision 94920246f7f5a0d4dae794058020cd67c5701056
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.widget; 15 16import android.animation.TimeAnimator; 17import android.content.Context; 18import android.graphics.PointF; 19import android.graphics.Rect; 20import android.support.v7.widget.LinearSmoothScroller; 21import android.support.v7.widget.RecyclerView; 22import android.support.v7.widget.RecyclerView.Recycler; 23import android.support.v7.widget.RecyclerView.State; 24 25import static android.support.v7.widget.RecyclerView.NO_ID; 26import static android.support.v7.widget.RecyclerView.NO_POSITION; 27import static android.support.v7.widget.RecyclerView.HORIZONTAL; 28import static android.support.v7.widget.RecyclerView.VERTICAL; 29 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.FocusFinder; 33import android.view.Gravity; 34import android.view.View; 35import android.view.ViewParent; 36import android.view.View.MeasureSpec; 37import android.view.ViewGroup.MarginLayoutParams; 38import android.view.ViewGroup; 39import android.view.animation.DecelerateInterpolator; 40import android.view.animation.Interpolator; 41 42import java.io.PrintWriter; 43import java.io.StringWriter; 44import java.util.ArrayList; 45import java.util.List; 46 47final class GridLayoutManager extends RecyclerView.LayoutManager { 48 49 /* 50 * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}. 51 * The class currently does two internal jobs: 52 * - Saves optical bounds insets. 53 * - Caches focus align view center. 54 */ 55 static class LayoutParams extends RecyclerView.LayoutParams { 56 57 // The view is saved only during animation. 58 private View mView; 59 60 // For placement 61 private int mLeftInset; 62 private int mTopInset; 63 private int mRighInset; 64 private int mBottomInset; 65 66 // For alignment 67 private int mAlignX; 68 private int mAlignY; 69 70 public LayoutParams(Context c, AttributeSet attrs) { 71 super(c, attrs); 72 } 73 74 public LayoutParams(int width, int height) { 75 super(width, height); 76 } 77 78 public LayoutParams(MarginLayoutParams source) { 79 super(source); 80 } 81 82 public LayoutParams(ViewGroup.LayoutParams source) { 83 super(source); 84 } 85 86 public LayoutParams(RecyclerView.LayoutParams source) { 87 super(source); 88 } 89 90 public LayoutParams(LayoutParams source) { 91 super(source); 92 } 93 94 int getAlignX() { 95 return mAlignX; 96 } 97 98 int getAlignY() { 99 return mAlignY; 100 } 101 102 int getOpticalLeft(View view) { 103 return view.getLeft() + mLeftInset; 104 } 105 106 int getOpticalTop(View view) { 107 return view.getTop() + mTopInset; 108 } 109 110 int getOpticalRight(View view) { 111 return view.getRight() - mRighInset; 112 } 113 114 int getOpticalBottom(View view) { 115 return view.getBottom() - mBottomInset; 116 } 117 118 int getOpticalWidth(View view) { 119 return view.getWidth() - mLeftInset - mRighInset; 120 } 121 122 int getOpticalHeight(View view) { 123 return view.getHeight() - mTopInset - mBottomInset; 124 } 125 126 int getOpticalLeftInset() { 127 return mLeftInset; 128 } 129 130 int getOpticalRightInset() { 131 return mRighInset; 132 } 133 134 int getOpticalTopInset() { 135 return mTopInset; 136 } 137 138 int getOpticalBottomInset() { 139 return mBottomInset; 140 } 141 142 void setAlignX(int alignX) { 143 mAlignX = alignX; 144 } 145 146 void setAlignY(int alignY) { 147 mAlignY = alignY; 148 } 149 150 void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) { 151 mLeftInset = leftInset; 152 mTopInset = topInset; 153 mRighInset = rightInset; 154 mBottomInset = bottomInset; 155 } 156 157 private void invalidateItemDecoration() { 158 ViewParent parent = mView.getParent(); 159 if (parent instanceof RecyclerView) { 160 // TODO: we only need invalidate parent if it has ItemDecoration 161 ((RecyclerView) parent).invalidate(); 162 } 163 } 164 } 165 166 private static final String TAG = "GridLayoutManager"; 167 private static final boolean DEBUG = false; 168 169 private static final Interpolator sDefaultAnimationChildLayoutInterpolator 170 = new DecelerateInterpolator(); 171 172 private static final long DEFAULT_CHILD_ANIMATION_DURATION_MS = 250; 173 174 private String getTag() { 175 return TAG + ":" + mBaseGridView.getId(); 176 } 177 178 private final BaseGridView mBaseGridView; 179 180 /** 181 * The orientation of a "row". 182 */ 183 private int mOrientation = HORIZONTAL; 184 185 private RecyclerView.State mState; 186 private RecyclerView.Recycler mRecycler; 187 188 private boolean mInLayout = false; 189 190 private OnChildSelectedListener mChildSelectedListener = null; 191 192 /** 193 * The focused position, it's not the currently visually aligned position 194 * but it is the final position that we intend to focus on. If there are 195 * multiple setSelection() called, mFocusPosition saves last value. 196 */ 197 private int mFocusPosition = NO_POSITION; 198 199 /** 200 * Force a full layout under certain situations. 201 */ 202 private boolean mForceFullLayout; 203 204 /** 205 * True if layout is enabled. 206 */ 207 private boolean mLayoutEnabled = true; 208 209 /** 210 * The scroll offsets of the viewport relative to the entire view. 211 */ 212 private int mScrollOffsetPrimary; 213 private int mScrollOffsetSecondary; 214 215 /** 216 * User-specified row height/column width. Can be WRAP_CONTENT. 217 */ 218 private int mRowSizeSecondaryRequested; 219 220 /** 221 * The fixed size of each grid item in the secondary direction. This corresponds to 222 * the row height, equal for all rows. Grid items may have variable length 223 * in the primary direction. 224 */ 225 private int mFixedRowSizeSecondary; 226 227 /** 228 * Tracks the secondary size of each row. 229 */ 230 private int[] mRowSizeSecondary; 231 232 /** 233 * Flag controlling whether the current/next layout should 234 * be updating the secondary size of rows. 235 */ 236 private boolean mRowSecondarySizeRefresh; 237 238 /** 239 * The maximum measured size of the view. 240 */ 241 private int mMaxSizeSecondary; 242 243 /** 244 * Margin between items. 245 */ 246 private int mHorizontalMargin; 247 /** 248 * Margin between items vertically. 249 */ 250 private int mVerticalMargin; 251 /** 252 * Margin in main direction. 253 */ 254 private int mMarginPrimary; 255 /** 256 * Margin in second direction. 257 */ 258 private int mMarginSecondary; 259 /** 260 * How to position child in secondary direction. 261 */ 262 private int mGravity = Gravity.LEFT | Gravity.TOP; 263 /** 264 * The number of rows in the grid. 265 */ 266 private int mNumRows; 267 /** 268 * Number of rows requested, can be 0 to be determined by parent size and 269 * rowHeight. 270 */ 271 private int mNumRowsRequested = 1; 272 273 /** 274 * Tracking start/end position of each row for visible items. 275 */ 276 private StaggeredGrid.Row[] mRows; 277 278 /** 279 * Saves grid information of each view. 280 */ 281 private StaggeredGrid mGrid; 282 /** 283 * Position of first item (included) that has attached views. 284 */ 285 private int mFirstVisiblePos; 286 /** 287 * Position of last item (included) that has attached views. 288 */ 289 private int mLastVisiblePos; 290 291 /** 292 * Focus Scroll strategy. 293 */ 294 private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED; 295 /** 296 * Defines how item view is aligned in the window. 297 */ 298 private final WindowAlignment mWindowAlignment = new WindowAlignment(); 299 300 /** 301 * Defines how item view is aligned. 302 */ 303 private final ItemAlignment mItemAlignment = new ItemAlignment(); 304 305 /** 306 * Dimensions of the view, width or height depending on orientation. 307 */ 308 private int mSizePrimary; 309 310 /** 311 * Allow DPAD key to navigate out at the front of the View (where position = 0), 312 * default is false. 313 */ 314 private boolean mFocusOutFront; 315 316 /** 317 * Allow DPAD key to navigate out at the end of the view, default is false. 318 */ 319 private boolean mFocusOutEnd; 320 321 /** 322 * True if focus search is disabled. 323 */ 324 private boolean mFocusSearchDisabled; 325 326 /** 327 * True if prune child, might be disabled during transition. 328 */ 329 private boolean mPruneChild = true; 330 331 private int[] mTempDeltas = new int[2]; 332 333 private boolean mUseDeltaInPreLayout; 334 335 private int mDeltaInPreLayout, mDeltaSecondaryInPreLayout; 336 337 /** 338 * Temporaries used for measuring. 339 */ 340 private int[] mMeasuredDimension = new int[2]; 341 342 public GridLayoutManager(BaseGridView baseGridView) { 343 mBaseGridView = baseGridView; 344 } 345 346 public void setOrientation(int orientation) { 347 if (orientation != HORIZONTAL && orientation != VERTICAL) { 348 if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation); 349 return; 350 } 351 352 mOrientation = orientation; 353 mWindowAlignment.setOrientation(orientation); 354 mItemAlignment.setOrientation(orientation); 355 mForceFullLayout = true; 356 } 357 358 public int getFocusScrollStrategy() { 359 return mFocusScrollStrategy; 360 } 361 362 public void setFocusScrollStrategy(int focusScrollStrategy) { 363 mFocusScrollStrategy = focusScrollStrategy; 364 } 365 366 public void setWindowAlignment(int windowAlignment) { 367 mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment); 368 } 369 370 public int getWindowAlignment() { 371 return mWindowAlignment.mainAxis().getWindowAlignment(); 372 } 373 374 public void setWindowAlignmentOffset(int alignmentOffset) { 375 mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset); 376 } 377 378 public int getWindowAlignmentOffset() { 379 return mWindowAlignment.mainAxis().getWindowAlignmentOffset(); 380 } 381 382 public void setWindowAlignmentOffsetPercent(float offsetPercent) { 383 mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent); 384 } 385 386 public float getWindowAlignmentOffsetPercent() { 387 return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent(); 388 } 389 390 public void setItemAlignmentOffset(int alignmentOffset) { 391 mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset); 392 updateChildAlignments(); 393 } 394 395 public int getItemAlignmentOffset() { 396 return mItemAlignment.mainAxis().getItemAlignmentOffset(); 397 } 398 399 public void setItemAlignmentOffsetWithPadding(boolean withPadding) { 400 mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding); 401 updateChildAlignments(); 402 } 403 404 public boolean isItemAlignmentOffsetWithPadding() { 405 return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding(); 406 } 407 408 public void setItemAlignmentOffsetPercent(float offsetPercent) { 409 mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent); 410 updateChildAlignments(); 411 } 412 413 public float getItemAlignmentOffsetPercent() { 414 return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent(); 415 } 416 417 public void setItemAlignmentViewId(int viewId) { 418 mItemAlignment.mainAxis().setItemAlignmentViewId(viewId); 419 updateChildAlignments(); 420 } 421 422 public int getItemAlignmentViewId() { 423 return mItemAlignment.mainAxis().getItemAlignmentViewId(); 424 } 425 426 public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) { 427 mFocusOutFront = throughFront; 428 mFocusOutEnd = throughEnd; 429 } 430 431 public void setNumRows(int numRows) { 432 if (numRows < 0) throw new IllegalArgumentException(); 433 mNumRowsRequested = numRows; 434 mForceFullLayout = true; 435 } 436 437 /** 438 * Set the row height. May be WRAP_CONTENT, or a size in pixels. 439 */ 440 public void setRowHeight(int height) { 441 if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) { 442 mRowSizeSecondaryRequested = height; 443 } else { 444 throw new IllegalArgumentException("Invalid row height: " + height); 445 } 446 } 447 448 public void setItemMargin(int margin) { 449 mVerticalMargin = mHorizontalMargin = margin; 450 mMarginPrimary = mMarginSecondary = margin; 451 } 452 453 public void setVerticalMargin(int margin) { 454 if (mOrientation == HORIZONTAL) { 455 mMarginSecondary = mVerticalMargin = margin; 456 } else { 457 mMarginPrimary = mVerticalMargin = margin; 458 } 459 } 460 461 public void setHorizontalMargin(int margin) { 462 if (mOrientation == HORIZONTAL) { 463 mMarginPrimary = mHorizontalMargin = margin; 464 } else { 465 mMarginSecondary = mHorizontalMargin = margin; 466 } 467 } 468 469 public int getVerticalMargin() { 470 return mVerticalMargin; 471 } 472 473 public int getHorizontalMargin() { 474 return mHorizontalMargin; 475 } 476 477 public void setGravity(int gravity) { 478 mGravity = gravity; 479 } 480 481 protected boolean hasDoneFirstLayout() { 482 return mGrid != null; 483 } 484 485 public void setOnChildSelectedListener(OnChildSelectedListener listener) { 486 mChildSelectedListener = listener; 487 } 488 489 private int getPositionByView(View view) { 490 if (view == null) { 491 return NO_POSITION; 492 } 493 RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view); 494 if (vh == null) { 495 return NO_POSITION; 496 } 497 return vh.getPosition(); 498 } 499 500 private int getPositionByIndex(int index) { 501 return getPositionByView(getChildAt(index)); 502 } 503 504 private void dispatchChildSelected() { 505 if (mChildSelectedListener == null) { 506 return; 507 } 508 if (mFocusPosition != NO_POSITION) { 509 View view = findViewByPosition(mFocusPosition); 510 if (view != null) { 511 RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view); 512 mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition, 513 vh == null? NO_ID: vh.getItemId()); 514 return; 515 } 516 } 517 mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID); 518 } 519 520 @Override 521 public boolean canScrollHorizontally() { 522 // We can scroll horizontally if we have horizontal orientation, or if 523 // we are vertical and have more than one column. 524 return mOrientation == HORIZONTAL || mNumRows > 1; 525 } 526 527 @Override 528 public boolean canScrollVertically() { 529 // We can scroll vertically if we have vertical orientation, or if we 530 // are horizontal and have more than one row. 531 return mOrientation == VERTICAL || mNumRows > 1; 532 } 533 534 @Override 535 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 536 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 537 ViewGroup.LayoutParams.WRAP_CONTENT); 538 } 539 540 @Override 541 public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) { 542 return new LayoutParams(context, attrs); 543 } 544 545 @Override 546 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 547 if (lp instanceof LayoutParams) { 548 return new LayoutParams((LayoutParams) lp); 549 } else if (lp instanceof RecyclerView.LayoutParams) { 550 return new LayoutParams((RecyclerView.LayoutParams) lp); 551 } else if (lp instanceof MarginLayoutParams) { 552 return new LayoutParams((MarginLayoutParams) lp); 553 } else { 554 return new LayoutParams(lp); 555 } 556 } 557 558 protected View getViewForPosition(int position) { 559 return mRecycler.getViewForPosition(position); 560 } 561 562 final int getOpticalLeft(View v) { 563 return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v); 564 } 565 566 final int getOpticalRight(View v) { 567 return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v); 568 } 569 570 final int getOpticalTop(View v) { 571 return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v); 572 } 573 574 final int getOpticalBottom(View v) { 575 return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v); 576 } 577 578 private int getViewMin(View v) { 579 return (mOrientation == HORIZONTAL) ? getOpticalLeft(v) : getOpticalTop(v); 580 } 581 582 private int getViewMax(View v) { 583 return (mOrientation == HORIZONTAL) ? getOpticalRight(v) : getOpticalBottom(v); 584 } 585 586 private int getViewCenter(View view) { 587 return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view); 588 } 589 590 private int getViewCenterSecondary(View view) { 591 return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view); 592 } 593 594 private int getViewCenterX(View v) { 595 LayoutParams p = (LayoutParams) v.getLayoutParams(); 596 return p.getOpticalLeft(v) + p.getAlignX(); 597 } 598 599 private int getViewCenterY(View v) { 600 LayoutParams p = (LayoutParams) v.getLayoutParams(); 601 return p.getOpticalTop(v) + p.getAlignY(); 602 } 603 604 /** 605 * Save Recycler and State for convenience. Must be paired with leaveContext(). 606 */ 607 private void saveContext(Recycler recycler, State state) { 608 if (mRecycler != null || mState != null) { 609 Log.e(TAG, "Recycler information was not released, bug!"); 610 } 611 mRecycler = recycler; 612 mState = state; 613 } 614 615 /** 616 * Discard saved Recycler and State. 617 */ 618 private void leaveContext() { 619 mRecycler = null; 620 mState = null; 621 } 622 623 /** 624 * Re-initialize data structures for a data change or handling invisible 625 * selection. The method tries its best to preserve position information so 626 * that staggered grid looks same before and after re-initialize. 627 * @param focusPosition The initial focusPosition that we would like to 628 * focus on. 629 * @return Actual position that can be focused on. 630 */ 631 private int init(int focusPosition) { 632 633 final int newItemCount = mState.getItemCount(); 634 635 if (focusPosition == NO_POSITION && newItemCount > 0) { 636 // if focus position is never set before, initialize it to 0 637 focusPosition = 0; 638 } 639 // If adapter has changed then caches are invalid; otherwise, 640 // we try to maintain each row's position if number of rows keeps the same 641 // and existing mGrid contains the focusPosition. 642 if (mRows != null && mNumRows == mRows.length && 643 mGrid != null && mGrid.getSize() > 0 && focusPosition >= 0 && 644 focusPosition >= mGrid.getFirstIndex() && 645 focusPosition <= mGrid.getLastIndex()) { 646 // strip mGrid to a subset (like a column) that contains focusPosition 647 mGrid.stripDownTo(focusPosition); 648 // make sure that remaining items do not exceed new adapter size 649 int firstIndex = mGrid.getFirstIndex(); 650 int lastIndex = mGrid.getLastIndex(); 651 if (DEBUG) { 652 Log .v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " + lastIndex); 653 } 654 for (int i = lastIndex; i >=firstIndex; i--) { 655 if (i >= newItemCount) { 656 mGrid.removeLast(); 657 } 658 } 659 if (mGrid.getSize() == 0) { 660 focusPosition = newItemCount - 1; 661 // initialize row start locations 662 for (int i = 0; i < mNumRows; i++) { 663 mRows[i].low = 0; 664 mRows[i].high = 0; 665 } 666 if (DEBUG) Log.v(getTag(), "mGrid zero size"); 667 } else { 668 // initialize row start locations 669 for (int i = 0; i < mNumRows; i++) { 670 mRows[i].low = Integer.MAX_VALUE; 671 mRows[i].high = Integer.MIN_VALUE; 672 } 673 firstIndex = mGrid.getFirstIndex(); 674 lastIndex = mGrid.getLastIndex(); 675 if (focusPosition > lastIndex) { 676 focusPosition = mGrid.getLastIndex(); 677 } 678 if (DEBUG) { 679 Log.v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " 680 + lastIndex + " focusPosition " + focusPosition); 681 } 682 // fill rows with minimal view positions of the subset 683 for (int i = firstIndex; i <= lastIndex; i++) { 684 View v = findViewByPosition(i); 685 if (v == null) { 686 continue; 687 } 688 int row = mGrid.getLocation(i).row; 689 int low = getViewMin(v) + mScrollOffsetPrimary; 690 if (low < mRows[row].low) { 691 mRows[row].low = mRows[row].high = low; 692 } 693 } 694 int firstItemRowPosition = mRows[mGrid.getLocation(firstIndex).row].low; 695 if (firstItemRowPosition == Integer.MAX_VALUE) { 696 firstItemRowPosition = 0; 697 } 698 if (mState.didStructureChange()) { 699 // if there is structure change, the removed item might be in the 700 // subset, so it is meaningless to maintain the low locations. 701 for (int i = 0; i < mNumRows; i++) { 702 mRows[i].low = firstItemRowPosition; 703 mRows[i].high = firstItemRowPosition; 704 } 705 } else { 706 // fill other rows that does not include the subset using first item 707 for (int i = 0; i < mNumRows; i++) { 708 if (mRows[i].low == Integer.MAX_VALUE) { 709 mRows[i].low = mRows[i].high = firstItemRowPosition; 710 } 711 } 712 } 713 } 714 715 // Same adapter, we can reuse any attached views 716 detachAndScrapAttachedViews(mRecycler); 717 718 } else { 719 // otherwise recreate data structure 720 mRows = new StaggeredGrid.Row[mNumRows]; 721 722 for (int i = 0; i < mNumRows; i++) { 723 mRows[i] = new StaggeredGrid.Row(); 724 } 725 mGrid = new StaggeredGridDefault(); 726 if (newItemCount == 0) { 727 focusPosition = NO_POSITION; 728 } else if (focusPosition >= newItemCount) { 729 focusPosition = newItemCount - 1; 730 } 731 732 // Adapter may have changed so remove all attached views permanently 733 removeAndRecycleAllViews(mRecycler); 734 735 mScrollOffsetPrimary = 0; 736 mScrollOffsetSecondary = 0; 737 mWindowAlignment.reset(); 738 } 739 740 mGrid.setProvider(mGridProvider); 741 // mGrid share the same Row array information 742 mGrid.setRows(mRows); 743 mFirstVisiblePos = mLastVisiblePos = NO_POSITION; 744 745 initScrollController(); 746 updateScrollSecondAxis(); 747 748 return focusPosition; 749 } 750 751 private int getRowSizeSecondary(int rowIndex) { 752 if (mFixedRowSizeSecondary != 0) { 753 return mFixedRowSizeSecondary; 754 } 755 if (mRowSizeSecondary == null) { 756 return 0; 757 } 758 return mRowSizeSecondary[rowIndex]; 759 } 760 761 private int getRowStartSecondary(int rowIndex) { 762 int start = 0; 763 for (int i = 0; i < rowIndex; i++) { 764 start += getRowSizeSecondary(i) + mMarginSecondary; 765 } 766 return start; 767 } 768 769 private int getSizeSecondary() { 770 return getRowStartSecondary(mNumRows - 1) + getRowSizeSecondary(mNumRows - 1); 771 } 772 773 private void measureScrapChild(int position, int widthSpec, int heightSpec, 774 int[] measuredDimension) { 775 View view = mRecycler.getViewForPosition(position); 776 if (view != null) { 777 LayoutParams p = (LayoutParams) view.getLayoutParams(); 778 int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, 779 getPaddingLeft() + getPaddingRight(), p.width); 780 int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, 781 getPaddingTop() + getPaddingBottom(), p.height); 782 view.measure(childWidthSpec, childHeightSpec); 783 measuredDimension[0] = view.getMeasuredWidth(); 784 measuredDimension[1] = view.getMeasuredHeight(); 785 mRecycler.recycleView(view); 786 } 787 } 788 789 private boolean processRowSizeSecondary(boolean measure) { 790 if (mFixedRowSizeSecondary != 0) { 791 return false; 792 } 793 794 if (mGrid == null) { 795 if (mState.getItemCount() > 0) { 796 measureScrapChild(mFocusPosition == NO_POSITION ? 0 : mFocusPosition, 797 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 798 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 799 mMeasuredDimension); 800 if (DEBUG) Log.v(TAG, "measured scrap child: " + mMeasuredDimension[0] + 801 " " + mMeasuredDimension[1]); 802 } else { 803 mMeasuredDimension[0] = mMeasuredDimension[1] = 0; 804 } 805 } 806 807 List<Integer>[] rows = mGrid == null ? null : 808 mGrid.getItemPositionsInRows(mFirstVisiblePos, mLastVisiblePos); 809 boolean changed = false; 810 811 for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) { 812 int rowSize = 0; 813 814 final int rowItemCount = rows == null ? 1 : rows[rowIndex].size(); 815 if (DEBUG) Log.v(getTag(), "processRowSizeSecondary row " + rowIndex + 816 " rowItemCount " + rowItemCount); 817 818 for (int i = 0; i < rowItemCount; i++) { 819 if (rows != null) { 820 final int position = rows[rowIndex].get(i); 821 final View view = findViewByPosition(position); 822 if (measure && view.isLayoutRequested()) { 823 measureChild(view); 824 } 825 mMeasuredDimension[0] = view.getMeasuredWidth(); 826 mMeasuredDimension[1] = view.getMeasuredHeight(); 827 } 828 final int secondarySize = mOrientation == HORIZONTAL ? 829 mMeasuredDimension[1] : mMeasuredDimension[0]; 830 if (secondarySize > rowSize) { 831 rowSize = secondarySize; 832 } 833 } 834 if (DEBUG) Log.v(getTag(), "row " + rowIndex + " rowItemCount " + rowItemCount + 835 " rowSize " + rowSize); 836 837 if (mRowSizeSecondary[rowIndex] != rowSize) { 838 if (DEBUG) Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex] + 839 ", " + rowSize); 840 841 mRowSizeSecondary[rowIndex] = rowSize; 842 changed = true; 843 } 844 } 845 846 return changed; 847 } 848 849 /** 850 * Checks if we need to update row secondary sizes. 851 */ 852 private void updateRowSecondarySizeRefresh() { 853 mRowSecondarySizeRefresh = processRowSizeSecondary(false); 854 if (mRowSecondarySizeRefresh) { 855 if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set"); 856 forceRequestLayout(); 857 } 858 } 859 860 private void forceRequestLayout() { 861 if (DEBUG) Log.v(getTag(), "forceRequestLayout"); 862 // RecyclerView prevents us from requesting layout in many cases 863 // (during layout, during scroll, etc.) 864 // For secondary row size wrap_content support we currently need a 865 // second layout pass to update the measured size after having measured 866 // and added child views in layoutChildren. 867 // Force the second layout by posting a delayed runnable. 868 // TODO: investigate allowing a second layout pass, 869 // or move child add/measure logic to the measure phase. 870 mBaseGridView.getHandler().post(new Runnable() { 871 @Override 872 public void run() { 873 if (DEBUG) Log.v(getTag(), "request Layout from runnable"); 874 requestLayout(); 875 } 876 }); 877 } 878 879 @Override 880 public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) { 881 saveContext(recycler, state); 882 883 int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary; 884 int measuredSizeSecondary; 885 if (mOrientation == HORIZONTAL) { 886 sizePrimary = MeasureSpec.getSize(widthSpec); 887 sizeSecondary = MeasureSpec.getSize(heightSpec); 888 modeSecondary = MeasureSpec.getMode(heightSpec); 889 paddingSecondary = getPaddingTop() + getPaddingBottom(); 890 } else { 891 sizeSecondary = MeasureSpec.getSize(widthSpec); 892 sizePrimary = MeasureSpec.getSize(heightSpec); 893 modeSecondary = MeasureSpec.getMode(widthSpec); 894 paddingSecondary = getPaddingLeft() + getPaddingRight(); 895 } 896 if (DEBUG) Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) + 897 " heightSpec " + Integer.toHexString(heightSpec) + 898 " modeSecondary " + Integer.toHexString(modeSecondary) + 899 " sizeSecondary " + sizeSecondary + " " + this); 900 901 mMaxSizeSecondary = sizeSecondary; 902 903 if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) { 904 mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested; 905 mFixedRowSizeSecondary = 0; 906 907 if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) { 908 mRowSizeSecondary = new int[mNumRows]; 909 } 910 911 // Measure all current children and update cached row heights 912 processRowSizeSecondary(true); 913 914 switch (modeSecondary) { 915 case MeasureSpec.UNSPECIFIED: 916 measuredSizeSecondary = getSizeSecondary() + paddingSecondary; 917 break; 918 case MeasureSpec.AT_MOST: 919 measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary, 920 mMaxSizeSecondary); 921 break; 922 case MeasureSpec.EXACTLY: 923 measuredSizeSecondary = mMaxSizeSecondary; 924 break; 925 default: 926 throw new IllegalStateException("wrong spec"); 927 } 928 929 } else { 930 switch (modeSecondary) { 931 case MeasureSpec.UNSPECIFIED: 932 if (mRowSizeSecondaryRequested == 0) { 933 if (mOrientation == HORIZONTAL) { 934 throw new IllegalStateException("Must specify rowHeight or view height"); 935 } else { 936 throw new IllegalStateException("Must specify columnWidth or view width"); 937 } 938 } 939 mFixedRowSizeSecondary = mRowSizeSecondaryRequested; 940 mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested; 941 measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mMarginSecondary 942 * (mNumRows - 1) + paddingSecondary; 943 break; 944 case MeasureSpec.AT_MOST: 945 case MeasureSpec.EXACTLY: 946 if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) { 947 mNumRows = 1; 948 mFixedRowSizeSecondary = sizeSecondary - paddingSecondary; 949 } else if (mNumRowsRequested == 0) { 950 mFixedRowSizeSecondary = mRowSizeSecondaryRequested; 951 mNumRows = (sizeSecondary + mMarginSecondary) 952 / (mRowSizeSecondaryRequested + mMarginSecondary); 953 } else if (mRowSizeSecondaryRequested == 0) { 954 mNumRows = mNumRowsRequested; 955 mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary 956 * (mNumRows - 1)) / mNumRows; 957 } else { 958 mNumRows = mNumRowsRequested; 959 mFixedRowSizeSecondary = mRowSizeSecondaryRequested; 960 } 961 measuredSizeSecondary = sizeSecondary; 962 if (modeSecondary == MeasureSpec.AT_MOST) { 963 int childrenSize = mFixedRowSizeSecondary * mNumRows + mMarginSecondary 964 * (mNumRows - 1) + paddingSecondary; 965 if (childrenSize < measuredSizeSecondary) { 966 measuredSizeSecondary = childrenSize; 967 } 968 } 969 break; 970 default: 971 throw new IllegalStateException("wrong spec"); 972 } 973 } 974 if (mOrientation == HORIZONTAL) { 975 setMeasuredDimension(sizePrimary, measuredSizeSecondary); 976 } else { 977 setMeasuredDimension(measuredSizeSecondary, sizePrimary); 978 } 979 if (DEBUG) { 980 Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary + 981 " measuredSizeSecondary " + measuredSizeSecondary + 982 " mFixedRowSizeSecondary " + mFixedRowSizeSecondary + 983 " mNumRows " + mNumRows); 984 } 985 986 leaveContext(); 987 } 988 989 private void measureChild(View child) { 990 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 991 final int secondarySpec = (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) ? 992 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) : 993 MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY); 994 int widthSpec, heightSpec; 995 996 if (mOrientation == HORIZONTAL) { 997 widthSpec = ViewGroup.getChildMeasureSpec( 998 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 999 0, lp.width); 1000 heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.height); 1001 } else { 1002 heightSpec = ViewGroup.getChildMeasureSpec( 1003 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 1004 0, lp.height); 1005 widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.width); 1006 } 1007 1008 child.measure(widthSpec, heightSpec); 1009 1010 if (DEBUG) Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) + 1011 " widthSpec " + Integer.toHexString(widthSpec) + 1012 " heightSpec " + Integer.toHexString(heightSpec) + 1013 " measuredWidth " + child.getMeasuredWidth() + 1014 " measuredHeight " + child.getMeasuredHeight()); 1015 if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height); 1016 } 1017 1018 private StaggeredGrid.Provider mGridProvider = new StaggeredGrid.Provider() { 1019 1020 @Override 1021 public int getCount() { 1022 return mState.getItemCount(); 1023 } 1024 1025 @Override 1026 public void createItem(int index, int rowIndex, boolean append) { 1027 View v = getViewForPosition(index); 1028 if (mFirstVisiblePos >= 0) { 1029 // when StaggeredGrid append or prepend item, we must guarantee 1030 // that sibling item has created views already. 1031 if (append && index != mLastVisiblePos + 1) { 1032 throw new RuntimeException(); 1033 } else if (!append && index != mFirstVisiblePos - 1) { 1034 throw new RuntimeException(); 1035 } 1036 } 1037 1038 // See recyclerView docs: we don't need re-add scraped view if it was removed. 1039 if (!((RecyclerView.LayoutParams) v.getLayoutParams()).isItemRemoved()) { 1040 if (append) { 1041 addView(v); 1042 } else { 1043 addView(v, 0); 1044 } 1045 measureChild(v); 1046 } 1047 1048 int length = mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight(); 1049 int start, end; 1050 if (append) { 1051 start = mRows[rowIndex].high; 1052 if (start != mRows[rowIndex].low) { 1053 // if there are existing item in the row, add margin between 1054 start += mMarginPrimary; 1055 } else { 1056 final int lastRow = mRows.length - 1; 1057 if (lastRow != rowIndex && mRows[lastRow].high != mRows[lastRow].low) { 1058 // if there are existing item in the last row, insert 1059 // the new item after the last item of last row. 1060 start = mRows[lastRow].high + mMarginPrimary; 1061 } 1062 } 1063 end = start + length; 1064 mRows[rowIndex].high = end; 1065 } else { 1066 end = mRows[rowIndex].low; 1067 if (end != mRows[rowIndex].high) { 1068 end -= mMarginPrimary; 1069 } else if (0 != rowIndex && mRows[0].high != mRows[0].low) { 1070 // if there are existing item in the first row, insert 1071 // the new item before the first item of first row. 1072 end = mRows[0].low - mMarginPrimary; 1073 } 1074 start = end - length; 1075 mRows[rowIndex].low = start; 1076 } 1077 if (mFirstVisiblePos < 0) { 1078 mFirstVisiblePos = mLastVisiblePos = index; 1079 } else { 1080 if (append) { 1081 mLastVisiblePos++; 1082 } else { 1083 mFirstVisiblePos--; 1084 } 1085 } 1086 if (DEBUG) Log.v(getTag(), "start " + start + " end " + end); 1087 int startSecondary = getRowStartSecondary(rowIndex) - mScrollOffsetSecondary; 1088 layoutChild(rowIndex, v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary, 1089 startSecondary); 1090 if (DEBUG) { 1091 Log.d(getTag(), "addView " + index + " " + v); 1092 } 1093 if (index == mFirstVisiblePos) { 1094 updateScrollMin(); 1095 } 1096 if (index == mLastVisiblePos) { 1097 updateScrollMax(); 1098 } 1099 } 1100 }; 1101 1102 private void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) { 1103 int sizeSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight() 1104 : v.getMeasuredWidth(); 1105 if (mFixedRowSizeSecondary > 0) { 1106 sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary); 1107 } 1108 final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 1109 final int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; 1110 if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP 1111 || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) { 1112 // do nothing 1113 } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM 1114 || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) { 1115 startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary; 1116 } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL 1117 || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) { 1118 startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2; 1119 } 1120 int left, top, right, bottom; 1121 if (mOrientation == HORIZONTAL) { 1122 left = start; 1123 top = startSecondary; 1124 right = end; 1125 bottom = startSecondary + sizeSecondary; 1126 } else { 1127 top = start; 1128 left = startSecondary; 1129 bottom = end; 1130 right = startSecondary + sizeSecondary; 1131 } 1132 v.layout(left, top, right, bottom); 1133 updateChildOpticalInsets(v, left, top, right, bottom); 1134 updateChildAlignments(v); 1135 } 1136 1137 private void updateChildOpticalInsets(View v, int left, int top, int right, int bottom) { 1138 LayoutParams p = (LayoutParams) v.getLayoutParams(); 1139 p.setOpticalInsets(left - v.getLeft(), top - v.getTop(), 1140 v.getRight() - right, v.getBottom() - bottom); 1141 } 1142 1143 private void updateChildAlignments(View v) { 1144 LayoutParams p = (LayoutParams) v.getLayoutParams(); 1145 p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v)); 1146 p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v)); 1147 } 1148 1149 private void updateChildAlignments() { 1150 for (int i = 0, c = getChildCount(); i < c; i++) { 1151 updateChildAlignments(getChildAt(i)); 1152 } 1153 } 1154 1155 private boolean needsAppendVisibleItem() { 1156 if (mLastVisiblePos < mFocusPosition) { 1157 return true; 1158 } 1159 int right = mScrollOffsetPrimary + mSizePrimary; 1160 for (int i = 0; i < mNumRows; i++) { 1161 if (mRows[i].low == mRows[i].high) { 1162 if (mRows[i].high < right) { 1163 return true; 1164 } 1165 } else if (mRows[i].high < right - mMarginPrimary) { 1166 return true; 1167 } 1168 } 1169 return false; 1170 } 1171 1172 private boolean needsPrependVisibleItem() { 1173 if (mFirstVisiblePos > mFocusPosition) { 1174 return true; 1175 } 1176 for (int i = 0; i < mNumRows; i++) { 1177 if (mRows[i].low == mRows[i].high) { 1178 if (mRows[i].low > mScrollOffsetPrimary) { 1179 return true; 1180 } 1181 } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) { 1182 return true; 1183 } 1184 } 1185 return false; 1186 } 1187 1188 // Append one column if possible and return true if reach end. 1189 private boolean appendOneVisibleItem() { 1190 while (true) { 1191 if (mLastVisiblePos != NO_POSITION && mLastVisiblePos < mState.getItemCount() -1 && 1192 mLastVisiblePos < mGrid.getLastIndex()) { 1193 // append invisible view of saved location till last row 1194 final int index = mLastVisiblePos + 1; 1195 final int row = mGrid.getLocation(index).row; 1196 mGridProvider.createItem(index, row, true); 1197 if (row == mNumRows - 1) { 1198 return false; 1199 } 1200 } else if ((mLastVisiblePos == NO_POSITION && mState.getItemCount() > 0) || 1201 (mLastVisiblePos != NO_POSITION && 1202 mLastVisiblePos < mState.getItemCount() - 1)) { 1203 mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary); 1204 return false; 1205 } else { 1206 return true; 1207 } 1208 } 1209 } 1210 1211 private void appendVisibleItems() { 1212 while (needsAppendVisibleItem()) { 1213 if (appendOneVisibleItem()) { 1214 break; 1215 } 1216 } 1217 } 1218 1219 // Prepend one column if possible and return true if reach end. 1220 private boolean prependOneVisibleItem() { 1221 while (true) { 1222 if (mFirstVisiblePos > 0) { 1223 if (mFirstVisiblePos > mGrid.getFirstIndex()) { 1224 // prepend invisible view of saved location till first row 1225 final int index = mFirstVisiblePos - 1; 1226 final int row = mGrid.getLocation(index).row; 1227 mGridProvider.createItem(index, row, false); 1228 if (row == 0) { 1229 return false; 1230 } 1231 } else { 1232 mGrid.prependItems(mScrollOffsetPrimary); 1233 return false; 1234 } 1235 } else { 1236 return true; 1237 } 1238 } 1239 } 1240 1241 private void prependVisibleItems() { 1242 while (needsPrependVisibleItem()) { 1243 if (prependOneVisibleItem()) { 1244 break; 1245 } 1246 } 1247 } 1248 1249 private void removeChildAt(int position) { 1250 View v = findViewByPosition(position); 1251 if (v != null) { 1252 if (DEBUG) { 1253 Log.d(getTag(), "removeAndRecycleViewAt " + position); 1254 } 1255 removeAndRecycleView(v, mRecycler); 1256 } 1257 } 1258 1259 private void removeInvisibleViewsAtEnd() { 1260 if (!mPruneChild) { 1261 return; 1262 } 1263 boolean update = false; 1264 while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) { 1265 View view = findViewByPosition(mLastVisiblePos); 1266 if (getViewMin(view) > mSizePrimary) { 1267 removeChildAt(mLastVisiblePos); 1268 mLastVisiblePos--; 1269 update = true; 1270 } else { 1271 break; 1272 } 1273 } 1274 if (update) { 1275 updateRowsMinMax(); 1276 } 1277 } 1278 1279 private void removeInvisibleViewsAtFront() { 1280 if (!mPruneChild) { 1281 return; 1282 } 1283 boolean update = false; 1284 while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) { 1285 View view = findViewByPosition(mFirstVisiblePos); 1286 if (getViewMax(view) < 0) { 1287 removeChildAt(mFirstVisiblePos); 1288 mFirstVisiblePos++; 1289 update = true; 1290 } else { 1291 break; 1292 } 1293 } 1294 if (update) { 1295 updateRowsMinMax(); 1296 } 1297 } 1298 1299 private void updateRowsMinMax() { 1300 if (mFirstVisiblePos < 0) { 1301 return; 1302 } 1303 for (int i = 0; i < mNumRows; i++) { 1304 mRows[i].low = Integer.MAX_VALUE; 1305 mRows[i].high = Integer.MIN_VALUE; 1306 } 1307 for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) { 1308 View view = findViewByPosition(i); 1309 int row = mGrid.getLocation(i).row; 1310 int low = getViewMin(view) + mScrollOffsetPrimary; 1311 if (low < mRows[row].low) { 1312 mRows[row].low = low; 1313 } 1314 int high = getViewMax(view) + mScrollOffsetPrimary; 1315 if (high > mRows[row].high) { 1316 mRows[row].high = high; 1317 } 1318 } 1319 } 1320 1321 // Fast layout when there is no structure change, adapter change, etc. 1322 protected void fastRelayout() { 1323 initScrollController(); 1324 1325 List<Integer>[] rows = mGrid.getItemPositionsInRows(mFirstVisiblePos, mLastVisiblePos); 1326 1327 // relayout and repositioning views on each row 1328 for (int i = 0; i < mNumRows; i++) { 1329 List<Integer> row = rows[i]; 1330 final int startSecondary = getRowStartSecondary(i) - mScrollOffsetSecondary; 1331 for (int j = 0, size = row.size(); j < size; j++) { 1332 final int position = row.get(j); 1333 final View view = findViewByPosition(position); 1334 int primaryDelta, start, end; 1335 1336 if (mOrientation == HORIZONTAL) { 1337 final int primarySize = view.getMeasuredWidth(); 1338 if (view.isLayoutRequested()) { 1339 measureChild(view); 1340 } 1341 start = getViewMin(view); 1342 end = start + view.getMeasuredWidth(); 1343 primaryDelta = view.getMeasuredWidth() - primarySize; 1344 if (primaryDelta != 0) { 1345 for (int k = j + 1; k < size; k++) { 1346 findViewByPosition(row.get(k)).offsetLeftAndRight(primaryDelta); 1347 } 1348 } 1349 } else { 1350 final int primarySize = view.getMeasuredHeight(); 1351 if (view.isLayoutRequested()) { 1352 measureChild(view); 1353 } 1354 start = getViewMin(view); 1355 end = start + view.getMeasuredHeight(); 1356 primaryDelta = view.getMeasuredHeight() - primarySize; 1357 if (primaryDelta != 0) { 1358 for (int k = j + 1; k < size; k++) { 1359 findViewByPosition(row.get(k)).offsetTopAndBottom(primaryDelta); 1360 } 1361 } 1362 } 1363 layoutChild(i, view, start, end, startSecondary); 1364 } 1365 } 1366 1367 updateRowsMinMax(); 1368 appendVisibleItems(); 1369 prependVisibleItems(); 1370 1371 updateRowsMinMax(); 1372 updateScrollMin(); 1373 updateScrollMax(); 1374 updateScrollSecondAxis(); 1375 1376 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) { 1377 View focusView = findViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition); 1378 scrollToView(focusView, false); 1379 } 1380 } 1381 1382 private void removeAndRecycleAllViews(RecyclerView.Recycler recycler) { 1383 if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount()); 1384 for (int i = getChildCount() - 1; i >= 0; i--) { 1385 removeAndRecycleViewAt(i, recycler); 1386 } 1387 } 1388 1389 // Lays out items based on the current scroll position 1390 @Override 1391 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1392 if (DEBUG) { 1393 Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary " 1394 + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary 1395 + " inPreLayout " + state.isPreLayout() 1396 + " didStructureChange " + state.didStructureChange() 1397 + " mForceFullLayout " + mForceFullLayout); 1398 Log.v(getTag(), "width " + getWidth() + " height " + getHeight()); 1399 } 1400 1401 if (mNumRows == 0) { 1402 // haven't done measure yet 1403 return; 1404 } 1405 final int itemCount = state.getItemCount(); 1406 if (itemCount < 0) { 1407 return; 1408 } 1409 1410 if (!mLayoutEnabled) { 1411 discardLayoutInfo(); 1412 removeAndRecycleAllViews(recycler); 1413 return; 1414 } 1415 mInLayout = true; 1416 1417 saveContext(recycler, state); 1418 // Track the old focus view so we can adjust our system scroll position 1419 // so that any scroll animations happening now will remain valid. 1420 // We must use same delta in Pre Layout (if prelayout exists) and second layout. 1421 // So we cache the deltas in PreLayout and use it in second layout. 1422 int delta = 0, deltaSecondary = 0; 1423 if (!state.isPreLayout() && mUseDeltaInPreLayout) { 1424 delta = mDeltaInPreLayout; 1425 deltaSecondary = mDeltaSecondaryInPreLayout; 1426 } else { 1427 if (mFocusPosition != NO_POSITION 1428 && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) { 1429 View focusView = findViewByPosition(mFocusPosition); 1430 if (focusView != null) { 1431 delta = mWindowAlignment.mainAxis().getSystemScrollPos( 1432 getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary; 1433 deltaSecondary = 1434 mWindowAlignment.secondAxis().getSystemScrollPos( 1435 getViewCenterSecondary(focusView) + mScrollOffsetSecondary) 1436 - mScrollOffsetSecondary; 1437 if (mUseDeltaInPreLayout = state.isPreLayout()) { 1438 mDeltaInPreLayout = delta; 1439 mDeltaSecondaryInPreLayout = deltaSecondary; 1440 } 1441 } 1442 } 1443 } 1444 1445 final boolean hasDoneFirstLayout = hasDoneFirstLayout(); 1446 int savedFocusPos = mFocusPosition; 1447 boolean fastRelayout = false; 1448 if (!mState.didStructureChange() && !mForceFullLayout && hasDoneFirstLayout) { 1449 fastRelayout = true; 1450 fastRelayout(); 1451 } else { 1452 boolean hadFocus = mBaseGridView.hasFocus(); 1453 1454 int newFocusPosition = init(mFocusPosition); 1455 if (DEBUG) { 1456 Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition " 1457 + newFocusPosition); 1458 } 1459 1460 // depending on result of init(), either recreating everything 1461 // or try to reuse the row start positions near mFocusPosition 1462 if (mGrid.getSize() == 0) { 1463 // this is a fresh creating all items, starting from 1464 // mFocusPosition with a estimated row index. 1465 mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT); 1466 1467 // Can't track the old focus view 1468 delta = deltaSecondary = 0; 1469 1470 } else { 1471 // mGrid remembers Locations for the column that 1472 // contains mFocusePosition and also mRows remembers start 1473 // positions of each row. 1474 // Manually re-create child views for that column 1475 int firstIndex = mGrid.getFirstIndex(); 1476 int lastIndex = mGrid.getLastIndex(); 1477 for (int i = firstIndex; i <= lastIndex; i++) { 1478 mGridProvider.createItem(i, mGrid.getLocation(i).row, true); 1479 } 1480 } 1481 // add visible views at end until reach the end of window 1482 appendVisibleItems(); 1483 // add visible views at front until reach the start of window 1484 prependVisibleItems(); 1485 // multiple rounds: scrollToView of first round may drag first/last child into 1486 // "visible window" and we update scrollMin/scrollMax then run second scrollToView 1487 int oldFirstVisible; 1488 int oldLastVisible; 1489 do { 1490 oldFirstVisible = mFirstVisiblePos; 1491 oldLastVisible = mLastVisiblePos; 1492 View focusView = findViewByPosition(newFocusPosition); 1493 // we need force to initialize the child view's position 1494 scrollToView(focusView, false); 1495 if (focusView != null && hadFocus) { 1496 focusView.requestFocus(); 1497 } 1498 appendVisibleItems(); 1499 prependVisibleItems(); 1500 removeInvisibleViewsAtFront(); 1501 removeInvisibleViewsAtEnd(); 1502 } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible); 1503 } 1504 mForceFullLayout = false; 1505 1506 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) { 1507 scrollDirectionPrimary(-delta); 1508 scrollDirectionSecondary(-deltaSecondary); 1509 } 1510 appendVisibleItems(); 1511 prependVisibleItems(); 1512 removeInvisibleViewsAtFront(); 1513 removeInvisibleViewsAtEnd(); 1514 1515 if (DEBUG) { 1516 StringWriter sw = new StringWriter(); 1517 PrintWriter pw = new PrintWriter(sw); 1518 mGrid.debugPrint(pw); 1519 Log.d(getTag(), sw.toString()); 1520 } 1521 1522 if (mRowSecondarySizeRefresh) { 1523 mRowSecondarySizeRefresh = false; 1524 } else { 1525 updateRowSecondarySizeRefresh(); 1526 } 1527 1528 if (!state.isPreLayout()) { 1529 mUseDeltaInPreLayout = false; 1530 if (!fastRelayout || mFocusPosition != savedFocusPos) { 1531 dispatchChildSelected(); 1532 } 1533 } 1534 mInLayout = false; 1535 leaveContext(); 1536 if (DEBUG) Log.v(getTag(), "layoutChildren end"); 1537 } 1538 1539 private void offsetChildrenSecondary(int increment) { 1540 final int childCount = getChildCount(); 1541 if (mOrientation == HORIZONTAL) { 1542 for (int i = 0; i < childCount; i++) { 1543 getChildAt(i).offsetTopAndBottom(increment); 1544 } 1545 } else { 1546 for (int i = 0; i < childCount; i++) { 1547 getChildAt(i).offsetLeftAndRight(increment); 1548 } 1549 } 1550 } 1551 1552 private void offsetChildrenPrimary(int increment) { 1553 final int childCount = getChildCount(); 1554 if (mOrientation == VERTICAL) { 1555 for (int i = 0; i < childCount; i++) { 1556 getChildAt(i).offsetTopAndBottom(increment); 1557 } 1558 } else { 1559 for (int i = 0; i < childCount; i++) { 1560 getChildAt(i).offsetLeftAndRight(increment); 1561 } 1562 } 1563 } 1564 1565 @Override 1566 public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) { 1567 if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx); 1568 if (!mLayoutEnabled || !hasDoneFirstLayout()) { 1569 return 0; 1570 } 1571 saveContext(recycler, state); 1572 int result; 1573 if (mOrientation == HORIZONTAL) { 1574 result = scrollDirectionPrimary(dx); 1575 } else { 1576 result = scrollDirectionSecondary(dx); 1577 } 1578 leaveContext(); 1579 return result; 1580 } 1581 1582 @Override 1583 public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) { 1584 if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy); 1585 if (!mLayoutEnabled || !hasDoneFirstLayout()) { 1586 return 0; 1587 } 1588 saveContext(recycler, state); 1589 int result; 1590 if (mOrientation == VERTICAL) { 1591 result = scrollDirectionPrimary(dy); 1592 } else { 1593 result = scrollDirectionSecondary(dy); 1594 } 1595 leaveContext(); 1596 return result; 1597 } 1598 1599 // scroll in main direction may add/prune views 1600 private int scrollDirectionPrimary(int da) { 1601 if (da > 0) { 1602 if (!mWindowAlignment.mainAxis().isMaxUnknown()) { 1603 int maxScroll = mWindowAlignment.mainAxis().getMaxScroll(); 1604 if (mScrollOffsetPrimary + da > maxScroll) { 1605 da = maxScroll - mScrollOffsetPrimary; 1606 } 1607 } 1608 } else if (da < 0) { 1609 if (!mWindowAlignment.mainAxis().isMinUnknown()) { 1610 int minScroll = mWindowAlignment.mainAxis().getMinScroll(); 1611 if (mScrollOffsetPrimary + da < minScroll) { 1612 da = minScroll - mScrollOffsetPrimary; 1613 } 1614 } 1615 } 1616 if (da == 0) { 1617 return 0; 1618 } 1619 offsetChildrenPrimary(-da); 1620 mScrollOffsetPrimary += da; 1621 if (mInLayout) { 1622 return da; 1623 } 1624 1625 int childCount = getChildCount(); 1626 boolean updated; 1627 1628 if (da > 0) { 1629 appendVisibleItems(); 1630 } else if (da < 0) { 1631 prependVisibleItems(); 1632 } 1633 updated = getChildCount() > childCount; 1634 childCount = getChildCount(); 1635 1636 if (da > 0) { 1637 removeInvisibleViewsAtFront(); 1638 } else if (da < 0) { 1639 removeInvisibleViewsAtEnd(); 1640 } 1641 updated |= getChildCount() < childCount; 1642 1643 if (updated) { 1644 updateRowSecondarySizeRefresh(); 1645 } 1646 1647 mBaseGridView.invalidate(); 1648 return da; 1649 } 1650 1651 // scroll in second direction will not add/prune views 1652 private int scrollDirectionSecondary(int dy) { 1653 if (dy == 0) { 1654 return 0; 1655 } 1656 offsetChildrenSecondary(-dy); 1657 mScrollOffsetSecondary += dy; 1658 mBaseGridView.invalidate(); 1659 return dy; 1660 } 1661 1662 private void updateScrollMax() { 1663 if (mLastVisiblePos < 0) { 1664 return; 1665 } 1666 final boolean lastAvailable = mLastVisiblePos == mState.getItemCount() - 1; 1667 final boolean maxUnknown = mWindowAlignment.mainAxis().isMaxUnknown(); 1668 if (!lastAvailable && maxUnknown) { 1669 return; 1670 } 1671 int maxEdge = Integer.MIN_VALUE; 1672 int rowIndex = -1; 1673 for (int i = 0; i < mRows.length; i++) { 1674 if (mRows[i].high > maxEdge) { 1675 maxEdge = mRows[i].high; 1676 rowIndex = i; 1677 } 1678 } 1679 int maxScroll = Integer.MAX_VALUE; 1680 for (int i = mLastVisiblePos; i >= mFirstVisiblePos; i--) { 1681 StaggeredGrid.Location location = mGrid.getLocation(i); 1682 if (location != null && location.row == rowIndex) { 1683 int savedMaxEdge = mWindowAlignment.mainAxis().getMaxEdge(); 1684 mWindowAlignment.mainAxis().setMaxEdge(maxEdge); 1685 maxScroll = mWindowAlignment 1686 .mainAxis().getSystemScrollPos(mScrollOffsetPrimary 1687 + getViewCenter(findViewByPosition(i))); 1688 mWindowAlignment.mainAxis().setMaxEdge(savedMaxEdge); 1689 break; 1690 } 1691 } 1692 if (lastAvailable) { 1693 mWindowAlignment.mainAxis().setMaxEdge(maxEdge); 1694 mWindowAlignment.mainAxis().setMaxScroll(maxScroll); 1695 if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge + 1696 " scrollMax to " + maxScroll); 1697 } else { 1698 // the maxScroll for currently last visible item is larger, 1699 // so we must invalidate the max scroll value. 1700 if (maxScroll > mWindowAlignment.mainAxis().getMaxScroll()) { 1701 mWindowAlignment.mainAxis().invalidateScrollMax(); 1702 if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be " 1703 + "greater than " + maxScroll); 1704 } 1705 } 1706 } 1707 1708 private void updateScrollMin() { 1709 if (mFirstVisiblePos < 0) { 1710 return; 1711 } 1712 final boolean firstAvailable = mFirstVisiblePos == 0; 1713 final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown(); 1714 if (!firstAvailable && minUnknown) { 1715 return; 1716 } 1717 int minEdge = Integer.MAX_VALUE; 1718 int rowIndex = -1; 1719 for (int i = 0; i < mRows.length; i++) { 1720 if (mRows[i].low < minEdge) { 1721 minEdge = mRows[i].low; 1722 rowIndex = i; 1723 } 1724 } 1725 int minScroll = Integer.MIN_VALUE; 1726 for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) { 1727 StaggeredGrid.Location location = mGrid.getLocation(i); 1728 if (location != null && location.row == rowIndex) { 1729 int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge(); 1730 mWindowAlignment.mainAxis().setMinEdge(minEdge); 1731 minScroll = mWindowAlignment 1732 .mainAxis().getSystemScrollPos(mScrollOffsetPrimary 1733 + getViewCenter(findViewByPosition(i))); 1734 mWindowAlignment.mainAxis().setMinEdge(savedMinEdge); 1735 break; 1736 } 1737 } 1738 if (firstAvailable) { 1739 mWindowAlignment.mainAxis().setMinEdge(minEdge); 1740 mWindowAlignment.mainAxis().setMinScroll(minScroll); 1741 if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge + 1742 " scrollMin to " + minScroll); 1743 } else { 1744 // the minScroll for currently first visible item is smaller, 1745 // so we must invalidate the min scroll value. 1746 if (minScroll < mWindowAlignment.mainAxis().getMinScroll()) { 1747 mWindowAlignment.mainAxis().invalidateScrollMin(); 1748 if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be " 1749 + "less than " + minScroll); 1750 } 1751 } 1752 } 1753 1754 private void updateScrollSecondAxis() { 1755 mWindowAlignment.secondAxis().setMinEdge(0); 1756 mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary()); 1757 } 1758 1759 private void initScrollController() { 1760 mWindowAlignment.horizontal.setSize(getWidth()); 1761 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 1762 mWindowAlignment.vertical.setSize(getHeight()); 1763 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 1764 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 1765 1766 if (DEBUG) { 1767 Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary 1768 + " mWindowAlignment " + mWindowAlignment); 1769 } 1770 } 1771 1772 public void setSelection(RecyclerView parent, int position) { 1773 setSelection(parent, position, false); 1774 } 1775 1776 public void setSelectionSmooth(RecyclerView parent, int position) { 1777 setSelection(parent, position, true); 1778 } 1779 1780 public int getSelection() { 1781 return mFocusPosition; 1782 } 1783 1784 public void setSelection(RecyclerView parent, int position, boolean smooth) { 1785 if (mFocusPosition == position) { 1786 return; 1787 } 1788 View view = findViewByPosition(position); 1789 if (view != null) { 1790 scrollToView(view, smooth); 1791 } else { 1792 mFocusPosition = position; 1793 if (!mLayoutEnabled) { 1794 return; 1795 } 1796 if (smooth) { 1797 if (!hasDoneFirstLayout()) { 1798 Log.w(getTag(), "setSelectionSmooth should " + 1799 "not be called before first layout pass"); 1800 return; 1801 } 1802 LinearSmoothScroller linearSmoothScroller = 1803 new LinearSmoothScroller(parent.getContext()) { 1804 @Override 1805 public PointF computeScrollVectorForPosition(int targetPosition) { 1806 if (getChildCount() == 0) { 1807 return null; 1808 } 1809 final int firstChildPos = getPosition(getChildAt(0)); 1810 final int direction = targetPosition < firstChildPos ? -1 : 1; 1811 if (mOrientation == HORIZONTAL) { 1812 return new PointF(direction, 0); 1813 } else { 1814 return new PointF(0, direction); 1815 } 1816 } 1817 @Override 1818 protected void onTargetFound(View targetView, 1819 RecyclerView.State state, Action action) { 1820 if (hasFocus()) { 1821 targetView.requestFocus(); 1822 } else { 1823 dispatchChildSelected(); 1824 } 1825 if (getScrollPosition(targetView, mTempDeltas)) { 1826 int dx, dy; 1827 if (mOrientation == HORIZONTAL) { 1828 dx = mTempDeltas[0]; 1829 dy = mTempDeltas[1]; 1830 } else { 1831 dx = mTempDeltas[1]; 1832 dy = mTempDeltas[0]; 1833 } 1834 final int distance = (int) Math.sqrt(dx * dx + dy * dy); 1835 final int time = calculateTimeForDeceleration(distance); 1836 action.update(dx, dy, time, mDecelerateInterpolator); 1837 } 1838 } 1839 }; 1840 linearSmoothScroller.setTargetPosition(position); 1841 startSmoothScroll(linearSmoothScroller); 1842 } else { 1843 mForceFullLayout = true; 1844 parent.requestLayout(); 1845 } 1846 } 1847 } 1848 1849 @Override 1850 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 1851 boolean needsLayout = false; 1852 if (itemCount != 0) { 1853 if (mFirstVisiblePos < 0) { 1854 needsLayout = true; 1855 } else if (!(positionStart > mLastVisiblePos + 1 || 1856 positionStart + itemCount < mFirstVisiblePos - 1)) { 1857 needsLayout = true; 1858 } 1859 } 1860 if (needsLayout) { 1861 recyclerView.requestLayout(); 1862 } 1863 } 1864 1865 @Override 1866 public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) { 1867 if (mFocusSearchDisabled) { 1868 return true; 1869 } 1870 if (!mInLayout) { 1871 scrollToView(child, true); 1872 } 1873 return true; 1874 } 1875 1876 @Override 1877 public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect, 1878 boolean immediate) { 1879 if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect); 1880 return false; 1881 } 1882 1883 int getScrollOffsetX() { 1884 return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary; 1885 } 1886 1887 int getScrollOffsetY() { 1888 return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary; 1889 } 1890 1891 public void getViewSelectedOffsets(View view, int[] offsets) { 1892 int scrollOffsetX = getScrollOffsetX(); 1893 int scrollOffsetY = getScrollOffsetY(); 1894 int viewCenterX = scrollOffsetX + getViewCenterX(view); 1895 int viewCenterY = scrollOffsetY + getViewCenterY(view); 1896 offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX; 1897 offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY; 1898 } 1899 1900 /** 1901 * Scroll to a given child view and change mFocusPosition. 1902 */ 1903 private void scrollToView(View view, boolean smooth) { 1904 int newFocusPosition = getPositionByView(view); 1905 if (newFocusPosition != mFocusPosition) { 1906 mFocusPosition = newFocusPosition; 1907 if (!mInLayout) { 1908 dispatchChildSelected(); 1909 } 1910 } 1911 if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) { 1912 mBaseGridView.invalidate(); 1913 } 1914 if (view == null) { 1915 return; 1916 } 1917 if (!view.hasFocus() && mBaseGridView.hasFocus()) { 1918 // transfer focus to the child if it does not have focus yet (e.g. triggered 1919 // by setSelection()) 1920 view.requestFocus(); 1921 } 1922 if (getScrollPosition(view, mTempDeltas)) { 1923 scrollGrid(mTempDeltas[0], mTempDeltas[1], smooth); 1924 } 1925 } 1926 1927 private boolean getScrollPosition(View view, int[] deltas) { 1928 switch (mFocusScrollStrategy) { 1929 case BaseGridView.FOCUS_SCROLL_ALIGNED: 1930 default: 1931 return getAlignedPosition(view, deltas); 1932 case BaseGridView.FOCUS_SCROLL_ITEM: 1933 case BaseGridView.FOCUS_SCROLL_PAGE: 1934 return getNoneAlignedPosition(view, deltas); 1935 } 1936 } 1937 1938 private boolean getNoneAlignedPosition(View view, int[] deltas) { 1939 int pos = getPositionByView(view); 1940 int viewMin = getViewMin(view); 1941 int viewMax = getViewMax(view); 1942 // we either align "firstView" to left/top padding edge 1943 // or align "lastView" to right/bottom padding edge 1944 View firstView = null; 1945 View lastView = null; 1946 int paddingLow = mWindowAlignment.mainAxis().getPaddingLow(); 1947 int clientSize = mWindowAlignment.mainAxis().getClientSize(); 1948 final int row = mGrid.getLocation(pos).row; 1949 if (viewMin < paddingLow) { 1950 // view enters low padding area: 1951 firstView = view; 1952 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 1953 // scroll one "page" left/top, 1954 // align first visible item of the "page" at the low padding edge. 1955 while (!prependOneVisibleItem()) { 1956 List<Integer> positions = 1957 mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row]; 1958 firstView = findViewByPosition(positions.get(0)); 1959 if (viewMax - getViewMin(firstView) > clientSize) { 1960 if (positions.size() > 1) { 1961 firstView = findViewByPosition(positions.get(1)); 1962 } 1963 break; 1964 } 1965 } 1966 } 1967 } else if (viewMax > clientSize + paddingLow) { 1968 // view enters high padding area: 1969 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 1970 // scroll whole one page right/bottom, align view at the low padding edge. 1971 firstView = view; 1972 do { 1973 List<Integer> positions = 1974 mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row]; 1975 lastView = findViewByPosition(positions.get(positions.size() - 1)); 1976 if (getViewMax(lastView) - viewMin > clientSize) { 1977 lastView = null; 1978 break; 1979 } 1980 } while (!appendOneVisibleItem()); 1981 if (lastView != null) { 1982 // however if we reached end, we should align last view. 1983 firstView = null; 1984 } 1985 } else { 1986 lastView = view; 1987 } 1988 } 1989 int scrollPrimary = 0; 1990 int scrollSecondary = 0; 1991 if (firstView != null) { 1992 scrollPrimary = getViewMin(firstView) - paddingLow; 1993 } else if (lastView != null) { 1994 scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize); 1995 } 1996 View secondaryAlignedView; 1997 if (firstView != null) { 1998 secondaryAlignedView = firstView; 1999 } else if (lastView != null) { 2000 secondaryAlignedView = lastView; 2001 } else { 2002 secondaryAlignedView = view; 2003 } 2004 int viewCenterSecondary = mScrollOffsetSecondary + 2005 getViewCenterSecondary(secondaryAlignedView); 2006 scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary); 2007 scrollSecondary -= mScrollOffsetSecondary; 2008 if (scrollPrimary != 0 || scrollSecondary != 0) { 2009 deltas[0] = scrollPrimary; 2010 deltas[1] = scrollSecondary; 2011 return true; 2012 } 2013 return false; 2014 } 2015 2016 private boolean getAlignedPosition(View view, int[] deltas) { 2017 int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view); 2018 int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view); 2019 2020 int scrollPrimary = mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary); 2021 int scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary); 2022 if (DEBUG) { 2023 Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary 2024 +" " + mWindowAlignment); 2025 } 2026 scrollPrimary -= mScrollOffsetPrimary; 2027 scrollSecondary -= mScrollOffsetSecondary; 2028 if (scrollPrimary != 0 || scrollSecondary != 0) { 2029 deltas[0] = scrollPrimary; 2030 deltas[1] = scrollSecondary; 2031 return true; 2032 } 2033 return false; 2034 } 2035 2036 private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) { 2037 if (mInLayout) { 2038 scrollDirectionPrimary(scrollPrimary); 2039 scrollDirectionSecondary(scrollSecondary); 2040 } else { 2041 int scrollX; 2042 int scrollY; 2043 if (mOrientation == HORIZONTAL) { 2044 scrollX = scrollPrimary; 2045 scrollY = scrollSecondary; 2046 } else { 2047 scrollX = scrollSecondary; 2048 scrollY = scrollPrimary; 2049 } 2050 if (smooth) { 2051 mBaseGridView.smoothScrollBy(scrollX, scrollY); 2052 } else { 2053 mBaseGridView.scrollBy(scrollX, scrollY); 2054 } 2055 } 2056 } 2057 2058 public void setPruneChild(boolean pruneChild) { 2059 if (mPruneChild != pruneChild) { 2060 mPruneChild = pruneChild; 2061 if (mPruneChild) { 2062 requestLayout(); 2063 } 2064 } 2065 } 2066 2067 public boolean getPruneChild() { 2068 return mPruneChild; 2069 } 2070 2071 private int findImmediateChildIndex(View view) { 2072 while (view != null && view != mBaseGridView) { 2073 int index = mBaseGridView.indexOfChild(view); 2074 if (index >= 0) { 2075 return index; 2076 } 2077 view = (View) view.getParent(); 2078 } 2079 return NO_POSITION; 2080 } 2081 2082 void setFocusSearchDisabled(boolean disabled) { 2083 mFocusSearchDisabled = disabled; 2084 } 2085 2086 boolean isFocusSearchDisabled() { 2087 return mFocusSearchDisabled; 2088 } 2089 2090 @Override 2091 public View onInterceptFocusSearch(View focused, int direction) { 2092 if (mFocusSearchDisabled) { 2093 return focused; 2094 } 2095 return null; 2096 } 2097 2098 boolean hasPreviousViewInSameRow(int pos) { 2099 if (mGrid == null || pos == NO_POSITION) { 2100 return false; 2101 } 2102 if (mFirstVisiblePos > 0) { 2103 return true; 2104 } 2105 final int focusedRow = mGrid.getLocation(pos).row; 2106 for (int i = getChildCount() - 1; i >= 0; i--) { 2107 int position = getPositionByIndex(i); 2108 StaggeredGrid.Location loc = mGrid.getLocation(position); 2109 if (loc != null && loc.row == focusedRow) { 2110 if (position < pos) { 2111 return true; 2112 } 2113 } 2114 } 2115 return false; 2116 } 2117 2118 @Override 2119 public boolean onAddFocusables(RecyclerView recyclerView, 2120 ArrayList<View> views, int direction, int focusableMode) { 2121 if (mFocusSearchDisabled) { 2122 return true; 2123 } 2124 // If this viewgroup or one of its children currently has focus then we 2125 // consider our children for focus searching in main direction on the same row. 2126 // If this viewgroup has no focus and using focus align, we want the system 2127 // to ignore our children and pass focus to the viewgroup, which will pass 2128 // focus on to its children appropriately. 2129 // If this viewgroup has no focus and not using focus align, we want to 2130 // consider the child that does not overlap with padding area. 2131 if (recyclerView.hasFocus()) { 2132 final int movement = getMovement(direction); 2133 if (movement != PREV_ITEM && movement != NEXT_ITEM) { 2134 // Move on secondary direction uses default addFocusables(). 2135 return false; 2136 } 2137 final View focused = recyclerView.findFocus(); 2138 final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused)); 2139 // Add focusables of focused item. 2140 if (focusedPos != NO_POSITION) { 2141 findViewByPosition(focusedPos).addFocusables(views, direction, focusableMode); 2142 } 2143 final int focusedRow = mGrid != null && focusedPos != NO_POSITION ? 2144 mGrid.getLocation(focusedPos).row : NO_POSITION; 2145 // Add focusables of next neighbor of same row on the focus search direction. 2146 if (mGrid != null) { 2147 final int focusableCount = views.size(); 2148 for (int i = 0, count = getChildCount(); i < count; i++) { 2149 int index = movement == NEXT_ITEM ? i : count - 1 - i; 2150 final View child = getChildAt(index); 2151 if (child.getVisibility() != View.VISIBLE) { 2152 continue; 2153 } 2154 int position = getPositionByIndex(index); 2155 StaggeredGrid.Location loc = mGrid.getLocation(position); 2156 if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) { 2157 if (focusedPos == NO_POSITION || 2158 (movement == NEXT_ITEM && position > focusedPos) 2159 || (movement == PREV_ITEM && position < focusedPos)) { 2160 child.addFocusables(views, direction, focusableMode); 2161 if (views.size() > focusableCount) { 2162 break; 2163 } 2164 } 2165 } 2166 } 2167 } 2168 } else { 2169 if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) { 2170 // adding views not overlapping padding area to avoid scrolling in gaining focus 2171 int left = mWindowAlignment.mainAxis().getPaddingLow(); 2172 int right = mWindowAlignment.mainAxis().getClientSize() + left; 2173 int focusableCount = views.size(); 2174 for (int i = 0, count = getChildCount(); i < count; i++) { 2175 View child = getChildAt(i); 2176 if (child.getVisibility() == View.VISIBLE) { 2177 if (getViewMin(child) >= left && getViewMax(child) <= right) { 2178 child.addFocusables(views, direction, focusableMode); 2179 } 2180 } 2181 } 2182 // if we cannot find any, then just add all children. 2183 if (views.size() == focusableCount) { 2184 for (int i = 0, count = getChildCount(); i < count; i++) { 2185 View child = getChildAt(i); 2186 if (child.getVisibility() == View.VISIBLE) { 2187 child.addFocusables(views, direction, focusableMode); 2188 } 2189 } 2190 if (views.size() != focusableCount) { 2191 return true; 2192 } 2193 } else { 2194 return true; 2195 } 2196 // if still cannot find any, fall through and add itself 2197 } 2198 if (recyclerView.isFocusable()) { 2199 views.add(recyclerView); 2200 } 2201 } 2202 return true; 2203 } 2204 2205 @Override 2206 public View onFocusSearchFailed(View focused, int direction, Recycler recycler, 2207 RecyclerView.State state) { 2208 if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction); 2209 2210 saveContext(recycler, state); 2211 View view = null; 2212 int movement = getMovement(direction); 2213 final FocusFinder ff = FocusFinder.getInstance(); 2214 if (movement == NEXT_ITEM) { 2215 while (view == null && !appendOneVisibleItem()) { 2216 view = ff.findNextFocus(mBaseGridView, focused, direction); 2217 } 2218 } else if (movement == PREV_ITEM){ 2219 while (view == null && !prependOneVisibleItem()) { 2220 view = ff.findNextFocus(mBaseGridView, focused, direction); 2221 } 2222 } 2223 if (view == null) { 2224 // returning the same view to prevent focus lost when scrolling past the end of the list 2225 if (movement == PREV_ITEM) { 2226 view = mFocusOutFront ? null : focused; 2227 } else if (movement == NEXT_ITEM){ 2228 view = mFocusOutEnd ? null : focused; 2229 } 2230 } 2231 leaveContext(); 2232 if (DEBUG) Log.v(getTag(), "returning view " + view); 2233 return view; 2234 } 2235 2236 boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction, 2237 Rect previouslyFocusedRect) { 2238 switch (mFocusScrollStrategy) { 2239 case BaseGridView.FOCUS_SCROLL_ALIGNED: 2240 default: 2241 return gridOnRequestFocusInDescendantsAligned(recyclerView, 2242 direction, previouslyFocusedRect); 2243 case BaseGridView.FOCUS_SCROLL_PAGE: 2244 case BaseGridView.FOCUS_SCROLL_ITEM: 2245 return gridOnRequestFocusInDescendantsUnaligned(recyclerView, 2246 direction, previouslyFocusedRect); 2247 } 2248 } 2249 2250 private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView, 2251 int direction, Rect previouslyFocusedRect) { 2252 View view = findViewByPosition(mFocusPosition); 2253 if (view != null) { 2254 boolean result = view.requestFocus(direction, previouslyFocusedRect); 2255 if (!result && DEBUG) { 2256 Log.w(getTag(), "failed to request focus on " + view); 2257 } 2258 return result; 2259 } 2260 return false; 2261 } 2262 2263 private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView, 2264 int direction, Rect previouslyFocusedRect) { 2265 // focus to view not overlapping padding area to avoid scrolling in gaining focus 2266 int index; 2267 int increment; 2268 int end; 2269 int count = getChildCount(); 2270 if ((direction & View.FOCUS_FORWARD) != 0) { 2271 index = 0; 2272 increment = 1; 2273 end = count; 2274 } else { 2275 index = count - 1; 2276 increment = -1; 2277 end = -1; 2278 } 2279 int left = mWindowAlignment.mainAxis().getPaddingLow(); 2280 int right = mWindowAlignment.mainAxis().getClientSize() + left; 2281 for (int i = index; i != end; i += increment) { 2282 View child = getChildAt(i); 2283 if (child.getVisibility() == View.VISIBLE) { 2284 if (getViewMin(child) >= left && getViewMax(child) <= right) { 2285 if (child.requestFocus(direction, previouslyFocusedRect)) { 2286 return true; 2287 } 2288 } 2289 } 2290 } 2291 return false; 2292 } 2293 2294 private final static int PREV_ITEM = 0; 2295 private final static int NEXT_ITEM = 1; 2296 private final static int PREV_ROW = 2; 2297 private final static int NEXT_ROW = 3; 2298 2299 private int getMovement(int direction) { 2300 int movement = View.FOCUS_LEFT; 2301 2302 if (mOrientation == HORIZONTAL) { 2303 switch(direction) { 2304 case View.FOCUS_LEFT: 2305 movement = PREV_ITEM; 2306 break; 2307 case View.FOCUS_RIGHT: 2308 movement = NEXT_ITEM; 2309 break; 2310 case View.FOCUS_UP: 2311 movement = PREV_ROW; 2312 break; 2313 case View.FOCUS_DOWN: 2314 movement = NEXT_ROW; 2315 break; 2316 } 2317 } else if (mOrientation == VERTICAL) { 2318 switch(direction) { 2319 case View.FOCUS_LEFT: 2320 movement = PREV_ROW; 2321 break; 2322 case View.FOCUS_RIGHT: 2323 movement = NEXT_ROW; 2324 break; 2325 case View.FOCUS_UP: 2326 movement = PREV_ITEM; 2327 break; 2328 case View.FOCUS_DOWN: 2329 movement = NEXT_ITEM; 2330 break; 2331 } 2332 } 2333 2334 return movement; 2335 } 2336 2337 int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) { 2338 View view = findViewByPosition(mFocusPosition); 2339 if (view == null) { 2340 return i; 2341 } 2342 int focusIndex = recyclerView.indexOfChild(view); 2343 // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item 2344 // drawing order is 0 1 2 3 9 8 7 6 5 4 2345 if (i < focusIndex) { 2346 return i; 2347 } else if (i < childCount - 1) { 2348 return focusIndex + childCount - 1 - i; 2349 } else { 2350 return focusIndex; 2351 } 2352 } 2353 2354 @Override 2355 public void onAdapterChanged(RecyclerView.Adapter oldAdapter, 2356 RecyclerView.Adapter newAdapter) { 2357 discardLayoutInfo(); 2358 mFocusPosition = NO_POSITION; 2359 super.onAdapterChanged(oldAdapter, newAdapter); 2360 } 2361 2362 private void discardLayoutInfo() { 2363 mGrid = null; 2364 mRows = null; 2365 mRowSizeSecondary = null; 2366 mFirstVisiblePos = -1; 2367 mLastVisiblePos = -1; 2368 mRowSecondarySizeRefresh = false; 2369 } 2370 2371 public void setLayoutEnabled(boolean layoutEnabled) { 2372 if (mLayoutEnabled != layoutEnabled) { 2373 mLayoutEnabled = layoutEnabled; 2374 requestLayout(); 2375 } 2376 } 2377} 2378