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