| /* |
| * 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 android.support.car.widget; |
| |
| import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.os.Handler; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.support.annotation.IdRes; |
| import android.support.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.RestrictTo; |
| import android.support.annotation.UiThread; |
| import android.support.car.R; |
| import android.support.v7.widget.RecyclerView; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.widget.FrameLayout; |
| |
| /** |
| * Custom {@link android.support.v7.widget.RecyclerView} that displays a list of items that |
| * resembles a {@link android.widget.ListView} but also has page up and page down arrows on the |
| * left side. |
| */ |
| public class PagedListView extends FrameLayout { |
| /** 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. |
| */ |
| protected static final int PAGINATION_HOLD_DELAY_MS = 400; |
| |
| private static final String TAG = "PagedListView"; |
| private static final int INVALID_RESOURCE_ID = -1; |
| |
| protected final CarRecyclerView mRecyclerView; |
| protected final PagedLayoutManager mLayoutManager; |
| protected final Handler mHandler = new Handler(); |
| private final boolean mScrollBarEnabled; |
| private final PagedScrollBarView mScrollBarView; |
| |
| private int mRowsPerPage = -1; |
| protected RecyclerView.Adapter<? extends RecyclerView.ViewHolder> mAdapter; |
| |
| /** Maximum number of pages to show. */ |
| private int mMaxPages; |
| |
| protected 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 = 0; |
| |
| private boolean mNeedsFocus; |
| |
| /** |
| * Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to cap the number of |
| * items. |
| * |
| * <p>NOTE: it is still up to the adapter to use maxItems in {@link |
| * android.support.v7.widget.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 a {@link android.support.v7.widget.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, AttributeSet attrs) { |
| this(context, attrs, 0 /*defStyleAttrs*/, 0 /*defStyleRes*/); |
| } |
| |
| public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs) { |
| this(context, attrs, defStyleAttrs, 0 /*defStyleRes*/); |
| } |
| |
| public PagedListView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) { |
| this(context, attrs, defStyleAttrs, defStyleRes, 0); |
| } |
| |
| public PagedListView( |
| Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes, int layoutId) { |
| super(context, attrs, defStyleAttrs, defStyleRes); |
| if (layoutId == 0) { |
| layoutId = R.layout.car_paged_recycler_view; |
| } |
| LayoutInflater.from(context).inflate(layoutId, this /*root*/, true /*attachToRoot*/); |
| |
| TypedArray a = context.obtainStyledAttributes( |
| attrs, R.styleable.PagedListView, defStyleAttrs, defStyleRes); |
| mRecyclerView = (CarRecyclerView) findViewById(R.id.recycler_view); |
| boolean fadeLastItem = a.getBoolean(R.styleable.PagedListView_fadeLastItem, false); |
| mRecyclerView.setFadeLastItem(fadeLastItem); |
| boolean offsetRows = a.getBoolean(R.styleable.PagedListView_offsetRows, false); |
| |
| mMaxPages = getDefaultMaxPages(); |
| |
| mLayoutManager = new PagedLayoutManager(context); |
| mLayoutManager.setOffsetRows(offsetRows); |
| mRecyclerView.setLayoutManager(mLayoutManager); |
| mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener); |
| mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12); |
| mRecyclerView.setItemAnimator(new CarItemAnimator(mLayoutManager)); |
| |
| boolean offsetScrollBar = a.getBoolean(R.styleable.PagedListView_offsetScrollBar, false); |
| if (offsetScrollBar) { |
| MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); |
| params.setMarginStart(getResources().getDimensionPixelSize( |
| R.dimen.car_margin)); |
| params.setMarginEnd( |
| a.getDimensionPixelSize(R.styleable.PagedListView_listEndMargin, 0)); |
| mRecyclerView.setLayoutParams(params); |
| } |
| |
| if (a.getBoolean(R.styleable.PagedListView_showPagedListViewDivider, true)) { |
| int dividerStartMargin = a.getDimensionPixelSize( |
| R.styleable.PagedListView_dividerStartMargin, 0); |
| int dividerStartId = a.getResourceId( |
| R.styleable.PagedListView_alignDividerStartTo, INVALID_RESOURCE_ID); |
| int dividerEndId = a.getResourceId( |
| R.styleable.PagedListView_alignDividerEndTo, INVALID_RESOURCE_ID); |
| |
| mRecyclerView.addItemDecoration(new DividerDecoration(context, dividerStartMargin, |
| dividerStartId, dividerEndId)); |
| } |
| |
| int itemSpacing = a.getDimensionPixelSize(R.styleable.PagedListView_itemSpacing, 0); |
| if (itemSpacing > 0) { |
| mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing)); |
| } |
| |
| // Set this to true so that this view consumes clicks events and views underneath |
| // don't receive this click event. Without this it's possible to click places in the |
| // view that don't capture the event, and as a result, elements visually hidden consume |
| // the event. |
| setClickable(true); |
| |
| // 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 = (PagedScrollBarView) findViewById(R.id.paged_scroll_view); |
| mScrollBarView.setPaginationListener( |
| new PagedScrollBarView.PaginationListener() { |
| @Override |
| public void onPaginate(int direction) { |
| if (direction == PagedScrollBarView.PaginationListener.PAGE_UP) { |
| mRecyclerView.pageUp(); |
| if (mOnScrollListener != null) { |
| mOnScrollListener.onScrollUpButtonClicked(); |
| } |
| } else if (direction == PagedScrollBarView.PaginationListener.PAGE_DOWN) { |
| mRecyclerView.pageDown(); |
| if (mOnScrollListener != null) { |
| mOnScrollListener.onScrollDownButtonClicked(); |
| } |
| } else { |
| Log.e(TAG, "Unknown pagination direction (" + direction + ")"); |
| } |
| } |
| }); |
| |
| 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); |
| |
| // Modify the layout the Scroll Bar is not visible. |
| if (!mScrollBarEnabled) { |
| MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams(); |
| params.setMarginStart(0); |
| mRecyclerView.setLayoutParams(params); |
| } |
| |
| setDayNightStyle(DayNightStyle.AUTO); |
| a.recycle(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| mHandler.removeCallbacks(mUpdatePaginationRunnable); |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent e) { |
| if (e.getAction() == MotionEvent.ACTION_DOWN) { |
| // The user has interacted with the list using touch. All movements will now paginate |
| // the list. |
| mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_PAGE); |
| } |
| return super.onInterceptTouchEvent(e); |
| } |
| |
| @Override |
| public void requestChildFocus(View child, View focused) { |
| super.requestChildFocus(child, focused); |
| // The user has interacted with the list using the controller. Movements through the list |
| // will now be one row at a time. |
| mLayoutManager.setRowOffsetMode(PagedLayoutManager.ROW_OFFSET_MODE_INDIVIDUAL); |
| } |
| |
| /** |
| * 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) { |
| return -1; |
| } |
| return mLayoutManager.getPosition(v); |
| } |
| |
| @NonNull |
| public CarRecyclerView 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) { |
| mLayoutManager.scrollToPosition(position); |
| |
| // Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure |
| // the pagination arrows actually get updated. See b/http://b/15801119 |
| mHandler.post(mUpdatePaginationRunnable); |
| } |
| |
| /** 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(); |
| } |
| |
| /** @hide */ |
| @RestrictTo(LIBRARY_GROUP) |
| @NonNull |
| public PagedLayoutManager getLayoutManager() { |
| return mLayoutManager; |
| } |
| |
| @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(); |
| } |
| |
| /** |
| * @return The position of first visible child in the list. -1 will be returned if there is no |
| * child. |
| */ |
| public int getFirstFullyVisibleChildPosition() { |
| return mLayoutManager.getFirstFullyVisibleChildPosition(); |
| } |
| |
| /** |
| * @return The position of last visible child in the list. -1 will be returned if there is no |
| * child. |
| */ |
| public int getLastFullyVisibleChildPosition() { |
| return mLayoutManager.getLastFullyVisibleChildPosition(); |
| } |
| |
| /** |
| * Adds an {@link android.support.v7.widget.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 android.support.v7.widget.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(); |
| } |
| |
| /** |
| * Adds an {@link android.support.v7.widget.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 android.support.v7.widget.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(); |
| } |
| } |
| } |
| |
| /** |
| * Returns the {@link android.support.v7.widget.RecyclerView.ViewHolder} that corresponds to the |
| * last child in the PagedListView that is fully visible. |
| * |
| * @return The corresponding ViewHolder or {@code null} if none exists. |
| */ |
| @Nullable |
| public RecyclerView.ViewHolder getLastViewHolder() { |
| View lastFullyVisible = mLayoutManager.getLastFullyVisibleChild(); |
| if (lastFullyVisible == null) { |
| return null; |
| } |
| int lastFullyVisibleAdapterPosition = mLayoutManager.getPosition(lastFullyVisible); |
| RecyclerView.ViewHolder lastViewHolder = getRecyclerView() |
| .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition + 1); |
| // We want to get the very last ViewHolder in the list, even if it's only fully visible |
| // If it doesn't exist, return the last fully visible ViewHolder. |
| if (lastViewHolder == null) { |
| lastViewHolder = getRecyclerView() |
| .findViewHolderForAdapterPosition(lastFullyVisibleAdapterPosition); |
| } |
| return lastViewHolder; |
| } |
| |
| /** |
| * 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; |
| mLayoutManager.setOnScrollListener(mOnScrollListener); |
| } |
| |
| /** 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; |
| } |
| |
| /** |
| * 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 */ |
| protected 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. |
| |
| // 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 = mLayoutManager.getFocusedChild(); |
| View firstBorn = mLayoutManager.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*/); |
| } |
| |
| /** |
| * Returns the View at the given position within the list. |
| * |
| * @param position A position within the list. |
| * @return The View or {@code null} if no View exists at the given position. |
| */ |
| @Nullable |
| public View findViewByPosition(int position) { |
| return mLayoutManager.findViewByPosition(position); |
| } |
| |
| /** |
| * 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 |
| */ |
| protected void updatePaginationButtons(boolean animate) { |
| if (!mScrollBarEnabled) { |
| // Don't change the visibility of the ScrollBar unless it's enabled. |
| return; |
| } |
| |
| if ((mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) |
| || mLayoutManager.getItemCount() == 0) { |
| mScrollBarView.setVisibility(View.INVISIBLE); |
| } else { |
| mScrollBarView.setVisibility(View.VISIBLE); |
| } |
| mScrollBarView.setUpEnabled(shouldEnablePageUpButton()); |
| mScrollBarView.setDownEnabled(shouldEnablePageDownButton()); |
| |
| mScrollBarView.setParameters( |
| mRecyclerView.computeVerticalScrollRange(), |
| mRecyclerView.computeVerticalScrollOffset(), |
| mRecyclerView.computeVerticalScrollExtent(), |
| animate); |
| invalidate(); |
| } |
| |
| protected boolean shouldEnablePageUpButton() { |
| return !mLayoutManager.isAtTop(); |
| } |
| |
| protected boolean shouldEnablePageDownButton() { |
| return !mLayoutManager.isAtBottom(); |
| } |
| |
| @UiThread |
| protected 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); |
| } |
| } |
| |
| protected int calculateMaxItemCount() { |
| final View firstChild = mLayoutManager.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. |
| */ |
| protected void updateRowsPerPage() { |
| final View firstChild = mLayoutManager.getChildAt(0); |
| if (firstChild == null || firstChild.getHeight() == 0) { |
| mRowsPerPage = 1; |
| } else { |
| mRowsPerPage = Math.max(1, (getHeight() - getPaddingTop()) / firstChild.getHeight()); |
| } |
| } |
| |
| @Override |
| protected Parcelable onSaveInstanceState() { |
| SavedState savedState = new SavedState(super.onSaveInstanceState()); |
| savedState.mLayoutManagerState = mLayoutManager.onSaveInstanceState(); |
| return savedState; |
| } |
| |
| @Override |
| protected void onRestoreInstanceState(Parcelable state) { |
| SavedState savedState = (SavedState) state; |
| mLayoutManager.onRestoreInstanceState(savedState.mLayoutManagerState); |
| super.onRestoreInstanceState(savedState.getSuperState()); |
| } |
| |
| @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 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); |
| } |
| |
| /** The state that will be saved across configuration changes. */ |
| private static class SavedState extends BaseSavedState { |
| /** The state of the {@link #mLayoutManager} of this PagedListView. */ |
| Parcelable mLayoutManagerState; |
| |
| SavedState(Parcelable superState) { |
| super(superState); |
| } |
| |
| private SavedState(Parcel in) { |
| super(in); |
| mLayoutManagerState = |
| in.readParcelable(PagedLayoutManager.SavedState.class.getClassLoader()); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel out, int flags) { |
| super.writeToParcel(out, flags); |
| out.writeParcelable(mLayoutManagerState, flags); |
| } |
| |
| public static final ClassLoaderCreator<SavedState> CREATOR = |
| new ClassLoaderCreator<SavedState>() { |
| @Override |
| public SavedState createFromParcel(Parcel source, ClassLoader loader) { |
| return new SavedState(source); |
| } |
| |
| @Override |
| public SavedState createFromParcel(Parcel source) { |
| return createFromParcel(source, null /* loader */); |
| } |
| |
| @Override |
| public SavedState[] newArray(int size) { |
| return new SavedState[size]; |
| } |
| }; |
| } |
| |
| 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 (!mLayoutManager.isAtTop() && mLayoutManager.isAtBottom()) { |
| mOnScrollListener.onReachBottom(); |
| } else if (mLayoutManager.isAtTop() || !mLayoutManager.isAtBottom()) { |
| mOnScrollListener.onLeaveBottom(); |
| } |
| } |
| 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); |
| } |
| } |
| }; |
| |
| protected final Runnable mPaginationRunnable = |
| new Runnable() { |
| @Override |
| public void run() { |
| boolean upPressed = mScrollBarView.isUpPressed(); |
| boolean downPressed = mScrollBarView.isDownPressed(); |
| if (upPressed && downPressed) { |
| return; |
| } |
| if (upPressed) { |
| mRecyclerView.pageUp(); |
| } else if (downPressed) { |
| mRecyclerView.pageDown(); |
| } |
| } |
| }; |
| |
| private final Runnable mUpdatePaginationRunnable = |
| new Runnable() { |
| @Override |
| public void run() { |
| 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 menu leaves the bottom */ |
| public void onLeaveBottom() {} |
| /** Called when scroll up button is clicked */ |
| public void onScrollUpButtonClicked() {} |
| /** Called when scroll down button is clicked */ |
| public void onScrollDownButtonClicked() {} |
| /** 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) {} |
| |
| /** Called when the view scrolls up a page */ |
| public void onPageUp() {} |
| |
| /** Called when the view scrolls down a page */ |
| public void onPageDown() {} |
| } |
| |
| /** |
| * A {@link android.support.v7.widget.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 mHalfItemSpacing; |
| |
| private ItemSpacingDecoration(int itemSpacing) { |
| mHalfItemSpacing = itemSpacing / 2; |
| } |
| |
| @Override |
| public void getItemOffsets(Rect outRect, View view, RecyclerView parent, |
| RecyclerView.State state) { |
| super.getItemOffsets(outRect, view, parent, state); |
| // Skip top offset for first item and bottom offset for last. |
| int position = parent.getChildAdapterPosition(view); |
| if (position > 0) { |
| outRect.top = mHalfItemSpacing; |
| } |
| if (position < state.getItemCount() - 1) { |
| outRect.bottom = mHalfItemSpacing; |
| } |
| } |
| |
| /** |
| * @param itemSpacing sets spacing between each item. |
| */ |
| public void setItemSpacing(int itemSpacing) { |
| mHalfItemSpacing = itemSpacing / 2; |
| } |
| } |
| |
| /** |
| * A {@link android.support.v7.widget.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; |
| @IdRes private final int mDividerStartId; |
| @IdRes private final int mDividerEndId; |
| |
| /** |
| * @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, |
| @IdRes int dividerStartId, @IdRes int dividerEndId) { |
| mContext = context; |
| mDividerStartMargin = dividerStartMargin; |
| mDividerStartId = dividerStartId; |
| mDividerEndId = dividerEndId; |
| |
| Resources res = context.getResources(); |
| mPaint = new Paint(); |
| mPaint.setColor(res.getColor(R.color.car_list_divider)); |
| mDividerHeight = res.getDimensionPixelSize(R.dimen.car_divider_height); |
| } |
| |
| /** Updates the list divider color which may have changed due to a day night transition. */ |
| public void updateDividerColor() { |
| mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider)); |
| } |
| |
| @Override |
| public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { |
| // Draw a divider line between each item. No need to draw the line for the last item. |
| for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) { |
| View container = parent.getChildAt(i); |
| View nextContainer = parent.getChildAt(i + 1); |
| int spacing = nextContainer.getTop() - container.getBottom(); |
| |
| 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) { |
| continue; |
| } |
| |
| int left = mDividerStartMargin + startChild.getLeft(); |
| int right = endChild.getRight(); |
| 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); |
| // Skip top offset for first item and bottom offset for last. |
| int position = parent.getChildAdapterPosition(view); |
| if (position > 0) { |
| outRect.top = mDividerHeight / 2; |
| } |
| if (position < state.getItemCount() - 1) { |
| outRect.bottom = mDividerHeight / 2; |
| } |
| } |
| } |
| } |