Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2017 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 
     17 package androidx.car.widget;
     18 
     19 import static java.lang.annotation.RetentionPolicy.SOURCE;
     20 
     21 import android.content.Context;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Canvas;
     24 import android.graphics.Paint;
     25 import android.graphics.PointF;
     26 import android.graphics.Rect;
     27 import android.graphics.drawable.Drawable;
     28 import android.os.Bundle;
     29 import android.os.Handler;
     30 import android.os.Parcelable;
     31 import android.util.AttributeSet;
     32 import android.util.Log;
     33 import android.util.SparseArray;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.FrameLayout;
     38 
     39 import androidx.annotation.ColorRes;
     40 import androidx.annotation.IdRes;
     41 import androidx.annotation.IntDef;
     42 import androidx.annotation.NonNull;
     43 import androidx.annotation.Nullable;
     44 import androidx.annotation.UiThread;
     45 import androidx.annotation.VisibleForTesting;
     46 import androidx.car.R;
     47 import androidx.recyclerview.widget.GridLayoutManager;
     48 import androidx.recyclerview.widget.LinearLayoutManager;
     49 import androidx.recyclerview.widget.OrientationHelper;
     50 import androidx.recyclerview.widget.RecyclerView;
     51 
     52 import java.lang.annotation.Retention;
     53 
     54 /**
     55  * View that wraps a {@link RecyclerView} and a scroll bar that has
     56  * page up and down arrows. Interaction with this view is similar to a {@code RecyclerView} as it
     57  * takes the same adapter.
     58  *
     59  * <p>By default, this PagedListView utilizes a vertical {@link LinearLayoutManager} to display
     60  * its items.
     61  */
     62 public class PagedListView extends FrameLayout {
     63     /**
     64      * The key used to save the state of this PagedListView's super class in
     65      * {@link #onSaveInstanceState()}.
     66      */
     67     private static final String SAVED_SUPER_STATE_KEY = "PagedListViewSuperState";
     68 
     69     /**
     70      * The key used to save the state of {@link #mRecyclerView} so that it can be restored
     71      * on configuration change. The actual saving of state will be controlled by the LayoutManager
     72      * of the RecyclerView; this value simply ensures the state is passed on to the LayoutManager.
     73      */
     74     private static final String SAVED_RECYCLER_VIEW_STATE_KEY = "RecyclerViewState";
     75 
     76     /** Default maximum number of clicks allowed on a list */
     77     public static final int DEFAULT_MAX_CLICKS = 6;
     78 
     79     /**
     80      * Value to pass to {@link #setMaxPages(int)} to indicate there is no restriction on the
     81      * maximum number of pages to show.
     82      */
     83     public static final int UNLIMITED_PAGES = -1;
     84 
     85     /**
     86      * The amount of time after settling to wait before autoscrolling to the next page when the user
     87      * holds down a pagination button.
     88      */
     89     private static final int PAGINATION_HOLD_DELAY_MS = 400;
     90 
     91     /**
     92      * When doing a snap, offset the snap by this number of position and then do a smooth scroll to
     93      * the final position.
     94      */
     95     private static final int SNAP_SCROLL_OFFSET_POSITION = 2;
     96 
     97     private static final String TAG = "PagedListView";
     98     private static final int INVALID_RESOURCE_ID = -1;
     99 
    100     private RecyclerView mRecyclerView;
    101     private PagedSnapHelper mSnapHelper;
    102     private final Handler mHandler = new Handler();
    103     private boolean mScrollBarEnabled;
    104     @VisibleForTesting
    105     PagedScrollBarView mScrollBarView;
    106 
    107     /**
    108      * AlphaJumpOverlayView that will be null until the first time you tap the alpha jump button, at
    109      * which point we'll construct it and add it to the view hierarchy as a child of this frame
    110      * layout.
    111      */
    112     @Nullable private AlphaJumpOverlayView mAlphaJumpView;
    113 
    114     private int mRowsPerPage = -1;
    115     private RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;
    116 
    117     /** Maximum number of pages to show. */
    118     private int mMaxPages;
    119 
    120     private OnScrollListener mOnScrollListener;
    121 
    122     /** Number of visible rows per page */
    123     private int mDefaultMaxPages = DEFAULT_MAX_CLICKS;
    124 
    125     /** Used to check if there are more items added to the list. */
    126     private int mLastItemCount;
    127 
    128     private boolean mNeedsFocus;
    129 
    130     private OrientationHelper mOrientationHelper;
    131 
    132     @Gutter
    133     private int mGutter;
    134     private int mGutterSize;
    135 
    136     /**
    137      * Interface for a {@link RecyclerView.Adapter} to cap the number of
    138      * items.
    139      *
    140      * <p>NOTE: it is still up to the adapter to use maxItems in {@link
    141      * RecyclerView.Adapter#getItemCount()}.
    142      *
    143      * <p>the recommended way would be with:
    144      *
    145      * <pre>{@code
    146      * {@literal@}Override
    147      * public int getItemCount() {
    148      *   return Math.min(super.getItemCount(), mMaxItems);
    149      * }
    150      * }</pre>
    151      */
    152     public interface ItemCap {
    153         /**
    154          * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
    155          */
    156         int UNLIMITED = -1;
    157 
    158         /**
    159          * Sets the maximum number of items available in the adapter. A value less than '0' means
    160          * the list should not be capped.
    161          */
    162         void setMaxItems(int maxItems);
    163     }
    164 
    165     /**
    166      * Interface for controlling visibility of item dividers for individual items based on the
    167      * item's position.
    168      *
    169      * <p> NOTE: interface takes effect only when dividers are enabled.
    170      */
    171     public interface DividerVisibilityManager {
    172         /**
    173          * Given an item position, returns whether the divider below that item should be hidden.
    174          *
    175          * @param position item position inside the adapter.
    176          * @return true if divider is to be hidden, false if divider should be shown.
    177          */
    178         boolean shouldHideDivider(int position);
    179     }
    180 
    181     /**
    182      * The possible values for @{link #setGutter}. The default value is actually
    183      * {@link Gutter#BOTH}.
    184      */
    185     @IntDef({
    186             Gutter.NONE,
    187             Gutter.START,
    188             Gutter.END,
    189             Gutter.BOTH,
    190     })
    191     @Retention(SOURCE)
    192     public @interface Gutter {
    193         /**
    194          * No gutter on either side of the list items. The items will span the full width of the
    195          * {@link PagedListView}.
    196          */
    197         int NONE = 0;
    198 
    199         /**
    200          * Include a gutter only on the start side (that is, the same side as the scroll bar).
    201          */
    202         int START = 1;
    203 
    204         /**
    205          * Include a gutter only on the end side (that is, the opposite side of the scroll bar).
    206          */
    207         int END = 2;
    208 
    209         /**
    210          * Include a gutter on both sides of the list items. This is the default behaviour.
    211          */
    212         int BOTH = 3;
    213     }
    214 
    215     /**
    216      * Interface for a {@link RecyclerView.Adapter} to set the position
    217      * offset for the adapter to load the data.
    218      *
    219      * <p>For example in the adapter, if the positionOffset is 20, then for position 0 it will show
    220      * the item in position 20 instead, for position 1 it will show the item in position 21 instead
    221      * and so on.
    222      */
    223     public interface ItemPositionOffset {
    224         /** Sets the position offset for the adapter. */
    225         void setPositionOffset(int positionOffset);
    226     }
    227 
    228     public PagedListView(Context context) {
    229         super(context);
    230         init(context, null /* attrs */);
    231     }
    232 
    233     public PagedListView(Context context, AttributeSet attrs) {
    234         super(context, attrs);
    235         init(context, attrs);
    236     }
    237 
    238     public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) {
    239         super(context, attrs, defStyleAttrs);
    240         init(context, attrs);
    241     }
    242 
    243     public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
    244         super(context, attrs, defStyleAttrs, defStyleRes);
    245         init(context, attrs);
    246     }
    247 
    248     private void init(Context context, AttributeSet attrs) {
    249         LayoutInflater.from(context).inflate(R.layout.car_paged_recycler_view,
    250                 this /* root */, true /* attachToRoot */);
    251 
    252         TypedArray a = context.obtainStyledAttributes(
    253                 attrs, R.styleable.PagedListView, R.attr.pagedListViewStyle, 0 /* defStyleRes */);
    254         mRecyclerView = findViewById(R.id.recycler_view);
    255 
    256         mMaxPages = getDefaultMaxPages();
    257 
    258         RecyclerView.LayoutManager layoutManager =
    259                 new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
    260         mRecyclerView.setLayoutManager(layoutManager);
    261 
    262         mSnapHelper = new PagedSnapHelper(context);
    263         mSnapHelper.attachToRecyclerView(mRecyclerView);
    264 
    265         mRecyclerView.addOnScrollListener(mRecyclerViewOnScrollListener);
    266         mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
    267 
    268         if (a.getBoolean(R.styleable.PagedListView_verticallyCenterListContent, false)) {
    269             // Setting the height of wrap_content allows the RecyclerView to center itself.
    270             mRecyclerView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
    271         }
    272 
    273         int defaultGutterSize = getResources().getDimensionPixelSize(R.dimen.car_margin);
    274         mGutterSize = a.getDimensionPixelSize(R.styleable.PagedListView_gutterSize,
    275                 defaultGutterSize);
    276 
    277         if (a.hasValue(R.styleable.PagedListView_gutter)) {
    278             int gutter = a.getInt(R.styleable.PagedListView_gutter, Gutter.BOTH);
    279             setGutter(gutter);
    280         } else if (a.hasValue(R.styleable.PagedListView_offsetScrollBar)) {
    281             boolean offsetScrollBar =
    282                     a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false);
    283             if (offsetScrollBar) {
    284                 setGutter(Gutter.START);
    285             }
    286         } else {
    287             setGutter(Gutter.BOTH);
    288         }
    289 
    290         if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) {
    291             int dividerStartMargin = a.getDimensionPixelSize(
    292                     R.styleable.PagedListView_dividerStartMargin, 0);
    293             int dividerEndMargin = a.getDimensionPixelSize(
    294                     R.styleable.PagedListView_dividerEndMargin, 0);
    295             int dividerStartId = a.getResourceId(
    296                     R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID);
    297             int dividerEndId = a.getResourceId(
    298                     R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID);
    299 
    300             int listDividerColor = a.getResourceId(R.styleable.PagedListView_listDividerColor,
    301                     R.color.car_list_divider);
    302 
    303             mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin,
    304                     dividerEndMargin, dividerStartId, dividerEndId, listDividerColor));
    305         }
    306 
    307         int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0);
    308         if (itemSpacing > 0) {
    309             mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
    310         }
    311 
    312         int listContentTopMargin =
    313                 a.getDimensionPixelSize(R.styleable.PagedListView_listContentTopOffset, 0);
    314         if (listContentTopMargin > 0) {
    315             mRecyclerView.addItemDecoration(new TopOffsetDecoration(listContentTopMargin));
    316         }
    317 
    318         // Set focusable false explicitly to handle the behavior change in Android O where
    319         // clickable view becomes focusable by default.
    320         setFocusable(false);
    321 
    322         mScrollBarEnabled = a.getBoolean(R.styleable.PagedListView_scrollBarEnabled, true);
    323         mScrollBarView = findViewById(R.id.paged_scroll_view);
    324         mScrollBarView.setPaginationListener(new PagedScrollBarView.PaginationListener() {
    325             @Override
    326             public void onPaginate(int direction) {
    327                 switch (direction) {
    328                     case PagedScrollBarView.PaginationListener.PAGE_UP:
    329                         pageUp();
    330                         if (mOnScrollListener != null) {
    331                             mOnScrollListener.onScrollUpButtonClicked();
    332                         }
    333                         break;
    334                     case PagedScrollBarView.PaginationListener.PAGE_DOWN:
    335                         pageDown();
    336                         if (mOnScrollListener != null) {
    337                             mOnScrollListener.onScrollDownButtonClicked();
    338                         }
    339                         break;
    340                     default:
    341                         Log.e(TAG, "Unknown pagination direction (" + direction + ")");
    342                 }
    343             }
    344 
    345             @Override
    346             public void onAlphaJump() {
    347                 showAlphaJump();
    348             }
    349         });
    350 
    351         Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon);
    352         if (upButtonIcon != null) {
    353             setUpButtonIcon(upButtonIcon);
    354         }
    355 
    356         Drawable downButtonIcon = a.getDrawable(R.styleable.PagedListView_downButtonIcon);
    357         if (downButtonIcon != null) {
    358             setDownButtonIcon(downButtonIcon);
    359         }
    360 
    361         // Using getResourceId() over getColor() because setScrollbarColor() expects a color resId.
    362         int scrollBarColor = a.getResourceId(R.styleable.PagedListView_scrollBarColor, -1);
    363         if (scrollBarColor != -1) {
    364             setScrollbarColor(scrollBarColor);
    365         }
    366 
    367         mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE);
    368 
    369         if (mScrollBarEnabled) {
    370             // Use the top margin that is defined in the layout as the default value.
    371             int topMargin = a.getDimensionPixelSize(
    372                     R.styleable.PagedListView_scrollBarTopMargin,
    373                     ((MarginLayoutParams) mScrollBarView.getLayoutParams()).topMargin);
    374             setScrollBarTopMargin(topMargin);
    375         } else {
    376             MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
    377             params.setMarginStart(0);
    378         }
    379 
    380         if (a.hasValue(R.styleable.PagedListView_scrollBarContainerWidth)) {
    381             int carMargin = getResources().getDimensionPixelSize(R.dimen.car_margin);
    382             int scrollBarContainerWidth = a.getDimensionPixelSize(
    383                     R.styleable.PagedListView_scrollBarContainerWidth, carMargin);
    384             setScrollBarContainerWidth(scrollBarContainerWidth);
    385         }
    386 
    387         if (a.hasValue(R.styleable.PagedListView_dayNightStyle)) {
    388             @DayNightStyle int dayNightStyle =
    389                     a.getInt(R.styleable.PagedListView_dayNightStyle, DayNightStyle.AUTO);
    390             setDayNightStyle(dayNightStyle);
    391         } else {
    392             setDayNightStyle(DayNightStyle.AUTO);
    393         }
    394 
    395         a.recycle();
    396     }
    397 
    398     @Override
    399     protected void onDetachedFromWindow() {
    400         super.onDetachedFromWindow();
    401         mHandler.removeCallbacks(mUpdatePaginationRunnable);
    402     }
    403 
    404     /**
    405      * Returns the position of the given View in the list.
    406      *
    407      * @param v The View to check for.
    408      * @return The position or -1 if the given View is {@code null} or not in the list.
    409      */
    410     public int positionOf(@Nullable View v) {
    411         if (v == null || v.getParent() != mRecyclerView
    412                 || mRecyclerView.getLayoutManager() == null) {
    413             return -1;
    414         }
    415         return mRecyclerView.getLayoutManager().getPosition(v);
    416     }
    417 
    418     /**
    419      * Set the gutter to the specified value.
    420      *
    421      * <p>The gutter is the space to the start/end of the list view items and will be equal in size
    422      * to the scroll bars. By default, there is a gutter to both the left and right of the list
    423      * view items, to account for the scroll bar.
    424      *
    425      * @param gutter A {@link Gutter} value that identifies which sides to apply the gutter to.
    426      */
    427     public void setGutter(@Gutter int gutter) {
    428         mGutter = gutter;
    429 
    430         int startMargin = 0;
    431         int endMargin = 0;
    432         if ((mGutter & Gutter.START) != 0) {
    433             startMargin = mGutterSize;
    434         }
    435         if ((mGutter & Gutter.END) != 0) {
    436             endMargin = mGutterSize;
    437         }
    438         MarginLayoutParams layoutParams = (MarginLayoutParams) mRecyclerView.getLayoutParams();
    439         layoutParams.setMarginStart(startMargin);
    440         layoutParams.setMarginEnd(endMargin);
    441         // requestLayout() isn't sufficient because we also need to resolveLayoutParams().
    442         mRecyclerView.setLayoutParams(layoutParams);
    443 
    444         // If there's a gutter, set ClipToPadding to false so that CardView's shadow will still
    445         // appear outside of the padding.
    446         mRecyclerView.setClipToPadding(startMargin == 0 && endMargin == 0);
    447 
    448     }
    449 
    450     /**
    451      * Sets the size of the gutter that appears at the start, end or both sizes of the items in
    452      * the {@code PagedListView}.
    453      *
    454      * @param gutterSize The size of the gutter in pixels.
    455      * @see #setGutter(int)
    456      */
    457     public void setGutterSize(int gutterSize) {
    458         mGutterSize = gutterSize;
    459 
    460         // Call setGutter to reset the gutter.
    461         setGutter(mGutter);
    462     }
    463 
    464     /**
    465      * Sets the width of the container that holds the scrollbar. The scrollbar will be centered
    466      * within this width.
    467      *
    468      * @param width The width of the scrollbar container.
    469      */
    470     public void setScrollBarContainerWidth(int width) {
    471         ViewGroup.LayoutParams layoutParams = mScrollBarView.getLayoutParams();
    472         layoutParams.width = width;
    473         mScrollBarView.requestLayout();
    474     }
    475 
    476     /**
    477      * Sets the top margin above the scroll bar. By default, this margin is 0.
    478      *
    479      * @param topMargin The top margin.
    480      */
    481     public void setScrollBarTopMargin(int topMargin) {
    482         MarginLayoutParams params = (MarginLayoutParams) mScrollBarView.getLayoutParams();
    483         params.topMargin = topMargin;
    484         mScrollBarView.requestLayout();
    485     }
    486 
    487     /**
    488      * Sets an offset above the first item in the {@code PagedListView}. This offset is scrollable
    489      * with the contents of the list.
    490      *
    491      * @param offset The top offset to add.
    492      */
    493     public void setListContentTopOffset(int offset) {
    494         TopOffsetDecoration existing = null;
    495         for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
    496             RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
    497             if (itemDecoration instanceof TopOffsetDecoration) {
    498                 existing = (TopOffsetDecoration) itemDecoration;
    499                 break;
    500             }
    501         }
    502 
    503         if (offset == 0 && existing != null) {
    504             mRecyclerView.removeItemDecoration(existing);
    505         } else if (existing == null) {
    506             mRecyclerView.addItemDecoration(new TopOffsetDecoration(offset));
    507         } else {
    508             existing.setTopOffset(offset);
    509         }
    510         mRecyclerView.invalidateItemDecorations();
    511     }
    512 
    513     @NonNull
    514     public RecyclerView getRecyclerView() {
    515         return mRecyclerView;
    516     }
    517 
    518     /**
    519      * Scrolls to the given position in the PagedListView.
    520      *
    521      * @param position The position in the list to scroll to.
    522      */
    523     public void scrollToPosition(int position) {
    524         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    525         if (layoutManager == null) {
    526             return;
    527         }
    528 
    529         RecyclerView.SmoothScroller smoothScroller = mSnapHelper.createScroller(layoutManager);
    530         smoothScroller.setTargetPosition(position);
    531 
    532         layoutManager.startSmoothScroll(smoothScroller);
    533 
    534         // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure
    535         // the pagination arrows actually get updated. See b/15801119
    536         mHandler.post(mUpdatePaginationRunnable);
    537     }
    538 
    539     /**
    540      * Snap to the given position. This method will snap instantly to a position that's "close" to
    541      * the given position and then animate a short decelerate to indicate the direction that the
    542      * snap happened.
    543      *
    544      * @param position The position in the list to scroll to.
    545      */
    546     public void snapToPosition(int position) {
    547         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    548 
    549         if (layoutManager == null) {
    550             return;
    551         }
    552 
    553         int startPosition = position;
    554         if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
    555             PointF vector = ((RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager)
    556                     .computeScrollVectorForPosition(position);
    557             // A positive value in the vector means scrolling down, so should offset by scrolling to
    558             // an item previous in the list.
    559             int offsetDirection = (vector == null || vector.y > 0) ? -1 : 1;
    560             startPosition += offsetDirection * SNAP_SCROLL_OFFSET_POSITION;
    561 
    562             // Clamp the start position.
    563             startPosition = Math.max(0, Math.min(startPosition, layoutManager.getItemCount() - 1));
    564         } else {
    565             // If the LayoutManager doesn't implement ScrollVectorProvider (the default for
    566             // PagedListView, LinearLayoutManager does, but if the user has overridden it) then we
    567             // cannot compute the direction we need to scroll. So just snap instantly instead.
    568             Log.w(TAG, "LayoutManager is not a ScrollVectorProvider, can't do snap animation.");
    569         }
    570 
    571         if (layoutManager instanceof LinearLayoutManager) {
    572             ((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(startPosition, 0);
    573         } else {
    574             layoutManager.scrollToPosition(startPosition);
    575         }
    576 
    577         if (startPosition != position) {
    578             // The actual scroll above happens on the next update, so we wait for that to finish
    579             // before doing the smooth scroll.
    580             post(() -> scrollToPosition(position));
    581         }
    582     }
    583 
    584     /** Sets the icon to be used for the up button. */
    585     public void setUpButtonIcon(Drawable icon) {
    586         mScrollBarView.setUpButtonIcon(icon);
    587     }
    588 
    589     /** Sets the icon to be used for the down button. */
    590     public void setDownButtonIcon(Drawable icon) {
    591         mScrollBarView.setDownButtonIcon(icon);
    592     }
    593 
    594     /**
    595      * Sets the adapter for the list.
    596      *
    597      * <p>The given Adapter can implement {@link ItemCap} if it wishes to control the behavior of
    598      * a max number of items. Otherwise, methods in the PagedListView to limit the content, such as
    599      * {@link #setMaxPages(int)}, will do nothing.
    600      */
    601     public void setAdapter(
    602             @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
    603         mAdapter = adapter;
    604         mRecyclerView.setAdapter(adapter);
    605 
    606         updateMaxItems();
    607         updateAlphaJump();
    608     }
    609 
    610     /**
    611      * Sets {@link DividerVisibilityManager} on all {@code DividerDecoration} item decorations.
    612      *
    613      * @param dvm {@code DividerVisibilityManager} to be set.
    614      */
    615     public void setDividerVisibilityManager(DividerVisibilityManager dvm) {
    616         int decorCount = mRecyclerView.getItemDecorationCount();
    617         for (int i = 0; i < decorCount; i++) {
    618             RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
    619             if (decor instanceof DividerDecoration) {
    620                 ((DividerDecoration) decor).setVisibilityManager(dvm);
    621             }
    622         }
    623         mRecyclerView.invalidateItemDecorations();
    624     }
    625 
    626     @Nullable
    627     @SuppressWarnings("unchecked")
    628     public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
    629         return mRecyclerView.getAdapter();
    630     }
    631 
    632     /**
    633      * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a
    634      * page is defined as the number of items that fit completely on the screen at once.
    635      *
    636      * <p>Passing {@link #UNLIMITED_PAGES} will remove any restrictions on a maximum number
    637      * of pages.
    638      *
    639      * <p>Note that for any restriction on maximum pages to work, the adapter passed to this
    640      * PagedListView needs to implement {@link ItemCap}.
    641      *
    642      * @param maxPages The maximum number of pages that fit on the screen. Should be positive or
    643      * {@link #UNLIMITED_PAGES}.
    644      */
    645     public void setMaxPages(int maxPages) {
    646         mMaxPages = Math.max(UNLIMITED_PAGES, maxPages);
    647         updateMaxItems();
    648     }
    649 
    650     /**
    651      * Returns the maximum number of pages allowed in the PagedListView. This number is set by
    652      * {@link #setMaxPages(int)}. If that method has not been called, then this value should match
    653      * the default value.
    654      *
    655      * @return The maximum number of pages to be shown or {@link #UNLIMITED_PAGES} if there is
    656      * no limit.
    657      */
    658     public int getMaxPages() {
    659         return mMaxPages;
    660     }
    661 
    662     /**
    663      * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of
    664      * PagedLayoutManager is null or the height of the first child is 0, it will return 1.
    665      */
    666     public int getRowsPerPage() {
    667         return mRowsPerPage;
    668     }
    669 
    670     /** Resets the maximum number of pages to be shown to be the default. */
    671     public void resetMaxPages() {
    672         mMaxPages = getDefaultMaxPages();
    673         updateMaxItems();
    674     }
    675 
    676     /**
    677      * Adds an {@link RecyclerView.ItemDecoration} to this PagedListView.
    678      *
    679      * @param decor The decoration to add.
    680      * @see RecyclerView#addItemDecoration(RecyclerView.ItemDecoration)
    681      */
    682     public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
    683         mRecyclerView.addItemDecoration(decor);
    684     }
    685 
    686     /**
    687      * Removes the given {@link RecyclerView.ItemDecoration} from this
    688      * PagedListView.
    689      *
    690      * <p>The decoration will function the same as the item decoration for a {@link RecyclerView}.
    691      *
    692      * @param decor The decoration to remove.
    693      * @see RecyclerView#removeItemDecoration(RecyclerView.ItemDecoration)
    694      */
    695     public void removeItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
    696         mRecyclerView.removeItemDecoration(decor);
    697     }
    698 
    699     /**
    700      * Sets spacing between each item in the list. The spacing will not be added before the first
    701      * item and after the last.
    702      *
    703      * @param itemSpacing the spacing between each item.
    704      */
    705     public void setItemSpacing(int itemSpacing) {
    706         ItemSpacingDecoration existing = null;
    707         for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
    708             RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
    709             if (itemDecoration instanceof ItemSpacingDecoration) {
    710                 existing = (ItemSpacingDecoration) itemDecoration;
    711                 break;
    712             }
    713         }
    714 
    715         if (itemSpacing == 0 && existing != null) {
    716             mRecyclerView.removeItemDecoration(existing);
    717         } else if (existing == null) {
    718             mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
    719         } else {
    720             existing.setItemSpacing(itemSpacing);
    721         }
    722         mRecyclerView.invalidateItemDecorations();
    723     }
    724 
    725     /**
    726      * Sets the color of scrollbar.
    727      *
    728      * <p>Custom color ignores {@link DayNightStyle}. Calling {@link #resetScrollbarColor} resets to
    729      * default color.
    730      *
    731      * @param color Resource identifier of the color.
    732      */
    733     public void setScrollbarColor(@ColorRes int color) {
    734         mScrollBarView.setThumbColor(color);
    735     }
    736 
    737     /**
    738      * Resets the color of scrollbar to default.
    739      */
    740     public void resetScrollbarColor() {
    741         mScrollBarView.resetThumbColor();
    742     }
    743 
    744     /**
    745      * Adds an {@link RecyclerView.OnItemTouchListener} to this
    746      * PagedListView.
    747      *
    748      * <p>The listener will function the same as the listener for a regular {@link RecyclerView}.
    749      *
    750      * @param touchListener The touch listener to add.
    751      * @see RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener)
    752      */
    753     public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
    754         mRecyclerView.addOnItemTouchListener(touchListener);
    755     }
    756 
    757     /**
    758      * Removes the given {@link RecyclerView.OnItemTouchListener} from
    759      * the PagedListView.
    760      *
    761      * @param touchListener The touch listener to remove.
    762      * @see RecyclerView#removeOnItemTouchListener(RecyclerView.OnItemTouchListener)
    763      */
    764     public void removeOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
    765         mRecyclerView.removeOnItemTouchListener(touchListener);
    766     }
    767 
    768     /**
    769      * Sets how this {@link PagedListView} responds to day/night configuration changes. By
    770      * default, the PagedListView is darker in the day and lighter at night.
    771      *
    772      * @param dayNightStyle A value from {@link DayNightStyle}.
    773      * @see DayNightStyle
    774      */
    775     public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
    776         // Update the scrollbar
    777         mScrollBarView.setDayNightStyle(dayNightStyle);
    778 
    779         int decorCount = mRecyclerView.getItemDecorationCount();
    780         for (int i = 0; i < decorCount; i++) {
    781             RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
    782             if (decor instanceof DividerDecoration) {
    783                 ((DividerDecoration) decor).updateDividerColor();
    784             }
    785         }
    786     }
    787 
    788     /**
    789      * Sets the {@link OnScrollListener} that will be notified of scroll events within the
    790      * PagedListView.
    791      *
    792      * @param listener The scroll listener to set.
    793      */
    794     public void setOnScrollListener(OnScrollListener listener) {
    795         mOnScrollListener = listener;
    796     }
    797 
    798     /** Returns the page the given position is on, starting with page 0. */
    799     public int getPage(int position) {
    800         if (mRowsPerPage == -1) {
    801             return -1;
    802         }
    803         if (mRowsPerPage == 0) {
    804             return 0;
    805         }
    806         return position / mRowsPerPage;
    807     }
    808 
    809     private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
    810         if (mOrientationHelper == null || mOrientationHelper.getLayoutManager() != layoutManager) {
    811             // PagedListView is assumed to be a list that always vertically scrolls.
    812             mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
    813         }
    814         return mOrientationHelper;
    815     }
    816 
    817     /**
    818      * Scrolls the contents of the RecyclerView up a page. A page is defined as the height of the
    819      * {@code PagedListView}.
    820      *
    821      * <p>The resulting first item in the list will be snapped to so that it is completely visible.
    822      * If this is not possible due to the first item being taller than the containing
    823      * {@code PagedListView}, then the snapping will not occur.
    824      */
    825     public void pageUp() {
    826         if (mRecyclerView.getLayoutManager() == null || mRecyclerView.getChildCount() == 0) {
    827             return;
    828         }
    829 
    830         // Use OrientationHelper to calculate scroll distance in order to match snapping behavior.
    831         OrientationHelper orientationHelper =
    832                 getOrientationHelper(mRecyclerView.getLayoutManager());
    833 
    834         int screenSize = mRecyclerView.getHeight();
    835         int scrollDistance = screenSize;
    836         // The iteration order matters. In case where there are 2 items longer than screen size, we
    837         // want to focus on upcoming view.
    838         for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
    839             /*
    840              * We treat child View longer than screen size differently:
    841              * 1) When it enters screen, next pageUp will align its bottom with parent bottom;
    842              * 2) When it leaves screen, next pageUp will align its top with parent top.
    843              */
    844             View child = mRecyclerView.getChildAt(i);
    845             if (child.getHeight() > screenSize) {
    846                 if (orientationHelper.getDecoratedEnd(child) < screenSize) {
    847                     // Child view bottom is entering screen. Align its bottom with parent bottom.
    848                     scrollDistance = screenSize - orientationHelper.getDecoratedEnd(child);
    849                 } else if (-screenSize < orientationHelper.getDecoratedStart(child)
    850                         && orientationHelper.getDecoratedStart(child) < 0) {
    851                     // Child view top is about to enter screen - its distance to parent top
    852                     // is less than a full scroll. Align child top with parent top.
    853                     scrollDistance = Math.abs(orientationHelper.getDecoratedStart(child));
    854                 }
    855                 // There can be two items that are longer than the screen. We stop at the first one.
    856                 // This is affected by the iteration order.
    857                 break;
    858             }
    859         }
    860         // Distance should always be positive. Negate its value to scroll up.
    861         mRecyclerView.smoothScrollBy(0, -scrollDistance);
    862     }
    863 
    864     /**
    865      * Scrolls the contents of the RecyclerView down a page. A page is defined as the height of the
    866      * {@code PagedListView}.
    867      *
    868      * <p>This method will attempt to bring the last item in the list as the first item. If the
    869      * current first item in the list is taller than the {@code PagedListView}, then it will be
    870      * scrolled the length of a page, but not snapped to.
    871      */
    872     public void pageDown() {
    873         if (mRecyclerView.getLayoutManager() == null || mRecyclerView.getChildCount() == 0) {
    874             return;
    875         }
    876 
    877         OrientationHelper orientationHelper =
    878                 getOrientationHelper(mRecyclerView.getLayoutManager());
    879         int screenSize = mRecyclerView.getHeight();
    880         int scrollDistance = screenSize;
    881 
    882         // If the last item is partially visible, page down should bring it to the top.
    883         View lastChild = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1);
    884         if (mRecyclerView.getLayoutManager().isViewPartiallyVisible(lastChild,
    885                 /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false)) {
    886             scrollDistance = orientationHelper.getDecoratedStart(lastChild);
    887             if (scrollDistance < 0) {
    888                 // Scroll value can be negative if the child is longer than the screen size and the
    889                 // visible area of the screen does not show the start of the child.
    890                 // Scroll to the next screen if the start value is negative
    891                 scrollDistance = screenSize;
    892             }
    893         }
    894 
    895         // The iteration order matters. In case where there are 2 items longer than screen size, we
    896         // want to focus on upcoming view (the one at the bottom of screen).
    897         for (int i = mRecyclerView.getChildCount() - 1; i >= 0; i--) {
    898             /* We treat child View longer than screen size differently:
    899              * 1) When it enters screen, next pageDown will align its top with parent top;
    900              * 2) When it leaves screen, next pageDown will align its bottom with parent bottom.
    901              */
    902             View child = mRecyclerView.getChildAt(i);
    903             if (child.getHeight() > screenSize) {
    904                 if (orientationHelper.getDecoratedStart(child) > 0) {
    905                     // Child view top is entering screen. Align its top with parent top.
    906                     scrollDistance = orientationHelper.getDecoratedStart(child);
    907                 } else if (screenSize < orientationHelper.getDecoratedEnd(child)
    908                         && orientationHelper.getDecoratedEnd(child) < 2 * screenSize) {
    909                     // Child view bottom is about to enter screen - its distance to parent bottom
    910                     // is less than a full scroll. Align child bottom with parent bottom.
    911                     scrollDistance = orientationHelper.getDecoratedEnd(child) - screenSize;
    912                 }
    913                 // There can be two items that are longer than the screen. We stop at the first one.
    914                 // This is affected by the iteration order.
    915                 break;
    916             }
    917         }
    918 
    919         mRecyclerView.smoothScrollBy(0, scrollDistance);
    920     }
    921 
    922     /**
    923      * Sets the default number of pages that this PagedListView is limited to.
    924      *
    925      * @param newDefault The default number of pages. Should be positive.
    926      */
    927     public void setDefaultMaxPages(int newDefault) {
    928         if (newDefault < 0) {
    929             return;
    930         }
    931         mDefaultMaxPages = newDefault;
    932         resetMaxPages();
    933     }
    934 
    935     /** Returns the default number of pages the list should have */
    936     private int getDefaultMaxPages() {
    937         // assume list shown in response to a click, so, reduce number of clicks by one
    938         return mDefaultMaxPages - 1;
    939     }
    940 
    941     @Override
    942     public void onLayout(boolean changed, int left, int top, int right, int bottom) {
    943         // if a late item is added to the top of the layout after the layout is stabilized, causing
    944         // the former top item to be pushed to the 2nd page, the focus will still be on the former
    945         // top item. Since our car layout manager tries to scroll the viewport so that the focused
    946         // item is visible, the view port will be on the 2nd page. That means the newly added item
    947         // will not be visible, on the first page.
    948 
    949         // what we want to do is: if the formerly focused item is the first one in the list, any
    950         // item added above it will make the focus to move to the new first item.
    951         // if the focus is not on the formerly first item, then we don't need to do anything. Let
    952         // the layout manager do the job and scroll the viewport so the currently focused item
    953         // is visible.
    954         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    955 
    956         if (layoutManager == null) {
    957             return;
    958         }
    959 
    960         // we need to calculate whether we want to request focus here, before the super call,
    961         // because after the super call, the first born might be changed.
    962         View focusedChild = layoutManager.getFocusedChild();
    963         View firstBorn = layoutManager.getChildAt(0);
    964 
    965         super.onLayout(changed, left, top, right, bottom);
    966 
    967         if (mAdapter != null) {
    968             int itemCount = mAdapter.getItemCount();
    969             if (Log.isLoggable(TAG, Log.DEBUG)) {
    970                 Log.d(TAG, String.format(
    971                         "onLayout hasFocus: %s, mLastItemCount: %s, itemCount: %s, "
    972                                 + "focusedChild: %s, firstBorn: %s, isInTouchMode: %s, "
    973                                 + "mNeedsFocus: %s",
    974                         hasFocus(),
    975                         mLastItemCount,
    976                         itemCount,
    977                         focusedChild,
    978                         firstBorn,
    979                         isInTouchMode(),
    980                         mNeedsFocus));
    981             }
    982             updateMaxItems();
    983             // This is a workaround for missing focus because isInTouchMode() is not always
    984             // returning the right value.
    985             // This is okay for the Engine release since focus is always showing.
    986             // However, in Tala and Fender, we want to show focus only when the user uses
    987             // hardware controllers, so we need to revisit this logic. b/22990605.
    988             if (mNeedsFocus && itemCount > 0) {
    989                 if (focusedChild == null) {
    990                     requestFocus();
    991                 }
    992                 mNeedsFocus = false;
    993             }
    994             if (itemCount > mLastItemCount && focusedChild == firstBorn) {
    995                 requestFocus();
    996             }
    997             mLastItemCount = itemCount;
    998         }
    999 
   1000         if (!mScrollBarEnabled) {
   1001             // Don't change the visibility of the ScrollBar unless it's enabled.
   1002             return;
   1003         }
   1004 
   1005         boolean isAtStart = isAtStart();
   1006         boolean isAtEnd = isAtEnd();
   1007 
   1008         if ((isAtStart && isAtEnd) || layoutManager.getItemCount() == 0) {
   1009             mScrollBarView.setVisibility(View.INVISIBLE);
   1010             return;
   1011         }
   1012 
   1013         mScrollBarView.setVisibility(View.VISIBLE);
   1014         mScrollBarView.setUpEnabled(!isAtStart);
   1015         mScrollBarView.setDownEnabled(!isAtEnd);
   1016 
   1017         if (mRecyclerView.getLayoutManager().canScrollVertically()) {
   1018             mScrollBarView.setParametersInLayout(
   1019                     mRecyclerView.computeVerticalScrollRange(),
   1020                     mRecyclerView.computeVerticalScrollOffset(),
   1021                     mRecyclerView.computeVerticalScrollExtent());
   1022         } else {
   1023             mScrollBarView.setParametersInLayout(
   1024                     mRecyclerView.computeHorizontalScrollRange(),
   1025                     mRecyclerView.computeHorizontalScrollOffset(),
   1026                     mRecyclerView.computeHorizontalScrollExtent());
   1027         }
   1028     }
   1029 
   1030     /**
   1031      * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
   1032      * being called as a result of adapter changes, it should be called after the new layout has
   1033      * been calculated because the method of determining scrollbar visibility uses the current
   1034      * layout. If this is called after an adapter change but before the new layout, the visibility
   1035      * determination may not be correct.
   1036      *
   1037      * @param animate {@code true} if the scrollbar should animate to its new position.
   1038      *                {@code false} if no animation is used
   1039      */
   1040     private void updatePaginationButtons(boolean animate) {
   1041         if (!mScrollBarEnabled) {
   1042             // Don't change the visibility of the ScrollBar unless it's enabled.
   1043             return;
   1044         }
   1045 
   1046         boolean isAtStart = isAtStart();
   1047         boolean isAtEnd = isAtEnd();
   1048         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
   1049 
   1050         if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
   1051             mScrollBarView.setVisibility(View.INVISIBLE);
   1052         } else {
   1053             mScrollBarView.setVisibility(View.VISIBLE);
   1054         }
   1055         mScrollBarView.setUpEnabled(!isAtStart);
   1056         mScrollBarView.setDownEnabled(!isAtEnd);
   1057 
   1058         if (layoutManager == null) {
   1059             return;
   1060         }
   1061 
   1062         if (mRecyclerView.getLayoutManager().canScrollVertically()) {
   1063             mScrollBarView.setParameters(
   1064                     mRecyclerView.computeVerticalScrollRange(),
   1065                     mRecyclerView.computeVerticalScrollOffset(),
   1066                     mRecyclerView.computeVerticalScrollExtent(), animate);
   1067         } else {
   1068             mScrollBarView.setParameters(
   1069                     mRecyclerView.computeHorizontalScrollRange(),
   1070                     mRecyclerView.computeHorizontalScrollOffset(),
   1071                     mRecyclerView.computeHorizontalScrollExtent(), animate);
   1072         }
   1073 
   1074         invalidate();
   1075     }
   1076 
   1077     /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
   1078     public boolean isAtStart() {
   1079         return mSnapHelper.isAtStart(mRecyclerView.getLayoutManager());
   1080     }
   1081 
   1082     /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
   1083     public boolean isAtEnd() {
   1084         return mSnapHelper.isAtEnd(mRecyclerView.getLayoutManager());
   1085     }
   1086 
   1087     @UiThread
   1088     private void updateMaxItems() {
   1089         if (mAdapter == null) {
   1090             return;
   1091         }
   1092 
   1093         // Ensure mRowsPerPage regardless of if the adapter implements ItemCap.
   1094         updateRowsPerPage();
   1095 
   1096         // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
   1097         if (!(mAdapter instanceof ItemCap)) {
   1098             return;
   1099         }
   1100 
   1101         final int originalCount = mAdapter.getItemCount();
   1102         ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount());
   1103         final int newCount = mAdapter.getItemCount();
   1104         if (newCount == originalCount) {
   1105             return;
   1106         }
   1107 
   1108         if (newCount < originalCount) {
   1109             mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
   1110         } else {
   1111             mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
   1112         }
   1113     }
   1114 
   1115     private int calculateMaxItemCount() {
   1116         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
   1117         if (layoutManager == null) {
   1118             return -1;
   1119         }
   1120 
   1121         View firstChild = layoutManager.getChildAt(0);
   1122         if (firstChild == null || firstChild.getHeight() == 0) {
   1123             return -1;
   1124         } else {
   1125             return (mMaxPages < 0) ? -1 : mRowsPerPage * mMaxPages;
   1126         }
   1127     }
   1128 
   1129     /**
   1130      * Updates the rows number per current page, which is used for calculating how many items we
   1131      * want to show.
   1132      */
   1133     private void updateRowsPerPage() {
   1134         RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
   1135         if (layoutManager == null) {
   1136             mRowsPerPage = 1;
   1137             return;
   1138         }
   1139 
   1140         View firstChild = layoutManager.getChildAt(0);
   1141         if (firstChild == null || firstChild.getHeight() == 0) {
   1142             mRowsPerPage = 1;
   1143         } else {
   1144             mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight());
   1145         }
   1146     }
   1147 
   1148     @Override
   1149     public Parcelable onSaveInstanceState() {
   1150         Bundle bundle = new Bundle();
   1151         bundle.putParcelable(SAVED_SUPER_STATE_KEY, super.onSaveInstanceState());
   1152 
   1153         SparseArray<Parcelable> recyclerViewState = new SparseArray<>();
   1154         mRecyclerView.saveHierarchyState(recyclerViewState);
   1155         bundle.putSparseParcelableArray(SAVED_RECYCLER_VIEW_STATE_KEY, recyclerViewState);
   1156 
   1157         return bundle;
   1158     }
   1159 
   1160     @Override
   1161     public void onRestoreInstanceState(Parcelable state) {
   1162         if (!(state instanceof Bundle)) {
   1163             super.onRestoreInstanceState(state);
   1164             return;
   1165         }
   1166 
   1167         Bundle bundle = (Bundle) state;
   1168         mRecyclerView.restoreHierarchyState(
   1169                 bundle.getSparseParcelableArray(SAVED_RECYCLER_VIEW_STATE_KEY));
   1170 
   1171         super.onRestoreInstanceState(bundle.getParcelable(SAVED_SUPER_STATE_KEY));
   1172     }
   1173 
   1174     @Override
   1175     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
   1176         // There is the possibility of multiple PagedListViews on a page. This means that the ids
   1177         // of the child Views of PagedListView are no longer unique, and onSaveInstanceState()
   1178         // cannot be used as is. As a result, PagedListViews needs to manually dispatch the instance
   1179         // states. Call dispatchFreezeSelfOnly() so that no child views have onSaveInstanceState()
   1180         // called by the system.
   1181         dispatchFreezeSelfOnly(container);
   1182     }
   1183 
   1184     @Override
   1185     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
   1186         // Prevent onRestoreInstanceState() from being called on child Views. Instead, PagedListView
   1187         // will manually handle passing the state. See the comment in dispatchSaveInstanceState()
   1188         // for more information.
   1189         dispatchThawSelfOnly(container);
   1190     }
   1191 
   1192     private void updateAlphaJump() {
   1193         boolean supportsAlphaJump = (mAdapter instanceof IAlphaJumpAdapter);
   1194         mScrollBarView.setShowAlphaJump(supportsAlphaJump);
   1195     }
   1196 
   1197     private void showAlphaJump() {
   1198         if (mAlphaJumpView == null && mAdapter instanceof IAlphaJumpAdapter) {
   1199             mAlphaJumpView = new AlphaJumpOverlayView(getContext());
   1200             mAlphaJumpView.init(this, (IAlphaJumpAdapter) mAdapter);
   1201             addView(mAlphaJumpView);
   1202         }
   1203 
   1204         mAlphaJumpView.setVisibility(View.VISIBLE);
   1205     }
   1206 
   1207     private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
   1208             new RecyclerView.OnScrollListener() {
   1209                 @Override
   1210                 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
   1211                     if (mOnScrollListener != null) {
   1212                         mOnScrollListener.onScrolled(recyclerView, dx, dy);
   1213 
   1214                         if (!isAtStart() && isAtEnd()) {
   1215                             mOnScrollListener.onReachBottom();
   1216                         }
   1217                     }
   1218                     updatePaginationButtons(false);
   1219                 }
   1220 
   1221                 @Override
   1222                 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
   1223                     if (mOnScrollListener != null) {
   1224                         mOnScrollListener.onScrollStateChanged(recyclerView, newState);
   1225                     }
   1226                     if (newState == RecyclerView.SCROLL_STATE_IDLE) {
   1227                         mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS);
   1228                     }
   1229                 }
   1230             };
   1231 
   1232     private final Runnable mPaginationRunnable =
   1233             new Runnable() {
   1234                 @Override
   1235                 public void run() {
   1236                     boolean upPressed = mScrollBarView.isUpPressed();
   1237                     boolean downPressed = mScrollBarView.isDownPressed();
   1238                     if (upPressed && downPressed) {
   1239                         return;
   1240                     }
   1241                     if (upPressed) {
   1242                         pageUp();
   1243                     } else if (downPressed) {
   1244                         pageDown();
   1245                     }
   1246                 }
   1247             };
   1248 
   1249     private final Runnable mUpdatePaginationRunnable =
   1250             () -> updatePaginationButtons(true /*animate*/);
   1251 
   1252     /** Used to listen for {@code PagedListView} scroll events. */
   1253     public abstract static class OnScrollListener {
   1254         /**
   1255          * Called when the {@code PagedListView} has been scrolled so that the last item is
   1256          * completely visible.
   1257          */
   1258         public void onReachBottom() {}
   1259         /** Called when scroll up button is clicked */
   1260         public void onScrollUpButtonClicked() {}
   1261         /** Called when scroll down button is clicked */
   1262         public void onScrollDownButtonClicked() {}
   1263 
   1264         /**
   1265          * Called when RecyclerView.OnScrollListener#onScrolled is called. See
   1266          * RecyclerView.OnScrollListener
   1267          */
   1268         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}
   1269 
   1270         /** See RecyclerView.OnScrollListener */
   1271         public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}
   1272     }
   1273 
   1274     /**
   1275      * A {@link RecyclerView.ItemDecoration} that will add spacing
   1276      * between each item in the RecyclerView that it is added to.
   1277      */
   1278     private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
   1279         private int mItemSpacing;
   1280 
   1281         private ItemSpacingDecoration(int itemSpacing) {
   1282             mItemSpacing = itemSpacing;
   1283         }
   1284 
   1285         @Override
   1286         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
   1287                 RecyclerView.State state) {
   1288             super.getItemOffsets(outRect, view, parent, state);
   1289             int position = parent.getChildAdapterPosition(view);
   1290 
   1291             // Skip offset for last item except for GridLayoutManager.
   1292             if (position == state.getItemCount() - 1
   1293                     && !(parent.getLayoutManager() instanceof GridLayoutManager)) {
   1294                 return;
   1295             }
   1296 
   1297             outRect.bottom = mItemSpacing;
   1298         }
   1299 
   1300         /**
   1301          * @param itemSpacing sets spacing between each item.
   1302          */
   1303         public void setItemSpacing(int itemSpacing) {
   1304             mItemSpacing = itemSpacing;
   1305         }
   1306     }
   1307 
   1308     /**
   1309      * A {@link RecyclerView.ItemDecoration} that will draw a dividing
   1310      * line between each item in the RecyclerView that it is added to.
   1311      */
   1312     private static class DividerDecoration extends RecyclerView.ItemDecoration {
   1313         private final Context mContext;
   1314         private final Paint mPaint;
   1315         private final int mDividerHeight;
   1316         private final int mDividerStartMargin;
   1317         private final int mDividerEndMargin;
   1318         @IdRes private final int mDividerStartId;
   1319         @IdRes private final int mDividerEndId;
   1320         @ColorRes private final int mListDividerColor;
   1321         private DividerVisibilityManager mVisibilityManager;
   1322 
   1323         /**
   1324          * @param dividerStartMargin The start offset of the dividing line. This offset will be
   1325          *     relative to {@code dividerStartId} if that value is given.
   1326          * @param dividerStartId A child view id whose starting edge will be used as the starting
   1327          *     edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top
   1328          *     container of each child view will be used.
   1329          * @param dividerEndId A child view id whose ending edge will be used as the starting edge
   1330          *     of the dividing lin.e If this value is {@link #INVALID_RESOURCE_ID}, then the top
   1331          *     container view of each child will be used.
   1332          */
   1333         private DividerDecoration(Context context, int dividerStartMargin,
   1334                 int dividerEndMargin, @IdRes int dividerStartId, @IdRes int dividerEndId,
   1335                 @ColorRes int listDividerColor) {
   1336             mContext = context;
   1337             mDividerStartMargin = dividerStartMargin;
   1338             mDividerEndMargin = dividerEndMargin;
   1339             mDividerStartId = dividerStartId;
   1340             mDividerEndId = dividerEndId;
   1341             mListDividerColor = listDividerColor;
   1342 
   1343             mPaint = new Paint();
   1344             mPaint.setColor(mContext.getColor(listDividerColor));
   1345             mDividerHeight = mContext.getResources().getDimensionPixelSize(
   1346                     R.dimen.car_list_divider_height);
   1347         }
   1348 
   1349         /** Updates the list divider color which may have changed due to a day night transition. */
   1350         public void updateDividerColor() {
   1351             mPaint.setColor(mContext.getColor(mListDividerColor));
   1352         }
   1353 
   1354         /** Sets {@link DividerVisibilityManager} on the DividerDecoration.*/
   1355         public void setVisibilityManager(DividerVisibilityManager dvm) {
   1356             mVisibilityManager = dvm;
   1357         }
   1358 
   1359         @Override
   1360         public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
   1361             boolean usesGridLayoutManager = parent.getLayoutManager() instanceof GridLayoutManager;
   1362             for (int i = 0; i < parent.getChildCount(); i++) {
   1363                 View container = parent.getChildAt(i);
   1364                 int itemPosition = parent.getChildAdapterPosition(container);
   1365 
   1366                 if (hideDividerForAdapterPosition(itemPosition)) {
   1367                     continue;
   1368                 }
   1369 
   1370                 View nextVerticalContainer;
   1371                 if (usesGridLayoutManager) {
   1372                     // Find an item in next row to calculate vertical space.
   1373                     int lastItem = GridLayoutManagerUtils.getLastIndexOnSameRow(i, parent);
   1374                     nextVerticalContainer = parent.getChildAt(lastItem + 1);
   1375                 } else {
   1376                     nextVerticalContainer = parent.getChildAt(i + 1);
   1377                 }
   1378                 if (nextVerticalContainer == null) {
   1379                     // Skip drawing divider for the last row in GridLayoutManager, or the last
   1380                     // item (presumably in LinearLayoutManager).
   1381                     continue;
   1382                 }
   1383                 int spacing = nextVerticalContainer.getTop() - container.getBottom();
   1384                 drawDivider(c, container, spacing);
   1385             }
   1386         }
   1387 
   1388         /**
   1389          * Draws a divider under {@code container}.
   1390          *
   1391          * @param spacing between {@code container} and next view.
   1392          */
   1393         private void drawDivider(Canvas c, View container, int spacing) {
   1394             View startChild =
   1395                     mDividerStartId != INVALID_RESOURCE_ID
   1396                             ? container.findViewById(mDividerStartId)
   1397                             : container;
   1398 
   1399             View endChild =
   1400                     mDividerEndId != INVALID_RESOURCE_ID
   1401                             ? container.findViewById(mDividerEndId)
   1402                             : container;
   1403 
   1404             if (startChild == null || endChild == null) {
   1405                 return;
   1406             }
   1407 
   1408             Rect containerRect = new Rect();
   1409             container.getGlobalVisibleRect(containerRect);
   1410 
   1411             Rect startRect = new Rect();
   1412             startChild.getGlobalVisibleRect(startRect);
   1413 
   1414             Rect endRect = new Rect();
   1415             endChild.getGlobalVisibleRect(endRect);
   1416 
   1417             int left = container.getLeft() + mDividerStartMargin
   1418                     + (startRect.left - containerRect.left);
   1419             int right = container.getRight()  - mDividerEndMargin
   1420                     - (endRect.right - containerRect.right);
   1421             // "(spacing + divider height) / 2" aligns the center of divider to that of spacing
   1422             // between two items.
   1423             // When spacing is an odd value (e.g. created by other decoration), space under divider
   1424             // is greater by 1dp.
   1425             int bottom = container.getBottom() + (spacing + mDividerHeight) / 2;
   1426             int top = bottom - mDividerHeight;
   1427 
   1428             c.drawRect(left, top, right, bottom, mPaint);
   1429         }
   1430 
   1431         @Override
   1432         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
   1433                 RecyclerView.State state) {
   1434             super.getItemOffsets(outRect, view, parent, state);
   1435             int pos = parent.getChildAdapterPosition(view);
   1436             if (hideDividerForAdapterPosition(pos)) {
   1437                 return;
   1438             }
   1439             // Add an bottom offset to all items that should have divider, even when divider is not
   1440             // drawn for the bottom item(s).
   1441             // With GridLayoutManager it's difficult to tell whether a view is in the last row.
   1442             // This is to keep expected behavior consistent.
   1443             outRect.bottom = mDividerHeight;
   1444         }
   1445 
   1446         private boolean hideDividerForAdapterPosition(int position) {
   1447             return mVisibilityManager != null && mVisibilityManager.shouldHideDivider(position);
   1448         }
   1449     }
   1450 
   1451     /**
   1452      * A {@link RecyclerView.ItemDecoration} that will add a top offset
   1453      * to the first item in the RecyclerView it is added to.
   1454      */
   1455     private static class TopOffsetDecoration extends RecyclerView.ItemDecoration {
   1456         private int mTopOffset;
   1457 
   1458         private TopOffsetDecoration(int topOffset) {
   1459             mTopOffset = topOffset;
   1460         }
   1461 
   1462         @Override
   1463         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
   1464                 RecyclerView.State state) {
   1465             super.getItemOffsets(outRect, view, parent, state);
   1466             int position = parent.getChildAdapterPosition(view);
   1467             if (parent.getLayoutManager() instanceof GridLayoutManager
   1468                     && position < GridLayoutManagerUtils.getFirstRowItemCount(parent)) {
   1469                 // For GridLayoutManager, top offset should be set for all items in the first row.
   1470                 // Otherwise the top items will be visually uneven.
   1471                 outRect.top = mTopOffset;
   1472             } else if (position == 0) {
   1473                 // Only set the offset for the first item.
   1474                 outRect.top = mTopOffset;
   1475             }
   1476         }
   1477 
   1478         /**
   1479          * @param topOffset sets spacing between each item.
   1480          */
   1481         public void setTopOffset(int topOffset) {
   1482             mTopOffset = topOffset;
   1483         }
   1484     }
   1485 }
   1486