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