Home | History | Annotate | Download | only in ui
      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 {
    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 
    163     public CarLayoutManager(Context context) {
    164         mContext = context;
    165     }
    166 
    167     @Override
    168     public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    169         return new RecyclerView.LayoutParams(
    170                 ViewGroup.LayoutParams.MATCH_PARENT,
    171                 ViewGroup.LayoutParams.WRAP_CONTENT);
    172     }
    173 
    174     @Override
    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
    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
    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
    370     public void scrollToPosition(int position) {
    371         mPendingScrollPosition = position;
    372         requestLayout();
    373     }
    374 
    375     @Override
    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
    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
    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
    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
    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
    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
    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
    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
    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      */
    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 
    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      */
    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      */
    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      */
    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 
    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 
    684     public void setRowOffsetMode(@RowOffsetMode int mode) {
    685         if (mode == mRowOffsetMode) {
    686             return;
    687         }
    688         mRowOffsetMode = mode;
    689         offsetRows();
    690     }
    691 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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 
   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
   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
   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
   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
   1496         protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
   1497             return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
   1498         }
   1499 
   1500         @Override
   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 
   1506         public int getTargetPosition() {
   1507             return mTargetPosition;
   1508         }
   1509     }
   1510 }
   1511