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