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