blob: c1c609d95a41366da90cd39fa37f50ffc871a89b [file] [log] [blame]
Priyank Singh7e87d5c2019-08-29 10:38:52 -07001/*
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 */
Brad Stenning298e58b2019-09-12 15:29:54 -070016package com.android.car.ui.pagedrecyclerview;
Priyank Singh7e87d5c2019-08-29 10:38:52 -070017
18import static java.lang.annotation.RetentionPolicy.SOURCE;
19
20import android.car.drivingstate.CarUxRestrictions;
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.os.Parcel;
24import android.os.Parcelable;
Roberto Perezfcbe6f52019-09-11 08:13:12 -070025import android.text.TextUtils;
Priyank Singh7e87d5c2019-08-29 10:38:52 -070026import android.util.AttributeSet;
27import android.util.Log;
28import android.util.SparseArray;
Priyank Singhfb27a2a2019-09-18 15:18:07 -070029import android.view.ContextThemeWrapper;
Priyank Singh7e87d5c2019-08-29 10:38:52 -070030import android.view.View;
31import android.view.ViewGroup;
32import android.view.ViewTreeObserver.OnGlobalLayoutListener;
33
34import androidx.annotation.IntDef;
35import androidx.annotation.NonNull;
36import androidx.annotation.Nullable;
Ram Parameswaran696388b2019-10-01 19:04:58 -070037import androidx.annotation.VisibleForTesting;
Priyank Singh7e87d5c2019-08-29 10:38:52 -070038import androidx.recyclerview.widget.GridLayoutManager;
39import androidx.recyclerview.widget.LinearLayoutManager;
40import androidx.recyclerview.widget.RecyclerView;
41
Brad Stenning298e58b2019-09-12 15:29:54 -070042import com.android.car.ui.R;
43import com.android.car.ui.pagedrecyclerview.decorations.grid.GridDividerItemDecoration;
44import com.android.car.ui.pagedrecyclerview.decorations.grid.GridOffsetItemDecoration;
45import com.android.car.ui.pagedrecyclerview.decorations.linear.LinearDividerItemDecoration;
46import com.android.car.ui.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration;
47import com.android.car.ui.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration.OffsetPosition;
Priyank Singhd231eaa2019-09-18 15:18:07 -070048import com.android.car.ui.toolbar.Toolbar;
Cole Fauste73e1942019-09-24 15:25:40 -070049import com.android.car.ui.utils.CarUxRestrictionsUtil;
Priyank Singh7e87d5c2019-08-29 10:38:52 -070050
51import java.lang.annotation.Retention;
52
53/**
54 * View that extends a {@link RecyclerView} and creates a nested {@code RecyclerView} which could
55 * potentially include a scrollbar that has page up and down arrows. Interaction with this view is
56 * similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
57 */
Priyank Singhd231eaa2019-09-18 15:18:07 -070058public final class PagedRecyclerView extends RecyclerView implements
Cole Faust6cc28e42019-09-19 13:45:41 -070059 Toolbar.OnHeightChangedListener {
Priyank Singh7e87d5c2019-08-29 10:38:52 -070060
61 private static final boolean DEBUG = false;
62 private static final String TAG = "PagedRecyclerView";
63
64 private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
65 private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener;
66
67 private boolean mScrollBarEnabled;
68 private int mScrollBarContainerWidth;
69 @ScrollBarPosition
70 private int mScrollBarPosition;
71 private boolean mScrollBarAboveRecyclerView;
72 private String mScrollBarClass;
73 private boolean mFullyInitialized;
74 private float mScrollBarPaddingStart;
75 private float mScrollBarPaddingEnd;
76 private Context mContext;
77
78 @Gutter
79 private int mGutter;
80 private int mGutterSize;
Priyank Singh57e7a292019-09-30 13:48:38 -070081 @VisibleForTesting
82 RecyclerView mNestedRecyclerView;
Priyank Singh7e87d5c2019-08-29 10:38:52 -070083 private Adapter<?> mAdapter;
84 private ScrollBar mScrollBar;
Priyank Singhd231eaa2019-09-18 15:18:07 -070085 private int mInitialTopPadding;
Priyank Singh7e87d5c2019-08-29 10:38:52 -070086
87 private GridOffsetItemDecoration mOffsetItemDecoration;
88 private GridDividerItemDecoration mDividerItemDecoration;
89 @PagedRecyclerViewLayout
90 int mPagedRecyclerViewLayout;
91 private int mNumOfColumns;
92
93 /**
94 * The possible values for @{link #setGutter}. The default value is actually {@link
95 * PagedRecyclerView.Gutter#BOTH}.
96 */
97 @IntDef({
98 Gutter.NONE,
99 Gutter.START,
100 Gutter.END,
101 Gutter.BOTH,
102 })
103 @Retention(SOURCE)
104 public @interface Gutter {
105 /**
106 * No gutter on either side of the list items. The items will span the full width of the
107 * RecyclerView
108 */
109 int NONE = 0;
110
111 /** Include a gutter only on the start side (that is, the same side as the scroll bar). */
112 int START = 1;
113
114 /** Include a gutter only on the end side (that is, the opposite side of the scroll bar). */
115 int END = 2;
116
117 /** Include a gutter on both sides of the list items. This is the default behaviour. */
118 int BOTH = 3;
119 }
120
121 /**
122 * The possible values for setScrollbarPosition. The default value is actually {@link
123 * PagedRecyclerView.ScrollBarPosition#START}.
124 */
125 @IntDef({
126 ScrollBarPosition.START,
127 ScrollBarPosition.END,
128 })
129 @Retention(SOURCE)
130 public @interface ScrollBarPosition {
131 /** Position the scrollbar to the left of the screen. This is default. */
132 int START = 0;
133
134 /** Position scrollbar to the right of the screen. */
135 int END = 2;
136 }
137
138 /**
139 * The possible values for setScrollbarPosition. The default value is actually {@link
140 * PagedRecyclerViewLayout#LINEAR}.
141 */
142 @IntDef({
143 PagedRecyclerViewLayout.LINEAR,
144 PagedRecyclerViewLayout.GRID,
145 })
146 @Retention(SOURCE)
147 public @interface PagedRecyclerViewLayout {
148 /** Position the scrollbar to the left of the screen. This is default. */
149 int LINEAR = 0;
150
151 /** Position scrollbar to the right of the screen. */
152 int GRID = 2;
153 }
154
155 /**
156 * Interface for a {@link RecyclerView.Adapter} to cap the number of items.
157 *
158 * <p>NOTE: it is still up to the adapter to use maxItems in {@link
159 * RecyclerView.Adapter#getItemCount()}.
160 *
161 * <p>the recommended way would be with:
162 *
163 * <pre>{@code
164 * {@literal@}Override
165 * public int getItemCount() {
166 * return Math.min(super.getItemCount(), mMaxItems);
167 * }
168 * }</pre>
169 */
170 public interface ItemCap {
171 /** A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit. */
172 int UNLIMITED = -1;
173
174 /**
175 * Sets the maximum number of items available in the adapter. A value less than '0' means
176 * the
177 * list should not be capped.
178 */
179 void setMaxItems(int maxItems);
180 }
181
182 /**
183 * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
184 * inner
185 * recycler view within its bounds, this layout manager should always have 0 padding.
186 */
187 private static class PagedRecyclerViewLayoutManager extends LinearLayoutManager {
188 PagedRecyclerViewLayoutManager(Context context) {
189 super(context);
190 }
191
192 @Override
193 public int getPaddingTop() {
194 return 0;
195 }
196
197 @Override
198 public int getPaddingBottom() {
199 return 0;
200 }
201
202 @Override
203 public int getPaddingStart() {
204 return 0;
205 }
206
207 @Override
208 public int getPaddingEnd() {
209 return 0;
210 }
211
212 @Override
213 public boolean canScrollHorizontally() {
214 return false;
215 }
216
217 @Override
218 public boolean canScrollVertically() {
219 return false;
220 }
221 }
222
223 /**
224 * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
225 * inner
226 * recycler view within its bounds, this layout manager should always have 0 padding.
227 */
228 private static class GridPagedRecyclerViewLayoutManager extends GridLayoutManager {
229 GridPagedRecyclerViewLayoutManager(Context context, int numOfColumns) {
230 super(context, numOfColumns);
231 }
232
233 @Override
234 public int getPaddingTop() {
235 return 0;
236 }
237
238 @Override
239 public int getPaddingBottom() {
240 return 0;
241 }
242
243 @Override
244 public int getPaddingStart() {
245 return 0;
246 }
247
248 @Override
249 public int getPaddingEnd() {
250 return 0;
251 }
252 }
253
254 public PagedRecyclerView(@NonNull Context context) {
255 this(context, null, 0);
256 }
257
258 public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
259 this(context, attrs, 0);
260 }
261
262 public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
263 super(context, attrs, defStyle);
264
265 mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
266 mListener = this::updateCarUxRestrictions;
267
268 init(context, attrs, defStyle);
269 }
270
271 private void init(Context context, AttributeSet attrs, int defStyleAttr) {
272 TypedArray a =
273 context.obtainStyledAttributes(
274 attrs, R.styleable.PagedRecyclerView, defStyleAttr,
Brad Stenning667accc2019-09-13 09:55:18 -0700275 R.style.Widget_CarUi_PagedRecyclerView);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700276
Brad Stenningbe10fe32019-09-12 13:32:03 -0700277 mScrollBarEnabled = context.getResources().getBoolean(R.bool.car_ui_scrollbar_enable);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700278 mFullyInitialized = false;
279
280 if (!mScrollBarEnabled) {
281 a.recycle();
282 mFullyInitialized = true;
283 return;
284 }
285
286 mNestedRecyclerView =
Priyank Singhfb27a2a2019-09-18 15:18:07 -0700287 new RecyclerView(new ContextThemeWrapper(context,
288 R.style.Widget_CarUi_PagedRecyclerView_NestedRecyclerView), attrs,
Brad Stenning667accc2019-09-13 09:55:18 -0700289 R.style.Widget_CarUi_PagedRecyclerView_NestedRecyclerView);
Priyank Singhfb27a2a2019-09-18 15:18:07 -0700290 mNestedRecyclerView.setVerticalScrollBarEnabled(false);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700291 mScrollBarPaddingStart =
Brad Stenningbe10fe32019-09-12 13:32:03 -0700292 context.getResources().getDimension(R.dimen.car_ui_scrollbar_padding_start);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700293 mScrollBarPaddingEnd =
Brad Stenningbe10fe32019-09-12 13:32:03 -0700294 context.getResources().getDimension(R.dimen.car_ui_scrollbar_padding_end);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700295
296 mPagedRecyclerViewLayout =
297 a.getInt(R.styleable.PagedRecyclerView_layoutStyle, PagedRecyclerViewLayout.LINEAR);
298 mNumOfColumns = a.getInt(R.styleable.PagedRecyclerView_numOfColumns, /* defValue= */ 2);
299 boolean enableDivider =
Priyank Singh452d2002019-09-20 14:57:00 -0700300 a.getBoolean(R.styleable.PagedRecyclerView_enableDivider, /* defValue= */ false);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700301
302 if (mPagedRecyclerViewLayout == PagedRecyclerViewLayout.LINEAR) {
303
304 int linearTopOffset =
305 a.getInteger(R.styleable.PagedRecyclerView_startOffset, /* defValue= */ 0);
306 int linearBottomOffset =
307 a.getInteger(R.styleable.PagedRecyclerView_endOffset, /* defValue= */ 0);
308
309 if (enableDivider) {
310 RecyclerView.ItemDecoration dividerItemDecoration =
311 new LinearDividerItemDecoration(
Brad Stenningbe10fe32019-09-12 13:32:03 -0700312 context.getDrawable(R.drawable.car_ui_pagedrecyclerview_divider));
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700313 super.addItemDecoration(dividerItemDecoration);
314 }
315 RecyclerView.ItemDecoration topOffsetItemDecoration =
316 new LinearOffsetItemDecoration(linearTopOffset, OffsetPosition.START);
317 super.addItemDecoration(topOffsetItemDecoration);
318
319 RecyclerView.ItemDecoration bottomOffsetItemDecoration =
320 new LinearOffsetItemDecoration(linearBottomOffset, OffsetPosition.END);
321 super.addItemDecoration(bottomOffsetItemDecoration);
322 } else {
323
324 int gridTopOffset =
325 a.getInteger(R.styleable.PagedRecyclerView_startOffset, /* defValue= */ 0);
326 int gridBottomOffset =
327 a.getInteger(R.styleable.PagedRecyclerView_endOffset, /* defValue= */ 0);
328
329 if (enableDivider) {
330 mDividerItemDecoration =
331 new GridDividerItemDecoration(
Brad Stenningbe10fe32019-09-12 13:32:03 -0700332 context.getDrawable(R.drawable.car_ui_divider),
333 context.getDrawable(R.drawable.car_ui_divider),
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700334 mNumOfColumns);
335 super.addItemDecoration(mDividerItemDecoration);
336 }
337
338 mOffsetItemDecoration =
339 new GridOffsetItemDecoration(gridTopOffset, mNumOfColumns,
340 OffsetPosition.START);
341 super.addItemDecoration(mOffsetItemDecoration);
342
343 GridOffsetItemDecoration bottomOffsetItemDecoration =
344 new GridOffsetItemDecoration(gridBottomOffset, mNumOfColumns,
345 OffsetPosition.END);
346 super.addItemDecoration(bottomOffsetItemDecoration);
347 }
348
349 super.setLayoutManager(new PagedRecyclerViewLayoutManager(context));
350 super.setAdapter(new PagedRecyclerViewAdapter());
351 super.setNestedScrollingEnabled(false);
352 super.setClipToPadding(false);
353
354 // Gutter
Brad Stenningbe10fe32019-09-12 13:32:03 -0700355 mGutter = context.getResources().getInteger(R.integer.car_ui_scrollbar_gutter);
356 mGutterSize = getResources().getDimensionPixelSize(R.dimen.car_ui_scrollbar_margin);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700357
358 mScrollBarContainerWidth =
359 (int) context.getResources().getDimension(
Brad Stenningbe10fe32019-09-12 13:32:03 -0700360 R.dimen.car_ui_scrollbar_container_width);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700361
362 mScrollBarPosition = context.getResources().getInteger(
Brad Stenningbe10fe32019-09-12 13:32:03 -0700363 R.integer.car_ui_scrollbar_position);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700364
365 mScrollBarAboveRecyclerView =
Brad Stenningbe10fe32019-09-12 13:32:03 -0700366 context.getResources().getBoolean(R.bool.car_ui_scrollbar_above_recycler_view);
367 mScrollBarClass = context.getResources().getString(R.string.car_ui_scrollbar_component);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700368 a.recycle();
369 this.mContext = context;
370 // Apply inner RV layout changes after the layout has been calculated for this view.
371 this.getViewTreeObserver()
372 .addOnGlobalLayoutListener(
373 new OnGlobalLayoutListener() {
374 @Override
375 public void onGlobalLayout() {
376 // View holder layout is still pending.
377 if (PagedRecyclerView.this.findViewHolderForAdapterPosition(0)
378 == null) {
379 return;
380 }
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700381 PagedRecyclerView.this.getViewTreeObserver()
382 .removeOnGlobalLayoutListener(this);
383 initNestedRecyclerView();
384 setNestedViewLayout();
385
386 mNestedRecyclerView
387 .getViewTreeObserver()
388 .addOnGlobalLayoutListener(
389 new OnGlobalLayoutListener() {
390 @Override
391 public void onGlobalLayout() {
392 mNestedRecyclerView
393 .getViewTreeObserver()
394 .removeOnGlobalLayoutListener(this);
395 ViewGroup.LayoutParams params =
396 getLayoutParams();
397 params.height = getMeasuredHeight();
398 setLayoutParams(params);
399 createScrollBarFromConfig();
Priyank Singhd231eaa2019-09-18 15:18:07 -0700400 if (mInitialTopPadding == 0) {
401 mInitialTopPadding = getPaddingTop();
402 }
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700403 mFullyInitialized = true;
404 }
405 });
406 }
407 });
408 }
409
Priyank Singhd231eaa2019-09-18 15:18:07 -0700410 @Override
411 public void onHeightChanged(int height) {
412 setPaddingRelative(getPaddingStart(), mInitialTopPadding + height,
413 getPaddingEnd(), getPaddingBottom());
414 }
415
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700416 /**
417 * Returns {@code true} if the {@PagedRecyclerView} is fully drawn. Using a global layout
418 * mListener
419 * may not necessarily signify that this view is fully drawn (i.e. when the scrollbar is
420 * enabled).
421 * This is because the inner views (scrollbar and inner recycler view) are drawn after the
422 * outer
423 * views are finished.
424 */
425 public boolean fullyInitialized() {
426 return mFullyInitialized;
427 }
428
429 /** Sets the number of columns in which grid needs to be divided. */
430 public void setNumOfColumns(int numberOfColumns) {
431 mNumOfColumns = numberOfColumns;
Priyank Singh452d2002019-09-20 14:57:00 -0700432 if (mOffsetItemDecoration != null) {
433 mOffsetItemDecoration.setNumOfColumns(mNumOfColumns);
434 }
435 if (mDividerItemDecoration != null) {
436 mDividerItemDecoration.setNumOfColumns(mNumOfColumns);
437 }
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700438 }
439
440 /**
441 * Returns the {@link LayoutManager} for the {@link RecyclerView} displaying the content.
442 *
443 * <p>In cases where the scroll bar is visible and the nested {@link RecyclerView} is displaying
444 * content, {@link #getLayoutManager()} cannot be used because it returns the {@link
445 * LayoutManager} of the outer {@link RecyclerView}. {@link #getLayoutManager()} could not be
446 * overridden to return the effective manager due to interference with accessibility node tree
447 * traversal.
448 */
449 @Nullable
450 public LayoutManager getEffectiveLayoutManager() {
451 if (mScrollBarEnabled) {
452 return mNestedRecyclerView.getLayoutManager();
453 }
454 return super.getLayoutManager();
455 }
456
457 @Override
Priyank Singhfb27a2a2019-09-18 15:18:07 -0700458 public LayoutManager getLayoutManager() {
459 if (mScrollBarEnabled) {
460 return mNestedRecyclerView.getLayoutManager();
461 }
462 return super.getLayoutManager();
463 }
464
465 @Override
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700466 protected void onAttachedToWindow() {
467 super.onAttachedToWindow();
468 mCarUxRestrictionsUtil.register(mListener);
469 }
470
471 @Override
472 protected void onDetachedFromWindow() {
473 super.onDetachedFromWindow();
474 mCarUxRestrictionsUtil.unregister(mListener);
475 }
476
477 private void updateCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
478 // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
479 if (!(mAdapter instanceof ItemCap)) {
480 return;
481 }
482
483 int maxItems = ItemCap.UNLIMITED;
484 if ((carUxRestrictions.getActiveRestrictions()
485 & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT)
486 != 0) {
487 maxItems = carUxRestrictions.getMaxCumulativeContentItems();
488 }
489
490 int originalCount = mAdapter.getItemCount();
491 ((ItemCap) mAdapter).setMaxItems(maxItems);
492 int newCount = mAdapter.getItemCount();
493
494 if (newCount == originalCount) {
495 return;
496 }
497
498 if (newCount < originalCount) {
499 mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
500 } else {
501 mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
502 }
503 }
504
505 @Override
506 public void setClipToPadding(boolean clipToPadding) {
507 if (mScrollBarEnabled) {
508 mNestedRecyclerView.setClipToPadding(clipToPadding);
509 } else {
510 super.setClipToPadding(clipToPadding);
511 }
512 }
513
514 @SuppressWarnings("rawtypes")
515 @Override
516 public void setAdapter(@Nullable Adapter adapter) {
517
Priyank Singhfb27a2a2019-09-18 15:18:07 -0700518 if (mScrollBarEnabled && mPagedRecyclerViewLayout == PagedRecyclerViewLayout.LINEAR) {
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700519 mNestedRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
Priyank Singhfb27a2a2019-09-18 15:18:07 -0700520 } else if (mPagedRecyclerViewLayout == PagedRecyclerViewLayout.LINEAR) {
521 super.setLayoutManager(new LinearLayoutManager(mContext));
522 } else if (mScrollBarEnabled) {
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700523 mNestedRecyclerView.setLayoutManager(
524 new GridPagedRecyclerViewLayoutManager(mContext, mNumOfColumns));
525 setNumOfColumns(mNumOfColumns);
Priyank Singhfb27a2a2019-09-18 15:18:07 -0700526 } else {
527 super.setLayoutManager(
528 new GridPagedRecyclerViewLayoutManager(mContext, mNumOfColumns));
529 setNumOfColumns(mNumOfColumns);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700530 }
531
532 this.mAdapter = adapter;
533 if (mScrollBarEnabled) {
534 mNestedRecyclerView.setAdapter(adapter);
535 } else {
536 super.setAdapter(adapter);
537 }
538 }
539
540 @Nullable
541 @Override
542 public Adapter<?> getAdapter() {
543 if (mScrollBarEnabled) {
544 return mNestedRecyclerView.getAdapter();
545 }
546 return super.getAdapter();
547 }
548
549 @Override
550 public void setLayoutManager(@Nullable LayoutManager layout) {
551 if (mScrollBarEnabled) {
552 mNestedRecyclerView.setLayoutManager(layout);
553 } else {
554 super.setLayoutManager(layout);
555 }
556 }
557
558 @Override
559 public void setOnScrollChangeListener(OnScrollChangeListener l) {
560 if (mScrollBarEnabled) {
561 mNestedRecyclerView.setOnScrollChangeListener(l);
562 } else {
563 super.setOnScrollChangeListener(l);
564 }
565 }
566
567 @Override
568 public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
569 if (mScrollBarEnabled) {
570 mNestedRecyclerView.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
571 } else {
572 super.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
573 }
574 }
575
576 @Override
577 public void setFadingEdgeLength(int length) {
578 if (mScrollBarEnabled) {
579 mNestedRecyclerView.setFadingEdgeLength(length);
580 } else {
581 super.setFadingEdgeLength(length);
582 }
583 }
584
585 @Override
586 public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
587 if (mScrollBarEnabled) {
588 mNestedRecyclerView.addItemDecoration(decor, index);
589 } else {
590 super.addItemDecoration(decor, index);
591 }
592 }
593
594 @Override
595 public void addItemDecoration(@NonNull ItemDecoration decor) {
596 if (mScrollBarEnabled) {
597 mNestedRecyclerView.addItemDecoration(decor);
598 } else {
599 super.addItemDecoration(decor);
600 }
601 }
602
603 @Override
604 public void setItemAnimator(@Nullable ItemAnimator animator) {
605 if (mScrollBarEnabled) {
606 mNestedRecyclerView.setItemAnimator(animator);
607 } else {
608 super.setItemAnimator(animator);
609 }
610 }
611
612 @Override
613 public void setPadding(int left, int top, int right, int bottom) {
614 if (mScrollBarEnabled) {
615 mNestedRecyclerView.setPadding(left, top, right, bottom);
616 if (mScrollBar != null) {
617 mScrollBar.requestLayout();
618 }
619 } else {
620 super.setPadding(left, top, right, bottom);
621 }
622 }
623
624 @Override
625 public void setPaddingRelative(int start, int top, int end, int bottom) {
626 if (mScrollBarEnabled) {
627 mNestedRecyclerView.setPaddingRelative(start, top, end, bottom);
628 if (mScrollBar != null) {
629 mScrollBar.requestLayout();
630 }
631 } else {
632 super.setPaddingRelative(start, top, end, bottom);
633 }
634 }
635
636 @Override
637 public ViewHolder findViewHolderForLayoutPosition(int position) {
638 if (mScrollBarEnabled) {
639 return mNestedRecyclerView.findViewHolderForLayoutPosition(position);
640 } else {
641 return super.findViewHolderForLayoutPosition(position);
642 }
643 }
644
645 @Override
646 public ViewHolder findContainingViewHolder(View view) {
647 if (mScrollBarEnabled) {
648 return mNestedRecyclerView.findContainingViewHolder(view);
649 } else {
650 return super.findContainingViewHolder(view);
651 }
652 }
653
654 @Override
655 @Nullable
656 public View findChildViewUnder(float x, float y) {
657 if (mScrollBarEnabled) {
658 return mNestedRecyclerView.findChildViewUnder(x, y);
659 } else {
660 return super.findChildViewUnder(x, y);
661 }
662 }
663
664 @Override
665 public void addOnScrollListener(@NonNull OnScrollListener listener) {
666 if (mScrollBarEnabled) {
667 mNestedRecyclerView.addOnScrollListener(listener);
668 } else {
669 super.addOnScrollListener(listener);
670 }
671 }
672
673 @Override
674 public void removeOnScrollListener(@NonNull OnScrollListener listener) {
675 if (mScrollBarEnabled) {
676 mNestedRecyclerView.removeOnScrollListener(listener);
677 } else {
678 super.removeOnScrollListener(listener);
679 }
680 }
681
682 @Override
683 public int getPaddingStart() {
684 return mScrollBarEnabled ? mNestedRecyclerView.getPaddingStart() : super.getPaddingStart();
685 }
686
687 @Override
688 public int getPaddingEnd() {
689 return mScrollBarEnabled ? mNestedRecyclerView.getPaddingEnd() : super.getPaddingEnd();
690 }
691
692 @Override
693 public int getPaddingTop() {
694 return mScrollBarEnabled ? mNestedRecyclerView.getPaddingTop() : super.getPaddingTop();
695 }
696
697 @Override
698 public int getPaddingBottom() {
699 return mScrollBarEnabled ? mNestedRecyclerView.getPaddingBottom()
700 : super.getPaddingBottom();
701 }
702
703 @Override
704 public void setVisibility(int visibility) {
705 super.setVisibility(visibility);
706 if (mScrollBarEnabled) {
707 mNestedRecyclerView.setVisibility(visibility);
708 }
709 }
710
711 private void initNestedRecyclerView() {
712 PagedRecyclerViewAdapter.NestedRowViewHolder vh =
713 (PagedRecyclerViewAdapter.NestedRowViewHolder)
714 this.findViewHolderForAdapterPosition(0);
715 if (vh == null) {
716 throw new Error("Outer RecyclerView failed to initialize.");
717 }
718
719 vh.frameLayout.addView(mNestedRecyclerView);
720 }
721
722 private void createScrollBarFromConfig() {
723 if (DEBUG) {
724 Log.d(TAG, "createScrollBarFromConfig");
725 }
726
727 Class<?> cls;
728 try {
Roberto Perezfcbe6f52019-09-11 08:13:12 -0700729 cls = !TextUtils.isEmpty(mScrollBarClass)
Roberto Pereze8437c12019-09-03 17:30:20 -0700730 ? getContext().getClassLoader().loadClass(mScrollBarClass)
731 : DefaultScrollBar.class;
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700732 } catch (Throwable t) {
733 throw andLog("Error loading scroll bar component: " + mScrollBarClass, t);
734 }
735 try {
736 mScrollBar = (ScrollBar) cls.getDeclaredConstructor().newInstance();
737 } catch (Throwable t) {
738 throw andLog("Error creating scroll bar component: " + mScrollBarClass, t);
739 }
740
741 mScrollBar.initialize(
742 mNestedRecyclerView, mScrollBarContainerWidth, mScrollBarPosition,
743 mScrollBarAboveRecyclerView);
744
745 mScrollBar.setPadding((int) mScrollBarPaddingStart, (int) mScrollBarPaddingEnd);
746
747 if (DEBUG) {
748 Log.d(TAG, "started " + mScrollBar.getClass().getSimpleName());
749 }
750 }
751
752 /**
Priyank Singhd231eaa2019-09-18 15:18:07 -0700753 * Sets the scrollbar's padding start (top) and end (bottom).
754 * This padding is applied in addition to the padding of the inner RecyclerView.
755 */
756 public void setScrollBarPadding(int paddingStart, int paddingEnd) {
757 if (mScrollBarEnabled) {
758 mScrollBarPaddingStart = paddingStart;
759 mScrollBarPaddingEnd = paddingEnd;
760
761 if (mScrollBar != null) {
762 mScrollBar.setPadding(paddingStart, paddingEnd);
763 }
764 }
765 }
766
767 /**
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700768 * Set the nested view's layout to the specified value.
769 *
770 * <p>The mGutter is the space to the start/end of the list view items and will be equal in size
771 * to
772 * the scroll bars. By default, there is a mGutter to both the left and right of the list view
773 * items, to account for the scroll bar.
774 */
775 private void setNestedViewLayout() {
776 int startMargin = 0;
777 int endMargin = 0;
778 if ((mGutter & Gutter.START) != 0) {
779 startMargin = mGutterSize;
780 }
781 if ((mGutter & Gutter.END) != 0) {
782 endMargin = mGutterSize;
783 }
784
785 MarginLayoutParams layoutParams =
786 (MarginLayoutParams) mNestedRecyclerView.getLayoutParams();
787
788 layoutParams.setMarginStart(startMargin);
789 layoutParams.setMarginEnd(endMargin);
790
791 layoutParams.height = LayoutParams.MATCH_PARENT;
792 layoutParams.width = super.getLayoutManager().getWidth() - startMargin - endMargin;
793 // requestLayout() isn't sufficient because we also need to resolveLayoutParams().
794 mNestedRecyclerView.setLayoutParams(layoutParams);
795
796 // If there's a mGutter, set ClipToPadding to false so that CardView's shadow will still
797 // appear outside of the padding.
798 mNestedRecyclerView.setClipToPadding(startMargin == 0 && endMargin == 0);
799 }
800
801 private static RuntimeException andLog(String msg, Throwable t) {
802 Log.e(TAG, msg, t);
803 throw new RuntimeException(msg, t);
804 }
805
806 @Override
807 public Parcelable onSaveInstanceState() {
808 Parcelable superState = super.onSaveInstanceState();
809 SavedState ss = new SavedState(superState, getContext());
810 if (mScrollBarEnabled) {
811 mNestedRecyclerView.saveHierarchyState(ss.mNestedRecyclerViewState);
812 }
813 return ss;
814 }
815
816 @Override
817 public void onRestoreInstanceState(Parcelable state) {
818 if (!(state instanceof SavedState)) {
819 Log.w(TAG, "onRestoreInstanceState called with an unsupported state");
820 super.onRestoreInstanceState(state);
821 } else {
822 SavedState ss = (SavedState) state;
823 super.onRestoreInstanceState(ss.getSuperState());
824 if (mScrollBarEnabled) {
825 mNestedRecyclerView.restoreHierarchyState(ss.mNestedRecyclerViewState);
826 }
827 }
828 }
829
830 static class SavedState extends BaseSavedState {
831 SparseArray<Parcelable> mNestedRecyclerViewState;
832 Context mContext;
833
834 SavedState(Parcelable superState, Context c) {
835 super(superState);
836 mContext = c;
837 mNestedRecyclerViewState = new SparseArray<>();
838 }
839
Roberto Pereze8437c12019-09-03 17:30:20 -0700840 @SuppressWarnings("unchecked")
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700841 private SavedState(Parcel in) {
842 super(in);
843 mNestedRecyclerViewState = in.readSparseArray(mContext.getClassLoader());
844 }
845
Roberto Pereze8437c12019-09-03 17:30:20 -0700846 @SuppressWarnings("unchecked")
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700847 @Override
848 public void writeToParcel(Parcel out, int flags) {
849 super.writeToParcel(out, flags);
Roberto Pereze8437c12019-09-03 17:30:20 -0700850 out.writeSparseArray((SparseArray<Object>) (Object) mNestedRecyclerViewState);
Priyank Singh7e87d5c2019-08-29 10:38:52 -0700851 }
852
853 public static final Parcelable.Creator<SavedState> CREATOR =
854 new Parcelable.Creator<SavedState>() {
855 @Override
856 public SavedState createFromParcel(Parcel in) {
857 return new SavedState(in);
858 }
859
860 @Override
861 public SavedState[] newArray(int size) {
862 return new SavedState[size];
863 }
864 };
865 }
866}