| /* |
| * 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 androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; |
| |
| 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.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.RestrictTo; |
| 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; |
| private 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; |
| |
| private 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. |
| */ |
| boolean shouldHideDivider(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, null /* attrs */); |
| } |
| |
| public PagedListView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| init(context, attrs); |
| } |
| |
| public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) { |
| super(context, attrs, defStyleAttrs); |
| init(context, attrs); |
| } |
| |
| public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { |
| super(context, attrs, defStyleAttrs, defStyleRes); |
| init(context, attrs); |
| } |
| |
| private void init(Context context, AttributeSet attrs) { |
| LayoutInflater.from(context).inflate(R.layout.car_paged_recycler_view, |
| this /* root */, true /* attachToRoot */); |
| |
| TypedArray a = context.obtainStyledAttributes( |
| attrs, R.styleable.PagedListView, R.attr.pagedListViewStyle, 0 /* defStyleRes */); |
| 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); |
| |
| 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(); |
| } |
| }); |
| |
| 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); |
| } |
| |
| mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE); |
| |
| if (mScrollBarEnabled) { |
| int topMargin = |
| a.getDimensionPixelSize(R.styleable.PagedListView_scrollBarTopMargin, 0); |
| 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. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| public void pageUp() { |
| // 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. |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP) |
| public void pageDown() { |
| 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); |
| } |
| |
| // 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; |
| } |
| |
| // We need to update the scroll buttons after layout has happened. |
| // Determining if a scrollbar is necessary requires looking at the layout of the child |
| // views. Therefore, this determination can only be done after layout has happened. |
| // Note: don't animate here to prevent b/26849677 |
| updatePaginationButtons(false /*animate*/); |
| } |
| |
| /** |
| * 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 |
| */ |
| private 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); |
| } |
| |
| private 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); |
| } |
| } |
| }; |
| |
| private 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 menu reaches the bottom */ |
| 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 the alpha jump button is clicked. */ |
| public void onAlphaJumpButtonClicked() {} |
| /** Called when scrolling to the previous page via up gesture */ |
| public void onGestureUp() {} |
| /** Called when scrolling to the next page via down gesture */ |
| public void onGestureDown() {} |
| |
| /** |
| * 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; |
| |
| private 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. |
| */ |
| private 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 (hideDividerForAdapterPosition(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); |
| int bottom = container.getBottom() + spacing / 2 + 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 (hideDividerForAdapterPosition(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 hideDividerForAdapterPosition(int position) { |
| return mVisibilityManager != null && mVisibilityManager.shouldHideDivider(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; |
| |
| private 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; |
| } |
| } |
| } |