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