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