GridLayoutManager.java revision 9e0a5a45e54dab959e171b1c82f5f07a82c7d0fc
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 // the maxScroll for currently last visible item is larger, 1758 // so we must invalidate the max scroll value. 1759 if (maxScroll > mWindowAlignment.mainAxis().getMaxScroll()) { 1760 mWindowAlignment.mainAxis().invalidateScrollMax(); 1761 if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be " 1762 + "greater than " + maxScroll); 1763 } 1764 } 1765 } 1766 1767 private void updateScrollMin() { 1768 int lowVisiblePos = (!mReverseFlowPrimary) ? mGrid.getFirstVisibleIndex() 1769 : mGrid.getLastVisibleIndex(); 1770 int lowMinPos = (!mReverseFlowPrimary) ? 0 : mState.getItemCount() - 1; 1771 if (lowVisiblePos < 0) { 1772 return; 1773 } 1774 final boolean lowAvailable = lowVisiblePos == lowMinPos; 1775 final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown(); 1776 if (!lowAvailable && minUnknown) { 1777 return; 1778 } 1779 int minEdge = mGrid.findRowMin(false, sTwoInts) + mScrollOffsetPrimary; 1780 int rowIndex = sTwoInts[0]; 1781 int pos = sTwoInts[1]; 1782 int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge(); 1783 mWindowAlignment.mainAxis().setMinEdge(minEdge); 1784 int minScroll = getPrimarySystemScrollPosition(findViewByPosition(pos)); 1785 mWindowAlignment.mainAxis().setMinEdge(savedMinEdge); 1786 1787 if (lowAvailable) { 1788 mWindowAlignment.mainAxis().setMinEdge(minEdge); 1789 mWindowAlignment.mainAxis().setMinScroll(minScroll); 1790 if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge + 1791 " scrollMin to " + minScroll); 1792 } else { 1793 // the minScroll for currently first visible item is smaller, 1794 // so we must invalidate the min scroll value. 1795 if (minScroll < mWindowAlignment.mainAxis().getMinScroll()) { 1796 mWindowAlignment.mainAxis().invalidateScrollMin(); 1797 if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be " 1798 + "less than " + minScroll); 1799 } 1800 } 1801 } 1802 1803 private void updateScrollSecondAxis() { 1804 mWindowAlignment.secondAxis().setMinEdge(0); 1805 mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary()); 1806 } 1807 1808 private void initScrollController() { 1809 mWindowAlignment.reset(); 1810 mWindowAlignment.horizontal.setSize(getWidth()); 1811 mWindowAlignment.vertical.setSize(getHeight()); 1812 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 1813 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 1814 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 1815 mScrollOffsetPrimary = -mWindowAlignment.mainAxis().getPaddingLow(); 1816 mScrollOffsetSecondary = -mWindowAlignment.secondAxis().getPaddingLow(); 1817 1818 if (DEBUG) { 1819 Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary 1820 + " mWindowAlignment " + mWindowAlignment 1821 + " mScrollOffsetPrimary " + mScrollOffsetPrimary); 1822 } 1823 } 1824 1825 private void updateScrollController() { 1826 // mScrollOffsetPrimary and mScrollOffsetSecondary includes the padding. 1827 // e.g. when topPadding is 16 for horizontal grid view, the initial 1828 // mScrollOffsetSecondary is -16. fastRelayout() put views based on offsets(not padding), 1829 // when padding changes to 20, we also need update mScrollOffsetSecondary to -20 before 1830 // fastRelayout() is performed 1831 int paddingPrimaryDiff, paddingSecondaryDiff; 1832 if (mOrientation == HORIZONTAL) { 1833 paddingPrimaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow(); 1834 paddingSecondaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow(); 1835 } else { 1836 paddingPrimaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow(); 1837 paddingSecondaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow(); 1838 } 1839 mScrollOffsetPrimary -= paddingPrimaryDiff; 1840 mScrollOffsetSecondary -= paddingSecondaryDiff; 1841 1842 mWindowAlignment.horizontal.setSize(getWidth()); 1843 mWindowAlignment.vertical.setSize(getHeight()); 1844 mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight()); 1845 mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom()); 1846 mSizePrimary = mWindowAlignment.mainAxis().getSize(); 1847 1848 if (DEBUG) { 1849 Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary 1850 + " mWindowAlignment " + mWindowAlignment 1851 + " mScrollOffsetPrimary " + mScrollOffsetPrimary); 1852 } 1853 } 1854 1855 public void setSelection(RecyclerView parent, int position) { 1856 setSelection(parent, position, false); 1857 } 1858 1859 public void setSelectionSmooth(RecyclerView parent, int position) { 1860 setSelection(parent, position, true); 1861 } 1862 1863 public int getSelection() { 1864 return mFocusPosition; 1865 } 1866 1867 public void setSelection(RecyclerView parent, int position, boolean smooth) { 1868 if (mFocusPosition != position && position != NO_POSITION) { 1869 scrollToSelection(parent, position, smooth); 1870 } 1871 } 1872 1873 private void scrollToSelection(RecyclerView parent, int position, boolean smooth) { 1874 if (TRACE) TraceHelper.beginSection("scrollToSelection"); 1875 View view = findViewByPosition(position); 1876 if (view != null) { 1877 mInSelection = true; 1878 scrollToView(view, smooth); 1879 mInSelection = false; 1880 } else { 1881 mFocusPosition = position; 1882 mFocusPositionOffset = 0; 1883 if (!mLayoutEnabled) { 1884 return; 1885 } 1886 if (smooth) { 1887 if (!hasDoneFirstLayout()) { 1888 Log.w(getTag(), "setSelectionSmooth should " + 1889 "not be called before first layout pass"); 1890 return; 1891 } 1892 startPositionSmoothScroller(position); 1893 } else { 1894 mForceFullLayout = true; 1895 parent.requestLayout(); 1896 } 1897 } 1898 if (TRACE) TraceHelper.endSection(); 1899 } 1900 1901 void startPositionSmoothScroller(int position) { 1902 LinearSmoothScroller linearSmoothScroller = 1903 new GridLinearSmoothScroller() { 1904 @Override 1905 public PointF computeScrollVectorForPosition(int targetPosition) { 1906 if (getChildCount() == 0) { 1907 return null; 1908 } 1909 final int firstChildPos = getPosition(getChildAt(0)); 1910 // TODO We should be able to deduce direction from bounds of current and target 1911 // focus, rather than making assumptions about positions and directionality 1912 final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos 1913 : targetPosition < firstChildPos; 1914 final int direction = isStart ? -1 : 1; 1915 if (mOrientation == HORIZONTAL) { 1916 return new PointF(direction, 0); 1917 } else { 1918 return new PointF(0, direction); 1919 } 1920 } 1921 1922 }; 1923 linearSmoothScroller.setTargetPosition(position); 1924 startSmoothScroll(linearSmoothScroller); 1925 } 1926 1927 private void processPendingMovement(boolean forward) { 1928 if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) { 1929 return; 1930 } 1931 if (mPendingMoveSmoothScroller == null) { 1932 // Stop existing scroller and create a new PendingMoveSmoothScroller. 1933 mBaseGridView.stopScroll(); 1934 PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller( 1935 forward ? 1 : -1); 1936 mFocusPositionOffset = 0; 1937 startSmoothScroll(linearSmoothScroller); 1938 if (linearSmoothScroller.isRunning()) { 1939 mPendingMoveSmoothScroller = linearSmoothScroller; 1940 } 1941 } else { 1942 if (forward) { 1943 mPendingMoveSmoothScroller.increasePendingMoves(); 1944 } else { 1945 mPendingMoveSmoothScroller.decreasePendingMoves(); 1946 } 1947 } 1948 } 1949 1950 @Override 1951 public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { 1952 if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart " 1953 + positionStart + " itemCount " + itemCount); 1954 if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) { 1955 int pos = mFocusPosition + mFocusPositionOffset; 1956 if (positionStart <= pos) { 1957 mFocusPositionOffset += itemCount; 1958 } 1959 } 1960 mChildrenStates.clear(); 1961 } 1962 1963 @Override 1964 public void onItemsChanged(RecyclerView recyclerView) { 1965 if (DEBUG) Log.v(getTag(), "onItemsChanged"); 1966 mFocusPositionOffset = 0; 1967 mChildrenStates.clear(); 1968 } 1969 1970 @Override 1971 public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { 1972 if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart " 1973 + positionStart + " itemCount " + itemCount); 1974 if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) { 1975 int pos = mFocusPosition + mFocusPositionOffset; 1976 if (positionStart <= pos) { 1977 if (positionStart + itemCount > pos) { 1978 // stop updating offset after the focus item was removed 1979 mFocusPositionOffset = Integer.MIN_VALUE; 1980 } else { 1981 mFocusPositionOffset -= itemCount; 1982 } 1983 } 1984 } 1985 mChildrenStates.clear(); 1986 } 1987 1988 @Override 1989 public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition, 1990 int itemCount) { 1991 if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition " 1992 + fromPosition + " toPosition " + toPosition); 1993 if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) { 1994 int pos = mFocusPosition + mFocusPositionOffset; 1995 if (fromPosition <= pos && pos < fromPosition + itemCount) { 1996 // moved items include focused position 1997 mFocusPositionOffset += toPosition - fromPosition; 1998 } else if (fromPosition < pos && toPosition > pos - itemCount) { 1999 // move items before focus position to after focused position 2000 mFocusPositionOffset -= itemCount; 2001 } else if (fromPosition > pos && toPosition < pos) { 2002 // move items after focus position to before focused position 2003 mFocusPositionOffset += itemCount; 2004 } 2005 } 2006 mChildrenStates.clear(); 2007 } 2008 2009 @Override 2010 public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) { 2011 if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart " 2012 + positionStart + " itemCount " + itemCount); 2013 for (int i = positionStart, end = positionStart + itemCount; i < end; i++) { 2014 mChildrenStates.remove(i); 2015 } 2016 } 2017 2018 @Override 2019 public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) { 2020 if (mFocusSearchDisabled) { 2021 return true; 2022 } 2023 if (getPositionByView(child) == NO_POSITION) { 2024 // This shouldn't happen, but in case it does be sure not to attempt a 2025 // scroll to a view whose item has been removed. 2026 return true; 2027 } 2028 if (!mInLayout && !mInSelection) { 2029 scrollToView(child, true); 2030 } 2031 return true; 2032 } 2033 2034 @Override 2035 public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect, 2036 boolean immediate) { 2037 if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect); 2038 return false; 2039 } 2040 2041 int getScrollOffsetX() { 2042 return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary; 2043 } 2044 2045 int getScrollOffsetY() { 2046 return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary; 2047 } 2048 2049 public void getViewSelectedOffsets(View view, int[] offsets) { 2050 if (mOrientation == HORIZONTAL) { 2051 offsets[0] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary; 2052 offsets[1] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary; 2053 } else { 2054 offsets[1] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary; 2055 offsets[0] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary; 2056 } 2057 } 2058 2059 private int getPrimarySystemScrollPosition(View view) { 2060 final int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view); 2061 final int viewMin = getViewMin(view); 2062 final int viewMax = getViewMax(view); 2063 // TODO: change to use State object in onRequestChildFocus() 2064 boolean isMin, isMax; 2065 if (!mReverseFlowPrimary) { 2066 isMin = mGrid.getFirstVisibleIndex() == 0; 2067 isMax = mGrid.getLastVisibleIndex() == (mState == null ? 2068 getItemCount() : mState.getItemCount()) - 1; 2069 } else { 2070 isMax = mGrid.getFirstVisibleIndex() == 0; 2071 isMin = mGrid.getLastVisibleIndex() == (mState == null ? 2072 getItemCount() : mState.getItemCount()) - 1; 2073 } 2074 for (int i = getChildCount() - 1; (isMin || isMax) && i >= 0; i--) { 2075 View v = getChildAt(i); 2076 if (v == view || v == null) { 2077 continue; 2078 } 2079 if (isMin && getViewMin(v) < viewMin) { 2080 isMin = false; 2081 } 2082 if (isMax && getViewMax(v) > viewMax) { 2083 isMax = false; 2084 } 2085 } 2086 return mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary, isMin, isMax); 2087 } 2088 2089 private int getSecondarySystemScrollPosition(View view) { 2090 int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view); 2091 int pos = getPositionByView(view); 2092 Grid.Location location = mGrid.getLocation(pos); 2093 final int row = location.row; 2094 final boolean isMin, isMax; 2095 if (!mReverseFlowSecondary) { 2096 isMin = row == 0; 2097 isMax = row == mGrid.getNumRows() - 1; 2098 } else { 2099 isMax = row == 0; 2100 isMin = row == mGrid.getNumRows() - 1; 2101 } 2102 return mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary, isMin, isMax); 2103 } 2104 2105 /** 2106 * Scroll to a given child view and change mFocusPosition. 2107 */ 2108 private void scrollToView(View view, boolean smooth) { 2109 int newFocusPosition = getPositionByView(view); 2110 if (newFocusPosition != mFocusPosition) { 2111 mFocusPosition = newFocusPosition; 2112 mFocusPositionOffset = 0; 2113 if (!mInLayout) { 2114 dispatchChildSelected(); 2115 } 2116 } 2117 if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) { 2118 mBaseGridView.invalidate(); 2119 } 2120 if (view == null) { 2121 return; 2122 } 2123 if (!view.hasFocus() && mBaseGridView.hasFocus()) { 2124 // transfer focus to the child if it does not have focus yet (e.g. triggered 2125 // by setSelection()) 2126 view.requestFocus(); 2127 } 2128 if (!mScrollEnabled && smooth) { 2129 return; 2130 } 2131 if (getScrollPosition(view, sTwoInts)) { 2132 scrollGrid(sTwoInts[0], sTwoInts[1], smooth); 2133 } 2134 } 2135 2136 private boolean getScrollPosition(View view, int[] deltas) { 2137 switch (mFocusScrollStrategy) { 2138 case BaseGridView.FOCUS_SCROLL_ALIGNED: 2139 default: 2140 return getAlignedPosition(view, deltas); 2141 case BaseGridView.FOCUS_SCROLL_ITEM: 2142 case BaseGridView.FOCUS_SCROLL_PAGE: 2143 return getNoneAlignedPosition(view, deltas); 2144 } 2145 } 2146 2147 private boolean getNoneAlignedPosition(View view, int[] deltas) { 2148 int pos = getPositionByView(view); 2149 int viewMin = getViewMin(view); 2150 int viewMax = getViewMax(view); 2151 // we either align "firstView" to left/top padding edge 2152 // or align "lastView" to right/bottom padding edge 2153 View firstView = null; 2154 View lastView = null; 2155 int paddingLow = mWindowAlignment.mainAxis().getPaddingLow(); 2156 int clientSize = mWindowAlignment.mainAxis().getClientSize(); 2157 final int row = mGrid.getRowIndex(pos); 2158 if (viewMin < paddingLow) { 2159 // view enters low padding area: 2160 firstView = view; 2161 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 2162 // scroll one "page" left/top, 2163 // align first visible item of the "page" at the low padding edge. 2164 while (prependOneColumnVisibleItems()) { 2165 CircularIntArray positions = 2166 mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row]; 2167 firstView = findViewByPosition(positions.get(0)); 2168 if (viewMax - getViewMin(firstView) > clientSize) { 2169 if (positions.size() > 2) { 2170 firstView = findViewByPosition(positions.get(2)); 2171 } 2172 break; 2173 } 2174 } 2175 } 2176 } else if (viewMax > clientSize + paddingLow) { 2177 // view enters high padding area: 2178 if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) { 2179 // scroll whole one page right/bottom, align view at the low padding edge. 2180 firstView = view; 2181 do { 2182 CircularIntArray positions = 2183 mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row]; 2184 lastView = findViewByPosition(positions.get(positions.size() - 1)); 2185 if (getViewMax(lastView) - viewMin > clientSize) { 2186 lastView = null; 2187 break; 2188 } 2189 } while (appendOneColumnVisibleItems()); 2190 if (lastView != null) { 2191 // however if we reached end, we should align last view. 2192 firstView = null; 2193 } 2194 } else { 2195 lastView = view; 2196 } 2197 } 2198 int scrollPrimary = 0; 2199 int scrollSecondary = 0; 2200 if (firstView != null) { 2201 scrollPrimary = getViewMin(firstView) - paddingLow; 2202 } else if (lastView != null) { 2203 scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize); 2204 } 2205 View secondaryAlignedView; 2206 if (firstView != null) { 2207 secondaryAlignedView = firstView; 2208 } else if (lastView != null) { 2209 secondaryAlignedView = lastView; 2210 } else { 2211 secondaryAlignedView = view; 2212 } 2213 scrollSecondary = getSecondarySystemScrollPosition(secondaryAlignedView); 2214 scrollSecondary -= mScrollOffsetSecondary; 2215 if (scrollPrimary != 0 || scrollSecondary != 0) { 2216 deltas[0] = scrollPrimary; 2217 deltas[1] = scrollSecondary; 2218 return true; 2219 } 2220 return false; 2221 } 2222 2223 private boolean getAlignedPosition(View view, int[] deltas) { 2224 int scrollPrimary = getPrimarySystemScrollPosition(view); 2225 int scrollSecondary = getSecondarySystemScrollPosition(view); 2226 if (DEBUG) { 2227 Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary 2228 + " " + mWindowAlignment); 2229 Log.v(getTag(), "getAlignedPosition " + mScrollOffsetPrimary + " " + mScrollOffsetSecondary); 2230 } 2231 scrollPrimary -= mScrollOffsetPrimary; 2232 scrollSecondary -= mScrollOffsetSecondary; 2233 if (scrollPrimary != 0 || scrollSecondary != 0) { 2234 deltas[0] = scrollPrimary; 2235 deltas[1] = scrollSecondary; 2236 return true; 2237 } 2238 return false; 2239 } 2240 2241 private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) { 2242 if (mInLayout) { 2243 scrollDirectionPrimary(scrollPrimary); 2244 scrollDirectionSecondary(scrollSecondary); 2245 } else { 2246 int scrollX; 2247 int scrollY; 2248 if (mOrientation == HORIZONTAL) { 2249 scrollX = scrollPrimary; 2250 scrollY = scrollSecondary; 2251 } else { 2252 scrollX = scrollSecondary; 2253 scrollY = scrollPrimary; 2254 } 2255 if (smooth) { 2256 mBaseGridView.smoothScrollBy(scrollX, scrollY); 2257 } else { 2258 mBaseGridView.scrollBy(scrollX, scrollY); 2259 } 2260 } 2261 } 2262 2263 public void setPruneChild(boolean pruneChild) { 2264 if (mPruneChild != pruneChild) { 2265 mPruneChild = pruneChild; 2266 if (mPruneChild) { 2267 requestLayout(); 2268 } 2269 } 2270 } 2271 2272 public boolean getPruneChild() { 2273 return mPruneChild; 2274 } 2275 2276 public void setScrollEnabled(boolean scrollEnabled) { 2277 if (mScrollEnabled != scrollEnabled) { 2278 mScrollEnabled = scrollEnabled; 2279 if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED 2280 && mFocusPosition != NO_POSITION) { 2281 scrollToSelection(mBaseGridView, mFocusPosition, true); 2282 } 2283 } 2284 } 2285 2286 public boolean isScrollEnabled() { 2287 return mScrollEnabled; 2288 } 2289 2290 private int findImmediateChildIndex(View view) { 2291 while (view != null && view != mBaseGridView) { 2292 int index = mBaseGridView.indexOfChild(view); 2293 if (index >= 0) { 2294 return index; 2295 } 2296 view = (View) view.getParent(); 2297 } 2298 return NO_POSITION; 2299 } 2300 2301 void setFocusSearchDisabled(boolean disabled) { 2302 mFocusSearchDisabled = disabled; 2303 } 2304 2305 boolean isFocusSearchDisabled() { 2306 return mFocusSearchDisabled; 2307 } 2308 2309 @Override 2310 public View onInterceptFocusSearch(View focused, int direction) { 2311 if (mFocusSearchDisabled) { 2312 return focused; 2313 } 2314 return null; 2315 } 2316 2317 boolean hasPreviousViewInSameRow(int pos) { 2318 if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) { 2319 return false; 2320 } 2321 if (mGrid.getFirstVisibleIndex() > 0) { 2322 return true; 2323 } 2324 final int focusedRow = mGrid.getLocation(pos).row; 2325 for (int i = getChildCount() - 1; i >= 0; i--) { 2326 int position = getPositionByIndex(i); 2327 Grid.Location loc = mGrid.getLocation(position); 2328 if (loc != null && loc.row == focusedRow) { 2329 if (position < pos) { 2330 return true; 2331 } 2332 } 2333 } 2334 return false; 2335 } 2336 2337 @Override 2338 public boolean onAddFocusables(RecyclerView recyclerView, 2339 ArrayList<View> views, int direction, int focusableMode) { 2340 if (mFocusSearchDisabled) { 2341 return true; 2342 } 2343 // If this viewgroup or one of its children currently has focus then we 2344 // consider our children for focus searching in main direction on the same row. 2345 // If this viewgroup has no focus and using focus align, we want the system 2346 // to ignore our children and pass focus to the viewgroup, which will pass 2347 // focus on to its children appropriately. 2348 // If this viewgroup has no focus and not using focus align, we want to 2349 // consider the child that does not overlap with padding area. 2350 if (recyclerView.hasFocus()) { 2351 final int movement = getMovement(direction); 2352 if (movement != PREV_ITEM && movement != NEXT_ITEM) { 2353 // Move on secondary direction uses default addFocusables(). 2354 return false; 2355 } 2356 if (mPendingMoveSmoothScroller != null) { 2357 // don't find next focusable if has pending movement. 2358 return true; 2359 } 2360 final View focused = recyclerView.findFocus(); 2361 final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused)); 2362 // Add focusables of focused item. 2363 if (focusedPos != NO_POSITION) { 2364 findViewByPosition(focusedPos).addFocusables(views, direction, focusableMode); 2365 } 2366 final int focusedRow = mGrid != null && focusedPos != NO_POSITION ? 2367 mGrid.getLocation(focusedPos).row : NO_POSITION; 2368 // Add focusables of next neighbor of same row on the focus search direction. 2369 if (mGrid != null) { 2370 final int focusableCount = views.size(); 2371 for (int i = 0, count = getChildCount(); i < count; i++) { 2372 int index = movement == NEXT_ITEM ? i : count - 1 - i; 2373 final View child = getChildAt(index); 2374 if (child.getVisibility() != View.VISIBLE) { 2375 continue; 2376 } 2377 int position = getPositionByIndex(index); 2378 Grid.Location loc = mGrid.getLocation(position); 2379 if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) { 2380 if (focusedPos == NO_POSITION || 2381 (movement == NEXT_ITEM && position > focusedPos) 2382 || (movement == PREV_ITEM && position < focusedPos)) { 2383 child.addFocusables(views, direction, focusableMode); 2384 if (views.size() > focusableCount) { 2385 break; 2386 } 2387 } 2388 } 2389 } 2390 } 2391 } else { 2392 if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) { 2393 // adding views not overlapping padding area to avoid scrolling in gaining focus 2394 int left = mWindowAlignment.mainAxis().getPaddingLow(); 2395 int right = mWindowAlignment.mainAxis().getClientSize() + left; 2396 int focusableCount = views.size(); 2397 for (int i = 0, count = getChildCount(); i < count; i++) { 2398 View child = getChildAt(i); 2399 if (child.getVisibility() == View.VISIBLE) { 2400 if (getViewMin(child) >= left && getViewMax(child) <= right) { 2401 child.addFocusables(views, direction, focusableMode); 2402 } 2403 } 2404 } 2405 // if we cannot find any, then just add all children. 2406 if (views.size() == focusableCount) { 2407 for (int i = 0, count = getChildCount(); i < count; i++) { 2408 View child = getChildAt(i); 2409 if (child.getVisibility() == View.VISIBLE) { 2410 child.addFocusables(views, direction, focusableMode); 2411 } 2412 } 2413 if (views.size() != focusableCount) { 2414 return true; 2415 } 2416 } else { 2417 return true; 2418 } 2419 // if still cannot find any, fall through and add itself 2420 } 2421 if (recyclerView.isFocusable()) { 2422 views.add(recyclerView); 2423 } 2424 } 2425 return true; 2426 } 2427 2428 private boolean hasCreatedLastItem() { 2429 int count = mState.getItemCount(); 2430 return count == 0 || findViewByPosition(count - 1) != null; 2431 } 2432 2433 private boolean hasCreatedFirstItem() { 2434 int count = mState.getItemCount(); 2435 return count == 0 || findViewByPosition(0) != null; 2436 } 2437 2438 @Override 2439 public View onFocusSearchFailed(View focused, int direction, Recycler recycler, 2440 RecyclerView.State state) { 2441 if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction); 2442 2443 View view = null; 2444 int movement = getMovement(direction); 2445 final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE; 2446 // We still treat single-row (or TODO non-staggered) Grid special using position because: 2447 // we know exactly if an item should be selected (based on index) *before* it is 2448 // added to hierarchy. Child view can change layout when it is selected. 2449 // Knowing this ahead can avoid a second layout after view is inserted into tree. 2450 // We can reduce choppiness of vertical scrolling BrowseFragment where row view has 2451 // different layout padding when it is selected. 2452 // Multiple-rows Grid is different case: we don't know if the item should be selected 2453 // until we add it to hierarchy and measure it. Grid algorithm choose a row to put 2454 // the item based on the item size. FocusSearch mechanism will rely which row the item 2455 // is laid out then makes choice whether to select it. This can cause a second layout 2456 // if selected child has a different layout. 2457 if (mNumRows == 1) { 2458 if (movement == NEXT_ITEM) { 2459 int newPos = mFocusPosition + mNumRows; 2460 if (newPos < getItemCount() && mScrollEnabled && getChildCount() > 0) { 2461 int lastChildPos = getPosition(getChildAt(getChildCount() - 1)); 2462 if (newPos < lastChildPos + mNumRows * MAX_PENDING_MOVES) { 2463 setSelectionSmooth(mBaseGridView, newPos); 2464 } 2465 view = focused; 2466 } else { 2467 if (isScroll || !mFocusOutEnd) { 2468 view = focused; 2469 } 2470 } 2471 } else if (movement == PREV_ITEM) { 2472 int newPos = mFocusPosition - mNumRows; 2473 if (newPos >= 0 && mScrollEnabled && getChildCount() > 0) { 2474 int firstChildPos = getPosition(getChildAt(0)); 2475 if (newPos > firstChildPos - mNumRows * MAX_PENDING_MOVES) { 2476 setSelectionSmooth(mBaseGridView, newPos); 2477 } 2478 view = focused; 2479 } else { 2480 if (isScroll || !mFocusOutFront) { 2481 view = focused; 2482 } 2483 } 2484 } 2485 } else if (mNumRows > 1) { 2486 saveContext(recycler, state); 2487 if (movement == NEXT_ITEM) { 2488 if (isScroll || !mFocusOutEnd) { 2489 view = focused; 2490 } 2491 if (mScrollEnabled) { 2492 processPendingMovement(true); 2493 } 2494 } else if (movement == PREV_ITEM) { 2495 if (isScroll || !mFocusOutFront) { 2496 view = focused; 2497 } 2498 if (mScrollEnabled) { 2499 processPendingMovement(false); 2500 } 2501 } 2502 leaveContext(); 2503 } 2504 if (DEBUG) Log.v(getTag(), "returning view " + view); 2505 return view; 2506 } 2507 2508 boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction, 2509 Rect previouslyFocusedRect) { 2510 switch (mFocusScrollStrategy) { 2511 case BaseGridView.FOCUS_SCROLL_ALIGNED: 2512 default: 2513 return gridOnRequestFocusInDescendantsAligned(recyclerView, 2514 direction, previouslyFocusedRect); 2515 case BaseGridView.FOCUS_SCROLL_PAGE: 2516 case BaseGridView.FOCUS_SCROLL_ITEM: 2517 return gridOnRequestFocusInDescendantsUnaligned(recyclerView, 2518 direction, previouslyFocusedRect); 2519 } 2520 } 2521 2522 private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView, 2523 int direction, Rect previouslyFocusedRect) { 2524 View view = findViewByPosition(mFocusPosition); 2525 if (view != null) { 2526 boolean result = view.requestFocus(direction, previouslyFocusedRect); 2527 if (!result && DEBUG) { 2528 Log.w(getTag(), "failed to request focus on " + view); 2529 } 2530 return result; 2531 } 2532 return false; 2533 } 2534 2535 private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView, 2536 int direction, Rect previouslyFocusedRect) { 2537 // focus to view not overlapping padding area to avoid scrolling in gaining focus 2538 int index; 2539 int increment; 2540 int end; 2541 int count = getChildCount(); 2542 if ((direction & View.FOCUS_FORWARD) != 0) { 2543 index = 0; 2544 increment = 1; 2545 end = count; 2546 } else { 2547 index = count - 1; 2548 increment = -1; 2549 end = -1; 2550 } 2551 int left = mWindowAlignment.mainAxis().getPaddingLow(); 2552 int right = mWindowAlignment.mainAxis().getClientSize() + left; 2553 for (int i = index; i != end; i += increment) { 2554 View child = getChildAt(i); 2555 if (child.getVisibility() == View.VISIBLE) { 2556 if (getViewMin(child) >= left && getViewMax(child) <= right) { 2557 if (child.requestFocus(direction, previouslyFocusedRect)) { 2558 return true; 2559 } 2560 } 2561 } 2562 } 2563 return false; 2564 } 2565 2566 private final static int PREV_ITEM = 0; 2567 private final static int NEXT_ITEM = 1; 2568 private final static int PREV_ROW = 2; 2569 private final static int NEXT_ROW = 3; 2570 2571 private int getMovement(int direction) { 2572 int movement = View.FOCUS_LEFT; 2573 2574 if (mOrientation == HORIZONTAL) { 2575 switch(direction) { 2576 case View.FOCUS_LEFT: 2577 movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM; 2578 break; 2579 case View.FOCUS_RIGHT: 2580 movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM; 2581 break; 2582 case View.FOCUS_UP: 2583 movement = PREV_ROW; 2584 break; 2585 case View.FOCUS_DOWN: 2586 movement = NEXT_ROW; 2587 break; 2588 } 2589 } else if (mOrientation == VERTICAL) { 2590 switch(direction) { 2591 case View.FOCUS_LEFT: 2592 movement = (!mReverseFlowPrimary) ? PREV_ROW : NEXT_ROW; 2593 break; 2594 case View.FOCUS_RIGHT: 2595 movement = (!mReverseFlowPrimary) ? NEXT_ROW : PREV_ROW; 2596 break; 2597 case View.FOCUS_UP: 2598 movement = PREV_ITEM; 2599 break; 2600 case View.FOCUS_DOWN: 2601 movement = NEXT_ITEM; 2602 break; 2603 } 2604 } 2605 2606 return movement; 2607 } 2608 2609 int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) { 2610 View view = findViewByPosition(mFocusPosition); 2611 if (view == null) { 2612 return i; 2613 } 2614 int focusIndex = recyclerView.indexOfChild(view); 2615 // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item 2616 // drawing order is 0 1 2 3 9 8 7 6 5 4 2617 if (i < focusIndex) { 2618 return i; 2619 } else if (i < childCount - 1) { 2620 return focusIndex + childCount - 1 - i; 2621 } else { 2622 return focusIndex; 2623 } 2624 } 2625 2626 @Override 2627 public void onAdapterChanged(RecyclerView.Adapter oldAdapter, 2628 RecyclerView.Adapter newAdapter) { 2629 if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter); 2630 if (oldAdapter != null) { 2631 discardLayoutInfo(); 2632 mFocusPosition = NO_POSITION; 2633 mFocusPositionOffset = 0; 2634 mChildrenStates.clear(); 2635 } 2636 super.onAdapterChanged(oldAdapter, newAdapter); 2637 } 2638 2639 private void discardLayoutInfo() { 2640 mGrid = null; 2641 mRowSizeSecondary = null; 2642 mRowSecondarySizeRefresh = false; 2643 } 2644 2645 public void setLayoutEnabled(boolean layoutEnabled) { 2646 if (mLayoutEnabled != layoutEnabled) { 2647 mLayoutEnabled = layoutEnabled; 2648 requestLayout(); 2649 } 2650 } 2651 2652 void setChildrenVisibility(int visiblity) { 2653 mChildVisibility = visiblity; 2654 if (mChildVisibility != -1) { 2655 int count = getChildCount(); 2656 for (int i= 0; i < count; i++) { 2657 getChildAt(i).setVisibility(mChildVisibility); 2658 } 2659 } 2660 } 2661 2662 final static class SavedState implements Parcelable { 2663 2664 int index; // index inside adapter of the current view 2665 Bundle childStates = Bundle.EMPTY; 2666 2667 @Override 2668 public void writeToParcel(Parcel out, int flags) { 2669 out.writeInt(index); 2670 out.writeBundle(childStates); 2671 } 2672 2673 @SuppressWarnings("hiding") 2674 public static final Parcelable.Creator<SavedState> CREATOR = 2675 new Parcelable.Creator<SavedState>() { 2676 @Override 2677 public SavedState createFromParcel(Parcel in) { 2678 return new SavedState(in); 2679 } 2680 2681 @Override 2682 public SavedState[] newArray(int size) { 2683 return new SavedState[size]; 2684 } 2685 }; 2686 2687 @Override 2688 public int describeContents() { 2689 return 0; 2690 } 2691 2692 SavedState(Parcel in) { 2693 index = in.readInt(); 2694 childStates = in.readBundle(GridLayoutManager.class.getClassLoader()); 2695 } 2696 2697 SavedState() { 2698 } 2699 } 2700 2701 @Override 2702 public Parcelable onSaveInstanceState() { 2703 if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection()); 2704 SavedState ss = new SavedState(); 2705 for (int i = 0, count = getChildCount(); i < count; i++) { 2706 View view = getChildAt(i); 2707 int position = getPositionByView(view); 2708 if (position != NO_POSITION) { 2709 mChildrenStates.saveOnScreenView(view, position); 2710 } 2711 } 2712 ss.index = getSelection(); 2713 ss.childStates = mChildrenStates.saveAsBundle(); 2714 return ss; 2715 } 2716 2717 void onChildRecycled(RecyclerView.ViewHolder holder) { 2718 final int position = holder.getAdapterPosition(); 2719 if (position != NO_POSITION) { 2720 mChildrenStates.saveOffscreenView(holder.itemView, position); 2721 } 2722 } 2723 2724 @Override 2725 public void onRestoreInstanceState(Parcelable state) { 2726 if (!(state instanceof SavedState)) { 2727 return; 2728 } 2729 SavedState loadingState = (SavedState)state; 2730 mFocusPosition = loadingState.index; 2731 mFocusPositionOffset = 0; 2732 mChildrenStates.loadFromBundle(loadingState.childStates); 2733 mForceFullLayout = true; 2734 requestLayout(); 2735 if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition); 2736 } 2737} 2738