1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.support.car.ui; 17 18 import android.content.Context; 19 import android.graphics.PointF; 20 import android.support.annotation.IntDef; 21 import android.support.annotation.NonNull; 22 import android.support.v7.widget.LinearSmoothScroller; 23 import android.support.v7.widget.RecyclerView; 24 import android.util.DisplayMetrics; 25 import android.util.Log; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.view.animation.AccelerateInterpolator; 29 import android.view.animation.DecelerateInterpolator; 30 import android.view.animation.Interpolator; 31 32 import java.lang.annotation.Retention; 33 import java.lang.annotation.RetentionPolicy; 34 import java.util.ArrayList; 35 36 /** 37 * Custom {@link RecyclerView.LayoutManager} that behaves similar to LinearLayoutManager except that 38 * it has a few tricks up its sleeve. 39 * <ol> 40 * <li>In a normal ListView, when views reach the top of the list, they are clipped. In 41 * CarLayoutManager, views have the option of flying off of the top of the screen as the 42 * next row settles in to place. This functionality can be enabled or disabled with 43 * {@link #setOffsetRows(boolean)}. 44 * <li>Standard list physics is disabled. Instead, when the user scrolls, it will settle 45 * on the next page. {@link #FLING_THRESHOLD_TO_PAGINATE} and 46 * {@link #DRAG_DISTANCE_TO_PAGINATE} can be set to have the list settle on the next item 47 * instead of the next page for small gestures. 48 * <li>Items can scroll past the bottom edge of the screen. This helps with pagination so that 49 * the last page can be properly aligned. 50 * </ol> 51 * 52 * This LayoutManger should be used with {@link CarRecyclerView}. 53 */ 54 public class CarLayoutManager extends RecyclerView.LayoutManager { 55 private static final String TAG = "CarLayoutManager"; 56 private static final boolean DEBUG = true; 57 58 /** 59 * Any fling below the threshold will just scroll to the top fully visible row. The units is 60 * whatever {@link android.widget.Scroller} would return. 61 * 62 * A reasonable value is ~200 63 * 64 * This can be disabled by setting the threshold to -1. 65 */ 66 private static final int FLING_THRESHOLD_TO_PAGINATE = -1; 67 68 /** 69 * Any fling shorter than this threshold (in px) will just scroll to the top fully visible row. 70 * 71 * A reasonable value is 15. 72 * 73 * This can be disabled by setting the distance to -1. 74 */ 75 private static final int DRAG_DISTANCE_TO_PAGINATE = -1; 76 77 /** 78 * If you scroll really quickly, you can hit the end of the laid out rows before Android has a 79 * chance to layout more. To help counter this, we can layout a number of extra rows past 80 * wherever the focus is if necessary. 81 */ 82 private static final int NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS = 2; 83 84 /** 85 * Scroll bar calculation is a bit complicated. This basically defines the granularity we want 86 * our scroll bar to move. Set this to 1 means our scrollbar will have really jerky movement. 87 * Setting it too big will risk an overflow (although there is no performance impact). Ideally 88 * we want to set this higher than the height of our list view. We can't use our list view 89 * height directly though because we might run into situations where getHeight() returns 0, for 90 * example, when the view is not yet measured. 91 */ 92 private static final int SCROLL_RANGE = 1000; 93 94 @ScrollStyle private final int SCROLL_TYPE = MARIO; 95 96 @Retention(RetentionPolicy.SOURCE) 97 @IntDef({MARIO, SUPER_MARIO}) 98 private @interface ScrollStyle {} 99 private static final int MARIO = 0; 100 private static final int SUPER_MARIO = 1; 101 102 @Retention(RetentionPolicy.SOURCE) 103 @IntDef({BEFORE, AFTER}) 104 private @interface LayoutDirection {} 105 private static final int BEFORE = 0; 106 private static final int AFTER = 1; 107 108 @Retention(RetentionPolicy.SOURCE) 109 @IntDef({ROW_OFFSET_MODE_INDIVIDUAL, ROW_OFFSET_MODE_PAGE}) 110 public @interface RowOffsetMode {} 111 public static final int ROW_OFFSET_MODE_INDIVIDUAL = 0; 112 public static final int ROW_OFFSET_MODE_PAGE = 1; 113 114 public interface OnItemsChangedListener { onItemsChanged()115 void onItemsChanged(); 116 } 117 118 private final AccelerateInterpolator mDanglingRowInterpolator = new AccelerateInterpolator(2); 119 private final Context mContext; 120 121 /** Determines whether or not rows will be offset as they slide off screen **/ 122 private boolean mOffsetRows = false; 123 /** Determines whether rows will be offset individually or a page at a time **/ 124 @RowOffsetMode private int mRowOffsetMode = ROW_OFFSET_MODE_PAGE; 125 126 /** 127 * The LayoutManager only gets {@link #onScrollStateChanged(int)} updates. This enables the 128 * scroll state to be used anywhere. 129 */ 130 private int mScrollState = RecyclerView.SCROLL_STATE_IDLE; 131 /** 132 * Used to inspect the current scroll state to help with the various calculations. 133 **/ 134 private CarSmoothScroller mSmoothScroller; 135 private OnItemsChangedListener mItemsChangedListener; 136 137 /** The distance that the list has actually scrolled in the most recent drag gesture **/ 138 private int mLastDragDistance = 0; 139 /** True if the current drag was limited/capped because it was at some boundary **/ 140 private boolean mReachedLimitOfDrag; 141 /** 142 * The values are continuously updated to keep track of where the current page boundaries are 143 * on screen. The anchor page break is the page break that is currently within or at the 144 * top of the viewport. The Upper page break is the page break before it and the lower page 145 * break is the page break after it. 146 * 147 * A page break will be set to -1 if it is unknown or n/a. 148 * @see #updatePageBreakPositions() 149 */ 150 private int mItemCountDuringLastPageBreakUpdate; 151 private int mAnchorPageBreakPosition = 0; 152 private int mUpperPageBreakPosition = -1; 153 private int mLowerPageBreakPosition = -1; 154 /** Used in the bookkeeping of mario style scrolling to prevent extra calculations. **/ 155 private int mLastChildPositionToRequestFocus = -1; 156 private int mSampleViewHeight = -1; 157 158 /** 159 * Set the anchor to the following position on the next layout pass. 160 */ 161 private int mPendingScrollPosition = -1; 162 CarLayoutManager(Context context)163 public CarLayoutManager(Context context) { 164 mContext = context; 165 } 166 167 @Override generateDefaultLayoutParams()168 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 169 return new RecyclerView.LayoutParams( 170 ViewGroup.LayoutParams.MATCH_PARENT, 171 ViewGroup.LayoutParams.WRAP_CONTENT); 172 } 173 174 @Override canScrollVertically()175 public boolean canScrollVertically() { 176 return true; 177 } 178 179 /** 180 * onLayoutChildren is sort of like a "reset" for the layout state. At a high level, it should: 181 * <ol> 182 * <li>Check the current views to get the current state of affairs 183 * <li>Detach all views from the window (a lightweight operation) so that rows 184 * not re-added will be removed after onLayoutChildren. 185 * <li>Re-add rows as necessary. 186 * </ol> 187 * 188 * @see super#onLayoutChildren(RecyclerView.Recycler, RecyclerView.State) 189 */ 190 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)191 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 192 /** 193 * The anchor view is the first fully visible view on screen at the beginning 194 * of onLayoutChildren (or 0 if there is none). This row will be laid out first. After that, 195 * layoutNextRow will layout rows above and below it until the boundaries of what should 196 * be laid out have been reached. See {@link #shouldLayoutNextRow(View, int)} for 197 * more information. 198 */ 199 int anchorPosition = 0; 200 int anchorTop = -1; 201 if (mPendingScrollPosition == -1) { 202 View anchor = getFirstFullyVisibleChild(); 203 if (anchor != null) { 204 anchorPosition = getPosition(anchor); 205 anchorTop = getDecoratedTop(anchor); 206 } 207 } else { 208 anchorPosition = mPendingScrollPosition; 209 mPendingScrollPosition = -1; 210 mAnchorPageBreakPosition = anchorPosition; 211 mUpperPageBreakPosition = -1; 212 mLowerPageBreakPosition = -1; 213 } 214 215 if (DEBUG) { 216 Log.v(TAG, String.format( 217 ":: onLayoutChildren anchorPosition:%s, anchorTop:%s," 218 + " mPendingScrollPosition: %s, mAnchorPageBreakPosition:%s," 219 + " mUpperPageBreakPosition:%s, mLowerPageBreakPosition:%s", 220 anchorPosition, anchorTop, mPendingScrollPosition, mAnchorPageBreakPosition, 221 mUpperPageBreakPosition, mLowerPageBreakPosition)); 222 } 223 224 /** 225 * Detach all attached view for 2 reasons: 226 * <ol> 227 * <li> So that views are put in the scrap heap. This enables us to call 228 * {@link RecyclerView.Recycler#getViewForPosition(int)} which will either return 229 * one of these detached views if it is in the scrap heap, one from the 230 * recycled pool (will only call onBind in the adapter), or create an entirely new 231 * row if needed (will call onCreate and onBind in the adapter). 232 * <li> So that views are automatically removed if they are not manually re-added. 233 * </ol> 234 */ 235 detachAndScrapAttachedViews(recycler); 236 237 // Layout new rows. 238 View anchor = layoutAnchor(recycler, anchorPosition, anchorTop); 239 if (anchor != null) { 240 View adjacentRow = anchor; 241 while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) { 242 adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE); 243 } 244 adjacentRow = anchor; 245 while (shouldLayoutNextRow(state, adjacentRow, AFTER)) { 246 adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER); 247 } 248 } 249 250 updatePageBreakPositions(); 251 offsetRows(); 252 253 if (DEBUG&& getChildCount() > 1) { 254 Log.v(TAG, "Currently showing " + getChildCount() + " views " + 255 getPosition(getChildAt(0)) + " to " + 256 getPosition(getChildAt(getChildCount() - 1)) + " anchor " + anchorPosition); 257 } 258 } 259 260 /** 261 * scrollVerticallyBy does the work of what should happen when the list scrolls in addition 262 * to handling cases where the list hits the end. It should be lighter weight than 263 * onLayoutChildren. It doesn't have to detach all views. It only looks at the end of the list 264 * and removes views that have gone out of bounds and lays out new ones that scroll in. 265 * 266 * @param dy The amount that the list is supposed to scroll. 267 * > 0 means the list is scrolling down. 268 * < 0 means the list is scrolling up. 269 * @param recycler The recycler that enables views to be reused or created as they scroll in. 270 * @param state Various information about the current state of affairs. 271 * @return The amount the list actually scrolled. 272 * 273 * @see super#scrollVerticallyBy(int, RecyclerView.Recycler, RecyclerView.State) 274 */ 275 @Override scrollVerticallyBy( int dy, @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state)276 public int scrollVerticallyBy( 277 int dy, @NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state) { 278 // If the list is empty, we can prevent the overscroll glow from showing by just 279 // telling RecycerView that we scrolled. 280 if (getItemCount() == 0) { 281 return dy; 282 } 283 284 // Prevent redundant computations if there is definitely nowhere to scroll to. 285 if (getChildCount() <= 1 || dy == 0) { 286 return 0; 287 } 288 289 View firstChild = getChildAt(0); 290 if (firstChild == null) { 291 return 0; 292 } 293 int firstChildPosition = getPosition(firstChild); 294 RecyclerView.LayoutParams firstChildParams = getParams(firstChild); 295 int firstChildTopWithMargin = getDecoratedTop(firstChild) - firstChildParams.topMargin; 296 297 View lastFullyVisibleView = getChildAt(getLastFullyVisibleChildIndex()); 298 if (lastFullyVisibleView == null) { 299 return 0; 300 } 301 boolean isLastViewVisible = getPosition(lastFullyVisibleView) == getItemCount() - 1; 302 303 View firstFullyVisibleChild = getFirstFullyVisibleChild(); 304 if (firstFullyVisibleChild == null) { 305 return 0; 306 } 307 int firstFullyVisiblePosition = getPosition(firstFullyVisibleChild); 308 RecyclerView.LayoutParams firstFullyVisibleChildParams = getParams(firstFullyVisibleChild); 309 int topRemainingSpace = getDecoratedTop(firstFullyVisibleChild) 310 - firstFullyVisibleChildParams.topMargin - getPaddingTop(); 311 312 if (isLastViewVisible && firstFullyVisiblePosition == mAnchorPageBreakPosition 313 && dy > topRemainingSpace && dy > 0) { 314 // Prevent dragging down more than 1 page. As a side effect, this also prevents you 315 // from dragging past the bottom because if you are on the second to last page, it 316 // prevents you from dragging past the last page. 317 dy = topRemainingSpace; 318 mReachedLimitOfDrag = true; 319 } else if (dy < 0 && firstChildPosition == 0 320 && firstChildTopWithMargin + Math.abs(dy) > getPaddingTop()) { 321 // Prevent scrolling past the beginning 322 dy = firstChildTopWithMargin - getPaddingTop(); 323 mReachedLimitOfDrag = true; 324 } else { 325 mReachedLimitOfDrag = false; 326 } 327 328 boolean isDragging = mScrollState == RecyclerView.SCROLL_STATE_DRAGGING; 329 if (isDragging) { 330 mLastDragDistance += dy; 331 } 332 // We offset by -dy because the views translate in the opposite direction that the 333 // list scrolls (think about it.) 334 offsetChildrenVertical(-dy); 335 336 // This is the meat of this function. We remove views on the trailing edge of the scroll 337 // and add views at the leading edge as necessary. 338 View adjacentRow; 339 if (dy > 0) { 340 recycleChildrenFromStart(recycler); 341 adjacentRow = getChildAt(getChildCount() - 1); 342 while (shouldLayoutNextRow(state, adjacentRow, AFTER)) { 343 adjacentRow = layoutNextRow(recycler, adjacentRow, AFTER); 344 } 345 } else { 346 recycleChildrenFromEnd(recycler); 347 adjacentRow = getChildAt(0); 348 while (shouldLayoutNextRow(state, adjacentRow, BEFORE)) { 349 adjacentRow = layoutNextRow(recycler, adjacentRow, BEFORE); 350 } 351 } 352 // Now that the correct views are laid out, offset rows as necessary so we can do whatever 353 // fancy animation we want such as having the top view fly off the screen as the next one 354 // settles in to place. 355 updatePageBreakPositions(); 356 offsetRows(); 357 358 if (getChildCount() > 1) { 359 if (DEBUG) { 360 Log.v(TAG, String.format("Currently showing %d views (%d to %d)", 361 getChildCount(), getPosition(getChildAt(0)), 362 getPosition(getChildAt(getChildCount() - 1)))); 363 } 364 } 365 366 return dy; 367 } 368 369 @Override scrollToPosition(int position)370 public void scrollToPosition(int position) { 371 mPendingScrollPosition = position; 372 requestLayout(); 373 } 374 375 @Override smoothScrollToPosition( RecyclerView recyclerView, RecyclerView.State state, int position)376 public void smoothScrollToPosition( 377 RecyclerView recyclerView, RecyclerView.State state, int position) { 378 /** 379 * startSmoothScroll will handle stopping the old one if there is one. 380 * We only keep a copy of it to handle the translation of rows as they slide off the screen 381 * in {@link #offsetRowsWithPageBreak()} 382 */ 383 mSmoothScroller = new CarSmoothScroller(mContext, position); 384 mSmoothScroller.setTargetPosition(position); 385 startSmoothScroll(mSmoothScroller); 386 } 387 388 /** 389 * Miscellaneous bookkeeping. 390 */ 391 @Override onScrollStateChanged(int state)392 public void onScrollStateChanged(int state) { 393 if (DEBUG) { 394 Log.v(TAG, ":: onScrollStateChanged " + state); 395 } 396 if (state == RecyclerView.SCROLL_STATE_IDLE) { 397 // If the focused view is off screen, give focus to one that is. 398 // If the first fully visible view is first in the list, focus the first item. 399 // Otherwise, focus the second so that you have the first item as scrolling context. 400 View focusedChild = getFocusedChild(); 401 if (focusedChild != null 402 && (getDecoratedTop(focusedChild) >= getHeight() - getPaddingBottom() 403 || getDecoratedBottom(focusedChild) <= getPaddingTop())) { 404 focusedChild.clearFocus(); 405 requestLayout(); 406 } 407 408 } else if (state == RecyclerView.SCROLL_STATE_DRAGGING) { 409 mLastDragDistance = 0; 410 } 411 412 if (state != RecyclerView.SCROLL_STATE_SETTLING) { 413 mSmoothScroller = null; 414 } 415 416 mScrollState = state; 417 updatePageBreakPositions(); 418 } 419 420 @Override onItemsChanged(RecyclerView recyclerView)421 public void onItemsChanged(RecyclerView recyclerView) { 422 super.onItemsChanged(recyclerView); 423 if (mItemsChangedListener != null) { 424 mItemsChangedListener.onItemsChanged(); 425 } 426 // When item changed, our sample view height is no longer accurate, and need to be 427 // recomputed. 428 mSampleViewHeight = -1; 429 } 430 431 /** 432 * Gives us the opportunity to override the order of the focused views. 433 * By default, it will just go from top to bottom. However, if there is no focused views, we 434 * take over the logic and start the focused views from the middle of what is visible and move 435 * from there until the end of the laid out views in the specified direction. 436 */ 437 @Override onAddFocusables( RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode)438 public boolean onAddFocusables( 439 RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) { 440 View focusedChild = getFocusedChild(); 441 if (focusedChild != null) { 442 // If there is a view that already has focus, we can just return false and the normal 443 // Android addFocusables will work fine. 444 return false; 445 } 446 447 // Now we know that there isn't a focused view. We need to set up focusables such that 448 // instead of just focusing the first item that has been laid out, it focuses starting 449 // from a visible item. 450 451 int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); 452 if (firstFullyVisibleChildIndex == -1) { 453 // Somehow there is a focused view but there is no fully visible view. There shouldn't 454 // be a way for this to happen but we'd better stop here and return instead of 455 // continuing on with -1. 456 Log.w(TAG, "There is a focused child but no first fully visible child."); 457 return false; 458 } 459 View firstFullyVisibleChild = getChildAt(firstFullyVisibleChildIndex); 460 int firstFullyVisibleChildPosition = getPosition(firstFullyVisibleChild); 461 462 int firstFocusableChildIndex = firstFullyVisibleChildIndex; 463 if (firstFullyVisibleChildPosition > 0 && firstFocusableChildIndex + 1 < getItemCount()) { 464 // We are somewhere in the middle of the list. Instead of starting focus on the first 465 // item, start focus on the second item to give some context that we aren't at 466 // the beginning. 467 firstFocusableChildIndex++; 468 } 469 470 if (direction == View.FOCUS_FORWARD) { 471 // Iterate from the first focusable view to the end. 472 for (int i = firstFocusableChildIndex; i < getChildCount(); i++) { 473 views.add(getChildAt(i)); 474 } 475 return true; 476 } else if (direction == View.FOCUS_BACKWARD) { 477 // Iterate from the first focusable view to the beginning. 478 for (int i = firstFocusableChildIndex; i >= 0; i--) { 479 views.add(getChildAt(i)); 480 } 481 return true; 482 } 483 return false; 484 } 485 486 @Override onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)487 public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, 488 RecyclerView.State state) { 489 return null; 490 } 491 492 /** 493 * This is the function that decides where to scroll to when a new view is focused. 494 * You can get the position of the currently focused child through the child parameter. 495 * Once you have that, determine where to smooth scroll to and scroll there. 496 * 497 * @param parent The RecyclerView hosting this LayoutManager 498 * @param state Current state of RecyclerView 499 * @param child Direct child of the RecyclerView containing the newly focused view 500 * @param focused The newly focused view. This may be the same view as child or it may be null 501 * @return true if the default scroll behavior should be suppressed 502 */ 503 @Override onRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused)504 public boolean onRequestChildFocus(RecyclerView parent, RecyclerView.State state, 505 View child, View focused) { 506 if (child == null) { 507 Log.w(TAG, "onRequestChildFocus with a null child!"); 508 return true; 509 } 510 511 if (DEBUG) { 512 Log.v(TAG, String.format(":: onRequestChildFocus child: %s, focused: %s", child, 513 focused)); 514 } 515 516 // We have several distinct scrolling methods. Each implementation has been delegated 517 // to its own method. 518 if (SCROLL_TYPE == MARIO) { 519 return onRequestChildFocusMarioStyle(parent, child); 520 } else if (SCROLL_TYPE == SUPER_MARIO) { 521 return onRequestChildFocusSuperMarioStyle(parent, state, child); 522 } else { 523 throw new IllegalStateException("Unknown scroll type (" + SCROLL_TYPE + ")"); 524 } 525 } 526 527 /** 528 * Goal: the scrollbar maintains the same size throughout scrolling and that the scrollbar 529 * reaches the bottom of the screen when the last item is fully visible. This is because 530 * there are multiple points that could be considered the bottom since the last item can scroll 531 * past the bottom edge of the screen. 532 * 533 * To find the extent, we divide the number of items that can fit on screen by the number of 534 * items in total. 535 */ 536 @Override computeVerticalScrollExtent(RecyclerView.State state)537 public int computeVerticalScrollExtent(RecyclerView.State state) { 538 if (getChildCount() <= 1) { 539 return 0; 540 } 541 542 int sampleViewHeight = getSampleViewHeight(); 543 int availableHeight = getAvailableHeight(); 544 int sampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight; 545 546 if (state.getItemCount() <= sampleViewsThatCanFitOnScreen) { 547 return SCROLL_RANGE; 548 } else { 549 return SCROLL_RANGE * sampleViewsThatCanFitOnScreen / state.getItemCount(); 550 } 551 } 552 553 /** 554 * The scrolling offset is calculated by determining what position is at the top of the list. 555 * However, instead of using fixed integer positions for each row, the scroll position is 556 * factored in and the position is recalculated as a float that takes in to account the 557 * current scroll state. This results in a smooth animation for the scrollbar when the user 558 * scrolls the list. 559 */ 560 @Override computeVerticalScrollOffset(RecyclerView.State state)561 public int computeVerticalScrollOffset(RecyclerView.State state) { 562 View firstChild = getFirstFullyVisibleChild(); 563 if (firstChild == null) { 564 return 0; 565 } 566 567 RecyclerView.LayoutParams params = getParams(firstChild); 568 int firstChildPosition = getPosition(firstChild); 569 570 // Assume the previous view is the same height as the current one. 571 float percentOfPreviousViewShowing = (getDecoratedTop(firstChild) - params.topMargin) 572 / (float) (getDecoratedMeasuredHeight(firstChild) 573 + params.topMargin + params.bottomMargin); 574 // If the previous view is actually larger than the current one then this the percent 575 // can be greater than 1. 576 percentOfPreviousViewShowing = Math.min(percentOfPreviousViewShowing, 1); 577 578 float currentPosition = (float) firstChildPosition - percentOfPreviousViewShowing; 579 580 int sampleViewHeight = getSampleViewHeight(); 581 int availableHeight = getAvailableHeight(); 582 int numberOfSampleViewsThatCanFitOnScreen = availableHeight / sampleViewHeight; 583 int positionWhenLastItemIsVisible = 584 state.getItemCount() - numberOfSampleViewsThatCanFitOnScreen; 585 586 if (positionWhenLastItemIsVisible <= 0) { 587 return 0; 588 } 589 590 if (currentPosition >= positionWhenLastItemIsVisible) { 591 return SCROLL_RANGE; 592 } 593 594 return (int) (SCROLL_RANGE * currentPosition / positionWhenLastItemIsVisible); 595 } 596 597 /** 598 * The range of the scrollbar can be understood as the granularity of how we want the 599 * scrollbar to scroll. 600 */ 601 @Override computeVerticalScrollRange(RecyclerView.State state)602 public int computeVerticalScrollRange(RecyclerView.State state) { 603 return SCROLL_RANGE; 604 } 605 606 /** 607 * @return The first view that starts on screen. It assumes that it fully fits on the screen 608 * though. If the first fully visible child is also taller than the screen then it will 609 * still be returned. However, since the LayoutManager snaps to view starts, having 610 * a row that tall would lead to a broken experience anyways. 611 */ getFirstFullyVisibleChildIndex()612 public int getFirstFullyVisibleChildIndex() { 613 for (int i = 0; i < getChildCount(); i++) { 614 View child = getChildAt(i); 615 RecyclerView.LayoutParams params = getParams(child); 616 if (getDecoratedTop(child) - params.topMargin >= getPaddingTop()) { 617 return i; 618 } 619 } 620 return -1; 621 } 622 getFirstFullyVisibleChild()623 public View getFirstFullyVisibleChild() { 624 int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); 625 View firstChild = null; 626 if (firstFullyVisibleChildIndex != -1) { 627 firstChild = getChildAt(firstFullyVisibleChildIndex); 628 } 629 return firstChild; 630 } 631 632 /** 633 * @return The last view that ends on screen. It assumes that the start is also on screen 634 * though. If the last fully visible child is also taller than the screen then it will 635 * still be returned. However, since the LayoutManager snaps to view starts, having 636 * a row that tall would lead to a broken experience anyways. 637 */ getLastFullyVisibleChildIndex()638 public int getLastFullyVisibleChildIndex() { 639 for (int i = getChildCount() - 1; i >= 0; i--) { 640 View child = getChildAt(i); 641 RecyclerView.LayoutParams params = getParams(child); 642 int childBottom = getDecoratedBottom(child) + params.bottomMargin; 643 int listBottom = getHeight() - getPaddingBottom(); 644 if (childBottom <= listBottom) { 645 return i; 646 } 647 } 648 return -1; 649 } 650 651 /** 652 * @return Whether or not the first view is fully visible. 653 */ isAtTop()654 public boolean isAtTop() { 655 // getFirstFullyVisibleChildIndex() can return -1 which indicates that there are no views 656 // and also means that the list is at the top. 657 return getFirstFullyVisibleChildIndex() <= 0; 658 } 659 660 /** 661 * @return Whether or not the last view is fully visible. 662 */ isAtBottom()663 public boolean isAtBottom() { 664 int lastFullyVisibleChildIndex = getLastFullyVisibleChildIndex(); 665 if (lastFullyVisibleChildIndex == -1) { 666 return true; 667 } 668 View lastFullyVisibleChild = getChildAt(lastFullyVisibleChildIndex); 669 return getPosition(lastFullyVisibleChild) == getItemCount() - 1; 670 } 671 setOffsetRows(boolean offsetRows)672 public void setOffsetRows(boolean offsetRows) { 673 mOffsetRows = offsetRows; 674 if (offsetRows) { 675 offsetRows(); 676 } else { 677 int childCount = getChildCount(); 678 for (int i = 0; i < childCount; i++) { 679 getChildAt(i).setTranslationY(0); 680 } 681 } 682 } 683 setRowOffsetMode(@owOffsetMode int mode)684 public void setRowOffsetMode(@RowOffsetMode int mode) { 685 if (mode == mRowOffsetMode) { 686 return; 687 } 688 mRowOffsetMode = mode; 689 offsetRows(); 690 } 691 setItemsChangedListener(OnItemsChangedListener listener)692 public void setItemsChangedListener(OnItemsChangedListener listener) { 693 mItemsChangedListener = listener; 694 } 695 696 /** 697 * Finish the pagination taking into account where the gesture started (not where we are now). 698 * 699 * @return Whether the list was scrolled as a result of the fling. 700 */ settleScrollForFling(RecyclerView parent, int flingVelocity)701 public boolean settleScrollForFling(RecyclerView parent, int flingVelocity) { 702 if (getChildCount() == 0) { 703 return false; 704 } 705 706 if (mReachedLimitOfDrag) { 707 return false; 708 } 709 710 // If the fling was too slow or too short, settle on the first fully visible row instead. 711 if (Math.abs(flingVelocity) <= FLING_THRESHOLD_TO_PAGINATE 712 || Math.abs(mLastDragDistance) <= DRAG_DISTANCE_TO_PAGINATE) { 713 int firstFullyVisibleChildIndex = getFirstFullyVisibleChildIndex(); 714 if (firstFullyVisibleChildIndex != -1) { 715 int scrollPosition = getPosition(getChildAt(firstFullyVisibleChildIndex)); 716 parent.smoothScrollToPosition(scrollPosition); 717 return true; 718 } 719 return false; 720 } 721 722 // Finish the pagination taking into account where the gesture 723 // started (not where we are now). 724 boolean isDownGesture = flingVelocity > 0 725 || (flingVelocity == 0 && mLastDragDistance >= 0); 726 boolean isUpGesture = flingVelocity < 0 727 || (flingVelocity == 0 && mLastDragDistance < 0); 728 if (isDownGesture && mLowerPageBreakPosition != -1) { 729 // If the last view is fully visible then only settle on the first fully visible view 730 // instead of the original page down position. However, don't page down if the last 731 // item has come fully into view. 732 parent.smoothScrollToPosition(mAnchorPageBreakPosition); 733 return true; 734 } else if (isUpGesture && mUpperPageBreakPosition != -1) { 735 parent.smoothScrollToPosition(mUpperPageBreakPosition); 736 return true; 737 } else { 738 Log.e(TAG, "Error setting scroll for fling! flingVelocity: \t" + flingVelocity + 739 "\tlastDragDistance: " + mLastDragDistance + "\tpageUpAtStartOfDrag: " + 740 mUpperPageBreakPosition + "\tpageDownAtStartOfDrag: " + 741 mLowerPageBreakPosition); 742 // As a last resort, at the last smooth scroller target position if there is one. 743 if (mSmoothScroller != null) { 744 parent.smoothScrollToPosition(mSmoothScroller.getTargetPosition()); 745 return true; 746 } 747 } 748 return false; 749 } 750 751 /** 752 * @return The position that paging up from the current position would settle at. 753 */ 754 public int getPageUpPosition() { 755 return mUpperPageBreakPosition; 756 } 757 758 /** 759 * @return The position that paging down from the current position would settle at. 760 */ 761 public int getPageDownPosition() { 762 return mLowerPageBreakPosition; 763 } 764 765 /** 766 * Layout the anchor row. The anchor row is the first fully visible row. 767 * 768 * @param anchorTop The decorated top of the anchor. If it is not known or should be reset 769 * to the top, pass -1. 770 */ 771 private View layoutAnchor(RecyclerView.Recycler recycler, int anchorPosition, int anchorTop) { 772 if (anchorPosition > getItemCount() - 1) { 773 return null; 774 } 775 View anchor = recycler.getViewForPosition(anchorPosition); 776 RecyclerView.LayoutParams params = getParams(anchor); 777 measureChildWithMargins(anchor, 0, 0); 778 int left = getPaddingLeft() + params.leftMargin; 779 int top = (anchorTop == -1) ? params.topMargin : anchorTop; 780 int right = left + getDecoratedMeasuredWidth(anchor); 781 int bottom = top + getDecoratedMeasuredHeight(anchor); 782 layoutDecorated(anchor, left, top, right, bottom); 783 addView(anchor); 784 return anchor; 785 } 786 787 /** 788 * Lays out the next row in the specified direction next to the specified adjacent row. 789 * 790 * @param recycler The recycler from which a new view can be created. 791 * @param adjacentRow The View of the adjacent row which will be used to position the new one. 792 * @param layoutDirection The side of the adjacent row that the new row will be laid out on. 793 * 794 * @return The new row that was laid out. 795 */ 796 private View layoutNextRow(RecyclerView.Recycler recycler, View adjacentRow, 797 @LayoutDirection int layoutDirection) { 798 799 int adjacentRowPosition = getPosition(adjacentRow); 800 int newRowPosition = adjacentRowPosition; 801 if (layoutDirection == BEFORE) { 802 newRowPosition = adjacentRowPosition - 1; 803 } else if (layoutDirection == AFTER) { 804 newRowPosition = adjacentRowPosition + 1; 805 } 806 807 // Because we detach all rows in onLayoutChildren, this will often just return a view from 808 // the scrap heap. 809 View newRow = recycler.getViewForPosition(newRowPosition); 810 811 measureChildWithMargins(newRow, 0, 0); 812 RecyclerView.LayoutParams newRowParams = 813 (RecyclerView.LayoutParams) newRow.getLayoutParams(); 814 RecyclerView.LayoutParams adjacentRowParams = 815 (RecyclerView.LayoutParams) adjacentRow.getLayoutParams(); 816 int left = getPaddingLeft() + newRowParams.leftMargin; 817 int right = left + getDecoratedMeasuredWidth(newRow); 818 int top, bottom; 819 if (layoutDirection == BEFORE) { 820 bottom = adjacentRow.getTop() - adjacentRowParams.topMargin - newRowParams.bottomMargin; 821 top = bottom - getDecoratedMeasuredHeight(newRow); 822 } else { 823 top = getDecoratedBottom(adjacentRow) + 824 adjacentRowParams.bottomMargin + newRowParams.topMargin; 825 bottom = top + getDecoratedMeasuredHeight(newRow); 826 } 827 layoutDecorated(newRow, left, top, right, bottom); 828 829 if (layoutDirection == BEFORE) { 830 addView(newRow, 0); 831 } else { 832 addView(newRow); 833 } 834 835 return newRow; 836 } 837 838 /** 839 * @return Whether another row should be laid out in the specified direction. 840 */ 841 private boolean shouldLayoutNextRow(RecyclerView.State state, View adjacentRow, 842 @LayoutDirection int layoutDirection) { 843 int adjacentRowPosition = getPosition(adjacentRow); 844 845 if (layoutDirection == BEFORE) { 846 if (adjacentRowPosition == 0) { 847 // We already laid out the first row. 848 return false; 849 } 850 } else if (layoutDirection == AFTER) { 851 if (adjacentRowPosition >= state.getItemCount() - 1) { 852 // We already laid out the last row. 853 return false; 854 } 855 } 856 857 // If we are scrolling layout views until the target position. 858 if (mSmoothScroller != null) { 859 if (layoutDirection == BEFORE 860 && adjacentRowPosition >= mSmoothScroller.getTargetPosition()) { 861 return true; 862 } else if (layoutDirection == AFTER 863 && adjacentRowPosition <= mSmoothScroller.getTargetPosition()) { 864 return true; 865 } 866 } 867 868 View focusedRow = getFocusedChild(); 869 if (focusedRow != null) { 870 int focusedRowPosition = getPosition(focusedRow); 871 if (layoutDirection == BEFORE && adjacentRowPosition 872 >= focusedRowPosition - NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) { 873 return true; 874 } else if (layoutDirection == AFTER && adjacentRowPosition 875 <= focusedRowPosition + NUM_EXTRA_ROWS_TO_LAYOUT_PAST_FOCUS) { 876 return true; 877 } 878 } 879 880 RecyclerView.LayoutParams params = getParams(adjacentRow); 881 int adjacentRowTop = getDecoratedTop(adjacentRow) - params.topMargin; 882 int adjacentRowBottom = getDecoratedBottom(adjacentRow) - params.bottomMargin; 883 if (layoutDirection == BEFORE 884 && adjacentRowTop < getPaddingTop() - getHeight()) { 885 // View is more than 1 page past the top of the screen and also past where the user has 886 // scrolled to. We want to keep one page past the top to make the scroll up calculation 887 // easier and scrolling smoother. 888 return false; 889 } else if (layoutDirection == AFTER 890 && adjacentRowBottom > getHeight() - getPaddingBottom()) { 891 // View is off of the bottom and also past where the user has scrolled to. 892 return false; 893 } 894 895 return true; 896 } 897 898 /** 899 * Remove and recycle views that are no longer needed. 900 */ recycleChildrenFromStart(RecyclerView.Recycler recycler)901 private void recycleChildrenFromStart(RecyclerView.Recycler recycler) { 902 // Start laying out children one page before the top of the viewport. 903 int childrenStart = getPaddingTop() - getHeight(); 904 905 int focusedChildPosition = Integer.MAX_VALUE; 906 View focusedChild = getFocusedChild(); 907 if (focusedChild != null) { 908 focusedChildPosition = getPosition(focusedChild); 909 } 910 911 // Count the number of views that should be removed. 912 int detachedCount = 0; 913 int childCount = getChildCount(); 914 for (int i = 0; i < childCount; i++) { 915 final View child = getChildAt(i); 916 int childEnd = getDecoratedBottom(child); 917 int childPosition = getPosition(child); 918 919 if (childEnd >= childrenStart || childPosition >= focusedChildPosition - 1) { 920 break; 921 } 922 923 detachedCount++; 924 } 925 926 // Remove the number of views counted above. Done by removing the first child n times. 927 while (--detachedCount >= 0) { 928 final View child = getChildAt(0); 929 removeAndRecycleView(child, recycler); 930 } 931 } 932 933 /** 934 * Remove and recycle views that are no longer needed. 935 */ recycleChildrenFromEnd(RecyclerView.Recycler recycler)936 private void recycleChildrenFromEnd(RecyclerView.Recycler recycler) { 937 // Layout views until the end of the viewport. 938 int childrenEnd = getHeight(); 939 940 int focusedChildPosition = Integer.MIN_VALUE + 1; 941 View focusedChild = getFocusedChild(); 942 if (focusedChild != null) { 943 focusedChildPosition = getPosition(focusedChild); 944 } 945 946 // Count the number of views that should be removed. 947 int firstDetachedPos = 0; 948 int detachedCount = 0; 949 int childCount = getChildCount(); 950 for (int i = childCount - 1; i >= 0; i--) { 951 final View child = getChildAt(i); 952 int childStart = getDecoratedTop(child); 953 int childPosition = getPosition(child); 954 955 if (childStart <= childrenEnd || childPosition <= focusedChildPosition - 1) { 956 break; 957 } 958 959 firstDetachedPos = i; 960 detachedCount++; 961 } 962 963 while (--detachedCount >= 0) { 964 final View child = getChildAt(firstDetachedPos); 965 removeAndRecycleView(child, recycler); 966 } 967 } 968 969 /** 970 * Offset rows to do fancy animations. If {@link #mOffsetRows} is false, this will do nothing. 971 * 972 * @see #offsetRowsIndividually() 973 * @see #offsetRowsByPage() 974 */ offsetRows()975 public void offsetRows() { 976 if (!mOffsetRows) { 977 return; 978 } 979 980 if (mRowOffsetMode == ROW_OFFSET_MODE_PAGE) { 981 offsetRowsByPage(); 982 } else if (mRowOffsetMode == ROW_OFFSET_MODE_INDIVIDUAL) { 983 offsetRowsIndividually(); 984 } 985 } 986 987 /** 988 * Offset the single row that is scrolling off the screen such that by the time the next row 989 * reaches the top, it will have accelerated completely off of the screen. 990 */ offsetRowsIndividually()991 private void offsetRowsIndividually() { 992 if (getChildCount() == 0) { 993 if (DEBUG) { 994 Log.d(TAG, ":: offsetRowsIndividually getChildCount=0"); 995 } 996 return; 997 } 998 999 // Identify the dangling row. It will be the first row that is at the top of the 1000 // list or above. 1001 int danglingChildIndex = -1; 1002 for (int i = getChildCount() - 1; i >= 0; i--) { 1003 View child = getChildAt(i); 1004 if (getDecoratedTop(child) - getParams(child).topMargin <= getPaddingTop()) { 1005 danglingChildIndex = i; 1006 break; 1007 } 1008 } 1009 1010 mAnchorPageBreakPosition = danglingChildIndex; 1011 1012 if (DEBUG) { 1013 Log.v(TAG, ":: offsetRowsIndividually danglingChildIndex: " + danglingChildIndex); 1014 } 1015 1016 // Calculate the total amount that the view will need to scroll in order to go completely 1017 // off screen. 1018 RecyclerView rv = (RecyclerView) getChildAt(0).getParent(); 1019 int[] locs = new int[2]; 1020 rv.getLocationInWindow(locs); 1021 int listTopInWindow = locs[1] + rv.getPaddingTop(); 1022 int maxDanglingViewTranslation; 1023 1024 int childCount = getChildCount(); 1025 for (int i = 0; i < childCount; i++) { 1026 View child = getChildAt(i); 1027 RecyclerView.LayoutParams params = getParams(child); 1028 1029 maxDanglingViewTranslation = listTopInWindow; 1030 // If the child has a negative margin, we'll actually need to translate the view a 1031 // little but further to get it completely off screen. 1032 if (params.topMargin < 0) { 1033 maxDanglingViewTranslation -= params.topMargin; 1034 } 1035 if (params.bottomMargin < 0) { 1036 maxDanglingViewTranslation -= params.bottomMargin; 1037 } 1038 1039 if (i < danglingChildIndex) { 1040 child.setAlpha(0f); 1041 } else if (i > danglingChildIndex) { 1042 child.setAlpha(1f); 1043 child.setTranslationY(0); 1044 } else { 1045 int totalScrollDistance = getDecoratedMeasuredHeight(child) + 1046 params.topMargin + params.bottomMargin; 1047 1048 int distanceLeftInScroll = getDecoratedBottom(child) + 1049 params.bottomMargin - getPaddingTop(); 1050 float percentageIntoScroll = 1 - distanceLeftInScroll / (float) totalScrollDistance; 1051 float interpolatedPercentage = 1052 mDanglingRowInterpolator.getInterpolation(percentageIntoScroll); 1053 1054 child.setAlpha(1f); 1055 child.setTranslationY(-(maxDanglingViewTranslation * interpolatedPercentage)); 1056 } 1057 } 1058 } 1059 1060 /** 1061 * When the list scrolls, the entire page of rows will offset in one contiguous block. This 1062 * significantly reduces the amount of extra motion at the top of the screen. 1063 */ offsetRowsByPage()1064 private void offsetRowsByPage() { 1065 View anchorView = findViewByPosition(mAnchorPageBreakPosition); 1066 if (anchorView == null) { 1067 if (DEBUG) { 1068 Log.d(TAG, ":: offsetRowsByPage anchorView null"); 1069 } 1070 return; 1071 } 1072 int anchorViewTop = getDecoratedTop(anchorView) - getParams(anchorView).topMargin; 1073 1074 View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition); 1075 int upperViewTop = getDecoratedTop(upperPageBreakView) 1076 - getParams(upperPageBreakView).topMargin; 1077 1078 int scrollDistance = upperViewTop - anchorViewTop; 1079 1080 int distanceLeft = anchorViewTop - getPaddingTop(); 1081 float scrollPercentage = (Math.abs(scrollDistance) - distanceLeft) 1082 / (float) Math.abs(scrollDistance); 1083 1084 if (DEBUG) { 1085 Log.d(TAG, String.format( 1086 ":: offsetRowsByPage scrollDistance:%s, distanceLeft:%s, scrollPercentage:%s", 1087 scrollDistance, distanceLeft, scrollPercentage)); 1088 } 1089 1090 // Calculate the total amount that the view will need to scroll in order to go completely 1091 // off screen. 1092 RecyclerView rv = (RecyclerView) getChildAt(0).getParent(); 1093 int[] locs = new int[2]; 1094 rv.getLocationInWindow(locs); 1095 int listTopInWindow = locs[1] + rv.getPaddingTop(); 1096 1097 int childCount = getChildCount(); 1098 for (int i = 0; i < childCount; i++) { 1099 View child = getChildAt(i); 1100 int position = getPosition(child); 1101 if (position < mUpperPageBreakPosition) { 1102 child.setAlpha(0f); 1103 child.setTranslationY(-listTopInWindow); 1104 } else if (position < mAnchorPageBreakPosition) { 1105 // If the child has a negative margin, we need to offset the row by a little bit 1106 // extra so that it moves completely off screen. 1107 RecyclerView.LayoutParams params = getParams(child); 1108 int extraTranslation = 0; 1109 if (params.topMargin < 0) { 1110 extraTranslation -= params.topMargin; 1111 } 1112 if (params.bottomMargin < 0) { 1113 extraTranslation -= params.bottomMargin; 1114 } 1115 int translation = (int) ((listTopInWindow + extraTranslation) 1116 * mDanglingRowInterpolator.getInterpolation(scrollPercentage)); 1117 child.setAlpha(1f); 1118 child.setTranslationY(-translation); 1119 } else { 1120 child.setAlpha(1f); 1121 child.setTranslationY(0); 1122 } 1123 } 1124 } 1125 1126 /** 1127 * Update the page break positions based on the position of the views on screen. This should 1128 * be called whenever view move or change such as during a scroll or layout. 1129 */ updatePageBreakPositions()1130 private void updatePageBreakPositions() { 1131 if (getChildCount() == 0) { 1132 if (DEBUG) { 1133 Log.d(TAG, ":: updatePageBreakPosition getChildCount: 0"); 1134 } 1135 return; 1136 } 1137 1138 if (DEBUG) { 1139 Log.v(TAG, String.format(":: #BEFORE updatePageBreakPositions " + 1140 "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, " 1141 + "mLowerPageBreakPosition:%s", 1142 mAnchorPageBreakPosition, mUpperPageBreakPosition, mLowerPageBreakPosition)); 1143 } 1144 1145 // If the item count has changed, our page boundaries may no longer be accurate. This will 1146 // force the page boundaries to reset around the current view that is closest to the top. 1147 if (getItemCount() != mItemCountDuringLastPageBreakUpdate) { 1148 if (DEBUG) { 1149 Log.d(TAG, "Item count changed. Resetting page break positions."); 1150 } 1151 mAnchorPageBreakPosition = getPosition(getFirstFullyVisibleChild()); 1152 } 1153 mItemCountDuringLastPageBreakUpdate = getItemCount(); 1154 1155 if (mAnchorPageBreakPosition == -1) { 1156 Log.w(TAG, "Unable to update anchor positions. There is no anchor position."); 1157 return; 1158 } 1159 1160 View anchorPageBreakView = findViewByPosition(mAnchorPageBreakPosition); 1161 if (anchorPageBreakView == null) { 1162 return; 1163 } 1164 int topMargin = getParams(anchorPageBreakView).topMargin; 1165 int anchorTop = getDecoratedTop(anchorPageBreakView) - topMargin; 1166 View upperPageBreakView = findViewByPosition(mUpperPageBreakPosition); 1167 int upperPageBreakTop = upperPageBreakView == null ? Integer.MIN_VALUE : 1168 getDecoratedTop(upperPageBreakView) - getParams(upperPageBreakView).topMargin; 1169 1170 if (DEBUG) { 1171 Log.v(TAG, String.format(":: #MID updatePageBreakPositions topMargin:%s, anchorTop:%s" 1172 + "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, " 1173 + "mLowerPageBreakPosition:%s", topMargin, anchorTop, 1174 mAnchorPageBreakPosition, mUpperPageBreakPosition, mLowerPageBreakPosition)); 1175 } 1176 1177 if (anchorTop < getPaddingTop()) { 1178 // The anchor has moved above the viewport. We are now on the next page. Shift the page 1179 // break positions and calculate a new lower one. 1180 mUpperPageBreakPosition = mAnchorPageBreakPosition; 1181 mAnchorPageBreakPosition = mLowerPageBreakPosition; 1182 mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition); 1183 } else if (mAnchorPageBreakPosition > 0 && upperPageBreakTop >= getPaddingTop()) { 1184 // The anchor has moved below the viewport. We are now on the previous page. Shift 1185 // the page break positions and calculate a new upper one. 1186 mLowerPageBreakPosition = mAnchorPageBreakPosition; 1187 mAnchorPageBreakPosition = mUpperPageBreakPosition; 1188 mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition); 1189 } else { 1190 mUpperPageBreakPosition = calculatePreviousPageBreakPosition(mAnchorPageBreakPosition); 1191 mLowerPageBreakPosition = calculateNextPageBreakPosition(mAnchorPageBreakPosition); 1192 } 1193 1194 if (DEBUG) { 1195 Log.v(TAG, String.format(":: #AFTER updatePageBreakPositions " + 1196 "mAnchorPageBreakPosition:%s, mUpperPageBreakPosition:%s, " 1197 + "mLowerPageBreakPosition:%s", 1198 mAnchorPageBreakPosition, mUpperPageBreakPosition, mLowerPageBreakPosition)); 1199 } 1200 } 1201 1202 /** 1203 * @return The page break position of the page before the anchor page break position. However, 1204 * if it reaches the end of the laid out children or position 0, it will just return 1205 * that. 1206 */ calculatePreviousPageBreakPosition(int position)1207 private int calculatePreviousPageBreakPosition(int position) { 1208 if (position == -1) { 1209 return -1; 1210 } 1211 View referenceView = findViewByPosition(position); 1212 int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin; 1213 1214 int previousPagePosition = position; 1215 while (previousPagePosition > 0) { 1216 previousPagePosition--; 1217 View child = findViewByPosition(previousPagePosition); 1218 if (child == null) { 1219 // View has not been laid out yet. 1220 return previousPagePosition + 1; 1221 } 1222 1223 int childTop = getDecoratedTop(child) - getParams(child).topMargin; 1224 1225 if (childTop < referenceViewTop - getHeight()) { 1226 return previousPagePosition + 1; 1227 } 1228 } 1229 // Beginning of the list. 1230 return 0; 1231 } 1232 1233 /** 1234 * @return The page break position of the next page after the anchor page break position. 1235 * However, if it reaches the end of the laid out children or end of the list, it will 1236 * just return that. 1237 */ calculateNextPageBreakPosition(int position)1238 private int calculateNextPageBreakPosition(int position) { 1239 if (position == -1) { 1240 return -1; 1241 } 1242 1243 View referenceView = findViewByPosition(position); 1244 if (referenceView == null) { 1245 return position; 1246 } 1247 int referenceViewTop = getDecoratedTop(referenceView) - getParams(referenceView).topMargin; 1248 1249 int nextPagePosition = position; 1250 while (position < getItemCount() - 1) { 1251 nextPagePosition++; 1252 View child = findViewByPosition(nextPagePosition); 1253 if (child == null) { 1254 // The next view has not been laid out yet. 1255 return nextPagePosition - 1; 1256 } 1257 1258 int childBottom = getDecoratedBottom(child) + getParams(child).bottomMargin; 1259 if (childBottom - referenceViewTop > getHeight() - getPaddingTop()) { 1260 return nextPagePosition - 1; 1261 } 1262 } 1263 // End of the list. 1264 return nextPagePosition; 1265 } 1266 1267 /** 1268 * In this style, the focus will scroll down to the middle of the screen and lock there 1269 * so that moving in either direction will move the entire list by 1. 1270 */ onRequestChildFocusMarioStyle(RecyclerView parent, View child)1271 private boolean onRequestChildFocusMarioStyle(RecyclerView parent, View child) { 1272 int focusedPosition = getPosition(child); 1273 if (focusedPosition == mLastChildPositionToRequestFocus) { 1274 return true; 1275 } 1276 mLastChildPositionToRequestFocus = focusedPosition; 1277 1278 int availableHeight = getAvailableHeight(); 1279 int focusedChildTop = getDecoratedTop(child); 1280 int focusedChildBottom = getDecoratedBottom(child); 1281 1282 int childIndex = parent.indexOfChild(child); 1283 // Iterate through children starting at the focused child to find the child above it to 1284 // smooth scroll to such that the focused child will be as close to the middle of the screen 1285 // as possible. 1286 for (int i = childIndex; i >= 0; i--) { 1287 View childAtI = getChildAt(i); 1288 if (childAtI == null) { 1289 Log.e(TAG, "Child is null at index " + i); 1290 continue; 1291 } 1292 // We haven't found a view that is more than half of the recycler view height above it 1293 // but we've reached the top so we can't go any further. 1294 if (i == 0) { 1295 parent.smoothScrollToPosition(getPosition(childAtI)); 1296 break; 1297 } 1298 1299 // Because we want to scroll to the first view that is less than half of the screen 1300 // away from the focused view, we "look ahead" one view. When the look ahead view 1301 // is more than availableHeight / 2 away, the current child at i is the one we want to 1302 // scroll to. However, sometimes, that view can be null (ie, if the view is in 1303 // transition). In that case, just skip that view. 1304 1305 View childBefore = getChildAt(i - 1); 1306 if (childBefore == null) { 1307 continue; 1308 } 1309 int distanceToChildBeforeFromTop = focusedChildTop - getDecoratedTop(childBefore); 1310 int distanceToChildBeforeFromBottom = focusedChildBottom - getDecoratedTop(childBefore); 1311 1312 if (distanceToChildBeforeFromTop > availableHeight / 2 1313 || distanceToChildBeforeFromBottom > availableHeight) { 1314 parent.smoothScrollToPosition(getPosition(childAtI)); 1315 break; 1316 } 1317 } 1318 return true; 1319 } 1320 1321 /** 1322 * In this style, you can free scroll in the middle of the list but if you get to the edge, 1323 * the list will advance to ensure that there is context ahead of the focused item. 1324 */ onRequestChildFocusSuperMarioStyle(RecyclerView parent, RecyclerView.State state, View child)1325 private boolean onRequestChildFocusSuperMarioStyle(RecyclerView parent, 1326 RecyclerView.State state, View child) { 1327 int focusedPosition = getPosition(child); 1328 if (focusedPosition == mLastChildPositionToRequestFocus) { 1329 return true; 1330 } 1331 mLastChildPositionToRequestFocus = focusedPosition; 1332 1333 int bottomEdgeThatMustBeOnScreen; 1334 int focusedIndex = parent.indexOfChild(child); 1335 // The amount of the last card at the end that must be showing to count as visible. 1336 int peekAmount = mContext.getResources() 1337 .getDimensionPixelSize(R.dimen.car_last_card_peek_amount); 1338 if (focusedPosition == state.getItemCount() - 1) { 1339 // The last item is focused. 1340 bottomEdgeThatMustBeOnScreen = getDecoratedBottom(child); 1341 } else if (focusedIndex == getChildCount() - 1) { 1342 // The last laid out item is focused. Scroll enough so that the next card has at least 1343 // the peek size visible 1344 ViewGroup.MarginLayoutParams params = 1345 (ViewGroup.MarginLayoutParams) child.getLayoutParams(); 1346 // We add params.topMargin as an estimate because we don't actually know the top margin 1347 // of the next row. 1348 bottomEdgeThatMustBeOnScreen = getDecoratedBottom(child) + 1349 params.bottomMargin + params.topMargin + peekAmount; 1350 } else { 1351 View nextChild = getChildAt(focusedIndex + 1); 1352 bottomEdgeThatMustBeOnScreen = getDecoratedTop(nextChild) + peekAmount; 1353 } 1354 1355 if (bottomEdgeThatMustBeOnScreen > getHeight()) { 1356 // We're going to have to scroll because the bottom edge that must be on screen is past 1357 // the bottom. 1358 int topEdgeToFindViewUnder = getPaddingTop() + 1359 bottomEdgeThatMustBeOnScreen - getHeight(); 1360 1361 View nextChild = null; 1362 for (int i = 0; i < getChildCount(); i++) { 1363 View potentialNextChild = getChildAt(i); 1364 RecyclerView.LayoutParams params = getParams(potentialNextChild); 1365 float top = getDecoratedTop(potentialNextChild) - params.topMargin; 1366 if (top >= topEdgeToFindViewUnder) { 1367 nextChild = potentialNextChild; 1368 break; 1369 } 1370 } 1371 1372 if (nextChild == null) { 1373 Log.e(TAG, "There is no view under " + topEdgeToFindViewUnder); 1374 return true; 1375 } 1376 int nextChildPosition = getPosition(nextChild); 1377 parent.smoothScrollToPosition(nextChildPosition); 1378 } else { 1379 int firstFullyVisibleIndex = getFirstFullyVisibleChildIndex(); 1380 if (focusedIndex <= firstFullyVisibleIndex) { 1381 parent.smoothScrollToPosition(Math.max(focusedPosition - 1, 0)); 1382 } 1383 } 1384 return true; 1385 } 1386 1387 /** 1388 * We don't actually know the size of every single view, only what is currently laid out. 1389 * This makes it difficult to do accurate scrollbar calculations. However, lists in the car 1390 * often consist of views with identical heights. Because of that, we can use 1391 * a single sample view to do our calculations for. The main exceptions are in the first items 1392 * of a list (hero card, last call card, etc) so if the first view is at position 0, we pick 1393 * the next one. 1394 * 1395 * @return The decorated measured height of the sample view plus its margins. 1396 */ getSampleViewHeight()1397 private int getSampleViewHeight() { 1398 if (mSampleViewHeight != -1) { 1399 return mSampleViewHeight; 1400 } 1401 int sampleViewIndex = getFirstFullyVisibleChildIndex(); 1402 View sampleView = getChildAt(sampleViewIndex); 1403 if (getPosition(sampleView) == 0 && sampleViewIndex < getChildCount() - 1) { 1404 sampleView = getChildAt(++sampleViewIndex); 1405 } 1406 RecyclerView.LayoutParams params = getParams(sampleView); 1407 int height = 1408 getDecoratedMeasuredHeight(sampleView) + params.topMargin + params.bottomMargin; 1409 if (height == 0) { 1410 // This can happen if the view isn't measured yet. 1411 Log.w(TAG, "The sample view has a height of 0. Returning a dummy value for now " + 1412 "that won't be cached."); 1413 height = mContext.getResources().getDimensionPixelSize(R.dimen.car_sample_row_height); 1414 } else { 1415 mSampleViewHeight = height; 1416 } 1417 return height; 1418 } 1419 1420 /** 1421 * @return The height of the RecyclerView excluding padding. 1422 */ getAvailableHeight()1423 private int getAvailableHeight() { 1424 return getHeight() - getPaddingTop() - getPaddingBottom(); 1425 } 1426 1427 /** 1428 * @return {@link RecyclerView.LayoutParams} for the given view or null if it isn't a child 1429 * of {@link RecyclerView}. 1430 */ getParams(View view)1431 private static RecyclerView.LayoutParams getParams(View view) { 1432 return (RecyclerView.LayoutParams) view.getLayoutParams(); 1433 } 1434 1435 /** 1436 * Custom {@link LinearSmoothScroller} that has: 1437 * a) Custom control over the speed of scrolls. 1438 * b) Scrolling snaps to start. All of our scrolling logic depends on that. 1439 * c) Keeps track of some state of the current scroll so that can aid in things like 1440 * the scrollbar calculations. 1441 */ 1442 private final class CarSmoothScroller extends LinearSmoothScroller { 1443 /** This value (150) was hand tuned by UX for what felt right. **/ 1444 private static final float MILLISECONDS_PER_INCH = 150f; 1445 /** This value (0.45) was hand tuned by UX for what felt right. **/ 1446 private static final float DECELERATION_TIME_DIVISOR = 0.45f; 1447 private static final int NON_TOUCH_MAX_DECELERATION_MS = 1000; 1448 1449 /** This value (1.8) was hand tuned by UX for what felt right. **/ 1450 private final Interpolator mInterpolator = new DecelerateInterpolator(1.8f); 1451 1452 private final boolean mHasTouch; 1453 private final int mTargetPosition; 1454 1455 CarSmoothScroller(Context context, int targetPosition)1456 public CarSmoothScroller(Context context, int targetPosition) { 1457 super(context); 1458 mTargetPosition = targetPosition; 1459 mHasTouch = mContext.getResources().getBoolean(R.bool.car_true_for_touch); 1460 } 1461 1462 @Override computeScrollVectorForPosition(int i)1463 public PointF computeScrollVectorForPosition(int i) { 1464 if (getChildCount() == 0) { 1465 return null; 1466 } 1467 final int firstChildPos = getPosition(getChildAt(getFirstFullyVisibleChildIndex())); 1468 final int direction = (mTargetPosition < firstChildPos) ? -1 : 1; 1469 return new PointF(0, direction); 1470 } 1471 1472 @Override getVerticalSnapPreference()1473 protected int getVerticalSnapPreference() { 1474 // This is key for most of the scrolling logic that guarantees that scrolling 1475 // will settle with a view aligned to the top. 1476 return LinearSmoothScroller.SNAP_TO_START; 1477 } 1478 1479 @Override onTargetFound(View targetView, RecyclerView.State state, Action action)1480 protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { 1481 int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START); 1482 if (dy == 0) { 1483 if (DEBUG) { 1484 Log.d(TAG, "Scroll distance is 0"); 1485 } 1486 return; 1487 } 1488 1489 final int time = calculateTimeForDeceleration(dy); 1490 if (time > 0) { 1491 action.update(0, -dy, time, mInterpolator); 1492 } 1493 } 1494 1495 @Override calculateSpeedPerPixel(DisplayMetrics displayMetrics)1496 protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { 1497 return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; 1498 } 1499 1500 @Override calculateTimeForDeceleration(int dx)1501 protected int calculateTimeForDeceleration(int dx) { 1502 int time = (int) Math.ceil(calculateTimeForScrolling(dx) / DECELERATION_TIME_DIVISOR); 1503 return mHasTouch ? time : Math.min(time, NON_TOUCH_MAX_DECELERATION_MS); 1504 } 1505 getTargetPosition()1506 public int getTargetPosition() { 1507 return mTargetPosition; 1508 } 1509 } 1510 } 1511