PagedListView.java

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.car.widget;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.ColorRes;
import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.car.R;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;

import java.lang.annotation.Retention;

/**
 * View that wraps a {@link RecyclerView} and a scroll bar that has
 * page up and down arrows. Interaction with this view is similar to a {@code RecyclerView} as it
 * takes the same adapter.
 *
 * <p>By default, this PagedListView utilizes a vertical {@link LinearLayoutManager} to display
 * its items.
 */
public class PagedListView extends FrameLayout {
    /**
     * The key used to save the state of this PagedListView's super class in
     * {@link #onSaveInstanceState()}.
     */
    private static final String SAVED_SUPER_STATE_KEY = "PagedListViewSuperState";

    /**
     * The key used to save the state of {@link #mRecyclerView} so that it can be restored
     * on configuration change. The actual saving of state will be controlled by the LayoutManager
     * of the RecyclerView; this value simply ensures the state is passed on to the LayoutManager.
     */
    private static final String SAVED_RECYCLER_VIEW_STATE_KEY = "RecyclerViewState";

    /** Default maximum number of clicks allowed on a list */
    public static final int DEFAULT_MAX_CLICKS = 6;

    /**
     * Value to pass to {@link #setMaxPages(int)} to indicate there is no restriction on the
     * maximum number of pages to show.
     */
    public static final int UNLIMITED_PAGES = -1;

    /**
     * The amount of time after settling to wait before autoscrolling to the next page when the user
     * holds down a pagination button.
     */
    private static final int PAGINATION_HOLD_DELAY_MS = 400;

    /**
     * When doing a snap, offset the snap by this number of position and then do a smooth scroll to
     * the final position.
     */
    private static final int SNAP_SCROLL_OFFSET_POSITION = 2;

    private static final String TAG = "PagedListView";
    private static final int INVALID_RESOURCE_ID = -1;

    private RecyclerView mRecyclerView;
    private PagedSnapHelper mSnapHelper;
    final Handler mHandler = new Handler();
    private boolean mScrollBarEnabled;
    @VisibleForTesting
    PagedScrollBarView mScrollBarView;

    /**
     * AlphaJumpOverlayView that will be null until the first time you tap the alpha jump button, at
     * which point we'll construct it and add it to the view hierarchy as a child of this frame
     * layout.
     */
    @Nullable private AlphaJumpOverlayView mAlphaJumpView;

    private int mRowsPerPage = -1;
    private RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter;

    /** Maximum number of pages to show. */
    private int mMaxPages;

    OnScrollListener mOnScrollListener;

    /** Number of visible rows per page */
    private int mDefaultMaxPages = DEFAULT_MAX_CLICKS;

    /** Used to check if there are more items added to the list. */
    private int mLastItemCount;

    private boolean mNeedsFocus;

    private OrientationHelper mOrientationHelper;

    @Gutter
    private int mGutter;
    private int mGutterSize;

    /**
     * Interface for a {@link RecyclerView.Adapter} to cap the number of
     * items.
     *
     * <p>NOTE: it is still up to the adapter to use maxItems in {@link
     * RecyclerView.Adapter#getItemCount()}.
     *
     * <p>the recommended way would be with:
     *
     * <pre>{@code
     * {@literal@}Override
     * public int getItemCount() {
     *   return Math.min(super.getItemCount(), mMaxItems);
     * }
     * }</pre>
     */
    public interface ItemCap {
        /**
         * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
         */
        int UNLIMITED = -1;

        /**
         * Sets the maximum number of items available in the adapter. A value less than '0' means
         * the list should not be capped.
         */
        void setMaxItems(int maxItems);
    }

    /**
     * Interface for controlling visibility of item dividers for individual items based on the
     * item's position.
     *
     * <p> NOTE: interface takes effect only when dividers are enabled.
     */
    public interface DividerVisibilityManager {
        /**
         * Given an item position, returns whether the divider below that item should be hidden.
         *
         * @param position item position inside the adapter.
         * @return true if divider is to be hidden, false if divider should be shown.
         *
         * @deprecated use {@link #getShowDivider(int)}.
         */
        @Deprecated
        boolean shouldHideDivider(int position);

        /**
         * Given an item position, returns whether the divider below that item should be shown.
         *
         * @param position item position inside the adapter.
         * @return {@code true} if divider is to be shown; {@code false} if hidden.
         */
        boolean getShowDivider(int position);
    }

    /**
     * The possible values for @{link #setGutter}. The default value is actually
     * {@link Gutter#BOTH}.
     */
    @IntDef({
            Gutter.NONE,
            Gutter.START,
            Gutter.END,
            Gutter.BOTH,
    })
    @Retention(SOURCE)
    public @interface Gutter {
        /**
         * No gutter on either side of the list items. The items will span the full width of the
         * {@link PagedListView}.
         */
        int NONE = 0;

        /**
         * Include a gutter only on the start side (that is, the same side as the scroll bar).
         */
        int START = 1;

        /**
         * Include a gutter only on the end side (that is, the opposite side of the scroll bar).
         */
        int END = 2;

        /**
         * Include a gutter on both sides of the list items. This is the default behaviour.
         */
        int BOTH = 3;
    }

    /**
     * Interface for a {@link RecyclerView.Adapter} to set the position
     * offset for the adapter to load the data.
     *
     * <p>For example in the adapter, if the positionOffset is 20, then for position 0 it will show
     * the item in position 20 instead, for position 1 it will show the item in position 21 instead
     * and so on.
     */
    public interface ItemPositionOffset {
        /** Sets the position offset for the adapter. */
        void setPositionOffset(int positionOffset);
    }

    public PagedListView(Context context) {
        super(context);
        init(context, /* attrs= */ null, R.attr.pagedListViewStyle);
    }

