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