• 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 import static com.android.car.ui.utils.RotaryConstants.ROTARY_CONTAINER;
20 import static com.android.car.ui.utils.RotaryConstants.ROTARY_HORIZONTALLY_SCROLLABLE;
21 import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
22 import static com.android.car.ui.utils.ViewUtils.LazyLayoutView;
23 import static com.android.car.ui.utils.ViewUtils.setRotaryScrollEnabled;
24 
25 import android.car.drivingstate.CarUxRestrictions;
26 import android.content.Context;
27 import android.content.res.TypedArray;
28 import android.graphics.Rect;
29 import android.os.Parcelable;
30 import android.text.TextUtils;
31 import android.util.AttributeSet;
32 import android.view.InputDevice;
33 import android.view.LayoutInflater;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.ViewPropertyAnimator;
38 import android.widget.FrameLayout;
39 import android.widget.LinearLayout;
40 
41 import androidx.annotation.NonNull;
42 import androidx.annotation.Nullable;
43 import androidx.recyclerview.widget.GridLayoutManager;
44 import androidx.recyclerview.widget.LinearLayoutManager;
45 import androidx.recyclerview.widget.RecyclerView;
46 
47 import com.android.car.ui.R;
48 import com.android.car.ui.recyclerview.decorations.grid.GridDividerItemDecoration;
49 import com.android.car.ui.recyclerview.decorations.grid.GridOffsetItemDecoration;
50 import com.android.car.ui.recyclerview.decorations.linear.LinearDividerItemDecoration;
51 import com.android.car.ui.recyclerview.decorations.linear.LinearOffsetItemDecoration;
52 import com.android.car.ui.recyclerview.decorations.linear.LinearOffsetItemDecoration.OffsetPosition;
53 import com.android.car.ui.utils.CarUxRestrictionsUtil;
54 
55 import java.lang.reflect.Constructor;
56 import java.util.HashSet;
57 import java.util.Objects;
58 import java.util.Set;
59 
60 /**
61  * View that extends a {@link RecyclerView} and wraps itself into a {@link LinearLayout} which could
62  * potentially include a scrollbar that has page up and down arrows. Interaction with this view is
63  * similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
64  */
65 public final class CarUiRecyclerViewImpl extends CarUiRecyclerView implements LazyLayoutView {
66 
67     private static final String TAG = "CarUiRecyclerView";
68 
69     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener =
70             new UxRestrictionChangedListener();
71 
72     @NonNull
73     private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
74     private boolean mScrollBarEnabled;
75     @Nullable
76     private String mScrollBarClass;
77     private int mScrollBarPaddingTop;
78     private int mScrollBarPaddingBottom;
79     @Nullable
80     private ScrollBar mScrollBar;
81 
82     @Nullable
83     private GridOffsetItemDecoration mTopOffsetItemDecorationGrid;
84     @Nullable
85     private GridOffsetItemDecoration mBottomOffsetItemDecorationGrid;
86     @Nullable
87     private RecyclerView.ItemDecoration mTopOffsetItemDecorationLinear;
88     @Nullable
89     private RecyclerView.ItemDecoration mBottomOffsetItemDecorationLinear;
90     @Nullable
91     private GridDividerItemDecoration mDividerItemDecorationGrid;
92     @Nullable
93     private RecyclerView.ItemDecoration mDividerItemDecorationLinear;
94     private int mNumOfColumns;
95     private boolean mInstallingExtScrollBar = false;
96     private int mContainerVisibility = View.VISIBLE;
97     @Nullable
98     private Rect mContainerPadding;
99     @Nullable
100     private Rect mContainerPaddingRelative;
101     @Nullable
102     private ViewGroup mContainer;
103     @Size
104     private int mSize;
105 
106     // Set to true when when styled attributes are read and initialized.
107     private boolean mIsInitialized;
108     private boolean mEnableDividers;
109 
110     private boolean mHasScrolled = false;
111 
112     @NonNull
113     private final Set<Runnable> mOnLayoutCompletedListeners = new HashSet<>();
114 
115     private OnScrollListener mOnScrollListener = new OnScrollListener() {
116         @Override
117         public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
118             if (dx > 0 || dy > 0) {
119                 mHasScrolled = true;
120                 removeOnScrollListener(this);
121             }
122         }
123     };
124 
CarUiRecyclerViewImpl(@onNull Context context)125     public CarUiRecyclerViewImpl(@NonNull Context context) {
126         this(context, null);
127     }
128 
CarUiRecyclerViewImpl(@onNull Context context, @Nullable AttributeSet attrs)129     public CarUiRecyclerViewImpl(@NonNull Context context, @Nullable AttributeSet attrs) {
130         this(context, attrs, R.attr.carUiRecyclerViewStyle);
131     }
132 
CarUiRecyclerViewImpl(@onNull Context context, @Nullable AttributeSet attrs, int defStyle)133     public CarUiRecyclerViewImpl(@NonNull Context context, @Nullable AttributeSet attrs,
134             int defStyle) {
135         super(context, attrs, defStyle);
136         mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
137         init(context, attrs, defStyle);
138     }
139 
init(Context context, AttributeSet attrs, int defStyleAttr)140     private void init(Context context, AttributeSet attrs, int defStyleAttr) {
141         setClipToPadding(false);
142         TypedArray a = context.obtainStyledAttributes(
143                 attrs,
144                 R.styleable.CarUiRecyclerView,
145                 defStyleAttr,
146                 R.style.Widget_CarUi_CarUiRecyclerView);
147         initRotaryScroll(a);
148 
149         mScrollBarEnabled = context.getResources().getBoolean(R.bool.car_ui_scrollbar_enable);
150 
151         mScrollBarPaddingTop = context.getResources()
152                 .getDimensionPixelSize(R.dimen.car_ui_scrollbar_padding_top);
153         mScrollBarPaddingBottom = context.getResources()
154                 .getDimensionPixelSize(R.dimen.car_ui_scrollbar_padding_bottom);
155 
156         @CarUiRecyclerViewLayout int carUiRecyclerViewLayout =
157                 a.getInt(R.styleable.CarUiRecyclerView_layoutStyle, CarUiRecyclerViewLayout.LINEAR);
158         mNumOfColumns = a.getInt(R.styleable.CarUiRecyclerView_numOfColumns, /* defValue= */ 2);
159         mEnableDividers =
160                 a.getBoolean(R.styleable.CarUiRecyclerView_enableDivider, /* defValue= */ false);
161 
162         mDividerItemDecorationLinear = new LinearDividerItemDecoration(
163                 context.getDrawable(R.drawable.car_ui_recyclerview_divider));
164 
165         mDividerItemDecorationGrid =
166                 new GridDividerItemDecoration(
167                         context.getDrawable(R.drawable.car_ui_divider),
168                         context.getDrawable(R.drawable.car_ui_divider),
169                         mNumOfColumns);
170 
171         mTopOffsetItemDecorationLinear =
172                 new LinearOffsetItemDecoration(0, OffsetPosition.START);
173         mBottomOffsetItemDecorationLinear =
174                 new LinearOffsetItemDecoration(0, OffsetPosition.END);
175         mTopOffsetItemDecorationGrid =
176                 new GridOffsetItemDecoration(0, mNumOfColumns,
177                         OffsetPosition.START);
178         mBottomOffsetItemDecorationGrid =
179                 new GridOffsetItemDecoration(0, mNumOfColumns,
180                         OffsetPosition.END);
181 
182         mIsInitialized = true;
183 
184         // Check if a layout manager has already been set via XML
185         boolean isLayoutMangerSet = getLayoutManager() != null;
186         if (!isLayoutMangerSet && carUiRecyclerViewLayout
187                 == CarUiRecyclerView.CarUiRecyclerViewLayout.LINEAR) {
188             setLayoutManager(new LinearLayoutManager(getContext()) {
189                 @Override
190                 public void onLayoutCompleted(RecyclerView.State state) {
191                     super.onLayoutCompleted(state);
192                     // Iterate through a copied set instead of the original set because the original
193                     // set might be modified during iteration.
194                     Set<Runnable> onLayoutCompletedListeners =
195                         new HashSet<>(mOnLayoutCompletedListeners);
196                     for (Runnable runnable : onLayoutCompletedListeners) {
197                         runnable.run();
198                     }
199                 }
200             });
201         } else if (!isLayoutMangerSet && carUiRecyclerViewLayout
202                 == CarUiRecyclerView.CarUiRecyclerViewLayout.GRID) {
203             setLayoutManager(new GridLayoutManager(getContext(), mNumOfColumns) {
204                 @Override
205                 public void onLayoutCompleted(RecyclerView.State state) {
206                     super.onLayoutCompleted(state);
207                     // Iterate through a copied set instead of the original set because the original
208                     // set might be modified during iteration.
209                     Set<Runnable> onLayoutCompletedListeners =
210                         new HashSet<>(mOnLayoutCompletedListeners);
211                     for (Runnable runnable : onLayoutCompletedListeners) {
212                         runnable.run();
213                     }
214                 }
215             });
216         }
217         addOnScrollListener(mOnScrollListener);
218 
219         mSize = a.getInt(R.styleable.CarUiRecyclerView_carUiSize, SIZE_LARGE);
220 
221         a.recycle();
222 
223         if (!mScrollBarEnabled) {
224             return;
225         }
226 
227         mContainer = new FrameLayout(getContext());
228 
229         setVerticalScrollBarEnabled(false);
230         setHorizontalScrollBarEnabled(false);
231 
232         mScrollBarClass = context.getResources().getString(R.string.car_ui_scrollbar_component);
233     }
234 
235     @Override
setLayoutManager(@ullable LayoutManager layoutManager)236     public void setLayoutManager(@Nullable LayoutManager layoutManager) {
237         // Cannot setup item decorations before stylized attributes have been read.
238         if (mIsInitialized) {
239             addItemDecorations(layoutManager);
240         }
241         super.setLayoutManager(layoutManager);
242     }
243 
244     @Override
setLayoutStyle(CarUiLayoutStyle layoutStyle)245     public void setLayoutStyle(CarUiLayoutStyle layoutStyle) {
246         LayoutManager layoutManager;
247         if (layoutStyle.getLayoutType() == CarUiRecyclerViewLayout.LINEAR) {
248             layoutManager = new LinearLayoutManager(getContext(),
249                     layoutStyle.getOrientation(),
250                     layoutStyle.getReverseLayout()) {
251                 @Override
252                 public void onLayoutCompleted(RecyclerView.State state) {
253                     super.onLayoutCompleted(state);
254                     // Iterate through a copied set instead of the original set because the original
255                     // set might be modified during iteration.
256                     Set<Runnable> onLayoutCompletedListeners =
257                         new HashSet<>(mOnLayoutCompletedListeners);
258                     for (Runnable runnable : onLayoutCompletedListeners) {
259                         runnable.run();
260                     }
261                 }
262             };
263         } else {
264             layoutManager = new GridLayoutManager(getContext(),
265                     layoutStyle.getSpanCount(),
266                     layoutStyle.getOrientation(),
267                     layoutStyle.getReverseLayout()) {
268                 @Override
269                 public void onLayoutCompleted(RecyclerView.State state) {
270                     super.onLayoutCompleted(state);
271                     // Iterate through a copied set instead of the original set because the original
272                     // set might be modified during iteration.
273                     Set<Runnable> onLayoutCompletedListeners =
274                         new HashSet<>(mOnLayoutCompletedListeners);
275                     for (Runnable runnable : onLayoutCompletedListeners) {
276                         runnable.run();
277                     }
278                 }
279             };
280             // TODO(b/190444037): revisit usage of LayoutStyles and their casting
281             if (layoutStyle instanceof CarUiGridLayoutStyle) {
282                 ((GridLayoutManager) layoutManager).setSpanSizeLookup(
283                         ((CarUiGridLayoutStyle) layoutStyle).getSpanSizeLookup());
284             }
285         }
286         setLayoutManager(layoutManager);
287     }
288 
289     /**
290      * {@inheritDoc}
291      * <p>
292      * Note that this method will never return true if this view has no items in it's adapter. This
293      * is fine since an RecyclerView with empty items is not able to restore focus inside it.
294      */
295     @Override
isLayoutCompleted()296     public boolean isLayoutCompleted() {
297         RecyclerView.Adapter adapter = getAdapter();
298         return adapter != null && adapter.getItemCount() > 0 && !isComputingLayout();
299     }
300 
301     @Override
addOnLayoutCompleteListener(@ullable Runnable runnable)302     public void addOnLayoutCompleteListener(@Nullable Runnable runnable) {
303         if (runnable != null) {
304             mOnLayoutCompletedListeners.add(runnable);
305         }
306     }
307 
308     @Override
removeOnLayoutCompleteListener(@ullable Runnable runnable)309     public void removeOnLayoutCompleteListener(@Nullable Runnable runnable) {
310         if (runnable != null) {
311             mOnLayoutCompletedListeners.remove(runnable);
312         }
313     }
314 
315     @Override
getContainer()316     public View getContainer() {
317         return mContainer;
318     }
319 
320     // This method should not be invoked before item decorations are initialized by the #init()
321     // method.
addItemDecorations(LayoutManager layoutManager)322     private void addItemDecorations(LayoutManager layoutManager) {
323         // remove existing Item decorations.
324         removeItemDecoration(Objects.requireNonNull(mDividerItemDecorationGrid));
325         removeItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationGrid));
326         removeItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationGrid));
327         removeItemDecoration(Objects.requireNonNull(mDividerItemDecorationLinear));
328         removeItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationLinear));
329         removeItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationLinear));
330 
331         if (layoutManager instanceof GridLayoutManager) {
332             if (mEnableDividers) {
333                 addItemDecoration(Objects.requireNonNull(mDividerItemDecorationGrid));
334             }
335             addItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationGrid));
336             addItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationGrid));
337             setNumOfColumns(((GridLayoutManager) layoutManager).getSpanCount());
338         } else {
339             if (mEnableDividers) {
340                 addItemDecoration(Objects.requireNonNull(mDividerItemDecorationLinear));
341             }
342             addItemDecoration(Objects.requireNonNull(mTopOffsetItemDecorationLinear));
343             addItemDecoration(Objects.requireNonNull(mBottomOffsetItemDecorationLinear));
344         }
345     }
346 
347     /**
348      * If this view's {@code rotaryScrollEnabled} attribute is set to true, sets the content
349      * description so that the {@code RotaryService} will treat it as a scrollable container and
350      * initializes this view accordingly.
351      */
initRotaryScroll(@ullable TypedArray styledAttributes)352     private void initRotaryScroll(@Nullable TypedArray styledAttributes) {
353         boolean rotaryScrollEnabled = styledAttributes != null && styledAttributes.getBoolean(
354                 R.styleable.CarUiRecyclerView_rotaryScrollEnabled, /* defValue=*/ false);
355         if (rotaryScrollEnabled) {
356             int orientation = styledAttributes
357                     .getInt(R.styleable.CarUiRecyclerView_android_orientation,
358                             LinearLayout.VERTICAL);
359             setRotaryScrollEnabled(
360                     this, /* isVertical= */ orientation == LinearLayout.VERTICAL);
361         } else {
362             CharSequence contentDescription = getContentDescription();
363             rotaryScrollEnabled = contentDescription != null
364                     && (ROTARY_HORIZONTALLY_SCROLLABLE.contentEquals(contentDescription)
365                     || ROTARY_VERTICALLY_SCROLLABLE.contentEquals(contentDescription));
366         }
367 
368         // If rotary scrolling is enabled, set a generic motion event listener to convert
369         // SOURCE_ROTARY_ENCODER scroll events into SOURCE_MOUSE scroll events that RecyclerView
370         // knows how to handle.
371         setOnGenericMotionListener(rotaryScrollEnabled ? (v, event) -> {
372             if (event.getAction() == MotionEvent.ACTION_SCROLL) {
373                 if (event.getSource() == InputDevice.SOURCE_ROTARY_ENCODER) {
374                     MotionEvent mouseEvent = MotionEvent.obtain(event);
375                     mouseEvent.setSource(InputDevice.SOURCE_MOUSE);
376                     CarUiRecyclerViewImpl.super.onGenericMotionEvent(mouseEvent);
377                     return true;
378                 }
379             }
380             return false;
381         } : null);
382 
383         // If rotary scrolling is enabled, mark this view as focusable. This view will be focused
384         // when no focusable elements are visible.
385         setFocusable(rotaryScrollEnabled);
386 
387         // Focus this view before descendants so that the RotaryService can focus this view when it
388         // wants to.
389         setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
390 
391         // Disable the default focus highlight. No highlight should appear when this view is
392         // focused.
393         setDefaultFocusHighlightEnabled(false);
394 
395         // If rotary scrolling is enabled, set a focus change listener to highlight the scrollbar
396         // thumb when this recycler view is focused, i.e. when no focusable descendant is visible.
397         setOnFocusChangeListener(rotaryScrollEnabled ? (v, hasFocus) -> {
398             if (mScrollBar != null) mScrollBar.setHighlightThumb(hasFocus);
399         } : null);
400 
401         // This view is a rotary container if it's not a scrollable container.
402         if (!rotaryScrollEnabled) {
403             super.setContentDescription(ROTARY_CONTAINER);
404         }
405     }
406 
407     @Override
onRestoreInstanceState(Parcelable state)408     protected void onRestoreInstanceState(Parcelable state) {
409         super.onRestoreInstanceState(state);
410 
411         // If we're restoring an existing RecyclerView, consider
412         // it as having already scrolled some.
413         mHasScrolled = true;
414     }
415 
416     @Override
requestLayout()417     public void requestLayout() {
418         super.requestLayout();
419         if (mScrollBar != null) {
420             mScrollBar.requestLayout();
421         }
422     }
423 
424     /**
425      * Sets the number of columns in which grid needs to be divided.
426      */
setNumOfColumns(int numberOfColumns)427     private void setNumOfColumns(int numberOfColumns) {
428         mNumOfColumns = numberOfColumns;
429         if (mTopOffsetItemDecorationGrid != null) {
430             mTopOffsetItemDecorationGrid.setNumOfColumns(mNumOfColumns);
431         }
432         if (mDividerItemDecorationGrid != null) {
433             mDividerItemDecorationGrid.setNumOfColumns(mNumOfColumns);
434         }
435     }
436 
437     /**
438      * Changes the visibility of the entire container. If the container is not present i.e scrollbar
439      * is not visible then the visibility or Recyclerview is changed.
440      */
441     @Override
setVisibility(int visibility)442     public void setVisibility(int visibility) {
443         super.setVisibility(visibility);
444         mContainerVisibility = visibility;
445         if (mContainer != null) {
446             mContainer.setVisibility(visibility);
447         }
448     }
449 
450     @Override
onAttachedToWindow()451     protected void onAttachedToWindow() {
452         super.onAttachedToWindow();
453         mCarUxRestrictionsUtil.register(mListener);
454         if (mInstallingExtScrollBar || !mScrollBarEnabled) {
455             return;
456         }
457         // When CarUiRV is detached from the current parent and attached to the container with
458         // the scrollBar, onAttachedToWindow() will get called immediately when attaching the
459         // CarUiRV to the container. This flag will help us keep track of this state and avoid
460         // recursion. We also want to reset the state of this flag as soon as the container is
461         // successfully attached to the CarUiRV's original parent.
462         mInstallingExtScrollBar = true;
463         installExternalScrollBar();
464         mInstallingExtScrollBar = false;
465     }
466 
467     /**
468      * This method will detach the current recycler view from its parent and attach it to the
469      * container which is a LinearLayout. Later the entire container is attached to the parent where
470      * the recycler view was set with the same layout params.
471      */
installExternalScrollBar()472     private void installExternalScrollBar() {
473         if (mContainer.getParent() != null) {
474             // We've already installed the parent container.
475             // onAttachToWindow() can be called multiple times, but on the second time
476             // we will crash if we try to add mContainer as a child of a view again while
477             // it already has a parent.
478             return;
479         }
480 
481         mContainer.removeAllViews();
482         LayoutInflater inflater = LayoutInflater.from(getContext());
483 
484         switch (mSize) {
485             case SIZE_SMALL:
486                 // Small layout is rendered without scrollbar
487                 return;
488             case SIZE_MEDIUM:
489                 inflater.inflate(R.layout.car_ui_recycler_view_medium, mContainer, true);
490                 break;
491             case SIZE_LARGE:
492             default:
493                 inflater.inflate(R.layout.car_ui_recycler_view, mContainer, true);
494         }
495 
496         mContainer.setVisibility(mContainerVisibility);
497 
498         if (mContainerPadding != null) {
499             mContainer.setPadding(mContainerPadding.left, mContainerPadding.top,
500                     mContainerPadding.right, mContainerPadding.bottom);
501         } else if (mContainerPaddingRelative != null) {
502             mContainer.setPaddingRelative(mContainerPaddingRelative.left,
503                     mContainerPaddingRelative.top, mContainerPaddingRelative.right,
504                     mContainerPaddingRelative.bottom);
505         } else {
506             mContainer.setPadding(getPaddingLeft(), /* top= */ 0,
507                     getPaddingRight(), /* bottom= */ 0);
508             setPadding(/* left= */ 0, getPaddingTop(),
509                     /* right= */ 0, getPaddingBottom());
510         }
511 
512         mContainer.setLayoutParams(getLayoutParams());
513         ViewGroup parent = (ViewGroup) getParent();
514         int index = parent.indexOfChild(this);
515         parent.removeViewInLayout(this);
516 
517         FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
518                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
519         ((CarUiRecyclerViewContainer) requireViewByRefId(mContainer, R.id.car_ui_recycler_view))
520                 .addRecyclerView(this, params);
521         parent.addView(mContainer, index);
522 
523         createScrollBarFromConfig(requireViewByRefId(mContainer, R.id.car_ui_scroll_bar));
524     }
525 
createScrollBarFromConfig(@onNull View scrollView)526     private void createScrollBarFromConfig(@NonNull View scrollView) {
527         Class<?> cls;
528         try {
529             cls = !TextUtils.isEmpty(mScrollBarClass)
530                     ? getContext().getClassLoader().loadClass(mScrollBarClass)
531                     : DefaultScrollBar.class;
532         } catch (ReflectiveOperationException e) {
533             throw new IllegalArgumentException("Error loading scroll bar component: "
534                     + mScrollBarClass, e);
535         }
536         try {
537             Constructor<?> cnst = cls.getDeclaredConstructor();
538             cnst.setAccessible(true);
539             mScrollBar = (ScrollBar) cnst.newInstance();
540         } catch (ReflectiveOperationException e) {
541             throw new IllegalArgumentException("Error creating scroll bar component: "
542                     + mScrollBarClass, e);
543         }
544 
545         mScrollBar.initialize(this, scrollView);
546 
547         setScrollBarPadding(mScrollBarPaddingTop, mScrollBarPaddingBottom);
548     }
549 
550     @Override
setAlpha(float value)551     public void setAlpha(float value) {
552         if (mScrollBarEnabled) {
553             mContainer.setAlpha(value);
554         } else {
555             super.setAlpha(value);
556         }
557     }
558 
559     @Override
animate()560     public ViewPropertyAnimator animate() {
561         return mScrollBarEnabled ? mContainer.animate() : super.animate();
562     }
563 
564     @Override
onDetachedFromWindow()565     protected void onDetachedFromWindow() {
566         super.onDetachedFromWindow();
567         mCarUxRestrictionsUtil.unregister(mListener);
568     }
569 
570     @Override
getPaddingLeft()571     public int getPaddingLeft() {
572         if (mContainerPadding != null) {
573             return mContainerPadding.left;
574         }
575 
576         return super.getPaddingLeft();
577     }
578 
579     @Override
getPaddingRight()580     public int getPaddingRight() {
581         if (mContainerPadding != null) {
582             return mContainerPadding.right;
583         }
584 
585         return super.getPaddingRight();
586     }
587 
588     @Override
setPadding(int left, int top, int right, int bottom)589     public void setPadding(int left, int top, int right, int bottom) {
590         mContainerPaddingRelative = null;
591         if (mScrollBarEnabled) {
592             boolean isAtStart = (mScrollBar != null && mScrollBar.isAtStart());
593             super.setPadding(0, top, 0, bottom);
594             if (!mHasScrolled || isAtStart) {
595                 // If we haven't scrolled, and thus are still at the top of the screen,
596                 // we should stay scrolled to the top after applying padding. Without this
597                 // scroll, the padding will start scrolled offscreen. We need the padding
598                 // to be onscreen to shift the content into a good visible range.
599                 scrollToPosition(0);
600             }
601             mContainerPadding = new Rect(left, 0, right, 0);
602             if (mContainer != null) {
603                 mContainer.setPadding(left, 0, right, 0);
604             }
605             setScrollBarPadding(mScrollBarPaddingTop, mScrollBarPaddingBottom);
606         } else {
607             super.setPadding(left, top, right, bottom);
608         }
609     }
610 
611     @Override
setPaddingRelative(int start, int top, int end, int bottom)612     public void setPaddingRelative(int start, int top, int end, int bottom) {
613         mContainerPadding = null;
614         if (mScrollBarEnabled) {
615             super.setPaddingRelative(0, top, 0, bottom);
616             if (!mHasScrolled) {
617                 // If we haven't scrolled, and thus are still at the top of the screen,
618                 // we should stay scrolled to the top after applying padding. Without this
619                 // scroll, the padding will start scrolled offscreen. We need the padding
620                 // to be onscreen to shift the content into a good visible range.
621                 scrollToPosition(0);
622             }
623             mContainerPaddingRelative = new Rect(start, 0, end, 0);
624             if (mContainer != null) {
625                 mContainer.setPaddingRelative(start, 0, end, 0);
626             }
627             setScrollBarPadding(mScrollBarPaddingTop, mScrollBarPaddingBottom);
628         } else {
629             super.setPaddingRelative(start, top, end, bottom);
630         }
631     }
632 
633     /**
634      * Sets the scrollbar's padding top and bottom. This padding is applied in addition to the
635      * padding of the RecyclerView.
636      */
setScrollBarPadding(int paddingTop, int paddingBottom)637     private void setScrollBarPadding(int paddingTop, int paddingBottom) {
638         if (mScrollBarEnabled) {
639             mScrollBarPaddingTop = paddingTop;
640             mScrollBarPaddingBottom = paddingBottom;
641 
642             if (mScrollBar != null) {
643                 mScrollBar.setPadding(paddingTop + getPaddingTop(),
644                         paddingBottom + getPaddingBottom());
645             }
646         }
647     }
648 
649     @Override
setContentDescription(CharSequence contentDescription)650     public void setContentDescription(CharSequence contentDescription) {
651         super.setContentDescription(contentDescription);
652         initRotaryScroll(/* styledAttributes= */ null);
653     }
654 
655     @Override
setAdapter(@ullable Adapter adapter)656     public void setAdapter(@Nullable Adapter adapter) {
657         if (mScrollBar != null) {
658             // Make sure this is called before super so that scrollbar can get a reference to
659             // the adapter using RecyclerView#getAdapter()
660             mScrollBar.adapterChanged(adapter);
661         }
662         super.setAdapter(adapter);
663     }
664 
665     private class UxRestrictionChangedListener implements
666             CarUxRestrictionsUtil.OnUxRestrictionsChangedListener {
667 
668         @Override
onRestrictionsChanged(@onNull CarUxRestrictions carUxRestrictions)669         public void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions) {
670             Adapter<?> adapter = getAdapter();
671             // If the adapter does not implement ItemCap, then the max items on it cannot be
672             // updated.
673             if (!(adapter instanceof CarUiRecyclerView.ItemCap)) {
674                 return;
675             }
676 
677             int maxItems = CarUiRecyclerView.ItemCap.UNLIMITED;
678             if ((carUxRestrictions.getActiveRestrictions()
679                     & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT)
680                     != 0) {
681                 maxItems = carUxRestrictions.getMaxCumulativeContentItems();
682             }
683 
684             int originalCount = adapter.getItemCount();
685             ((CarUiRecyclerView.ItemCap) adapter).setMaxItems(maxItems);
686             int newCount = adapter.getItemCount();
687 
688             if (newCount == originalCount) {
689                 return;
690             }
691 
692             if (newCount < originalCount) {
693                 adapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
694             } else {
695                 adapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
696             }
697         }
698     }
699 }
700