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