• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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