GridLayoutManager.java revision d2104835a233e64b3b90182913542e58276aeb47
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 updateScrollMin(); 1497 updateScrollMax(); 1498 oldFirstVisible = mFirstVisiblePos; 1499 oldLastVisible = mLastVisiblePos; 1500 View focusView = findViewByPosition(newFocusPosition); 1501 // we need force to initialize the child view's position 1502 scrollToView(focusView, false); 1503 if (focusView != null && hadFocus) { 1504 focusView.requestFocus(); 1505 } 1506 appendVisibleItems(); 1507 prependVisibleItems(); 1508 removeInvisibleViewsAtFront(); 1509 removeInvisibleViewsAtEnd(); 1510 } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible); 1511 } 1512 mForceFullLayout = false; 1513 1514 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) { 1515 scrollDirectionPrimary(-delta); 1516 scrollDirectionSecondary(-deltaSecondary); 1517 } 1518 appendVisibleItems(); 1519 prependVisibleItems(); 1520 removeInvisibleViewsAtFront(); 1521 removeInvisibleViewsAtEnd(); 1522 1523 if (DEBUG) { 1524 StringWriter sw = new StringWriter(); 1525 PrintWriter pw = new PrintWriter(sw); 1526 mGrid.debugPrint(pw); 1527 Log.d(getTag(), sw.toString()); 1528 } 1529 1530 if (mRowSecondarySizeRefresh) { 1531 mRowSecondarySizeRefresh = false; 1532 } else { 1533 updateRowSecondarySizeRefresh(); 1534 } 1535 1536 if (!state.isPreLayout()) { 1537 mUseDeltaInPreLayout = false; 1538 if (!fastRelayout || mFocusPosition != savedFocusPos) { 1539 dispatchChildSelected(); 1540 } 1541 } 1542 mInLayout = false; 1543 leaveContext(); 1544 if (DEBUG) Log.v(getTag(), "layoutChildren end"); 1545 } 1546 1547 private void offsetChildrenSecondary(int increment) { 1548 final int childCount = getChildCount(); 1549 if (mOrientation == HORIZONTAL) { 1550 for (int i = 0; i < childCount; i++) { 1551 getChildAt(i).offsetTopAndBottom(increment); 1552 } 1553 } else { 1554 for (int i = 0; i < childCount; i++) { 1555 getChildAt(i).offsetLeftAndRight(increment); 1556 } 1557 } 1558 } 1559 1560 private void offsetChildrenPrimary(int increment) { 1561 final int childCount = getChildCount(); 1562 if (mOrientation == VERTICAL) { 1563 for (int i = 0; i < childCount; i++) { 1564 getChildAt(i).offsetTopAndBottom(increment); 1565 } 1566 } else { 1567 for (int i = 0; i < childCount; i++) { 1568 getChildAt(i).offsetLeftAndRight(increment); 1569 } 1570 } 1571 } 1572 1573 @Override 1574 public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) { 1575 if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx); 1576 if (!mLayoutEnabled || !hasDoneFirstLayout()) { 1577 return 0; 1578 } 1579 saveContext(recycler, state); 1580 int result; 1581 if (mOrientation == HORIZONTAL) { 1582 result = scrollDirectionPrimary(dx); 1583 } else { 1584 result = scrollDirectionSecondary(dx); 1585 } 1586 leaveContext(); 1587 return result; 1588 } 1589 1590 @Override 1591 public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) { 1592 if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy); 1593 if (!mLayoutEnabled || !hasDoneFirstLayout()) { 1594 return 0; 1595 } 1596 saveContext(recycler, state); 1597 int result; 1598 if (mOrientation == VERTICAL) { 1599 result = scrollDirectionPrimary(dy); 1600 } else { 1601 result = scrollDirectionSecondary(dy); 1602 } 1603 leaveContext(); 1604 return result; 1605 } 1606 1607 // scroll in main direction may add/prune views 1608 private int scrollDirectionPrimary(int da) { 1609 if (da > 0) { 1610 if (!mWindowAlignment.mainAxis().isMaxUnknown()) { 1611 int maxScroll = mWindowAlignment.mainAxis().getMaxScroll(); 1612 if (mScrollOffsetPrimary + da > maxScroll) { 1613 da = maxScroll - mScrollOffsetPrimary; 1614 } 1615 } 1616 } else if (da < 0) { 1617 if (!mWindowAlignment.mainAxis().isMinUnknown()) { 1618 int minScroll = mWindowAlignment.mainAxis().getMinScroll(); 1619 if (mScrollOffsetPrimary + da < minScroll) { 1620 da = minScroll - mScrollOffsetPrimary; 1621 } 1622 } 1623 } 1624 if (da == 0) { 1625 return 0; 1626 } 1627 offsetChildrenPrimary(-da); 1628 mScrollOffsetPrimary += da; 1629 if (mInLayout) { 1630 return da; 1631 } 1632 1633 int childCount = getChildCount(); 1634 boolean updated; 1635 1636 if (da > 0) { 1637 appendVisibleItems(); 1638 } else if (da < 0) { 1639 prependVisibleItems(); 1640 } 1641 updated = getChildCount() > childCount; 1642 childCount = getChildCount(); 1643 1644 if (da > 0) { 1645 removeInvisibleViewsAtFront(); 1646 } else if (da < 0) { 1647 removeInvisibleViewsAtEnd(); 1648 } 1649 updated |= getChildCount() < childCount; 1650 1651 if (updated) { 1652 updateRowSecondarySizeRefresh(); 1653 } 1654 1655 mBaseGridView.invalidate(); 1656 return da; 1657 } 1658 1659 // scroll in second direction will not add/prune views 1660 private int scrollDirectionSecondary(int dy) { 1661 if (dy == 0) { 1662 return 0; 1663 } 1664 offsetChildrenSecondary(-dy); 1665 mScrollOffsetSecondary += dy; 1666 mBaseGridView.invalidate(); 1667 return dy; 1668 } 1669 1670 private void updateScrollMax() { 1671 if (mLastVisiblePos < 0) { 1672 return; 1673 } 1674 final boolean lastAvailable = mLastVisiblePos == mState.getItemCount() - 1; 1675 final boolean maxUnknown = mWindowAlignment.mainAxis().isMaxUnknown(); 1676 if (!lastAvailable && maxUnknown) { 1677 return; 1678 } 1679 int maxEdge = Integer.MIN_VALUE; 1680 int rowIndex = -1; 1681 for (int i = 0; i < mRows.length; i++) { 1682 if (mRows[i].high > maxEdge) { 1683 maxEdge = mRows[i].high; 1684 rowIndex = i; 1685 } 1686 } 1687 int maxScroll = Integer.MAX_VALUE; 1688 for (int i = mLastVisiblePos; i >= mFirstVisiblePos; i--) { 1689 StaggeredGrid.Location location = mGrid.getLocation(i); 1690 if (location != null && location.row == rowIndex) { 1691 int savedMaxEdge = mWindowAlignment.mainAxis().getMaxEdge(); 1692 mWindowAlignment.mainAxis().setMaxEdge(maxEdge); 1693 maxScroll = getPrimarySystemScrollPosition(findViewByPosition(i)); 1694 mWindowAlignment.mainAxis().setMaxEdge(savedMaxEdge); 1695 break; 1696 } 1697 } 1698 if (lastAvailable) { 1699 mWindowAlignment.mainAxis().setMaxEdge(maxEdge); 1700 mWindowAlignment.mainAxis().setMaxScroll(maxScroll); 1701 if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge + 1702 " scrollMax to " + maxScroll); 1703 } else { 1704 // the maxScroll for currently last visible item is larger, 1705 // so we must invalidate the max scroll value. 1706 if (maxScroll > mWindowAlignment.mainAxis().getMaxScroll()) { 1707 mWindowAlignment.mainAxis().invalidateScrollMax(); 1708 if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be " 1709 + "greater than " + maxScroll); 1710 } 1711 } 1712 } 1713 1714 private void updateScrollMin() { 1715 if (mFirstVisiblePos < 0) { 1716 return; 1717 } 1718 final boolean firstAvailable = mFirstVisiblePos == 0; 1719 final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown(); 1720 if (!firstAvailable && minUnknown) { 1721 return; 1722 } 1723 int minEdge = Integer.MAX_VALUE; 1724 int rowIndex = -1; 1725 for (int i = 0; i < mRows.length; i++) { 1726 if (mRows[i].low < minEdge) { 1727 minEdge = mRows[i].low; 1728 rowIndex = i; 1729 } 1730 } 1731 int minScroll = Integer.MIN_VALUE; 1732 for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) { 1733 StaggeredGrid.Location location = mGrid.getLocation(i); 1734 if (location != null && location.row == rowIndex) { 1735 int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge(); 1736 mWindowAlignment.mainAxis().setMinEdge(minEdge); 1737 minScroll = getPrimarySystemScrollPosition(findViewByPosition(i)); 1738 mWindowAlignment.mainAxis().setMinEdge(savedMinEdge); 1739 break; 1740 } 1741 } 1742 if (firstAvailable) { 1743 mWindowAlignment.mainAxis().setMinEdge(minEdge); 1744 mWindowAlignment.mainAxis().setMinScroll(minScroll); 1745 if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge + 1746 " scrollMin to " + minScroll); 1747 } else { 1748 // the minScroll for currently first visible item is smaller, 1749 // so we must invalidate the min scroll value. 1750 if (minScroll < mWindowAlignment.mainAxis().getMinScroll()) { 1751 mWindowAlignment.mainAxis().invalidateScrollMin(); 1752 if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be " 1753 + "less than " + minScroll); 1754 } 1755 } 1756 } 1757 1758 private void updateScrollSecondAxis() { 1759 mWindowAlignment.secondAxis().setMinEdge(0); 1760 mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary()); 1761 } 1762 1763 private void initScrollController() { 1764 mWindowAlignment.horizontal.setSize(getWidth()); 1765 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 1766 mWindowAlignment.vertical.setSize(getHeight()); 1767 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 1768 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 1769 1770 if (DEBUG) { 1771 Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary 1772 + " mWindowAlignment " + mWindowAlignment); 1773 } 1774 } 1775 1776 public void setSelection(RecyclerView parent, int position) { 1777 setSelection(parent, position, false); 1778 } 1779 1780 public void setSelectionSmooth(RecyclerView parent, int position) { 1781 setSelection(parent, position, true); 1782 } 1783 1784 public int getSelection() { 1785 return mFocusPosition; 1786 } 1787 1788 public void setSelection(RecyclerView parent, int position, boolean smooth) { 1789 if (mFocusPosition == position) { 1790 return; 1791 } 1792 View view = findViewByPosition(position); 1793 if (view != null) { 1794 scrollToView(view, smooth); 1795 } else { 1796 mFocusPosition = position; 1797 if (!mLayoutEnabled) { 1798 return; 1799 } 1800 if (smooth) { 1801 if (!hasDoneFirstLayout()) { 1802 Log.w(getTag(), "setSelectionSmooth should " + 1803 "not be called before first layout pass"); 1804 return; 1805 } 1806 LinearSmoothScroller linearSmoothScroller = 1807 new LinearSmoothScroller(parent.getContext()) { 1808 @Override 1809 public PointF computeScrollVectorForPosition(int targetPosition) { 1810 if (getChildCount() == 0) { 1811 return null; 1812 } 1813 final int firstChildPos = getPosition(getChildAt(0)); 1814 final int direction = targetPosition < firstChildPos ? -1 : 1; 1815 if (mOrientation == HORIZONTAL) { 1816 return new PointF(direction, 0); 1817 } else { 1818 return new PointF(0, direction); 1819 } 1820 } 1821 @Override 1822 protected void onTargetFound(View targetView, 1823 RecyclerView.State state, Action action) { 1824 if (hasFocus()) { 1825 targetView.requestFocus(); 1826 } 1827 dispatchChildSelected(); 1828 if (getScrollPosition(targetView, mTempDeltas)) { 1829 int dx, dy; 1830 if (mOrientation == HORIZONTAL) { 1831 dx = mTempDeltas[0]; 1832 dy = mTempDeltas[1]; 1833 } else { 1834 dx = mTempDeltas[1]; 1835 dy = mTempDeltas[0]; 1836 } 1837 final int distance = (int) Math.sqrt(dx * dx + dy * dy); 1838 final int time = calculateTimeForDeceleration(distance); 1839 action.update(dx, dy, time, mDecelerateInterpolator); 1840 } 1841 } 1842 }; 1843 linearSmoothScroller.setTargetPosition(position); 1844 startSmoothScroll(linearSmoothScroller); 1845 } else { 1846 mForceFullLayout = true; 1847 parent.requestLayout(); 1848 } 1849 } 1850 } 1851 1852 @Override 1853 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 1854 boolean needsLayout = false; 1855 if (itemCount != 0) { 1856 if (mFirstVisiblePos < 0) { 1857 needsLayout = true; 1858 } else if (!(positionStart > mLastVisiblePos + 1 || 1859 positionStart + itemCount < mFirstVisiblePos - 1)) { 1860 needsLayout = true; 1861 } 1862 } 1863 if (needsLayout) { 1864 recyclerView.requestLayout(); 1865 } 1866 } 1867 1868 @Override 1869 public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) { 1870 if (mFocusSearchDisabled) { 1871 return true; 1872 } 1873 if (!mInLayout) { 1874 scrollToView(child, true); 1875 } 1876 return true; 1877 } 1878 1879 @Override 1880 public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect, 1881 boolean immediate) { 1882 if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect); 1883 return false; 1884 } 1885 1886 int getScrollOffsetX() { 1887 return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary; 1888 } 1889 1890 int getScrollOffsetY() { 1891 return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary; 1892 } 1893 1894 public void getViewSelectedOffsets(View view, int[] offsets) { 1895 if (mOrientation == HORIZONTAL) { 1896 offsets[0] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary; 1897 offsets[1] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary; 1898 } else { 1899 offsets[1] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary; 1900 offsets[0] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary; 1901 } 1902 } 1903 1904 private int getPrimarySystemScrollPosition(View view) { 1905 int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view); 1906 int pos = getPositionByView(view); 1907 StaggeredGrid.Location location = mGrid.getLocation(pos); 1908 final int row = location.row; 1909 boolean isFirst = mFirstVisiblePos == 0; 1910 // TODO: change to use State object in onRequestChildFocus() 1911 boolean isLast = mLastVisiblePos == (mState == null ? 1912 getItemCount() : mState.getItemCount()) - 1; 1913 if (isFirst || isLast) { 1914 for (int i = getChildCount() - 1; i >= 0; i--) { 1915 int position = getPositionByIndex(i); 1916 StaggeredGrid.Location loc = mGrid.getLocation(position); 1917 if (loc != null && loc.row == row) { 1918 if (position < pos) { 1919 isFirst = false; 1920 } else if (position > pos) { 1921 isLast = false; 1922 } 1923 } 1924 } 1925 } 1926 return mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary, isFirst, isLast); 1927 } 1928 1929 private int getSecondarySystemScrollPosition(View view) { 1930 int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view); 1931 int pos = getPositionByView(view); 1932 StaggeredGrid.Location location = mGrid.getLocation(pos); 1933 final int row = location.row; 1934 boolean isFirst = row == 0; 1935 boolean isLast = row == mGrid.getNumRows() - 1; 1936 return mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary, 1937 isFirst, isLast); 1938 } 1939 1940 /** 1941 * Scroll to a given child view and change mFocusPosition. 1942 */ 1943 private void scrollToView(View view, boolean smooth) { 1944 int newFocusPosition = getPositionByView(view); 1945 if (newFocusPosition != mFocusPosition) { 1946 mFocusPosition = newFocusPosition; 1947 if (!mInLayout) { 1948 dispatchChildSelected(); 1949 } 1950 } 1951 if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) { 1952 mBaseGridView.invalidate(); 1953 } 1954 if (view == null) { 1955 return; 1956 } 1957 if (!view.hasFocus() && mBaseGridView.hasFocus()) { 1958 // transfer focus to the child if it does not have focus yet (e.g. triggered 1959 // by setSelection()) 1960 view.requestFocus(); 1961 } 1962 if (!mScrollEnabled) { 1963 return; 1964 } 1965 if (getScrollPosition(view, mTempDeltas)) { 1966 scrollGrid(mTempDeltas[0], mTempDeltas[1], smooth); 1967 } 1968 } 1969 1970 private boolean getScrollPosition(View view, int[] deltas) { 1971 switch (mFocusScrollStrategy) { 1972 case BaseGridView.FOCUS_SCROLL_ALIGNED: 1973 default: 1974 return getAlignedPosition(view, deltas); 1975 case BaseGridView.FOCUS_SCROLL_ITEM: 1976 case BaseGridView.FOCUS_SCROLL_PAGE: 1977 return getNoneAlignedPosition(view, deltas); 1978 } 1979 } 1980 1981 private boolean getNoneAlignedPosition(View view, int[] deltas) { 1982 int pos = getPositionByView(view); 1983 int viewMin = getViewMin(view); 1984 int viewMax = getViewMax(view); 1985 // we either align "firstView" to left/top padding edge 1986 // or align "lastView" to right/bottom padding edge 1987 View firstView = null; 1988 View lastView = null; 1989 int paddingLow = mWindowAlignment.mainAxis().getPaddingLow(); 1990 int clientSize = mWindowAlignment.mainAxis().getClientSize(); 1991 final int row = mGrid.getLocation(pos).row; 1992 if (viewMin < paddingLow) { 1993 // view enters low padding area: 1994 firstView = view; 1995 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 1996 // scroll one "page" left/top, 1997 // align first visible item of the "page" at the low padding edge. 1998 while (!prependOneVisibleItem()) { 1999 List<Integer> positions = 2000 mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row]; 2001 firstView = findViewByPosition(positions.get(0)); 2002 if (viewMax - getViewMin(firstView) > clientSize) { 2003 if (positions.size() > 1) { 2004 firstView = findViewByPosition(positions.get(1)); 2005 } 2006 break; 2007 } 2008 } 2009 } 2010 } else if (viewMax > clientSize + paddingLow) { 2011 // view enters high padding area: 2012 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 2013 // scroll whole one page right/bottom, align view at the low padding edge. 2014 firstView = view; 2015 do { 2016 List<Integer> positions = 2017 mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row]; 2018 lastView = findViewByPosition(positions.get(positions.size() - 1)); 2019 if (getViewMax(lastView) - viewMin > clientSize) { 2020 lastView = null; 2021 break; 2022 } 2023 } while (!appendOneVisibleItem()); 2024 if (lastView != null) { 2025 // however if we reached end, we should align last view. 2026 firstView = null; 2027 } 2028 } else { 2029 lastView = view; 2030 } 2031 } 2032 int scrollPrimary = 0; 2033 int scrollSecondary = 0; 2034 if (firstView != null) { 2035 scrollPrimary = getViewMin(firstView) - paddingLow; 2036 } else if (lastView != null) { 2037 scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize); 2038 } 2039 View secondaryAlignedView; 2040 if (firstView != null) { 2041 secondaryAlignedView = firstView; 2042 } else if (lastView != null) { 2043 secondaryAlignedView = lastView; 2044 } else { 2045 secondaryAlignedView = view; 2046 } 2047 scrollSecondary = getSecondarySystemScrollPosition(secondaryAlignedView); 2048 scrollSecondary -= mScrollOffsetSecondary; 2049 if (scrollPrimary != 0 || scrollSecondary != 0) { 2050 deltas[0] = scrollPrimary; 2051 deltas[1] = scrollSecondary; 2052 return true; 2053 } 2054 return false; 2055 } 2056 2057 private boolean getAlignedPosition(View view, int[] deltas) { 2058 int scrollPrimary = getPrimarySystemScrollPosition(view); 2059 int scrollSecondary = getSecondarySystemScrollPosition(view); 2060 if (DEBUG) { 2061 Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary 2062 +" " + mWindowAlignment); 2063 } 2064 scrollPrimary -= mScrollOffsetPrimary; 2065 scrollSecondary -= mScrollOffsetSecondary; 2066 if (scrollPrimary != 0 || scrollSecondary != 0) { 2067 deltas[0] = scrollPrimary; 2068 deltas[1] = scrollSecondary; 2069 return true; 2070 } 2071 return false; 2072 } 2073 2074 private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) { 2075 if (mInLayout) { 2076 scrollDirectionPrimary(scrollPrimary); 2077 scrollDirectionSecondary(scrollSecondary); 2078 } else { 2079 int scrollX; 2080 int scrollY; 2081 if (mOrientation == HORIZONTAL) { 2082 scrollX = scrollPrimary; 2083 scrollY = scrollSecondary; 2084 } else { 2085 scrollX = scrollSecondary; 2086 scrollY = scrollPrimary; 2087 } 2088 if (smooth) { 2089 mBaseGridView.smoothScrollBy(scrollX, scrollY); 2090 } else { 2091 mBaseGridView.scrollBy(scrollX, scrollY); 2092 } 2093 } 2094 } 2095 2096 public void setPruneChild(boolean pruneChild) { 2097 if (mPruneChild != pruneChild) { 2098 mPruneChild = pruneChild; 2099 if (mPruneChild) { 2100 requestLayout(); 2101 } 2102 } 2103 } 2104 2105 public boolean getPruneChild() { 2106 return mPruneChild; 2107 } 2108 2109 public void setScrollEnabled(boolean scrollEnabled) { 2110 if (mScrollEnabled != scrollEnabled) { 2111 mScrollEnabled = scrollEnabled; 2112 if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) { 2113 View focusView = findViewByPosition(mFocusPosition == NO_POSITION ? 0 : 2114 mFocusPosition); 2115 if (focusView != null) { 2116 scrollToView(focusView, true); 2117 } 2118 } 2119 } 2120 } 2121 2122 public boolean isScrollEnabled() { 2123 return mScrollEnabled; 2124 } 2125 2126 private int findImmediateChildIndex(View view) { 2127 while (view != null && view != mBaseGridView) { 2128 int index = mBaseGridView.indexOfChild(view); 2129 if (index >= 0) { 2130 return index; 2131 } 2132 view = (View) view.getParent(); 2133 } 2134 return NO_POSITION; 2135 } 2136 2137 void setFocusSearchDisabled(boolean disabled) { 2138 mFocusSearchDisabled = disabled; 2139 } 2140 2141 boolean isFocusSearchDisabled() { 2142 return mFocusSearchDisabled; 2143 } 2144 2145 @Override 2146 public View onInterceptFocusSearch(View focused, int direction) { 2147 if (mFocusSearchDisabled) { 2148 return focused; 2149 } 2150 return null; 2151 } 2152 2153 boolean hasPreviousViewInSameRow(int pos) { 2154 if (mGrid == null || pos == NO_POSITION) { 2155 return false; 2156 } 2157 if (mFirstVisiblePos > 0) { 2158 return true; 2159 } 2160 final int focusedRow = mGrid.getLocation(pos).row; 2161 for (int i = getChildCount() - 1; i >= 0; i--) { 2162 int position = getPositionByIndex(i); 2163 StaggeredGrid.Location loc = mGrid.getLocation(position); 2164 if (loc != null && loc.row == focusedRow) { 2165 if (position < pos) { 2166 return true; 2167 } 2168 } 2169 } 2170 return false; 2171 } 2172 2173 @Override 2174 public boolean onAddFocusables(RecyclerView recyclerView, 2175 ArrayList<View> views, int direction, int focusableMode) { 2176 if (mFocusSearchDisabled) { 2177 return true; 2178 } 2179 // If this viewgroup or one of its children currently has focus then we 2180 // consider our children for focus searching in main direction on the same row. 2181 // If this viewgroup has no focus and using focus align, we want the system 2182 // to ignore our children and pass focus to the viewgroup, which will pass 2183 // focus on to its children appropriately. 2184 // If this viewgroup has no focus and not using focus align, we want to 2185 // consider the child that does not overlap with padding area. 2186 if (recyclerView.hasFocus()) { 2187 final int movement = getMovement(direction); 2188 if (movement != PREV_ITEM && movement != NEXT_ITEM) { 2189 // Move on secondary direction uses default addFocusables(). 2190 return false; 2191 } 2192 final View focused = recyclerView.findFocus(); 2193 final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused)); 2194 // Add focusables of focused item. 2195 if (focusedPos != NO_POSITION) { 2196 findViewByPosition(focusedPos).addFocusables(views, direction, focusableMode); 2197 } 2198 final int focusedRow = mGrid != null && focusedPos != NO_POSITION ? 2199 mGrid.getLocation(focusedPos).row : NO_POSITION; 2200 // Add focusables of next neighbor of same row on the focus search direction. 2201 if (mGrid != null) { 2202 final int focusableCount = views.size(); 2203 for (int i = 0, count = getChildCount(); i < count; i++) { 2204 int index = movement == NEXT_ITEM ? i : count - 1 - i; 2205 final View child = getChildAt(index); 2206 if (child.getVisibility() != View.VISIBLE) { 2207 continue; 2208 } 2209 int position = getPositionByIndex(index); 2210 StaggeredGrid.Location loc = mGrid.getLocation(position); 2211 if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) { 2212 if (focusedPos == NO_POSITION || 2213 (movement == NEXT_ITEM && position > focusedPos) 2214 || (movement == PREV_ITEM && position < focusedPos)) { 2215 child.addFocusables(views, direction, focusableMode); 2216 if (views.size() > focusableCount) { 2217 break; 2218 } 2219 } 2220 } 2221 } 2222 } 2223 } else { 2224 if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) { 2225 // adding views not overlapping padding area to avoid scrolling in gaining focus 2226 int left = mWindowAlignment.mainAxis().getPaddingLow(); 2227 int right = mWindowAlignment.mainAxis().getClientSize() + left; 2228 int focusableCount = views.size(); 2229 for (int i = 0, count = getChildCount(); i < count; i++) { 2230 View child = getChildAt(i); 2231 if (child.getVisibility() == View.VISIBLE) { 2232 if (getViewMin(child) >= left && getViewMax(child) <= right) { 2233 child.addFocusables(views, direction, focusableMode); 2234 } 2235 } 2236 } 2237 // if we cannot find any, then just add all children. 2238 if (views.size() == focusableCount) { 2239 for (int i = 0, count = getChildCount(); i < count; i++) { 2240 View child = getChildAt(i); 2241 if (child.getVisibility() == View.VISIBLE) { 2242 child.addFocusables(views, direction, focusableMode); 2243 } 2244 } 2245 if (views.size() != focusableCount) { 2246 return true; 2247 } 2248 } else { 2249 return true; 2250 } 2251 // if still cannot find any, fall through and add itself 2252 } 2253 if (recyclerView.isFocusable()) { 2254 views.add(recyclerView); 2255 } 2256 } 2257 return true; 2258 } 2259 2260 @Override 2261 public View onFocusSearchFailed(View focused, int direction, Recycler recycler, 2262 RecyclerView.State state) { 2263 if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction); 2264 2265 View view = null; 2266 int movement = getMovement(direction); 2267 if (mNumRows == 1) { 2268 // for simple row, use LinearSmoothScroller to smooth animation. 2269 // It will stay at a fixed cap speed in continuous scroll. 2270 if (movement == NEXT_ITEM) { 2271 int newPos = mFocusPosition + mNumRows; 2272 if (newPos < getItemCount()) { 2273 setSelectionSmooth(mBaseGridView, newPos); 2274 view = focused; 2275 } else { 2276 if (!mFocusOutEnd) { 2277 view = focused; 2278 } 2279 } 2280 } else if (movement == PREV_ITEM){ 2281 int newPos = mFocusPosition - mNumRows; 2282 if (newPos >= 0) { 2283 setSelectionSmooth(mBaseGridView, newPos); 2284 view = focused; 2285 } else { 2286 if (!mFocusOutFront) { 2287 view = focused; 2288 } 2289 } 2290 } 2291 } else if (mNumRows > 1) { 2292 // for possible staggered grid, we need guarantee focus to same row/column. 2293 // TODO: we may also use LinearSmoothScroller. 2294 saveContext(recycler, state); 2295 final FocusFinder ff = FocusFinder.getInstance(); 2296 if (movement == NEXT_ITEM) { 2297 while (view == null && !appendOneVisibleItem()) { 2298 view = ff.findNextFocus(mBaseGridView, focused, direction); 2299 } 2300 } else if (movement == PREV_ITEM){ 2301 while (view == null && !prependOneVisibleItem()) { 2302 view = ff.findNextFocus(mBaseGridView, focused, direction); 2303 } 2304 } 2305 if (view == null) { 2306 // returning the same view to prevent focus lost when scrolling past the end of the list 2307 if (movement == PREV_ITEM) { 2308 view = mFocusOutFront ? null : focused; 2309 } else if (movement == NEXT_ITEM){ 2310 view = mFocusOutEnd ? null : focused; 2311 } 2312 } 2313 leaveContext(); 2314 } 2315 if (DEBUG) Log.v(getTag(), "returning view " + view); 2316 return view; 2317 } 2318 2319 boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction, 2320 Rect previouslyFocusedRect) { 2321 switch (mFocusScrollStrategy) { 2322 case BaseGridView.FOCUS_SCROLL_ALIGNED: 2323 default: 2324 return gridOnRequestFocusInDescendantsAligned(recyclerView, 2325 direction, previouslyFocusedRect); 2326 case BaseGridView.FOCUS_SCROLL_PAGE: 2327 case BaseGridView.FOCUS_SCROLL_ITEM: 2328 return gridOnRequestFocusInDescendantsUnaligned(recyclerView, 2329 direction, previouslyFocusedRect); 2330 } 2331 } 2332 2333 private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView, 2334 int direction, Rect previouslyFocusedRect) { 2335 View view = findViewByPosition(mFocusPosition); 2336 if (view != null) { 2337 boolean result = view.requestFocus(direction, previouslyFocusedRect); 2338 if (!result && DEBUG) { 2339 Log.w(getTag(), "failed to request focus on " + view); 2340 } 2341 return result; 2342 } 2343 return false; 2344 } 2345 2346 private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView, 2347 int direction, Rect previouslyFocusedRect) { 2348 // focus to view not overlapping padding area to avoid scrolling in gaining focus 2349 int index; 2350 int increment; 2351 int end; 2352 int count = getChildCount(); 2353 if ((direction & View.FOCUS_FORWARD) != 0) { 2354 index = 0; 2355 increment = 1; 2356 end = count; 2357 } else { 2358 index = count - 1; 2359 increment = -1; 2360 end = -1; 2361 } 2362 int left = mWindowAlignment.mainAxis().getPaddingLow(); 2363 int right = mWindowAlignment.mainAxis().getClientSize() + left; 2364 for (int i = index; i != end; i += increment) { 2365 View child = getChildAt(i); 2366 if (child.getVisibility() == View.VISIBLE) { 2367 if (getViewMin(child) >= left && getViewMax(child) <= right) { 2368 if (child.requestFocus(direction, previouslyFocusedRect)) { 2369 return true; 2370 } 2371 } 2372 } 2373 } 2374 return false; 2375 } 2376 2377 private final static int PREV_ITEM = 0; 2378 private final static int NEXT_ITEM = 1; 2379 private final static int PREV_ROW = 2; 2380 private final static int NEXT_ROW = 3; 2381 2382 private int getMovement(int direction) { 2383 int movement = View.FOCUS_LEFT; 2384 2385 if (mOrientation == HORIZONTAL) { 2386 switch(direction) { 2387 case View.FOCUS_LEFT: 2388 movement = PREV_ITEM; 2389 break; 2390 case View.FOCUS_RIGHT: 2391 movement = NEXT_ITEM; 2392 break; 2393 case View.FOCUS_UP: 2394 movement = PREV_ROW; 2395 break; 2396 case View.FOCUS_DOWN: 2397 movement = NEXT_ROW; 2398 break; 2399 } 2400 } else if (mOrientation == VERTICAL) { 2401 switch(direction) { 2402 case View.FOCUS_LEFT: 2403 movement = PREV_ROW; 2404 break; 2405 case View.FOCUS_RIGHT: 2406 movement = NEXT_ROW; 2407 break; 2408 case View.FOCUS_UP: 2409 movement = PREV_ITEM; 2410 break; 2411 case View.FOCUS_DOWN: 2412 movement = NEXT_ITEM; 2413 break; 2414 } 2415 } 2416 2417 return movement; 2418 } 2419 2420 int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) { 2421 View view = findViewByPosition(mFocusPosition); 2422 if (view == null) { 2423 return i; 2424 } 2425 int focusIndex = recyclerView.indexOfChild(view); 2426 // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item 2427 // drawing order is 0 1 2 3 9 8 7 6 5 4 2428 if (i < focusIndex) { 2429 return i; 2430 } else if (i < childCount - 1) { 2431 return focusIndex + childCount - 1 - i; 2432 } else { 2433 return focusIndex; 2434 } 2435 } 2436 2437 @Override 2438 public void onAdapterChanged(RecyclerView.Adapter oldAdapter, 2439 RecyclerView.Adapter newAdapter) { 2440 discardLayoutInfo(); 2441 mFocusPosition = NO_POSITION; 2442 super.onAdapterChanged(oldAdapter, newAdapter); 2443 } 2444 2445 private void discardLayoutInfo() { 2446 mGrid = null; 2447 mRows = null; 2448 mRowSizeSecondary = null; 2449 mFirstVisiblePos = -1; 2450 mLastVisiblePos = -1; 2451 mRowSecondarySizeRefresh = false; 2452 } 2453 2454 public void setLayoutEnabled(boolean layoutEnabled) { 2455 if (mLayoutEnabled != layoutEnabled) { 2456 mLayoutEnabled = layoutEnabled; 2457 requestLayout(); 2458 } 2459 } 2460} 2461