    public PagedListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, R.attr.pagedListViewStyle);
    }

    public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) {
        super(context, attrs, defStyleAttrs);
        init(context, attrs, defStyleAttrs);
    }

    public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
        super(context, attrs, defStyleAttrs, defStyleRes);
        init(context, attrs, defStyleAttrs);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttrs) {
        LayoutInflater.from(context).inflate(R.layout.car_paged_recycler_view,
                /* root= */ this, /* attachToRoot= */ true);

        TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.PagedListView, defStyleAttrs, /* defStyleRes= */ 0);
        mRecyclerView = findViewById(R.id.recycler_view);

        mMaxPages = getDefaultMaxPages();

        RecyclerView.LayoutManager layoutManager =
                new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
        mRecyclerView.setLayoutManager(layoutManager);

        mSnapHelper = new PagedSnapHelper(context);
        mSnapHelper.attachToRecyclerView(mRecyclerView);

        mRecyclerView.addOnScrollListener(mRecyclerViewOnScrollListener);
        mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);

        if (a.getBoolean(R.styleable.PagedListView_verticallyCenterListContent, false)) {
            // Setting the height of wrap_content allows the RecyclerView to center itself.
            mRecyclerView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
        }

        int defaultGutterSize = getResources().getDimensionPixelSize(R.dimen.car_margin);
        mGutterSize = a.getDimensionPixelSize(R.styleable.PagedListView_gutterSize,
                defaultGutterSize);

        if (a.hasValue(R.styleable.PagedListView_gutter)) {
            int gutter = a.getInt(R.styleable.PagedListView_gutter, Gutter.BOTH);
            setGutter(gutter);
        } else if (a.hasValue(R.styleable.PagedListView_offsetScrollBar)) {
            boolean offsetScrollBar =
                    a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false);
            if (offsetScrollBar) {
                setGutter(Gutter.START);
            }
        } else {
            setGutter(Gutter.BOTH);
        }

        if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) {
            int dividerStartMargin = a.getDimensionPixelSize(
                    R.styleable.PagedListView_dividerStartMargin, 0);
            int dividerEndMargin = a.getDimensionPixelSize(
                    R.styleable.PagedListView_dividerEndMargin, 0);
            int dividerStartId = a.getResourceId(
                    R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID);
            int dividerEndId = a.getResourceId(
                    R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID);

            int listDividerColor = a.getResourceId(R.styleable.PagedListView_listDividerColor,
                    R.color.car_list_divider);

            mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin,
                    dividerEndMargin, dividerStartId, dividerEndId, listDividerColor));
        }

        int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0);
        if (itemSpacing > 0) {
            mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
        }

        int listContentTopMargin =
                a.getDimensionPixelSize(R.styleable.PagedListView_listContentTopOffset, 0);
        if (listContentTopMargin > 0) {
            mRecyclerView.addItemDecoration(new TopOffsetDecoration(listContentTopMargin));
        }

        // Set focusable false explicitly to handle the behavior change in Android O where
        // clickable view becomes focusable by default.
        setFocusable(false);

        mScrollBarEnabled = a.getBoolean(R.styleable.PagedListView_scrollBarEnabled, true);
        mScrollBarView = findViewById(R.id.paged_scroll_view);
        mScrollBarView.setPaginationListener(new PagedScrollBarView.PaginationListener() {
            @Override
            public void onPaginate(int direction) {
                switch (direction) {
                    case PagedScrollBarView.PaginationListener.PAGE_UP:
                        pageUp();
                        if (mOnScrollListener != null) {
                            mOnScrollListener.onScrollUpButtonClicked();
                        }
                        break;
                    case PagedScrollBarView.PaginationListener.PAGE_DOWN:
                        pageDown();
                        if (mOnScrollListener != null) {
                            mOnScrollListener.onScrollDownButtonClicked();
                        }
                        break;
                    default:
                        Log.e(TAG, "Unknown pagination direction (" + direction + ")");
                }
            }

            @Override
            public void onAlphaJump() {
                showAlphaJump();
            }
        });

        if (a.hasValue(R.styleable.PagedListView_scrollBarGravity)) {
            FrameLayout.LayoutParams layoutParams =
                    (FrameLayout.LayoutParams) mScrollBarView.getLayoutParams();
            layoutParams.gravity =
                    a.getInt(R.styleable.PagedListView_scrollBarGravity, Gravity.LEFT);
        }

        Drawable upButtonIcon = a.getDrawable(R.styleable.PagedListView_upButtonIcon);
        if (upButtonIcon != null) {
            setUpButtonIcon(upButtonIcon);
        }

        Drawable downButtonIcon = a.getDrawable(R.styleable.PagedListView_downButtonIcon);
        if (downButtonIcon != null) {
            setDownButtonIcon(downButtonIcon);
        }

        // Using getResourceId() over getColor() because setScrollbarColor() expects a color resId.
        int scrollBarColor = a.getResourceId(R.styleable.PagedListView_scrollBarColor, -1);
        if (scrollBarColor != -1) {
            setScrollbarColor(scrollBarColor);
        }

        mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE);

        if (mScrollBarEnabled) {
            // Use the top margin that is defined in the layout as the default value.
            int topMargin = a.getDimensionPixelSize(
                    R.styleable.PagedListView_scrollBarTopMargin,
                    ((MarginLayoutParams) mScrollBarView.getLayoutParams()).topMargin);
            setScrollBarTopMargin(topMargin);
        } else {
            MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
            params.setMarginStart(0);
        }

        if (a.hasValue(R.styleable.PagedListView_scrollBarContainerWidth)) {
            int carMargin = getResources().getDimensionPixelSize(R.dimen.car_margin);
            int scrollBarContainerWidth = a.getDimensionPixelSize(
                    R.styleable.PagedListView_scrollBarContainerWidth, carMargin);
            setScrollBarContainerWidth(scrollBarContainerWidth);
        }

        if (a.hasValue(R.styleable.PagedListView_dayNightStyle)) {
            @DayNightStyle int dayNightStyle =
                    a.getInt(R.styleable.PagedListView_dayNightStyle, DayNightStyle.AUTO);
            setDayNightStyle(dayNightStyle);
        } else {
            setDayNightStyle(DayNightStyle.AUTO);
        }

        a.recycle();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mHandler.removeCallbacks(mUpdatePaginationRunnable);
    }

    /**
     * Returns the position of the given View in the list.
     *
     * @param v The View to check for.
     * @return The position or -1 if the given View is {@code null} or not in the list.
     */
    public int positionOf(@Nullable View v) {
        if (v == null || v.getParent() != mRecyclerView
                || mRecyclerView.getLayoutManager() == null) {
            return -1;
        }
        return mRecyclerView.getLayoutManager().getPosition(v);
    }

    /**
     * Set the gutter to the specified value.
     *
     * <p>The gutter is the space to the start/end of the list view items and will be equal in size
     * to the scroll bars. By default, there is a gutter to both the left and right of the list
     * view items, to account for the scroll bar.
     *
     * @param gutter A {@link Gutter} value that identifies which sides to apply the gutter to.
     */
    public void setGutter(@Gutter int gutter) {
        mGutter = gutter;

        int startMargin = 0;
        int endMargin = 0;
        if ((mGutter & Gutter.START) != 0) {
            startMargin = mGutterSize;
        }
        if ((mGutter & Gutter.END) != 0) {
            endMargin = mGutterSize;
        }
        MarginLayoutParams layoutParams = (MarginLayoutParams) mRecyclerView.getLayoutParams();
        layoutParams.setMarginStart(startMargin);
        layoutParams.setMarginEnd(endMargin);
        // requestLayout() isn't sufficient because we also need to resolveLayoutParams().
        mRecyclerView.setLayoutParams(layoutParams);

        // If there's a gutter, set ClipToPadding to false so that CardView's shadow will still
        // appear outside of the padding.
        mRecyclerView.setClipToPadding(startMargin == 0 && endMargin == 0);

    }

    /**
     * Sets the size of the gutter that appears at the start, end or both sizes of the items in
     * the {@code PagedListView}.
     *
     * @param gutterSize The size of the gutter in pixels.
     * @see #setGutter(int)
     */
    public void setGutterSize(int gutterSize) {
        mGutterSize = gutterSize;

        // Call setGutter to reset the gutter.
        setGutter(mGutter);
    }

    /**
     * Sets the width of the container that holds the scrollbar. The scrollbar will be centered
     * within this width.
     *
     * @param width The width of the scrollbar container.
     */
    public void setScrollBarContainerWidth(int width) {
        ViewGroup.LayoutParams layoutParams = mScrollBarView.getLayoutParams();
        layoutParams.width = width;
        mScrollBarView.requestLayout();
    }

    /**
     * Sets the top margin above the scroll bar. By default, this margin is 0.
     *
     * @param topMargin The top margin.
     */
    public void setScrollBarTopMargin(int topMargin) {
        MarginLayoutParams params = (MarginLayoutParams) mScrollBarView.getLayoutParams();
        params.topMargin = topMargin;
        mScrollBarView.requestLayout();
    }

    /**
     * Sets an offset above the first item in the {@code PagedListView}. This offset is scrollable
     * with the contents of the list.
     *
     * @param offset The top offset to add.
     */
    public void setListContentTopOffset(int offset) {
        TopOffsetDecoration existing = null;
        for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
            RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
            if (itemDecoration instanceof TopOffsetDecoration) {
                existing = (TopOffsetDecoration) itemDecoration;
                break;
            }
        }

        if (offset == 0 && existing != null) {
            mRecyclerView.removeItemDecoration(existing);
        } else if (existing == null) {
            mRecyclerView.addItemDecoration(new TopOffsetDecoration(offset));
        } else {
            existing.setTopOffset(offset);
        }
        mRecyclerView.invalidateItemDecorations();
    }

    @NonNull
    public RecyclerView getRecyclerView() {
        return mRecyclerView;
    }

    /**
     * Scrolls to the given position in the PagedListView.
     *
     * @param position The position in the list to scroll to.
     */
    public void scrollToPosition(int position) {
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }

        RecyclerView.SmoothScroller smoothScroller = mSnapHelper.createScroller(layoutManager);
        smoothScroller.setTargetPosition(position);

        layoutManager.startSmoothScroll(smoothScroller);

        // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure
        // the pagination arrows actually get updated. See b/15801119
        mHandler.post(mUpdatePaginationRunnable);
    }

    /**
     * Snap to the given position. This method will snap instantly to a position that's "close" to
     * the given position and then animate a short decelerate to indicate the direction that the
     * snap happened.
     *
     * @param position The position in the list to scroll to.
     */
    public void snapToPosition(int position) {
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();

        if (layoutManager == null) {
            return;
        }

        int startPosition = position;
        if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
            PointF vector = ((RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager)
                    .computeScrollVectorForPosition(position);
            // A positive value in the vector means scrolling down, so should offset by scrolling to
            // an item previous in the list.
            int offsetDirection = (vector == null || vector.y > 0) ? -1 : 1;
            startPosition += offsetDirection * SNAP_SCROLL_OFFSET_POSITION;

            // Clamp the start position.
            startPosition = Math.max(0, Math.min(startPosition, layoutManager.getItemCount() - 1));
        } else {
            // If the LayoutManager doesn't implement ScrollVectorProvider (the default for
            // PagedListView, LinearLayoutManager does, but if the user has overridden it) then we
            // cannot compute the direction we need to scroll. So just snap instantly instead.
            Log.w(TAG, "LayoutManager is not a ScrollVectorProvider, can't do snap animation.");
        }

        if (layoutManager instanceof LinearLayoutManager) {
            ((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(startPosition, 0);
        } else {
            layoutManager.scrollToPosition(startPosition);
        }

        if (startPosition != position) {
            // The actual scroll above happens on the next update, so we wait for that to finish
            // before doing the smooth scroll.
            post(() -> scrollToPosition(position));
        }
    }

    /** Sets the icon to be used for the up button. */
    public void setUpButtonIcon(Drawable icon) {
        mScrollBarView.setUpButtonIcon(icon);
    }

    /** Sets the icon to be used for the down button. */
    public void setDownButtonIcon(Drawable icon) {
        mScrollBarView.setDownButtonIcon(icon);
    }

    /**
     * Sets the adapter for the list.
     *
     * <p>The given Adapter can implement {@link ItemCap} if it wishes to control the behavior of
     * a max number of items. Otherwise, methods in the PagedListView to limit the content, such as
     * {@link #setMaxPages(int)}, will do nothing.
     */
    public void setAdapter(
            @NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
        mAdapter = adapter;
        mRecyclerView.setAdapter(adapter);

        updateMaxItems();
        updateAlphaJump();
    }

    /**
     * Sets {@link DividerVisibilityManager} on all {@code DividerDecoration} item decorations.
     *
     * @param dvm {@code DividerVisibilityManager} to be set.
     */
    public void setDividerVisibilityManager(DividerVisibilityManager dvm) {
        int decorCount = mRecyclerView.getItemDecorationCount();
        for (int i = 0; i < decorCount; i++) {
            RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
            if (decor instanceof DividerDecoration) {
                ((DividerDecoration) decor).setVisibilityManager(dvm);
            }
        }
        mRecyclerView.invalidateItemDecorations();
    }

    @Nullable
    @SuppressWarnings("unchecked")
    public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
        return mRecyclerView.getAdapter();
    }

    /**
     * Sets the maximum number of the pages that can be shown in the PagedListView. The size of a
     * page is defined as the number of items that fit completely on the screen at once.
     *
     * <p>Passing {@link #UNLIMITED_PAGES} will remove any restrictions on a maximum number
     * of pages.
     *
     * <p>Note that for any restriction on maximum pages to work, the adapter passed to this
     * PagedListView needs to implement {@link ItemCap}.
     *
     * @param maxPages The maximum number of pages that fit on the screen. Should be positive or
     * {@link #UNLIMITED_PAGES}.
     */
    public void setMaxPages(int maxPages) {
        mMaxPages = Math.max(UNLIMITED_PAGES, maxPages);
        updateMaxItems();
    }

    /**
     * Returns the maximum number of pages allowed in the PagedListView. This number is set by
     * {@link #setMaxPages(int)}. If that method has not been called, then this value should match
     * the default value.
     *
     * @return The maximum number of pages to be shown or {@link #UNLIMITED_PAGES} if there is
     * no limit.
     */
    public int getMaxPages() {
        return mMaxPages;
    }

    /**
     * Gets the number of rows per page. Default value of mRowsPerPage is -1. If the first child of
     * PagedLayoutManager is null or the height of the first child is 0, it will return 1.
     */
    public int getRowsPerPage() {
        return mRowsPerPage;
    }

    /** Resets the maximum number of pages to be shown to be the default. */
    public void resetMaxPages() {
        mMaxPages = getDefaultMaxPages();
        updateMaxItems();
    }

    /**
     * Adds an {@link RecyclerView.ItemDecoration} to this PagedListView.
     *
     * @param decor The decoration to add.
     * @see RecyclerView#addItemDecoration(RecyclerView.ItemDecoration)
     */
    public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
        mRecyclerView.addItemDecoration(decor);
    }

    /**
     * Removes the given {@link RecyclerView.ItemDecoration} from this
     * PagedListView.
     *
     * <p>The decoration will function the same as the item decoration for a {@link RecyclerView}.
     *
     * @param decor The decoration to remove.
     * @see RecyclerView#removeItemDecoration(RecyclerView.ItemDecoration)
     */
    public void removeItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
        mRecyclerView.removeItemDecoration(decor);
    }

    /**
     * Sets spacing between each item in the list. The spacing will not be added before the first
     * item and after the last.
     *
     * @param itemSpacing the spacing between each item.
     */
    public void setItemSpacing(int itemSpacing) {
        ItemSpacingDecoration existing = null;
        for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
            RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
            if (itemDecoration instanceof ItemSpacingDecoration) {
                existing = (ItemSpacingDecoration) itemDecoration;
                break;
            }
        }

        if (itemSpacing == 0 && existing != null) {
            mRecyclerView.removeItemDecoration(existing);
        } else if (existing == null) {
            mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
        } else {
            existing.setItemSpacing(itemSpacing);
        }
        mRecyclerView.invalidateItemDecorations();
    }

    /**
     * Sets the color of scrollbar.
     *
     * <p>Custom color ignores {@link DayNightStyle}. Calling {@link #resetScrollbarColor} resets to
     * default color.
     *
     * @param color Resource identifier of the color.
     */
    public void setScrollbarColor(@ColorRes int color) {
        mScrollBarView.setThumbColor(color);
    }

    /**
     * Resets the color of scrollbar to default.
     */
    public void resetScrollbarColor() {
        mScrollBarView.resetThumbColor();
    }

    /**
     * Adds an {@link RecyclerView.OnItemTouchListener} to this
     * PagedListView.
     *
     * <p>The listener will function the same as the listener for a regular {@link RecyclerView}.
     *
     * @param touchListener The touch listener to add.
     * @see RecyclerView#addOnItemTouchListener(RecyclerView.OnItemTouchListener)
     */
    public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
        mRecyclerView.addOnItemTouchListener(touchListener);
    }

    /**
     * Removes the given {@link RecyclerView.OnItemTouchListener} from
     * the PagedListView.
     *
     * @param touchListener The touch listener to remove.
     * @see RecyclerView#removeOnItemTouchListener(RecyclerView.OnItemTouchListener)
     */
    public void removeOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener touchListener) {
        mRecyclerView.removeOnItemTouchListener(touchListener);
    }

    /**
     * Sets how this {@link PagedListView} responds to day/night configuration changes. By
     * default, the PagedListView is darker in the day and lighter at night.
     *
     * @param dayNightStyle A value from {@link DayNightStyle}.
     * @see DayNightStyle
     */
    public void setDayNightStyle(@DayNightStyle int dayNightStyle) {
        // Update the scrollbar
        mScrollBarView.setDayNightStyle(dayNightStyle);

        int decorCount = mRecyclerView.getItemDecorationCount();
        for (int i = 0; i < decorCount; i++) {
            RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
            if (decor instanceof DividerDecoration) {
                ((DividerDecoration) decor).updateDividerColor();
            }
        }
    }

    /**
     * Sets the {@link OnScrollListener} that will be notified of scroll events within the
     * PagedListView.
     *
     * @param listener The scroll listener to set.
     */
    public void setOnScrollListener(OnScrollListener listener) {
        mOnScrollListener = listener;
    }

    /** Returns the page the given position is on, starting with page 0. */
    public int getPage(int position) {
        if (mRowsPerPage == -1) {
            return -1;
        }
        if (mRowsPerPage == 0) {
            return 0;
        }
        return position / mRowsPerPage;
    }

    private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
        if (mOrientationHelper == null || mOrientationHelper.getLayoutManager() != layoutManager) {
            // PagedListView is assumed to be a list that always vertically scrolls.
            mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
        }
        return mOrientationHelper;
    }

    /**
     * Scrolls the contents of the RecyclerView up a page. A page is defined as the height of the
     * {@code PagedListView}.
     *
     * <p>The resulting first item in the list will be snapped to so that it is completely visible.
     * If this is not possible due to the first item being taller than the containing
     * {@code PagedListView}, then the snapping will not occur.
     */
    public void pageUp() {
        if (mRecyclerView.getLayoutManager() == null || mRecyclerView.getChildCount() == 0) {
            return;
        }

        // Use OrientationHelper to calculate scroll distance in order to match snapping behavior.
        OrientationHelper orientationHelper =
                getOrientationHelper(mRecyclerView.getLayoutManager());

        int screenSize = mRecyclerView.getHeight();
        int scrollDistance = screenSize;
        // The iteration order matters. In case where there are 2 items longer than screen size, we
        // want to focus on upcoming view.
        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
            /*
             * We treat child View longer than screen size differently:
             * 1) When it enters screen, next pageUp will align its bottom with parent bottom;
             * 2) When it leaves screen, next pageUp will align its top with parent top.
             */
            View child = mRecyclerView.getChildAt(i);
            if (child.getHeight() > screenSize) {
                if (orientationHelper.getDecoratedEnd(child) < screenSize) {
                    // Child view bottom is entering screen. Align its bottom with parent bottom.
                    scrollDistance = screenSize - orientationHelper.getDecoratedEnd(child);
                } else if (-screenSize < orientationHelper.getDecoratedStart(child)
                        && orientationHelper.getDecoratedStart(child) < 0) {
                    // Child view top is about to enter screen - its distance to parent top
                    // is less than a full scroll. Align child top with parent top.
                    scrollDistance = Math.abs(orientationHelper.getDecoratedStart(child));
                }
                // There can be two items that are longer than the screen. We stop at the first one.
                // This is affected by the iteration order.
                break;
            }
        }
        // Distance should always be positive. Negate its value to scroll up.
        mRecyclerView.smoothScrollBy(0, -scrollDistance);
    }

    /**
     * Scrolls the contents of the RecyclerView down a page. A page is defined as the height of the
     * {@code PagedListView}.
     *
     * <p>This method will attempt to bring the last item in the list as the first item. If the
     * current first item in the list is taller than the {@code PagedListView}, then it will be
     * scrolled the length of a page, but not snapped to.
     */
    public void pageDown() {
        if (mRecyclerView.getLayoutManager() == null || mRecyclerView.getChildCount() == 0) {
            return;
        }

        OrientationHelper orientationHelper =
                getOrientationHelper(mRecyclerView.getLayoutManager());
        int screenSize = mRecyclerView.getHeight();
        int scrollDistance = screenSize;

        // If the last item is partially visible, page down should bring it to the top.
        View lastChild = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1);
        if (mRecyclerView.getLayoutManager().isViewPartiallyVisible(lastChild,
                /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false)) {
            scrollDistance = orientationHelper.getDecoratedStart(lastChild);
            if (scrollDistance < 0) {
                // Scroll value can be negative if the child is longer than the screen size and the
                // visible area of the screen does not show the start of the child.
                // Scroll to the next screen if the start value is negative
                scrollDistance = screenSize;
            }
        }

        // The iteration order matters. In case where there are 2 items longer than screen size, we
        // want to focus on upcoming view (the one at the bottom of screen).
        for (int i = mRecyclerView.getChildCount() - 1; i >= 0; i--) {
            /* We treat child View longer than screen size differently:
             * 1) When it enters screen, next pageDown will align its top with parent top;
             * 2) When it leaves screen, next pageDown will align its bottom with parent bottom.
             */
            View child = mRecyclerView.getChildAt(i);
            if (child.getHeight() > screenSize) {
                if (orientationHelper.getDecoratedStart(child) > 0) {
                    // Child view top is entering screen. Align its top with parent top.
                    scrollDistance = orientationHelper.getDecoratedStart(child);
                } else if (screenSize < orientationHelper.getDecoratedEnd(child)
                        && orientationHelper.getDecoratedEnd(child) < 2 * screenSize) {
                    // Child view bottom is about to enter screen - its distance to parent bottom
                    // is less than a full scroll. Align child bottom with parent bottom.
                    scrollDistance = orientationHelper.getDecoratedEnd(child) - screenSize;
                }
                // There can be two items that are longer than the screen. We stop at the first one.
                // This is affected by the iteration order.
                break;
            }
        }

        mRecyclerView.smoothScrollBy(0, scrollDistance);
    }

    /**
     * Sets the default number of pages that this PagedListView is limited to.
     *
     * @param newDefault The default number of pages. Should be positive.
     */
    public void setDefaultMaxPages(int newDefault) {
        if (newDefault < 0) {
            return;
        }
        mDefaultMaxPages = newDefault;
        resetMaxPages();
    }

    /** Returns the default number of pages the list should have */
    private int getDefaultMaxPages() {
        // assume list shown in response to a click, so, reduce number of clicks by one
        return mDefaultMaxPages - 1;
    }

    @Override
    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // if a late item is added to the top of the layout after the layout is stabilized, causing
        // the former top item to be pushed to the 2nd page, the focus will still be on the former
        // top item. Since our car layout manager tries to scroll the viewport so that the focused
        // item is visible, the view port will be on the 2nd page. That means the newly added item
        // will not be visible, on the first page.

        // what we want to do is: if the formerly focused item is the first one in the list, any
        // item added above it will make the focus to move to the new first item.
        // if the focus is not on the formerly first item, then we don't need to do anything. Let
        // the layout manager do the job and scroll the viewport so the currently focused item
        // is visible.
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();

        if (layoutManager == null) {
            return;
        }

        // we need to calculate whether we want to request focus here, before the super call,
        // because after the super call, the first born might be changed.
        View focusedChild = layoutManager.getFocusedChild();
        View firstBorn = layoutManager.getChildAt(0);

        super.onLayout(changed, left, top, right, bottom);

        if (mAdapter != null) {
            int itemCount = mAdapter.getItemCount();
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, String.format(
                        "onLayout hasFocus: %s, mLastItemCount: %s, itemCount: %s, "
                                + "focusedChild: %s, firstBorn: %s, isInTouchMode: %s, "
                                + "mNeedsFocus: %s",
                        hasFocus(),
                        mLastItemCount,
                        itemCount,
                        focusedChild,
                        firstBorn,
                        isInTouchMode(),
                        mNeedsFocus));
            }
            updateMaxItems();
            // This is a workaround for missing focus because isInTouchMode() is not always
            // returning the right value.
            // This is okay for the Engine release since focus is always showing.
            // However, in Tala and Fender, we want to show focus only when the user uses
            // hardware controllers, so we need to revisit this logic. b/22990605.
            if (mNeedsFocus && itemCount > 0) {
                if (focusedChild == null) {
                    requestFocus();
                }
                mNeedsFocus = false;
            }
            if (itemCount > mLastItemCount && focusedChild == firstBorn) {
                requestFocus();
            }
            mLastItemCount = itemCount;
        }

        if (!mScrollBarEnabled) {
            // Don't change the visibility of the ScrollBar unless it's enabled.
            return;
        }

        boolean isAtStart = isAtStart();
        boolean isAtEnd = isAtEnd();

        if ((isAtStart && isAtEnd) || layoutManager.getItemCount() == 0) {
            mScrollBarView.setVisibility(View.INVISIBLE);
            return;
        }

        mScrollBarView.setVisibility(View.VISIBLE);
        mScrollBarView.setUpEnabled(!isAtStart);
        mScrollBarView.setDownEnabled(!isAtEnd);

        if (mRecyclerView.getLayoutManager().canScrollVertically()) {
            mScrollBarView.setParametersInLayout(
                    mRecyclerView.computeVerticalScrollRange(),
                    mRecyclerView.computeVerticalScrollOffset(),
                    mRecyclerView.computeVerticalScrollExtent());
        } else {
            mScrollBarView.setParametersInLayout(
                    mRecyclerView.computeHorizontalScrollRange(),
                    mRecyclerView.computeHorizontalScrollOffset(),
                    mRecyclerView.computeHorizontalScrollExtent());
        }
    }

    /**
     * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
     * being called as a result of adapter changes, it should be called after the new layout has
     * been calculated because the method of determining scrollbar visibility uses the current
     * layout. If this is called after an adapter change but before the new layout, the visibility
     * determination may not be correct.
     *
     * @param animate {@code true} if the scrollbar should animate to its new position.
     *                {@code false} if no animation is used
     */
    void updatePaginationButtons(boolean animate) {
        if (!mScrollBarEnabled) {
            // Don't change the visibility of the ScrollBar unless it's enabled.
            return;
        }

        boolean isAtStart = isAtStart();
        boolean isAtEnd = isAtEnd();
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();

        if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
            mScrollBarView.setVisibility(View.INVISIBLE);
        } else {
            mScrollBarView.setVisibility(View.VISIBLE);
        }
        mScrollBarView.setUpEnabled(!isAtStart);
        mScrollBarView.setDownEnabled(!isAtEnd);

        if (layoutManager == null) {
            return;
        }

        if (mRecyclerView.getLayoutManager().canScrollVertically()) {
            mScrollBarView.setParameters(
                    mRecyclerView.computeVerticalScrollRange(),
                    mRecyclerView.computeVerticalScrollOffset(),
                    mRecyclerView.computeVerticalScrollExtent(), animate);
        } else {
            mScrollBarView.setParameters(
                    mRecyclerView.computeHorizontalScrollRange(),
                    mRecyclerView.computeHorizontalScrollOffset(),
                    mRecyclerView.computeHorizontalScrollExtent(), animate);
        }

        invalidate();
    }

    /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
    public boolean isAtStart() {
        return mSnapHelper.isAtStart(mRecyclerView.getLayoutManager());
    }

    /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
    public boolean isAtEnd() {
        return mSnapHelper.isAtEnd(mRecyclerView.getLayoutManager());
    }

    @UiThread
    private void updateMaxItems() {
        if (mAdapter == null) {
            return;
        }

        // Ensure mRowsPerPage regardless of if the adapter implements ItemCap.
        updateRowsPerPage();

        // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
        if (!(mAdapter instanceof ItemCap)) {
            return;
        }

        final int originalCount = mAdapter.getItemCount();
        ((ItemCap) mAdapter).setMaxItems(calculateMaxItemCount());
        final int newCount = mAdapter.getItemCount();
        if (newCount == originalCount) {
            return;
        }

        if (newCount < originalCount) {
            mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
        } else {
            mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
        }
    }

    private int calculateMaxItemCount() {
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return -1;
        }

        View firstChild = layoutManager.getChildAt(0);
        if (firstChild == null || firstChild.getHeight() == 0) {
            return -1;
        } else {
            return (mMaxPages < 0) ? -1 : mRowsPerPage * mMaxPages;
        }
    }

    /**
     * Updates the rows number per current page, which is used for calculating how many items we
     * want to show.
     */
    private void updateRowsPerPage() {
        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            mRowsPerPage = 1;
            return;
        }

        View firstChild = layoutManager.getChildAt(0);
        if (firstChild == null || firstChild.getHeight() == 0) {
            mRowsPerPage = 1;
        } else {
            mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight());
        }
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable(SAVED_SUPER_STATE_KEY, super.onSaveInstanceState());

        SparseArray<Parcelable> recyclerViewState = new SparseArray<>();
        mRecyclerView.saveHierarchyState(recyclerViewState);
        bundle.putSparseParcelableArray(SAVED_RECYCLER_VIEW_STATE_KEY, recyclerViewState);

        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof Bundle)) {
            super.onRestoreInstanceState(state);
            return;
        }

        Bundle bundle = (Bundle) state;
        mRecyclerView.restoreHierarchyState(
                bundle.getSparseParcelableArray(SAVED_RECYCLER_VIEW_STATE_KEY));

        super.onRestoreInstanceState(bundle.getParcelable(SAVED_SUPER_STATE_KEY));
    }

    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        // There is the possibility of multiple PagedListViews on a page. This means that the ids
        // of the child Views of PagedListView are no longer unique, and onSaveInstanceState()
        // cannot be used as is. As a result, PagedListViews needs to manually dispatch the instance
        // states. Call dispatchFreezeSelfOnly() so that no child views have onSaveInstanceState()
        // called by the system.
        dispatchFreezeSelfOnly(container);
    }

    @Override
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
        // Prevent onRestoreInstanceState() from being called on child Views. Instead, PagedListView
        // will manually handle passing the state. See the comment in dispatchSaveInstanceState()
        // for more information.
        dispatchThawSelfOnly(container);
    }

    private void updateAlphaJump() {
        boolean supportsAlphaJump = (mAdapter instanceof IAlphaJumpAdapter);
        mScrollBarView.setShowAlphaJump(supportsAlphaJump);
    }

    void showAlphaJump() {
        if (mAlphaJumpView == null && mAdapter instanceof IAlphaJumpAdapter) {
            mAlphaJumpView = new AlphaJumpOverlayView(getContext());
            mAlphaJumpView.init(this, (IAlphaJumpAdapter) mAdapter);
            addView(mAlphaJumpView);
        }

        mAlphaJumpView.setVisibility(View.VISIBLE);
    }

    private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
            new RecyclerView.OnScrollListener() {
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (mOnScrollListener != null) {
                        mOnScrollListener.onScrolled(recyclerView, dx, dy);

                        if (!isAtStart() && isAtEnd()) {
                            mOnScrollListener.onReachBottom();
                        }
                    }
                    updatePaginationButtons(false);
                }

                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    if (mOnScrollListener != null) {
                        mOnScrollListener.onScrollStateChanged(recyclerView, newState);
                    }
                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                        mHandler.postDelayed(mPaginationRunnable, PAGINATION_HOLD_DELAY_MS);
                    }
                }
            };

    final Runnable mPaginationRunnable =
            new Runnable() {
                @Override
                public void run() {
                    boolean upPressed = mScrollBarView.isUpPressed();
                    boolean downPressed = mScrollBarView.isDownPressed();
                    if (upPressed && downPressed) {
                        return;
                    }
                    if (upPressed) {
                        pageUp();
                    } else if (downPressed) {
                        pageDown();
                    }
                }
            };

    private final Runnable mUpdatePaginationRunnable =
            () -> updatePaginationButtons(true /*animate*/);

    /** Used to listen for {@code PagedListView} scroll events. */
    public abstract static class OnScrollListener {
        /**
         * Called when the {@code PagedListView} has been scrolled so that the last item is
         * completely visible.
         */
        public void onReachBottom() {}
        /** Called when scroll up button is clicked */
        public void onScrollUpButtonClicked() {}
        /** Called when scroll down button is clicked */
        public void onScrollDownButtonClicked() {}

        /**
         * Called when RecyclerView.OnScrollListener#onScrolled is called. See
         * RecyclerView.OnScrollListener
         */
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {}

        /** See RecyclerView.OnScrollListener */
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}
    }

    /**
     * A {@link RecyclerView.ItemDecoration} that will add spacing
     * between each item in the RecyclerView that it is added to.
     */
    private static class ItemSpacingDecoration extends RecyclerView.ItemDecoration {
        private int mItemSpacing;

        ItemSpacingDecoration(int itemSpacing) {
            mItemSpacing = itemSpacing;
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            int position = parent.getChildAdapterPosition(view);

            // Skip offset for last item except for GridLayoutManager.
            if (position == state.getItemCount() - 1
                    && !(parent.getLayoutManager() instanceof GridLayoutManager)) {
                return;
            }

            outRect.bottom = mItemSpacing;
        }

        /**
         * @param itemSpacing sets spacing between each item.
         */
        public void setItemSpacing(int itemSpacing) {
            mItemSpacing = itemSpacing;
        }
    }

    /**
     * A {@link RecyclerView.ItemDecoration} that will draw a dividing
     * line between each item in the RecyclerView that it is added to.
     */
    private static class DividerDecoration extends RecyclerView.ItemDecoration {
        private final Context mContext;
        private final Paint mPaint;
        private final int mDividerHeight;
        private final int mDividerStartMargin;
        private final int mDividerEndMargin;
        @IdRes private final int mDividerStartId;
        @IdRes private final int mDividerEndId;
        @ColorRes private final int mListDividerColor;
        private DividerVisibilityManager mVisibilityManager;

        /**
         * @param dividerStartMargin The start offset of the dividing line. This offset will be
         *     relative to {@code dividerStartId} if that value is given.
         * @param dividerStartId A child view id whose starting edge will be used as the starting
         *     edge of the dividing line. If this value is {@link #INVALID_RESOURCE_ID}, the top
         *     container of each child view will be used.
         * @param dividerEndId A child view id whose ending edge will be used as the starting edge
         *     of the dividing lin.e If this value is {@link #INVALID_RESOURCE_ID}, then the top
         *     container view of each child will be used.
         */
        DividerDecoration(Context context, int dividerStartMargin,
                int dividerEndMargin, @IdRes int dividerStartId, @IdRes int dividerEndId,
                @ColorRes int listDividerColor) {
            mContext = context;
            mDividerStartMargin = dividerStartMargin;
            mDividerEndMargin = dividerEndMargin;
            mDividerStartId = dividerStartId;
            mDividerEndId = dividerEndId;
            mListDividerColor = listDividerColor;

            mPaint = new Paint();
            mPaint.setColor(mContext.getColor(listDividerColor));
            mDividerHeight = mContext.getResources().getDimensionPixelSize(
                    R.dimen.car_list_divider_height);
        }

        /** Updates the list divider color which may have changed due to a day night transition. */
        public void updateDividerColor() {
            mPaint.setColor(mContext.getColor(mListDividerColor));
        }

        /** Sets {@link DividerVisibilityManager} on the DividerDecoration.*/
        public void setVisibilityManager(DividerVisibilityManager dvm) {
            mVisibilityManager = dvm;
        }

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            boolean usesGridLayoutManager = parent.getLayoutManager() instanceof GridLayoutManager;
            for (int i = 0; i < parent.getChildCount(); i++) {
                View container = parent.getChildAt(i);
                int itemPosition = parent.getChildAdapterPosition(container);

                if (!showDividerForAdapterPosition(itemPosition)) {
                    continue;
                }

                View nextVerticalContainer;
                if (usesGridLayoutManager) {
                    // Find an item in next row to calculate vertical space.
                    int lastItem = GridLayoutManagerUtils.getLastIndexOnSameRow(i, parent);
                    nextVerticalContainer = parent.getChildAt(lastItem + 1);
                } else {
                    nextVerticalContainer = parent.getChildAt(i + 1);
                }
                if (nextVerticalContainer == null) {
                    // Skip drawing divider for the last row in GridLayoutManager, or the last
                    // item (presumably in LinearLayoutManager).
                    continue;
                }
                int spacing = nextVerticalContainer.getTop() - container.getBottom();
                drawDivider(c, container, spacing);
            }
        }

        /**
         * Draws a divider under {@code container}.
         *
         * @param spacing between {@code container} and next view.
         */
        private void drawDivider(Canvas c, View container, int spacing) {
            View startChild =
                    mDividerStartId != INVALID_RESOURCE_ID
                            ? container.findViewById(mDividerStartId)
                            : container;

            View endChild =
                    mDividerEndId != INVALID_RESOURCE_ID
                            ? container.findViewById(mDividerEndId)
                            : container;

            if (startChild == null || endChild == null) {
                return;
            }

            Rect containerRect = new Rect();
            container.getGlobalVisibleRect(containerRect);

            Rect startRect = new Rect();
            startChild.getGlobalVisibleRect(startRect);

            Rect endRect = new Rect();
            endChild.getGlobalVisibleRect(endRect);

            int left = container.getLeft() + mDividerStartMargin
                    + (startRect.left - containerRect.left);
            int right = container.getRight()  - mDividerEndMargin
                    - (endRect.right - containerRect.right);
            // "(spacing + divider height) / 2" aligns the center of divider to that of spacing
            // between two items.
            // When spacing is an odd value (e.g. created by other decoration), space under divider
            // is greater by 1dp.
            int bottom = container.getBottom() + (spacing + mDividerHeight) / 2;
            int top = bottom - mDividerHeight;

            c.drawRect(left, top, right, bottom, mPaint);
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            int pos = parent.getChildAdapterPosition(view);
            if (!showDividerForAdapterPosition(pos)) {
                return;
            }
            // Add an bottom offset to all items that should have divider, even when divider is not
            // drawn for the bottom item(s).
            // With GridLayoutManager it's difficult to tell whether a view is in the last row.
            // This is to keep expected behavior consistent.
            outRect.bottom = mDividerHeight;
        }

        private boolean showDividerForAdapterPosition(int position) {
            // If visibility manager is not set, default to show dividers.
            return mVisibilityManager == null || mVisibilityManager.getShowDivider(position);
        }
    }

    /**
     * A {@link RecyclerView.ItemDecoration} that will add a top offset
     * to the first item in the RecyclerView it is added to.
     */
    private static class TopOffsetDecoration extends RecyclerView.ItemDecoration {
        private int mTopOffset;

        TopOffsetDecoration(int topOffset) {
            mTopOffset = topOffset;
        }

        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
                RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            int position = parent.getChildAdapterPosition(view);
            if (parent.getLayoutManager() instanceof GridLayoutManager
                    && position < GridLayoutManagerUtils.getFirstRowItemCount(parent)) {
                // For GridLayoutManager, top offset should be set for all items in the first row.
                // Otherwise the top items will be visually uneven.
                outRect.top = mTopOffset;
            } else if (position == 0) {
                // Only set the offset for the first item.
                outRect.top = mTopOffset;
            }
        }

        /**
         * @param topOffset sets spacing between each item.
         */
        public void setTopOffset(int topOffset) {
            mTopOffset = topOffset;
        }
    }
}