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