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