GridLayoutManager.java revision 5a2782ae17df5331a594fe03d5d89251a8b9f6d4
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.Rect; 19import android.support.v7.widget.RecyclerView; 20import android.support.v7.widget.RecyclerView.Adapter; 21import android.support.v7.widget.RecyclerView.Recycler; 22 23import static android.support.v7.widget.RecyclerView.NO_ID; 24import static android.support.v7.widget.RecyclerView.NO_POSITION; 25import static android.support.v7.widget.RecyclerView.HORIZONTAL; 26import static android.support.v7.widget.RecyclerView.VERTICAL; 27 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.FocusFinder; 31import android.view.Gravity; 32import android.view.View; 33import android.view.ViewParent; 34import android.view.View.MeasureSpec; 35import android.view.ViewGroup.MarginLayoutParams; 36import android.view.ViewGroup; 37import android.view.animation.DecelerateInterpolator; 38import android.view.animation.Interpolator; 39 40import java.io.PrintWriter; 41import java.io.StringWriter; 42import java.util.ArrayList; 43 44final class GridLayoutManager extends RecyclerView.LayoutManager { 45 46 /* 47 * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}. 48 * The class currently does three internal jobs: 49 * - Saves optical bounds insets. 50 * - Caches focus align view center. 51 * - Manages child view layout animation. 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 // For animations 69 private TimeAnimator mAnimator; 70 private long mDuration; 71 private boolean mFirstAttached; 72 // current virtual view position (scrollOffset + left/top) in the GridLayoutManager 73 private int mViewX, mViewY; 74 // animation start value of translation x and y 75 private float mAnimationStartTranslationX, mAnimationStartTranslationY; 76 77 public LayoutParams(Context c, AttributeSet attrs) { 78 super(c, attrs); 79 } 80 81 public LayoutParams(int width, int height) { 82 super(width, height); 83 } 84 85 public LayoutParams(MarginLayoutParams source) { 86 super(source); 87 } 88 89 public LayoutParams(ViewGroup.LayoutParams source) { 90 super(source); 91 } 92 93 public LayoutParams(RecyclerView.LayoutParams source) { 94 super(source); 95 } 96 97 public LayoutParams(LayoutParams source) { 98 super(source); 99 } 100 101 void onViewAttached() { 102 endAnimate(); 103 mFirstAttached = true; 104 } 105 106 void onViewDetached() { 107 endAnimate(); 108 } 109 110 int getAlignX() { 111 return mAlignX; 112 } 113 114 int getAlignY() { 115 return mAlignY; 116 } 117 118 int getOpticalLeft(View view) { 119 return view.getLeft() + mLeftInset; 120 } 121 122 int getOpticalTop(View view) { 123 return view.getTop() + mTopInset; 124 } 125 126 int getOpticalRight(View view) { 127 return view.getRight() - mRighInset; 128 } 129 130 int getOpticalBottom(View view) { 131 return view.getBottom() - mBottomInset; 132 } 133 134 int getOpticalWidth(View view) { 135 return view.getWidth() - mLeftInset - mRighInset; 136 } 137 138 int getOpticalHeight(View view) { 139 return view.getHeight() - mTopInset - mBottomInset; 140 } 141 142 void setAlignX(int alignX) { 143 mAlignX = alignX; 144 } 145 146 void setAlignY(int alignY) { 147 mAlignY = alignY; 148 } 149 150 void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) { 151 mLeftInset = leftInset; 152 mTopInset = topInset; 153 mRighInset = rightInset; 154 mBottomInset = bottomInset; 155 } 156 157 private TimeAnimator.TimeListener mTimeListener = new TimeAnimator.TimeListener() { 158 @Override 159 public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { 160 if (mView == null) { 161 return; 162 } 163 if (totalTime >= mDuration) { 164 endAnimate(); 165 } else { 166 float fraction = (float) (totalTime / (double)mDuration); 167 float fractionToEnd = 1 - mAnimator 168 .getInterpolator().getInterpolation(fraction); 169 mView.setTranslationX(fractionToEnd * mAnimationStartTranslationX); 170 mView.setTranslationY(fractionToEnd * mAnimationStartTranslationY); 171 invalidateItemDecoration(); 172 } 173 } 174 }; 175 176 void startAnimate(GridLayoutManager layout, View view, long startDelay) { 177 if (mAnimator == null) { 178 mAnimator = new TimeAnimator(); 179 mAnimator.setTimeListener(mTimeListener); 180 } 181 if (mFirstAttached) { 182 // first time record the initial location and return without animation 183 // TODO do we need initial animation? 184 mViewX = layout.getScrollOffsetX() + getOpticalLeft(view); 185 mViewY = layout.getScrollOffsetY() + getOpticalTop(view); 186 mFirstAttached = false; 187 return; 188 } 189 if (!layout.isChildLayoutAnimated()) { 190 return; 191 } 192 mView = view; 193 int newViewX = layout.getScrollOffsetX() + getOpticalLeft(mView); 194 int newViewY = layout.getScrollOffsetY() + getOpticalTop(mView); 195 if (newViewX != mViewX || newViewY != mViewY) { 196 mAnimator.cancel(); 197 mAnimationStartTranslationX = mView.getTranslationX(); 198 mAnimationStartTranslationY = mView.getTranslationY(); 199 mAnimationStartTranslationX += mViewX - newViewX; 200 mAnimationStartTranslationY += mViewY - newViewY; 201 mDuration = layout.getChildLayoutAnimationDuration(); 202 mAnimator.setDuration(mDuration); 203 mAnimator.setInterpolator(layout.getChildLayoutAnimationInterpolator()); 204 mAnimator.setStartDelay(startDelay); 205 mAnimator.start(); 206 mViewX = newViewX; 207 mViewY = newViewY; 208 } 209 } 210 211 void endAnimate() { 212 if (mAnimator != null) { 213 mAnimator.end(); 214 } 215 if (mView != null) { 216 mView.setTranslationX(0); 217 mView.setTranslationY(0); 218 mView = null; 219 } 220 } 221 222 private void invalidateItemDecoration() { 223 ViewParent parent = mView.getParent(); 224 if (parent instanceof RecyclerView) { 225 // TODO: we only need invalidate parent if it has ItemDecoration 226 ((RecyclerView) parent).invalidate(); 227 } 228 } 229 } 230 231 private static final String TAG = "GridLayoutManager"; 232 private static final boolean DEBUG = false; 233 234 private static final Interpolator sDefaultAnimationChildLayoutInterpolator 235 = new DecelerateInterpolator(); 236 237 private static final long DEFAULT_CHILD_ANIMATION_DURATION_MS = 250; 238 239 private String getTag() { 240 return TAG + ":" + mBaseGridView.getId(); 241 } 242 243 private final BaseGridView mBaseGridView; 244 245 /** 246 * The orientation of a "row". 247 */ 248 private int mOrientation = HORIZONTAL; 249 250 private RecyclerView.Adapter mAdapter; 251 private RecyclerView.Recycler mRecycler; 252 253 private boolean mInLayout = false; 254 255 private OnChildSelectedListener mChildSelectedListener = null; 256 257 /** 258 * The focused position, it's not the currently visually aligned position 259 * but it is the final position that we intend to focus on. If there are 260 * multiple setSelection() called, mFocusPosition saves last value. 261 */ 262 private int mFocusPosition = NO_POSITION; 263 264 /** 265 * Force a full layout under certain situations. 266 */ 267 private boolean mForceFullLayout; 268 269 /** 270 * The scroll offsets of the viewport relative to the entire view. 271 */ 272 private int mScrollOffsetPrimary; 273 private int mScrollOffsetSecondary; 274 275 /** 276 * User-specified fixed size of each grid item in the secondary direction, can be 277 * 0 to be determined by parent size and number of rows. 278 */ 279 private int mItemLengthSecondaryRequested; 280 /** 281 * The fixed size of each grid item in the secondary direction. This corresponds to 282 * the row height, equal for all rows. Grid items may have variable length 283 * in the primary direction. 284 * 285 */ 286 private int mItemLengthSecondary; 287 288 /** 289 * Margin between items. 290 */ 291 private int mHorizontalMargin; 292 /** 293 * Margin between items vertically. 294 */ 295 private int mVerticalMargin; 296 /** 297 * Margin in main direction. 298 */ 299 private int mMarginPrimary; 300 /** 301 * Margin in second direction. 302 */ 303 private int mMarginSecondary; 304 /** 305 * How to position child in secondary direction. 306 */ 307 private int mGravity = Gravity.LEFT | Gravity.TOP; 308 /** 309 * The number of rows in the grid. 310 */ 311 private int mNumRows; 312 /** 313 * Number of rows requested, can be 0 to be determined by parent size and 314 * rowHeight. 315 */ 316 private int mNumRowsRequested = 1; 317 318 /** 319 * Tracking start/end position of each row for visible items. 320 */ 321 private StaggeredGrid.Row[] mRows; 322 323 /** 324 * Saves grid information of each view. 325 */ 326 private StaggeredGrid mGrid; 327 /** 328 * Position of first item (included) that has attached views. 329 */ 330 private int mFirstVisiblePos; 331 /** 332 * Position of last item (included) that has attached views. 333 */ 334 private int mLastVisiblePos; 335 336 /** 337 * Defines how item view is aligned in the window. 338 */ 339 private final WindowAlignment mWindowAlignment = new WindowAlignment(); 340 341 /** 342 * Defines how item view is aligned. 343 */ 344 private final ItemAlignment mItemAlignment = new ItemAlignment(); 345 346 /** 347 * Dimensions of the view, width or height depending on orientation. 348 */ 349 private int mSizePrimary; 350 351 /** 352 * Allow DPAD key to navigate out at the front of the View (where position = 0), 353 * default is false. 354 */ 355 private boolean mFocusOutFront; 356 357 /** 358 * Allow DPAD key to navigate out at the end of the view, default is false. 359 */ 360 private boolean mFocusOutEnd; 361 362 /** 363 * Animate layout changes from a child resizing or adding/removing a child. 364 */ 365 private boolean mAnimateChildLayout = true; 366 367 /** 368 * Interpolator used to animate layout of children. 369 */ 370 private Interpolator mAnimateLayoutChildInterpolator = sDefaultAnimationChildLayoutInterpolator; 371 372 /** 373 * Duration used to animate layout of children. 374 */ 375 private long mAnimateLayoutChildDuration = DEFAULT_CHILD_ANIMATION_DURATION_MS; 376 377 public GridLayoutManager(BaseGridView baseGridView) { 378 mBaseGridView = baseGridView; 379 } 380 381 public void setOrientation(int orientation) { 382 if (orientation != HORIZONTAL && orientation != VERTICAL) { 383 if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation); 384 return; 385 } 386 387 mOrientation = orientation; 388 mWindowAlignment.setOrientation(orientation); 389 mItemAlignment.setOrientation(orientation); 390 mForceFullLayout = true; 391 } 392 393 public void setWindowAlignment(int windowAlignment) { 394 mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment); 395 } 396 397 public int getWindowAlignment() { 398 return mWindowAlignment.mainAxis().getWindowAlignment(); 399 } 400 401 public void setWindowAlignmentOffset(int alignmentOffset) { 402 mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset); 403 } 404 405 public int getWindowAlignmentOffset() { 406 return mWindowAlignment.mainAxis().getWindowAlignmentOffset(); 407 } 408 409 public void setWindowAlignmentOffsetPercent(float offsetPercent) { 410 mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent); 411 } 412 413 public float getWindowAlignmentOffsetPercent() { 414 return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent(); 415 } 416 417 public void setItemAlignmentOffset(int alignmentOffset) { 418 mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset); 419 updateChildAlignments(); 420 } 421 422 public int getItemAlignmentOffset() { 423 return mItemAlignment.mainAxis().getItemAlignmentOffset(); 424 } 425 426 public void setItemAlignmentOffsetPercent(float offsetPercent) { 427 mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent); 428 updateChildAlignments(); 429 } 430 431 public float getItemAlignmentOffsetPercent() { 432 return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent(); 433 } 434 435 public void setItemAlignmentViewId(int viewId) { 436 mItemAlignment.mainAxis().setItemAlignmentViewId(viewId); 437 updateChildAlignments(); 438 } 439 440 public int getItemAlignmentViewId() { 441 return mItemAlignment.mainAxis().getItemAlignmentViewId(); 442 } 443 444 public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) { 445 mFocusOutFront = throughFront; 446 mFocusOutEnd = throughEnd; 447 } 448 449 public void setNumRows(int numRows) { 450 if (numRows < 0) throw new IllegalArgumentException(); 451 mNumRowsRequested = numRows; 452 mForceFullLayout = true; 453 } 454 455 public void setRowHeight(int height) { 456 if (height < 0) throw new IllegalArgumentException(); 457 mItemLengthSecondaryRequested = height; 458 } 459 460 public void setItemMargin(int margin) { 461 mVerticalMargin = mHorizontalMargin = margin; 462 mMarginPrimary = mMarginSecondary = margin; 463 } 464 465 public void setVerticalMargin(int margin) { 466 if (mOrientation == HORIZONTAL) { 467 mMarginSecondary = mVerticalMargin = margin; 468 } else { 469 mMarginPrimary = mVerticalMargin = margin; 470 } 471 } 472 473 public void setHorizontalMargin(int margin) { 474 if (mOrientation == HORIZONTAL) { 475 mMarginPrimary = mHorizontalMargin = margin; 476 } else { 477 mMarginSecondary = mHorizontalMargin = margin; 478 } 479 } 480 481 public int getVerticalMargin() { 482 return mVerticalMargin; 483 } 484 485 public int getHorizontalMargin() { 486 return mHorizontalMargin; 487 } 488 489 public void setGravity(int gravity) { 490 mGravity = gravity; 491 } 492 493 protected boolean hasDoneFirstLayout() { 494 return mGrid != null; 495 } 496 497 public void setOnChildSelectedListener(OnChildSelectedListener listener) { 498 mChildSelectedListener = listener; 499 } 500 501 private int getPositionByView(View view) { 502 return getPositionByIndex(mBaseGridView.indexOfChild(view)); 503 } 504 505 private int getPositionByIndex(int index) { 506 if (index < 0) { 507 return NO_POSITION; 508 } 509 return mFirstVisiblePos + index; 510 } 511 512 private View getViewByPosition(int position) { 513 int index = getIndexByPosition(position); 514 if (index < 0) { 515 return null; 516 } 517 return getChildAt(index); 518 } 519 520 private int getIndexByPosition(int position) { 521 if (mFirstVisiblePos < 0 || 522 position < mFirstVisiblePos || position > mLastVisiblePos) { 523 return NO_POSITION; 524 } 525 return position - mFirstVisiblePos; 526 } 527 528 private void dispatchChildSelected() { 529 if (mChildSelectedListener == null) { 530 return; 531 } 532 533 View view = getViewByPosition(mFocusPosition); 534 535 if (mFocusPosition != NO_POSITION) { 536 mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition, 537 mAdapter.getItemId(mFocusPosition)); 538 } else { 539 mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID); 540 } 541 } 542 543 @Override 544 public boolean canScrollHorizontally() { 545 // We can scroll horizontally if we have horizontal orientation, or if 546 // we are vertical and have more than one column. 547 return mOrientation == HORIZONTAL || mNumRows > 1; 548 } 549 550 @Override 551 public boolean canScrollVertically() { 552 // We can scroll vertically if we have vertical orientation, or if we 553 // are horizontal and have more than one row. 554 return mOrientation == VERTICAL || mNumRows > 1; 555 } 556 557 @Override 558 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 559 return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 560 ViewGroup.LayoutParams.WRAP_CONTENT); 561 } 562 563 @Override 564 public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) { 565 return new LayoutParams(context, attrs); 566 } 567 568 @Override 569 public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 570 if (lp instanceof LayoutParams) { 571 return new LayoutParams((LayoutParams) lp); 572 } else if (lp instanceof RecyclerView.LayoutParams) { 573 return new LayoutParams((RecyclerView.LayoutParams) lp); 574 } else if (lp instanceof MarginLayoutParams) { 575 return new LayoutParams((MarginLayoutParams) lp); 576 } else { 577 return new LayoutParams(lp); 578 } 579 } 580 581 protected View getViewForPosition(int position) { 582 View v = mRecycler.getViewForPosition(mAdapter, position); 583 if (v != null) { 584 ((LayoutParams) v.getLayoutParams()).onViewAttached(); 585 } 586 return v; 587 } 588 589 private int getViewMin(View v) { 590 LayoutParams p = (LayoutParams) v.getLayoutParams(); 591 return (mOrientation == HORIZONTAL) ? p.getOpticalLeft(v) : p.getOpticalTop(v); 592 } 593 594 private int getViewMax(View v) { 595 LayoutParams p = (LayoutParams) v.getLayoutParams(); 596 return (mOrientation == HORIZONTAL) ? p.getOpticalRight(v) : p.getOpticalBottom(v); 597 } 598 599 private int getViewCenter(View view) { 600 return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view); 601 } 602 603 private int getViewCenterSecondary(View view) { 604 return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view); 605 } 606 607 private int getViewCenterX(View v) { 608 LayoutParams p = (LayoutParams) v.getLayoutParams(); 609 return p.getOpticalLeft(v) + p.getAlignX(); 610 } 611 612 private int getViewCenterY(View v) { 613 LayoutParams p = (LayoutParams) v.getLayoutParams(); 614 return p.getOpticalTop(v) + p.getAlignY(); 615 } 616 617 /** 618 * Re-initialize data structures for a data change or handling invisible 619 * selection. The method tries its best to preserve position information so 620 * that staggered grid looks same before and after re-initialize. 621 * @param focusPosition The initial focusPosition that we would like to 622 * focus on. 623 * @return Actual position that can be focused on. 624 */ 625 private int init(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler, 626 int focusPosition) { 627 628 final int newItemCount = adapter.getItemCount(); 629 630 if (focusPosition == NO_POSITION && newItemCount > 0) { 631 // if focus position is never set before, initialize it to 0 632 focusPosition = 0; 633 } 634 // If adapter has changed then caches are invalid; otherwise, 635 // we try to maintain each row's position if number of rows keeps the same 636 // and existing mGrid contains the focusPosition. 637 if (mRows != null && mNumRows == mRows.length && 638 mGrid != null && mGrid.getSize() > 0 && focusPosition >= 0 && 639 focusPosition >= mGrid.getFirstIndex() && 640 focusPosition <= mGrid.getLastIndex()) { 641 // strip mGrid to a subset (like a column) that contains focusPosition 642 mGrid.stripDownTo(focusPosition); 643 // make sure that remaining items do not exceed new adapter size 644 int firstIndex = mGrid.getFirstIndex(); 645 int lastIndex = mGrid.getLastIndex(); 646 if (DEBUG) { 647 Log .v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " + lastIndex); 648 } 649 for (int i = lastIndex; i >=firstIndex; i--) { 650 if (i >= newItemCount) { 651 mGrid.removeLast(); 652 } 653 } 654 if (mGrid.getSize() == 0) { 655 focusPosition = newItemCount - 1; 656 // initialize row start locations 657 for (int i = 0; i < mNumRows; i++) { 658 mRows[i].low = 0; 659 mRows[i].high = 0; 660 } 661 if (DEBUG) Log.v(getTag(), "mGrid zero size"); 662 } else { 663 // initialize row start locations 664 for (int i = 0; i < mNumRows; i++) { 665 mRows[i].low = Integer.MAX_VALUE; 666 mRows[i].high = Integer.MIN_VALUE; 667 } 668 firstIndex = mGrid.getFirstIndex(); 669 lastIndex = mGrid.getLastIndex(); 670 if (focusPosition > lastIndex) { 671 focusPosition = mGrid.getLastIndex(); 672 } 673 if (DEBUG) { 674 Log.v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " 675 + lastIndex + " focusPosition " + focusPosition); 676 } 677 // fill rows with minimal view positions of the subset 678 for (int i = firstIndex; i <= lastIndex; i++) { 679 View v = getViewByPosition(i); 680 if (v == null) { 681 continue; 682 } 683 int row = mGrid.getLocation(i).row; 684 int low = getViewMin(v) + mScrollOffsetPrimary; 685 if (low < mRows[row].low) { 686 mRows[row].low = mRows[row].high = low; 687 } 688 } 689 // fill other rows that does not include the subset using first item 690 int firstItemRowPosition = mRows[mGrid.getLocation(firstIndex).row].low; 691 if (firstItemRowPosition == Integer.MAX_VALUE) { 692 firstItemRowPosition = 0; 693 } 694 for (int i = 0; i < mNumRows; i++) { 695 if (mRows[i].low == Integer.MAX_VALUE) { 696 mRows[i].low = mRows[i].high = firstItemRowPosition; 697 } 698 } 699 } 700 701 // Same adapter, we can reuse any attached views 702 detachAndScrapAttachedViews(recycler); 703 704 } else { 705 // otherwise recreate data structure 706 mRows = new StaggeredGrid.Row[mNumRows]; 707 for (int i = 0; i < mNumRows; i++) { 708 mRows[i] = new StaggeredGrid.Row(); 709 } 710 mGrid = new StaggeredGridDefault(); 711 if (newItemCount == 0) { 712 focusPosition = NO_POSITION; 713 } else if (focusPosition >= newItemCount) { 714 focusPosition = newItemCount - 1; 715 } 716 717 // Adapter may have changed so remove all attached views permanently 718 removeAllViews(); 719 720 mScrollOffsetPrimary = 0; 721 mScrollOffsetSecondary = 0; 722 mWindowAlignment.reset(); 723 } 724 725 mAdapter = adapter; 726 mRecycler = recycler; 727 mGrid.setProvider(mGridProvider); 728 // mGrid share the same Row array information 729 mGrid.setRows(mRows); 730 mFirstVisiblePos = mLastVisiblePos = NO_POSITION; 731 732 initScrollController(); 733 734 return focusPosition; 735 } 736 737 // TODO: use recyclerview support for measuring the whole container, once 738 // it's available. 739 void onMeasure(int widthSpec, int heightSpec, int[] result) { 740 int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary; 741 int measuredSizeSecondary; 742 if (mOrientation == HORIZONTAL) { 743 sizePrimary = MeasureSpec.getSize(widthSpec); 744 sizeSecondary = MeasureSpec.getSize(heightSpec); 745 modeSecondary = MeasureSpec.getMode(heightSpec); 746 paddingSecondary = getPaddingTop() + getPaddingBottom(); 747 } else { 748 sizeSecondary = MeasureSpec.getSize(widthSpec); 749 sizePrimary = MeasureSpec.getSize(heightSpec); 750 modeSecondary = MeasureSpec.getMode(widthSpec); 751 paddingSecondary = getPaddingLeft() + getPaddingRight(); 752 } 753 switch (modeSecondary) { 754 case MeasureSpec.UNSPECIFIED: 755 if (mItemLengthSecondaryRequested == 0) { 756 if (mOrientation == HORIZONTAL) { 757 throw new IllegalStateException("Must specify rowHeight or view height"); 758 } else { 759 throw new IllegalStateException("Must specify columnWidth or view width"); 760 } 761 } 762 mItemLengthSecondary = mItemLengthSecondaryRequested; 763 if (mNumRowsRequested == 0) { 764 mNumRows = 1; 765 } else { 766 mNumRows = mNumRowsRequested; 767 } 768 measuredSizeSecondary = mItemLengthSecondary * mNumRows + mMarginSecondary 769 * (mNumRows - 1) + paddingSecondary; 770 break; 771 case MeasureSpec.AT_MOST: 772 case MeasureSpec.EXACTLY: 773 if (mNumRowsRequested == 0 && mItemLengthSecondaryRequested == 0) { 774 mNumRows = 1; 775 mItemLengthSecondary = sizeSecondary - paddingSecondary; 776 } else if (mNumRowsRequested == 0) { 777 mItemLengthSecondary = mItemLengthSecondaryRequested; 778 mNumRows = (sizeSecondary + mMarginSecondary) 779 / (mItemLengthSecondaryRequested + mMarginSecondary); 780 } else if (mItemLengthSecondaryRequested == 0) { 781 mNumRows = mNumRowsRequested; 782 mItemLengthSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary 783 * (mNumRows - 1)) / mNumRows; 784 } else { 785 mNumRows = mNumRowsRequested; 786 mItemLengthSecondary = mItemLengthSecondaryRequested; 787 } 788 measuredSizeSecondary = sizeSecondary; 789 if (modeSecondary == MeasureSpec.AT_MOST) { 790 int childrenSize = mItemLengthSecondary * mNumRows + mMarginSecondary 791 * (mNumRows - 1) + paddingSecondary; 792 if (childrenSize < measuredSizeSecondary) { 793 measuredSizeSecondary = childrenSize; 794 } 795 } 796 break; 797 default: 798 throw new IllegalStateException("wrong spec"); 799 } 800 if (mOrientation == HORIZONTAL) { 801 result[0] = sizePrimary; 802 result[1] = measuredSizeSecondary; 803 } else { 804 result[0] = measuredSizeSecondary; 805 result[1] = sizePrimary; 806 } 807 if (DEBUG) { 808 Log.v(getTag(), "onMeasure result " + result[0] + ", " + result[1] 809 + " mItemLengthSecondary " + mItemLengthSecondary + " mNumRows " + mNumRows); 810 } 811 } 812 813 private void measureChild(View child) { 814 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 815 816 int widthSpec, heightSpec; 817 if (mOrientation == HORIZONTAL) { 818 widthSpec = ViewGroup.getChildMeasureSpec( 819 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, lp.width); 820 heightSpec = ViewGroup.getChildMeasureSpec(MeasureSpec.makeMeasureSpec( 821 mItemLengthSecondary, MeasureSpec.EXACTLY), 0, lp.height); 822 } else { 823 heightSpec = ViewGroup.getChildMeasureSpec( 824 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, lp.height); 825 widthSpec = ViewGroup.getChildMeasureSpec(MeasureSpec.makeMeasureSpec( 826 mItemLengthSecondary, MeasureSpec.EXACTLY), 0, lp.width); 827 } 828 829 child.measure(widthSpec, heightSpec); 830 } 831 832 private StaggeredGrid.Provider mGridProvider = new StaggeredGrid.Provider() { 833 834 @Override 835 public int getCount() { 836 return mAdapter.getItemCount(); 837 } 838 839 @Override 840 public void createItem(int index, int rowIndex, boolean append) { 841 View v = getViewForPosition(index); 842 if (mFirstVisiblePos >= 0) { 843 // when StaggeredGrid append or prepend item, we must guarantee 844 // that sibling item has created views already. 845 if (append && index != mLastVisiblePos + 1) { 846 throw new RuntimeException(); 847 } else if (!append && index != mFirstVisiblePos - 1) { 848 throw new RuntimeException(); 849 } 850 } 851 852 if (append) { 853 addView(v); 854 } else { 855 addView(v, 0); 856 } 857 858 measureChild(v); 859 860 int length = mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight(); 861 int start, end; 862 if (append) { 863 start = mRows[rowIndex].high; 864 if (start != mRows[rowIndex].low) { 865 // if there are existing item in the row, add margin between 866 start += mMarginPrimary; 867 } else { 868 final int lastRow = mRows.length - 1; 869 if (lastRow != rowIndex && mRows[lastRow].high != mRows[lastRow].low) { 870 // if there are existing item in the last row, insert 871 // the new item after the last item of last row. 872 start = mRows[lastRow].high + mMarginPrimary; 873 } 874 } 875 end = start + length; 876 mRows[rowIndex].high = end; 877 } else { 878 end = mRows[rowIndex].low; 879 if (end != mRows[rowIndex].high) { 880 end -= mMarginPrimary; 881 } else if (0 != rowIndex && mRows[0].high != mRows[0].low) { 882 // if there are existing item in the first row, insert 883 // the new item before the first item of first row. 884 end = mRows[0].low - mMarginPrimary; 885 } 886 start = end - length; 887 mRows[rowIndex].low = start; 888 } 889 if (mFirstVisiblePos < 0) { 890 mFirstVisiblePos = mLastVisiblePos = index; 891 } else { 892 if (append) { 893 mLastVisiblePos++; 894 } else { 895 mFirstVisiblePos--; 896 } 897 } 898 int startSecondary = rowIndex * (mItemLengthSecondary + mMarginSecondary); 899 layoutChild(v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary, 900 startSecondary - mScrollOffsetSecondary); 901 if (DEBUG) { 902 Log.d(getTag(), "addView " + index + " " + v); 903 } 904 updateScrollMin(); 905 updateScrollMax(); 906 } 907 }; 908 909 private void layoutChild(View v, int start, int end, int startSecondary) { 910 int measuredSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight() 911 : v.getMeasuredWidth(); 912 if (measuredSecondary > mItemLengthSecondary) { 913 measuredSecondary = mItemLengthSecondary; 914 } 915 final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; 916 final int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; 917 if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP 918 || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) { 919 // do nothing 920 } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM 921 || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) { 922 startSecondary += mItemLengthSecondary - measuredSecondary; 923 } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL 924 || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) { 925 startSecondary += (mItemLengthSecondary - measuredSecondary) / 2; 926 } 927 if (mOrientation == HORIZONTAL) { 928 v.layout(start, startSecondary, end, startSecondary + measuredSecondary); 929 } else { 930 v.layout(startSecondary, start, startSecondary + measuredSecondary, end); 931 } 932 updateChildAlignments(v); 933 } 934 935 private void updateChildAlignments(View v) { 936 LayoutParams p = (LayoutParams) v.getLayoutParams(); 937 p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v)); 938 p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v)); 939 } 940 941 private void updateChildAlignments() { 942 for (int i = 0, c = getChildCount(); i < c; i++) { 943 updateChildAlignments(getChildAt(i)); 944 } 945 } 946 947 /** 948 * append invisible view of saved location 949 */ 950 private void appendViewWithSavedLocation() { 951 int index = mLastVisiblePos + 1; 952 mGridProvider.createItem(index, mGrid.getLocation(index).row, true); 953 } 954 955 /** 956 * prepend invisible view of saved location 957 */ 958 private void prependViewWithSavedLocation() { 959 int index = mFirstVisiblePos - 1; 960 mGridProvider.createItem(index, mGrid.getLocation(index).row, false); 961 } 962 963 private boolean needsAppendVisibleItem() { 964 if (mLastVisiblePos < mFocusPosition) { 965 return true; 966 } 967 int right = mScrollOffsetPrimary + mSizePrimary; 968 for (int i = 0; i < mNumRows; i++) { 969 if (mRows[i].low == mRows[i].high) { 970 if (mRows[i].high < right) { 971 return true; 972 } 973 } else if (mRows[i].high < right - mMarginPrimary) { 974 return true; 975 } 976 } 977 return false; 978 } 979 980 private boolean needsPrependVisibleItem() { 981 if (mFirstVisiblePos > mFocusPosition) { 982 return true; 983 } 984 for (int i = 0; i < mNumRows; i++) { 985 if (mRows[i].low == mRows[i].high) { 986 if (mRows[i].low > mScrollOffsetPrimary) { 987 return true; 988 } 989 } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) { 990 return true; 991 } 992 } 993 return false; 994 } 995 996 // Append one column if possible and return true if reach end. 997 private boolean appendOneVisibleItem() { 998 if (mLastVisiblePos >= 0 && mLastVisiblePos < mGrid.getLastIndex()) { 999 appendViewWithSavedLocation(); 1000 } else if (mLastVisiblePos < mAdapter.getItemCount() - 1) { 1001 mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary); 1002 } else { 1003 return true; 1004 } 1005 return false; 1006 } 1007 1008 private void appendVisibleItems() { 1009 while (needsAppendVisibleItem()) { 1010 if (appendOneVisibleItem()) { 1011 break; 1012 } 1013 } 1014 } 1015 1016 // Prepend one column if possible and return true if reach end. 1017 private boolean prependOneVisibleItem() { 1018 if (mFirstVisiblePos > 0) { 1019 if (mFirstVisiblePos > mGrid.getFirstIndex()) { 1020 prependViewWithSavedLocation(); 1021 } else { 1022 mGrid.prependItems(mScrollOffsetPrimary); 1023 } 1024 } else { 1025 return true; 1026 } 1027 return false; 1028 } 1029 1030 private void prependVisibleItems() { 1031 while (needsPrependVisibleItem()) { 1032 if (prependOneVisibleItem()) { 1033 break; 1034 } 1035 } 1036 } 1037 1038 private void removeChildAt(int position) { 1039 View v = getViewByPosition(position); 1040 if (v != null) { 1041 if (DEBUG) { 1042 Log.d(getTag(), "removeAndRecycleViewAt " + position); 1043 } 1044 ((LayoutParams) v.getLayoutParams()).onViewDetached(); 1045 removeAndRecycleViewAt(getIndexByPosition(position), mRecycler); 1046 } 1047 } 1048 1049 private void removeInvisibleViewsAtEnd() { 1050 boolean update = false; 1051 while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) { 1052 View view = getViewByPosition(mLastVisiblePos); 1053 if (getViewMin(view) > mSizePrimary) { 1054 removeChildAt(mLastVisiblePos); 1055 mLastVisiblePos--; 1056 update = true; 1057 } else { 1058 break; 1059 } 1060 } 1061 if (update) { 1062 updateRowsMinMax(); 1063 } 1064 } 1065 1066 private void removeInvisibleViewsAtFront() { 1067 boolean update = false; 1068 while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) { 1069 View view = getViewByPosition(mFirstVisiblePos); 1070 if (getViewMax(view) < 0) { 1071 removeChildAt(mFirstVisiblePos); 1072 mFirstVisiblePos++; 1073 update = true; 1074 } else { 1075 break; 1076 } 1077 } 1078 if (update) { 1079 updateRowsMinMax(); 1080 } 1081 } 1082 1083 private void updateRowsMinMax() { 1084 if (mFirstVisiblePos < 0) { 1085 return; 1086 } 1087 for (int i = 0; i < mNumRows; i++) { 1088 mRows[i].low = Integer.MAX_VALUE; 1089 mRows[i].high = Integer.MIN_VALUE; 1090 } 1091 for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) { 1092 View view = getViewByPosition(i); 1093 int row = mGrid.getLocation(i).row; 1094 int low = getViewMin(view) + mScrollOffsetPrimary; 1095 if (low < mRows[row].low) { 1096 mRows[row].low = low; 1097 } 1098 int high = getViewMax(view) + mScrollOffsetPrimary; 1099 if (high > mRows[row].high) { 1100 mRows[row].high = high; 1101 } 1102 } 1103 } 1104 1105 /** 1106 * Relayout and re-positioning child for a possible new size and/or a new 1107 * start. 1108 * 1109 * @param view View to measure and layout. 1110 * @param start New start of the view or Integer.MIN_VALUE for not change. 1111 * @return New start of next view. 1112 */ 1113 private int updateChildView(View view, int start, int startSecondary) { 1114 if (start == Integer.MIN_VALUE) { 1115 start = getViewMin(view); 1116 } 1117 int end; 1118 if (mOrientation == HORIZONTAL) { 1119 if (view.isLayoutRequested() || view.getMeasuredHeight() != mItemLengthSecondary) { 1120 measureChild(view); 1121 } 1122 end = start + view.getMeasuredWidth(); 1123 } else { 1124 if (view.isLayoutRequested() || view.getMeasuredWidth() != mItemLengthSecondary) { 1125 measureChild(view); 1126 } 1127 end = start + view.getMeasuredHeight(); 1128 } 1129 1130 layoutChild(view, start, end, startSecondary); 1131 return end + mMarginPrimary; 1132 } 1133 1134 // create a temporary structure that remembers visible items from left to 1135 // right on each row 1136 private ArrayList<Integer>[] buildRows() { 1137 ArrayList<Integer>[] rows = new ArrayList[mNumRows]; 1138 for (int i = 0; i < mNumRows; i++) { 1139 rows[i] = new ArrayList(); 1140 } 1141 if (mFirstVisiblePos >= 0) { 1142 for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) { 1143 rows[mGrid.getLocation(i).row].add(i); 1144 } 1145 } 1146 return rows; 1147 } 1148 1149 // Fast layout when there is no structure change, adapter change, etc. 1150 protected void fastRelayout() { 1151 initScrollController(); 1152 1153 ArrayList<Integer>[] rows = buildRows(); 1154 1155 // relayout and repositioning views on each row 1156 for (int i = 0; i < mNumRows; i++) { 1157 ArrayList<Integer> row = rows[i]; 1158 int start = Integer.MIN_VALUE; 1159 int startSecondary = 1160 i * (mItemLengthSecondary + mMarginSecondary) - mScrollOffsetSecondary; 1161 for (int j = 0, size = row.size(); j < size; j++) { 1162 int position = row.get(j); 1163 start = updateChildView(getViewByPosition(position), start, startSecondary); 1164 } 1165 } 1166 1167 appendVisibleItems(); 1168 prependVisibleItems(); 1169 1170 updateRowsMinMax(); 1171 updateScrollMin(); 1172 updateScrollMax(); 1173 1174 View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition); 1175 if (focusView != null) { 1176 scrollToView(focusView, false); 1177 } 1178 } 1179 1180 // Lays out items based on the current scroll position 1181 @Override 1182 public void layoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler, 1183 boolean structureChanged) { 1184 if (DEBUG) { 1185 Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary " 1186 + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary 1187 + " structureChanged " + structureChanged 1188 + " mForceFullLayout " + mForceFullLayout); 1189 Log.v(getTag(), "width " + getWidth() + " height " + getHeight()); 1190 } 1191 1192 if (mNumRows == 0) { 1193 // haven't done measure yet 1194 return; 1195 } 1196 final int itemCount = adapter.getItemCount(); 1197 if (itemCount < 0) { 1198 return; 1199 } 1200 1201 mInLayout = true; 1202 1203 // Track the old focus view so we can adjust our system scroll position 1204 // so that any scroll animations happening now will remain valid. 1205 int delta = 0, deltaSecondary = 0; 1206 if (mFocusPosition != NO_POSITION) { 1207 View focusView = getViewByPosition(mFocusPosition); 1208 if (focusView != null) { 1209 delta = mWindowAlignment.mainAxis().getSystemScrollPos( 1210 getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary; 1211 deltaSecondary = 1212 mWindowAlignment.secondAxis().getSystemScrollPos( 1213 getViewCenterSecondary(focusView) + mScrollOffsetSecondary) 1214 - mScrollOffsetSecondary; 1215 } 1216 } 1217 1218 boolean hasDoneFirstLayout = hasDoneFirstLayout(); 1219 if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) { 1220 fastRelayout(); 1221 } else { 1222 boolean hadFocus = mBaseGridView.hasFocus(); 1223 1224 int newFocusPosition = init(adapter, recycler, mFocusPosition); 1225 if (DEBUG) { 1226 Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition " 1227 + newFocusPosition); 1228 } 1229 1230 // depending on result of init(), either recreating everything 1231 // or try to reuse the row start positions near mFocusPosition 1232 if (mGrid.getSize() == 0) { 1233 // this is a fresh creating all items, starting from 1234 // mFocusPosition with a estimated row index. 1235 mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT); 1236 1237 // Can't track the old focus view 1238 delta = deltaSecondary = 0; 1239 1240 } else { 1241 // mGrid remembers Locations for the column that 1242 // contains mFocusePosition and also mRows remembers start 1243 // positions of each row. 1244 // Manually re-create child views for that column 1245 int firstIndex = mGrid.getFirstIndex(); 1246 int lastIndex = mGrid.getLastIndex(); 1247 for (int i = firstIndex; i <= lastIndex; i++) { 1248 mGridProvider.createItem(i, mGrid.getLocation(i).row, true); 1249 } 1250 } 1251 // add visible views at end until reach the end of window 1252 appendVisibleItems(); 1253 // add visible views at front until reach the start of window 1254 prependVisibleItems(); 1255 // multiple rounds: scrollToView of first round may drag first/last child into 1256 // "visible window" and we update scrollMin/scrollMax then run second scrollToView 1257 int oldFirstVisible; 1258 int oldLastVisible; 1259 do { 1260 oldFirstVisible = mFirstVisiblePos; 1261 oldLastVisible = mLastVisiblePos; 1262 View focusView = getViewByPosition(newFocusPosition); 1263 // we need force to initialize the child view's position 1264 scrollToView(focusView, false); 1265 if (focusView != null && hadFocus) { 1266 focusView.requestFocus(); 1267 } 1268 appendVisibleItems(); 1269 prependVisibleItems(); 1270 removeInvisibleViewsAtFront(); 1271 removeInvisibleViewsAtEnd(); 1272 } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible); 1273 } 1274 mForceFullLayout = false; 1275 1276 scrollDirectionPrimary(-delta); 1277 scrollDirectionSecondary(-deltaSecondary); 1278 appendVisibleItems(); 1279 prependVisibleItems(); 1280 removeInvisibleViewsAtFront(); 1281 removeInvisibleViewsAtEnd(); 1282 1283 if (DEBUG) { 1284 StringWriter sw = new StringWriter(); 1285 PrintWriter pw = new PrintWriter(sw); 1286 mGrid.debugPrint(pw); 1287 Log.d(getTag(), sw.toString()); 1288 } 1289 1290 removeAndRecycleScrap(recycler); 1291 attemptAnimateLayoutChild(); 1292 1293 if (!hasDoneFirstLayout) { 1294 dispatchChildSelected(); 1295 } 1296 mInLayout = false; 1297 if (DEBUG) Log.v(getTag(), "layoutChildren end"); 1298 } 1299 1300 private void offsetChildrenSecondary(int increment) { 1301 final int childCount = getChildCount(); 1302 if (mOrientation == HORIZONTAL) { 1303 for (int i = 0; i < childCount; i++) { 1304 getChildAt(i).offsetTopAndBottom(increment); 1305 } 1306 } else { 1307 for (int i = 0; i < childCount; i++) { 1308 getChildAt(i).offsetLeftAndRight(increment); 1309 } 1310 } 1311 mScrollOffsetSecondary -= increment; 1312 } 1313 1314 private void offsetChildrenPrimary(int increment) { 1315 final int childCount = getChildCount(); 1316 if (mOrientation == VERTICAL) { 1317 for (int i = 0; i < childCount; i++) { 1318 getChildAt(i).offsetTopAndBottom(increment); 1319 } 1320 } else { 1321 for (int i = 0; i < childCount; i++) { 1322 getChildAt(i).offsetLeftAndRight(increment); 1323 } 1324 } 1325 mScrollOffsetPrimary -= increment; 1326 } 1327 1328 @Override 1329 public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler) { 1330 if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx); 1331 1332 if (mOrientation == HORIZONTAL) { 1333 return scrollDirectionPrimary(dx); 1334 } else { 1335 return scrollDirectionSecondary(dx); 1336 } 1337 } 1338 1339 @Override 1340 public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler) { 1341 if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy); 1342 if (mOrientation == VERTICAL) { 1343 return scrollDirectionPrimary(dy); 1344 } else { 1345 return scrollDirectionSecondary(dy); 1346 } 1347 } 1348 1349 // scroll in main direction may add/prune views 1350 private int scrollDirectionPrimary(int da) { 1351 offsetChildrenPrimary(-da); 1352 if (mInLayout) { 1353 return da; 1354 } 1355 if (da > 0) { 1356 appendVisibleItems(); 1357 removeInvisibleViewsAtFront(); 1358 } else if (da < 0) { 1359 prependVisibleItems(); 1360 removeInvisibleViewsAtEnd(); 1361 } 1362 attemptAnimateLayoutChild(); 1363 mBaseGridView.invalidate(); 1364 return da; 1365 } 1366 1367 // scroll in second direction will not add/prune views 1368 private int scrollDirectionSecondary(int dy) { 1369 offsetChildrenSecondary(-dy); 1370 mBaseGridView.invalidate(); 1371 return dy; 1372 } 1373 1374 private void updateScrollMax() { 1375 if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) { 1376 int maxEdge = Integer.MIN_VALUE; 1377 for (int i = 0; i < mRows.length; i++) { 1378 if (mRows[i].high > maxEdge) { 1379 maxEdge = mRows[i].high; 1380 } 1381 } 1382 mWindowAlignment.mainAxis().setMaxEdge(maxEdge); 1383 if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge); 1384 } 1385 } 1386 1387 private void updateScrollMin() { 1388 if (mFirstVisiblePos == 0) { 1389 int minEdge = Integer.MAX_VALUE; 1390 for (int i = 0; i < mRows.length; i++) { 1391 if (mRows[i].low < minEdge) { 1392 minEdge = mRows[i].low; 1393 } 1394 } 1395 mWindowAlignment.mainAxis().setMinEdge(minEdge); 1396 if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge); 1397 } 1398 } 1399 1400 private void initScrollController() { 1401 mWindowAlignment.horizontal.setSize(getWidth()); 1402 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 1403 mWindowAlignment.vertical.setSize(getHeight()); 1404 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 1405 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 1406 mWindowAlignment.mainAxis().invalidateScrollMin(); 1407 mWindowAlignment.mainAxis().invalidateScrollMax(); 1408 1409 // second axis min/max is determined at initialization, the mainAxis 1410 // min/max is determined later when we scroll to first or last item 1411 mWindowAlignment.secondAxis().setMinEdge(0); 1412 mWindowAlignment.secondAxis().setMaxEdge(mItemLengthSecondary * mNumRows + mMarginSecondary 1413 * (mNumRows - 1)); 1414 1415 if (DEBUG) { 1416 Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary + " " 1417 + " mItemLengthSecondary " + mItemLengthSecondary + " " + mWindowAlignment); 1418 } 1419 } 1420 1421 public void setSelection(RecyclerView parent, int position) { 1422 setSelection(parent, position, false); 1423 } 1424 1425 public void setSelectionSmooth(RecyclerView parent, int position) { 1426 setSelection(parent, position, true); 1427 } 1428 1429 public int getSelection() { 1430 return mFocusPosition; 1431 } 1432 1433 public void setSelection(RecyclerView parent, int position, boolean smooth) { 1434 if (mFocusPosition == position) { 1435 return; 1436 } 1437 View view = getViewByPosition(position); 1438 if (view != null) { 1439 scrollToView(view, smooth); 1440 } else { 1441 boolean right = position > mFocusPosition; 1442 mFocusPosition = position; 1443 if (smooth) { 1444 if (!hasDoneFirstLayout()) { 1445 Log.w(getTag(), "setSelectionSmooth should " + 1446 "not be called before first layout pass"); 1447 return; 1448 } 1449 if (right) { 1450 appendVisibleItems(); 1451 } else { 1452 prependVisibleItems(); 1453 } 1454 view = getViewByPosition(position); 1455 if (view != null) { 1456 scrollToView(view, smooth); 1457 } 1458 } else { 1459 mForceFullLayout = true; 1460 parent.requestLayout(); 1461 } 1462 } 1463 } 1464 1465 @Override 1466 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 1467 boolean needsLayout = false; 1468 if (itemCount != 0) { 1469 if (mFirstVisiblePos < 0) { 1470 needsLayout = true; 1471 } else if (!(positionStart > mLastVisiblePos + 1 || 1472 positionStart + itemCount < mFirstVisiblePos - 1)) { 1473 needsLayout = true; 1474 } 1475 } 1476 if (needsLayout) { 1477 recyclerView.requestLayout(); 1478 } 1479 } 1480 1481 @Override 1482 public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) { 1483 if (!mInLayout) { 1484 scrollToView(child, true); 1485 } 1486 return true; 1487 } 1488 1489 @Override 1490 public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect, 1491 boolean immediate) { 1492 if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect); 1493 return false; 1494 } 1495 1496 int getScrollOffsetX() { 1497 return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary; 1498 } 1499 1500 int getScrollOffsetY() { 1501 return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary; 1502 } 1503 1504 public void getViewSelectedOffsets(View view, int[] offsets) { 1505 int scrollOffsetX = getScrollOffsetX(); 1506 int scrollOffsetY = getScrollOffsetY(); 1507 int viewCenterX = scrollOffsetX + getViewCenterX(view); 1508 int viewCenterY = scrollOffsetY + getViewCenterY(view); 1509 offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX; 1510 offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY; 1511 } 1512 1513 /** 1514 * Scroll to a given child view and change mFocusPosition. 1515 */ 1516 private void scrollToView(View view, boolean smooth) { 1517 int newFocusPosition = getPositionByView(view); 1518 if (mInLayout || newFocusPosition != mFocusPosition) { 1519 mFocusPosition = newFocusPosition; 1520 dispatchChildSelected(); 1521 } 1522 if (view == null) { 1523 return; 1524 } 1525 if (!view.hasFocus() && mBaseGridView.hasFocus()) { 1526 // transfer focus to the child if it does not have focus yet (e.g. triggered 1527 // by setSelection()) 1528 view.requestFocus(); 1529 } 1530 int viewCenterY = getScrollOffsetY() + getViewCenterY(view); 1531 int viewCenterX = getScrollOffsetX() + getViewCenterX(view); 1532 if (DEBUG) { 1533 Log.v(getTag(), "scrollToView smooth=" + smooth + " pos=" + mFocusPosition + " " 1534 + viewCenterX+","+viewCenterY + " " + mWindowAlignment); 1535 } 1536 1537 if (mInLayout || viewCenterX != mWindowAlignment.horizontal.getScrollCenter() 1538 || viewCenterY != mWindowAlignment.vertical.getScrollCenter()) { 1539 mWindowAlignment.horizontal.updateScrollCenter(viewCenterX); 1540 mWindowAlignment.vertical.updateScrollCenter(viewCenterY); 1541 int scrollX = mWindowAlignment.horizontal.getSystemScrollPos(); 1542 int scrollY = mWindowAlignment.vertical.getSystemScrollPos(); 1543 if (DEBUG) { 1544 Log.v(getTag(), "adjustSystemScrollPos " + scrollX + " " + scrollY + " " 1545 + mWindowAlignment); 1546 } 1547 1548 scrollX -= getScrollOffsetX(); 1549 scrollY -= getScrollOffsetY(); 1550 1551 if (DEBUG) Log.v(getTag(), "scrollX " + scrollX + " scrollY " + scrollY); 1552 1553 if (mInLayout) { 1554 if (mOrientation == HORIZONTAL) { 1555 scrollDirectionPrimary(scrollX); 1556 scrollDirectionSecondary(scrollY); 1557 } else { 1558 scrollDirectionPrimary(scrollY); 1559 scrollDirectionSecondary(scrollX); 1560 } 1561 } else if (smooth) { 1562 mBaseGridView.smoothScrollBy(scrollX, scrollY); 1563 } else { 1564 mBaseGridView.scrollBy(scrollX, scrollY); 1565 } 1566 } 1567 } 1568 1569 public void setAnimateChildLayout(boolean animateChildLayout) { 1570 mAnimateChildLayout = animateChildLayout; 1571 if (!mAnimateChildLayout) { 1572 for (int i = 0, c = getChildCount(); i < c; i++) { 1573 ((LayoutParams) getChildAt(i).getLayoutParams()).endAnimate(); 1574 } 1575 } 1576 } 1577 1578 private void attemptAnimateLayoutChild() { 1579 for (int i = 0, c = getChildCount(); i < c; i++) { 1580 // TODO: start delay can be staggered 1581 View v = getChildAt(i); 1582 ((LayoutParams) v.getLayoutParams()).startAnimate(this, v, 0); 1583 } 1584 } 1585 1586 public boolean isChildLayoutAnimated() { 1587 return mAnimateChildLayout; 1588 } 1589 1590 public void setChildLayoutAnimationInterpolator(Interpolator interpolator) { 1591 mAnimateLayoutChildInterpolator = interpolator; 1592 } 1593 1594 public Interpolator getChildLayoutAnimationInterpolator() { 1595 return mAnimateLayoutChildInterpolator; 1596 } 1597 1598 public void setChildLayoutAnimationDuration(long duration) { 1599 mAnimateLayoutChildDuration = duration; 1600 } 1601 1602 public long getChildLayoutAnimationDuration() { 1603 return mAnimateLayoutChildDuration; 1604 } 1605 1606 private int findImmediateChildIndex(View view) { 1607 while (view != null && view != mBaseGridView) { 1608 int index = mBaseGridView.indexOfChild(view); 1609 if (index >= 0) { 1610 return index; 1611 } 1612 view = (View) view.getParent(); 1613 } 1614 return NO_POSITION; 1615 } 1616 1617 @Override 1618 public boolean onAddFocusables(RecyclerView recyclerView, 1619 ArrayList<View> views, int direction, int focusableMode) { 1620 // If this viewgroup or one of its children currently has focus then we 1621 // consider our children for focus searching. 1622 // Otherwise, we only want the system to ignore our children and pass 1623 // focus to the viewgroup, which will pass focus on to its children 1624 // appropriately. 1625 if (recyclerView.hasFocus()) { 1626 final int movement = getMovement(direction); 1627 if (movement != PREV_ITEM && movement != NEXT_ITEM) { 1628 // Move on secondary direction uses default addFocusables(). 1629 return false; 1630 } 1631 // Get current focus row. 1632 final View focused = recyclerView.findFocus(); 1633 final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused)); 1634 final int focusedRow = mGrid != null && focusedPos != NO_POSITION ? 1635 mGrid.getLocation(focusedPos).row : NO_POSITION; 1636 // Add focusables within the same row. 1637 final int focusableCount = views.size(); 1638 final int descendantFocusability = recyclerView.getDescendantFocusability(); 1639 if (mGrid != null && descendantFocusability != ViewGroup.FOCUS_BLOCK_DESCENDANTS) { 1640 for (int i = 0, count = getChildCount(); i < count; i++) { 1641 final View child = getChildAt(i); 1642 if (child.getVisibility() != View.VISIBLE) { 1643 continue; 1644 } 1645 StaggeredGrid.Location loc = mGrid.getLocation(getPositionByIndex(i)); 1646 if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) { 1647 child.addFocusables(views, direction, focusableMode); 1648 } 1649 } 1650 } 1651 // From ViewGroup.addFocusables(): 1652 // we add ourselves (if focusable) in all cases except for when we are 1653 // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is 1654 // to avoid the focus search finding layouts when a more precise search 1655 // among the focusable children would be more interesting. 1656 if (descendantFocusability != ViewGroup.FOCUS_AFTER_DESCENDANTS 1657 // No focusable descendants 1658 || (focusableCount == views.size())) { 1659 if (recyclerView.isFocusable()) { 1660 views.add(recyclerView); 1661 } 1662 } 1663 } else { 1664 if (recyclerView.isFocusable()) { 1665 views.add(recyclerView); 1666 } 1667 } 1668 return true; 1669 } 1670 1671 @Override 1672 public View onFocusSearchFailed(View focused, int direction, Adapter adapter, 1673 Recycler recycler) { 1674 if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction); 1675 1676 View view = null; 1677 int movement = getMovement(direction); 1678 final FocusFinder ff = FocusFinder.getInstance(); 1679 if (movement == NEXT_ITEM) { 1680 while (view == null && !appendOneVisibleItem()) { 1681 view = ff.findNextFocus(mBaseGridView, focused, direction); 1682 } 1683 } else if (movement == PREV_ITEM){ 1684 while (view == null && !prependOneVisibleItem()) { 1685 view = ff.findNextFocus(mBaseGridView, focused, direction); 1686 } 1687 } 1688 if (view == null) { 1689 // returning the same view to prevent focus lost when scrolling past the end of the list 1690 if (movement == PREV_ITEM) { 1691 view = mFocusOutFront ? null : focused; 1692 } else if (movement == NEXT_ITEM){ 1693 view = mFocusOutEnd ? null : focused; 1694 } 1695 } 1696 if (DEBUG) Log.v(getTag(), "returning view " + view); 1697 return view; 1698 } 1699 1700 private final static int PREV_ITEM = 0; 1701 private final static int NEXT_ITEM = 1; 1702 private final static int PREV_ROW = 2; 1703 private final static int NEXT_ROW = 3; 1704 1705 boolean focusSelectedChild(int direction, Rect previouslyFocusedRect) { 1706 View view = getViewByPosition(mFocusPosition); 1707 if (view != null) { 1708 if (!view.requestFocus(direction, previouslyFocusedRect)) { 1709 if (DEBUG) { 1710 Log.w(getTag(), "failed to request focus on " + view); 1711 } 1712 } else { 1713 return true; 1714 } 1715 } 1716 return false; 1717 } 1718 1719 private int getMovement(int direction) { 1720 int movement = View.FOCUS_LEFT; 1721 1722 if (mOrientation == HORIZONTAL) { 1723 switch(direction) { 1724 case View.FOCUS_LEFT: 1725 movement = PREV_ITEM; 1726 break; 1727 case View.FOCUS_RIGHT: 1728 movement = NEXT_ITEM; 1729 break; 1730 case View.FOCUS_UP: 1731 movement = PREV_ROW; 1732 break; 1733 case View.FOCUS_DOWN: 1734 movement = NEXT_ROW; 1735 break; 1736 } 1737 } else if (mOrientation == VERTICAL) { 1738 switch(direction) { 1739 case View.FOCUS_LEFT: 1740 movement = PREV_ROW; 1741 break; 1742 case View.FOCUS_RIGHT: 1743 movement = NEXT_ROW; 1744 break; 1745 case View.FOCUS_UP: 1746 movement = PREV_ITEM; 1747 break; 1748 case View.FOCUS_DOWN: 1749 movement = NEXT_ITEM; 1750 break; 1751 } 1752 } 1753 1754 return movement; 1755 } 1756 1757 @Override 1758 public void onAdapterChanged(RecyclerView.Adapter oldAdapter, 1759 RecyclerView.Adapter newAdapter) { 1760 mGrid = null; 1761 mRows = null; 1762 super.onAdapterChanged(oldAdapter, newAdapter); 1763 } 1764 1765} 1766