1 /* 2 * Copyright (C) 2019 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 package com.android.car.ui.recyclerview; 17 18 import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId; 19 20 import static java.lang.Math.max; 21 import static java.lang.Math.min; 22 23 import android.content.res.Resources; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.util.SparseArray; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.animation.AccelerateDecelerateInterpolator; 30 import android.view.animation.Interpolator; 31 32 import androidx.annotation.IntRange; 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 import androidx.annotation.VisibleForTesting; 36 import androidx.recyclerview.widget.OrientationHelper; 37 import androidx.recyclerview.widget.RecyclerView; 38 import androidx.recyclerview.widget.RecyclerView.LayoutManager; 39 40 import com.android.car.ui.R; 41 import com.android.car.ui.utils.CarUiUtils; 42 43 /** 44 * The default scroll bar widget for the {@link CarUiRecyclerView}. 45 * 46 * <p>Inspired by {@link androidx.car.widget.PagedListView}. Most pagination and scrolling logic 47 * has been ported from the PLV with minor updates. 48 */ 49 class DefaultScrollBar implements ScrollBar { 50 51 52 private float mButtonDisabledAlpha; 53 private CarUiSnapHelper mSnapHelper; 54 55 private View mScrollView; 56 private View mScrollTrack; 57 private View mScrollThumb; 58 private View mUpButton; 59 private View mDownButton; 60 private int mScrollbarThumbMinHeight; 61 62 private RecyclerView mRecyclerView; 63 64 private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator(); 65 66 private final Handler mHandler = new Handler(Looper.getMainLooper()); 67 68 private OrientationHelper mOrientationHelper; 69 70 private OnContinuousScrollListener mPageUpOnContinuousScrollListener; 71 private OnContinuousScrollListener mPageDownOnContinuousScrollListener; 72 73 @Override initialize(RecyclerView rv, View scrollView)74 public void initialize(RecyclerView rv, View scrollView) { 75 mRecyclerView = rv; 76 77 mScrollView = scrollView; 78 79 Resources res = rv.getContext().getResources(); 80 81 mButtonDisabledAlpha = CarUiUtils.getFloat(res, R.dimen.car_ui_button_disabled_alpha); 82 mScrollbarThumbMinHeight = (int) rv.getContext().getResources() 83 .getDimension(R.dimen.car_ui_scrollbar_min_thumb_height); 84 85 mUpButton = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_page_up); 86 View.OnClickListener paginateUpButtonOnClickListener = v -> pageUp(); 87 mUpButton.setOnClickListener(paginateUpButtonOnClickListener); 88 mPageUpOnContinuousScrollListener = new OnContinuousScrollListener(rv.getContext(), 89 paginateUpButtonOnClickListener); 90 mUpButton.setOnTouchListener(mPageUpOnContinuousScrollListener); 91 92 mDownButton = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_page_down); 93 View.OnClickListener paginateDownButtonOnClickListener = v -> pageDown(); 94 mDownButton.setOnClickListener(paginateDownButtonOnClickListener); 95 mPageDownOnContinuousScrollListener = new OnContinuousScrollListener(rv.getContext(), 96 paginateDownButtonOnClickListener); 97 mDownButton.setOnTouchListener(mPageDownOnContinuousScrollListener); 98 99 mScrollTrack = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_track); 100 mScrollThumb = requireViewByRefId(mScrollView, R.id.car_ui_scrollbar_thumb); 101 102 mSnapHelper = new CarUiSnapHelper(rv.getContext()); 103 getRecyclerView().setOnFlingListener(null); 104 mSnapHelper.attachToRecyclerView(getRecyclerView()); 105 106 // enables fast scrolling. 107 FastScroller fastScroller = new FastScroller(mRecyclerView, mScrollTrack, mScrollView); 108 fastScroller.enable(); 109 110 getRecyclerView().addOnScrollListener(mRecyclerViewOnScrollListener); 111 112 mScrollView.setVisibility(View.INVISIBLE); 113 mScrollView.addOnLayoutChangeListener( 114 (View v, 115 int left, 116 int top, 117 int right, 118 int bottom, 119 int oldLeft, 120 int oldTop, 121 int oldRight, 122 int oldBottom) -> mHandler.post(this::updatePaginationButtons)); 123 } 124 getRecyclerView()125 public RecyclerView getRecyclerView() { 126 return mRecyclerView; 127 } 128 129 @Override requestLayout()130 public void requestLayout() { 131 mScrollView.requestLayout(); 132 } 133 134 @Override setPadding(int paddingStart, int paddingEnd)135 public void setPadding(int paddingStart, int paddingEnd) { 136 mScrollView.setPadding(mScrollView.getPaddingLeft(), paddingStart, 137 mScrollView.getPaddingRight(), paddingEnd); 138 } 139 140 @Override adapterChanged(@ullable RecyclerView.Adapter adapter)141 public void adapterChanged(@Nullable RecyclerView.Adapter adapter) { 142 try { 143 if (mRecyclerView.getAdapter() != null) { 144 mRecyclerView.getAdapter().unregisterAdapterDataObserver(mAdapterChangeObserver); 145 } 146 if (adapter != null) { 147 adapter.registerAdapterDataObserver(mAdapterChangeObserver); 148 } 149 } catch (IllegalStateException e) { 150 // adapter is already registered. and we're trying to register again. 151 // or adapter was not registered and we're trying to unregister again. 152 // ignore. 153 } 154 } 155 156 /** 157 * Sets whether or not the up button on the scroll bar is clickable. 158 * 159 * @param enabled {@code true} if the up button is enabled. 160 */ setUpEnabled(boolean enabled)161 private void setUpEnabled(boolean enabled) { 162 // If the button is held down the button is disabled, the MotionEvent.ACTION_UP event on 163 // button release will not be sent to cancel pending scrolls. Manually cancel any pending 164 // scroll. 165 if (!enabled) { 166 mPageUpOnContinuousScrollListener.cancelPendingScroll(); 167 } 168 169 mUpButton.setEnabled(enabled); 170 mUpButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha); 171 } 172 173 /** 174 * Sets whether or not the down button on the scroll bar is clickable. 175 * 176 * @param enabled {@code true} if the down button is enabled. 177 */ setDownEnabled(boolean enabled)178 private void setDownEnabled(boolean enabled) { 179 // If the button is held down the button is disabled, the MotionEvent.ACTION_UP event on 180 // button release will not be sent to cancel pending scrolls. Manually cancel any pending 181 // scroll. 182 if (!enabled) { 183 mPageDownOnContinuousScrollListener.cancelPendingScroll(); 184 } 185 186 mDownButton.setEnabled(enabled); 187 mDownButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha); 188 } 189 190 /** 191 * Returns whether or not the down button on the scroll bar is clickable. 192 * 193 * @return {@code true} if the down button is enabled. {@code false} otherwise. 194 */ isDownEnabled()195 private boolean isDownEnabled() { 196 return mDownButton.isEnabled(); 197 } 198 199 /** 200 * Sets the range, offset and extent of the scroll bar. The range represents the size of a 201 * container for the scrollbar thumb; offset is the distance from the start of the container to 202 * where the thumb should be; and finally, extent is the size of the thumb. 203 * 204 * <p>These values can be expressed in arbitrary units, so long as they share the same units. 205 * The values should also be positive. 206 * 207 * @param range The range of the scrollbar's thumb 208 * @param offset The offset of the scrollbar's thumb 209 * @param extent The extent of the scrollbar's thumb 210 */ setParameters( @ntRangefrom = 0) int range, @IntRange(from = 0) int offset, @IntRange(from = 0) int extent)211 private void setParameters( 212 @IntRange(from = 0) int range, 213 @IntRange(from = 0) int offset, 214 @IntRange(from = 0) int extent) { 215 // Not laid out yet, so values cannot be calculated. 216 if (!mScrollView.isLaidOut()) { 217 return; 218 } 219 220 // If the scroll bars aren't visible, then no need to update. 221 if (mScrollView.getVisibility() != View.VISIBLE || range == 0) { 222 return; 223 } 224 225 int thumbLength = calculateScrollThumbLength(range, extent); 226 int thumbOffset = calculateScrollThumbOffset(range, offset, thumbLength); 227 228 // Sets the size of the thumb and request a redraw if needed. 229 ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams(); 230 231 if (lp.height != thumbLength || thumbLength < mScrollThumb.getHeight()) { 232 lp.height = thumbLength; 233 mScrollThumb.requestLayout(); 234 } 235 236 moveY(mScrollThumb, thumbOffset); 237 } 238 239 /** 240 * Calculates and returns how big the scroll bar thumb should be based on the given range and 241 * extent. 242 * 243 * @param range The total amount of space the scroll bar is allowed to roam over. 244 * @param extent The amount of space that the scroll bar takes up relative to the range. 245 * @return The height of the scroll bar thumb in pixels. 246 */ calculateScrollThumbLength(int range, int extent)247 private int calculateScrollThumbLength(int range, int extent) { 248 // Scale the length by the available space that the thumb can fill. 249 return max(Math.round(((float) extent / range) * mScrollTrack.getHeight()), 250 min(mScrollbarThumbMinHeight, mScrollTrack.getHeight())); 251 } 252 253 /** 254 * Calculates and returns how much the scroll thumb should be offset from the top of where it 255 * has been laid out. 256 * 257 * @param range The total amount of space the scroll bar is allowed to roam over. 258 * @param offset The amount the scroll bar should be offset, expressed in the same units as 259 * the given range. 260 * @param thumbLength The current length of the thumb in pixels. 261 * @return The amount the thumb should be offset in pixels. 262 */ calculateScrollThumbOffset(int range, int offset, int thumbLength)263 private int calculateScrollThumbOffset(int range, int offset, int thumbLength) { 264 // Ensure that if the user has reached the bottom of the list, then the scroll bar is 265 // aligned to the bottom as well. Otherwise, scale the offset appropriately. 266 // This offset will be a value relative to the parent of this scrollbar, so start by where 267 // the top of scrollbar track is. 268 return mScrollTrack.getTop() 269 + (isDownEnabled() 270 ? Math.round(((float) offset / range) * (mScrollTrack.getHeight() - thumbLength)) 271 : mScrollTrack.getHeight() - thumbLength); 272 } 273 274 /** 275 * Moves the given view to the specified 'y' position. 276 */ moveY(final View view, float newPosition)277 private void moveY(final View view, float newPosition) { 278 view.animate() 279 .y(newPosition) 280 .setDuration(/* duration= */ 0) 281 .setInterpolator(mPaginationInterpolator) 282 .start(); 283 } 284 285 private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener = 286 new RecyclerView.OnScrollListener() { 287 @Override 288 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { 289 updatePaginationButtons(); 290 cacheChildrenHeight(recyclerView.getLayoutManager()); 291 } 292 }; 293 private final SparseArray<Integer> mChildHeightByAdapterPosition = new SparseArray(); 294 295 private final RecyclerView.AdapterDataObserver mAdapterChangeObserver = 296 new RecyclerView.AdapterDataObserver() { 297 @Override 298 public void onChanged() { 299 clearCachedHeights(); 300 } 301 @Override 302 public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { 303 clearCachedHeights(); 304 } 305 @Override 306 public void onItemRangeChanged(int positionStart, int itemCount) { 307 clearCachedHeights(); 308 } 309 @Override 310 public void onItemRangeInserted(int positionStart, int itemCount) { 311 clearCachedHeights(); 312 } 313 @Override 314 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { 315 clearCachedHeights(); 316 } 317 @Override 318 public void onItemRangeRemoved(int positionStart, int itemCount) { 319 clearCachedHeights(); 320 } 321 }; 322 clearCachedHeights()323 private void clearCachedHeights() { 324 mChildHeightByAdapterPosition.clear(); 325 cacheChildrenHeight(getLayoutManager()); 326 } 327 cacheChildrenHeight(@ullable RecyclerView.LayoutManager layoutManager)328 private void cacheChildrenHeight(@Nullable RecyclerView.LayoutManager layoutManager) { 329 if (layoutManager == null) { 330 return; 331 } 332 for (int i = 0; i < layoutManager.getChildCount(); i++) { 333 View child = layoutManager.getChildAt(i); 334 int childPosition = layoutManager.getPosition(child); 335 if (mChildHeightByAdapterPosition.indexOfKey(childPosition) < 0) { 336 mChildHeightByAdapterPosition.put(childPosition, child.getHeight()); 337 } 338 } 339 } 340 estimateNextPositionScrollUp(int currentPos, int scrollDistance, OrientationHelper orientationHelper)341 private int estimateNextPositionScrollUp(int currentPos, int scrollDistance, 342 OrientationHelper orientationHelper) { 343 int nextPos = 0; 344 int distance = 0; 345 for (int i = currentPos - 1; i >= 0; i--) { 346 if (mChildHeightByAdapterPosition.indexOfKey(i) < 0) { 347 // Use the average height estimate when there is not enough data 348 nextPos = mSnapHelper.estimateNextPositionDiffForScrollDistance( 349 orientationHelper, -scrollDistance); 350 break; 351 } 352 if ((distance + mChildHeightByAdapterPosition.get(i)) > Math.abs(scrollDistance)) { 353 nextPos = i - currentPos + 1; 354 break; 355 } 356 distance += mChildHeightByAdapterPosition.get(i); 357 } 358 return nextPos; 359 } 360 getOrientationHelper(RecyclerView.LayoutManager layoutManager)361 private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) { 362 if (mOrientationHelper == null || mOrientationHelper.getLayoutManager() != layoutManager) { 363 // CarUiRecyclerView is assumed to be a list that always vertically scrolls. 364 mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager); 365 } 366 return mOrientationHelper; 367 } 368 369 /** 370 * Scrolls the contents of the RecyclerView up a page. A page is defined as the height of the 371 * {@code CarUiRecyclerView}. 372 * 373 * <p>The resulting first item in the list will be snapped to so that it is completely visible. 374 * If this is not possible due to the first item being taller than the containing {@code 375 * CarUiRecyclerView}, then the snapping will not occur. 376 */ pageUp()377 void pageUp() { 378 int currentOffset = computeVerticalScrollOffset(); 379 RecyclerView.LayoutManager layoutManager = getLayoutManager(); 380 if (layoutManager == null || layoutManager.getChildCount() == 0 || currentOffset == 0) { 381 return; 382 } 383 384 // Use OrientationHelper to calculate scroll distance in order to match snapping behavior. 385 OrientationHelper orientationHelper = getOrientationHelper(layoutManager); 386 int scrollDistance = orientationHelper.getTotalSpace(); 387 388 View currentPosView = getFirstMostVisibleChild(orientationHelper); 389 int currentPos = currentPosView != null ? getLayoutManager().getPosition( 390 currentPosView) : 0; 391 int nextPos = estimateNextPositionScrollUp(currentPos, 392 scrollDistance - Math.max(0, orientationHelper.getStartAfterPadding() 393 - orientationHelper.getDecoratedStart(currentPosView)), orientationHelper); 394 if (nextPos == 0) { 395 // Distance should always be positive. Negate its value to scroll up. 396 smoothScrollBy(0, -scrollDistance); 397 } else { 398 smoothScrollToPosition(Math.max(0, currentPos + nextPos)); 399 } 400 } 401 getFirstMostVisibleChild(OrientationHelper helper)402 private View getFirstMostVisibleChild(OrientationHelper helper) { 403 float mostVisiblePercent = 0; 404 View mostVisibleView = null; 405 406 for (int i = 0; i < getLayoutManager().getChildCount(); i++) { 407 View child = getLayoutManager().getChildAt(i); 408 float visiblePercentage = CarUiSnapHelper.getPercentageVisible(child, helper); 409 if (visiblePercentage == 1f) { 410 mostVisibleView = child; 411 break; 412 } else if (visiblePercentage > mostVisiblePercent) { 413 mostVisiblePercent = visiblePercentage; 414 mostVisibleView = child; 415 } 416 } 417 418 return mostVisibleView; 419 } 420 421 /** 422 * Scrolls the contents of the RecyclerView down a page. A page is defined as the height of the 423 * {@code CarUiRecyclerView}. 424 * 425 * <p>This method will attempt to bring the last item in the list as the first item. If the 426 * current first item in the list is taller than the {@code CarUiRecyclerView}, then it will be 427 * scrolled the length of a page, but not snapped to. 428 */ pageDown()429 void pageDown() { 430 RecyclerView.LayoutManager layoutManager = getLayoutManager(); 431 if (layoutManager == null || layoutManager.getChildCount() == 0) { 432 return; 433 } 434 435 OrientationHelper orientationHelper = getOrientationHelper(layoutManager); 436 int screenSize = orientationHelper.getTotalSpace(); 437 int scrollDistance = screenSize; 438 439 View currentPosView = getFirstMostVisibleChild(orientationHelper); 440 441 // If current view is partially visible and bottom of the view is below visible area of 442 // the recyclerview either scroll down one page (screenSize) or enough to align the bottom 443 // of the view with the bottom of the recyclerview. Note that this will not cause a snap, 444 // because the current view is already snapped to the top or it wouldn't be the most 445 // visible view. 446 if (layoutManager.isViewPartiallyVisible(currentPosView, 447 /* completelyVisible= */ false, /* acceptEndPointInclusion= */ false) 448 && orientationHelper.getDecoratedEnd(currentPosView) 449 > orientationHelper.getEndAfterPadding()) { 450 scrollDistance = Math.min(screenSize, 451 orientationHelper.getDecoratedEnd(currentPosView) 452 - orientationHelper.getEndAfterPadding()); 453 } 454 455 // Iterate over the childview (bottom to top) and stop when we find the first 456 // view that we can snap to and the scroll size is less than max scroll size (screenSize) 457 for (int i = layoutManager.getChildCount() - 1; i >= 0; i--) { 458 View child = layoutManager.getChildAt(i); 459 460 // Ignore the child if it's above the currentview, as scrolldown will only move down. 461 // Note that in case of gridview, child will not be the same as the currentview. 462 if (orientationHelper.getDecoratedStart(child) 463 <= orientationHelper.getDecoratedStart(currentPosView)) { 464 break; 465 } 466 467 // Ignore the child if the scroll distance is bigger than the max scroll size 468 if (orientationHelper.getDecoratedStart(child) 469 - orientationHelper.getStartAfterPadding() <= screenSize) { 470 // If the child is already fully visible we can scroll even further. 471 if (orientationHelper.getDecoratedEnd(child) 472 <= orientationHelper.getEndAfterPadding()) { 473 scrollDistance = orientationHelper.getDecoratedEnd(child) 474 - orientationHelper.getStartAfterPadding(); 475 } else { 476 scrollDistance = orientationHelper.getDecoratedStart(child) 477 - orientationHelper.getStartAfterPadding(); 478 } 479 break; 480 } 481 } 482 483 smoothScrollBy(0, scrollDistance); 484 } 485 486 /** 487 * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is 488 * being called as a result of adapter changes, it should be called after the new layout has 489 * been calculated because the method of determining scrollbar visibility uses the current 490 * layout. If this is called after an adapter change but before the new layout, the visibility 491 * determination may not be correct. 492 */ updatePaginationButtons()493 private void updatePaginationButtons() { 494 RecyclerView.LayoutManager layoutManager = getLayoutManager(); 495 496 if (layoutManager == null) { 497 mScrollView.setVisibility(View.INVISIBLE); 498 return; 499 } 500 501 boolean isAtStart = isAtStart(); 502 boolean isAtEnd = isAtEnd(); 503 504 // enable/disable the button before the view is shown. So there is no flicker. 505 setUpEnabled(!isAtStart); 506 setDownEnabled(!isAtEnd); 507 508 if ((isAtStart && isAtEnd) || layoutManager.getItemCount() == 0) { 509 mScrollView.setVisibility(View.INVISIBLE); 510 } else { 511 OrientationHelper orientationHelper = getOrientationHelper(layoutManager); 512 int screenSize = orientationHelper.getTotalSpace(); 513 int touchTargetSize = (int) getRecyclerView().getContext().getResources() 514 .getDimension(R.dimen.car_ui_touch_target_size); 515 ViewGroup.MarginLayoutParams upButtonLayoutParam = 516 (ViewGroup.MarginLayoutParams) mUpButton.getLayoutParams(); 517 int upButtonMargin = upButtonLayoutParam.topMargin 518 + upButtonLayoutParam.bottomMargin; 519 ViewGroup.MarginLayoutParams downButtonLayoutParam = 520 (ViewGroup.MarginLayoutParams) mDownButton.getLayoutParams(); 521 int downButtonMargin = downButtonLayoutParam.topMargin 522 + downButtonLayoutParam.bottomMargin; 523 int margin = upButtonMargin + downButtonMargin; 524 if (screenSize < 2 * touchTargetSize + margin) { 525 mScrollView.setVisibility(View.INVISIBLE); 526 } else { 527 ViewGroup.MarginLayoutParams trackLayoutParam = 528 (ViewGroup.MarginLayoutParams) mScrollTrack.getLayoutParams(); 529 int trackMargin = trackLayoutParam.topMargin 530 + trackLayoutParam.bottomMargin; 531 margin += trackMargin; 532 // touchTargetSize (for up button) + touchTargetSize (for down button) 533 // + max(touchTargetSize, mScrollbarThumbMinHeight) 534 // + margin (all margins added together) 535 if (screenSize < 2 * touchTargetSize 536 + max(touchTargetSize, mScrollbarThumbMinHeight) + margin) { 537 mScrollTrack.setVisibility(View.INVISIBLE); 538 mScrollThumb.setVisibility(View.INVISIBLE); 539 } else { 540 mScrollTrack.setVisibility(View.VISIBLE); 541 mScrollThumb.setVisibility(View.VISIBLE); 542 } 543 mScrollView.setVisibility(View.VISIBLE); 544 } 545 } 546 547 if (layoutManager.canScrollVertically()) { 548 setParameters( 549 computeVerticalScrollRange(), 550 computeVerticalScrollOffset(), 551 computeVerticalScrollExtent()); 552 } else { 553 setParameters( 554 computeHorizontalScrollRange(), 555 computeHorizontalScrollOffset(), 556 computeHorizontalScrollExtent()); 557 } 558 559 mScrollView.invalidate(); 560 } 561 562 /** 563 * Returns {@code true} if the RecyclerView is completely displaying the first item. 564 */ 565 @Override isAtStart()566 public boolean isAtStart() { 567 return mSnapHelper.isAtStart(getLayoutManager()); 568 } 569 570 @Override setHighlightThumb(boolean highlight)571 public void setHighlightThumb(boolean highlight) { 572 mScrollThumb.setActivated(highlight); 573 } 574 575 /** 576 * Returns {@code true} if the RecyclerView is completely displaying the last item. 577 */ isAtEnd()578 boolean isAtEnd() { 579 return mSnapHelper.isAtEnd(getLayoutManager()); 580 } 581 582 @VisibleForTesting getLayoutManager()583 LayoutManager getLayoutManager() { 584 return getRecyclerView().getLayoutManager(); 585 } 586 587 @VisibleForTesting smoothScrollToPosition(int max)588 void smoothScrollToPosition(int max) { 589 getRecyclerView().smoothScrollToPosition(max); 590 } 591 592 @VisibleForTesting smoothScrollBy(int dx, int dy)593 void smoothScrollBy(int dx, int dy) { 594 getRecyclerView().smoothScrollBy(dx, dy); 595 } 596 597 @VisibleForTesting computeVerticalScrollRange()598 int computeVerticalScrollRange() { 599 return getRecyclerView().computeVerticalScrollRange(); 600 } 601 602 @VisibleForTesting computeVerticalScrollOffset()603 int computeVerticalScrollOffset() { 604 return getRecyclerView().computeVerticalScrollOffset(); 605 } 606 607 @VisibleForTesting computeVerticalScrollExtent()608 int computeVerticalScrollExtent() { 609 return getRecyclerView().computeVerticalScrollExtent(); 610 } 611 612 @VisibleForTesting computeHorizontalScrollRange()613 int computeHorizontalScrollRange() { 614 return getRecyclerView().computeHorizontalScrollRange(); 615 } 616 617 @VisibleForTesting computeHorizontalScrollOffset()618 int computeHorizontalScrollOffset() { 619 return getRecyclerView().computeHorizontalScrollOffset(); 620 } 621 622 @VisibleForTesting computeHorizontalScrollExtent()623 int computeHorizontalScrollExtent() { 624 return getRecyclerView().computeHorizontalScrollExtent(); 625 } 626 } 627