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