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