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