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 
17 package com.android.car.apps.common.widget;
18 
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.car.drivingstate.CarUxRestrictions;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.util.AttributeSet;
27 import android.util.Log;
28 import android.util.SparseArray;
29 import android.view.View;
30 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
31 
32 import androidx.annotation.IntDef;
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.VisibleForTesting;
36 import androidx.recyclerview.widget.LinearLayoutManager;
37 import androidx.recyclerview.widget.RecyclerView;
38 
39 import com.android.car.apps.common.CarUxRestrictionsUtil;
40 import com.android.car.apps.common.R;
41 import com.android.car.apps.common.util.ScrollBarUI;
42 
43 import java.lang.annotation.Retention;
44 
45 /**
46  * View that extends a {@link RecyclerView} and creates a nested {@code RecyclerView} with an option
47  * to render a custom scroll bar that has page up and down arrows. Interaction with this view is
48  * similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
49  */
50 public final class PagedRecyclerView extends RecyclerView {
51 
52     private static final boolean DEBUG = false;
53     private static final String TAG = "PagedRecyclerView";
54 
55     private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
56     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener;
57 
58     private boolean mScrollBarEnabled;
59     private int mScrollBarContainerWidth;
60     private @ScrollBarPosition int mScrollBarPosition;
61     private boolean mScrollBarAboveRecyclerView;
62     private String mScrollBarClass;
63     private int mScrollBarPaddingStart;
64     private int mScrollBarPaddingEnd;
65     private boolean mFullyInitialized;
66 
67     @Gutter
68     private int mGutter;
69     private int mGutterSize;
70     private RecyclerView mNestedRecyclerView;
71     private Adapter mAdapter;
72     private ScrollBarUI mScrollBarUI;
73 
74     /**
75      * The possible values for @{link #setGutter}. The default value is actually
76      * {@link PagedRecyclerView.Gutter#BOTH}.
77      */
78     @IntDef({
79             Gutter.NONE,
80             Gutter.START,
81             Gutter.END,
82             Gutter.BOTH,
83     })
84 
85     @Retention(SOURCE)
86     public @interface Gutter {
87         /**
88          * No gutter on either side of the list items. The items will span the full width of the
89          * RecyclerView
90          */
91         int NONE = 0;
92 
93         /**
94          * Include a gutter only on the start side (that is, the same side as the scroll bar).
95          */
96         int START = 1;
97 
98         /**
99          * Include a gutter only on the end side (that is, the opposite side of the scroll bar).
100          */
101         int END = 2;
102 
103         /**
104          * Include a gutter on both sides of the list items. This is the default behaviour.
105          */
106         int BOTH = 3;
107     }
108 
109     /**
110      * The possible values for setScrollbarPosition. The default value is actually
111      * {@link PagedRecyclerView.ScrollBarPosition#START}.
112      */
113     @IntDef({
114             ScrollBarPosition.START,
115             ScrollBarPosition.END,
116     })
117 
118     @Retention(SOURCE)
119     public @interface ScrollBarPosition {
120         /**
121          * Position the scrollbar to the left of the screen. This is default.
122          */
123         int START = 0;
124 
125         /**
126          * Position scrollbar to the right of the screen.
127          */
128         int END = 2;
129     }
130 
131     /**
132      * Interface for a {@link RecyclerView.Adapter} to cap the number of items.
133      *
134      * <p>NOTE: it is still up to the adapter to use maxItems in {@link
135      * RecyclerView.Adapter#getItemCount()}.
136      *
137      * <p>the recommended way would be with:
138      *
139      * <pre>{@code
140      * {@literal@}Override
141      * public int getItemCount() {
142      *   return Math.min(super.getItemCount(), mMaxItems);
143      * }
144      * }</pre>
145      */
146     public interface ItemCap {
147         /**
148          * A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit.
149          */
150         int UNLIMITED = -1;
151 
152         /**
153          * Sets the maximum number of items available in the adapter. A value less than '0' means
154          * the list should not be capped.
155          */
setMaxItems(int maxItems)156         void setMaxItems(int maxItems);
157     }
158 
159     /**
160      * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
161      * inner recycler view within its bounds, this layout manager should always have 0 padding.
162      */
163     private class PagedRecyclerViewLayoutManager extends LinearLayoutManager {
PagedRecyclerViewLayoutManager(Context context)164         PagedRecyclerViewLayoutManager(Context context) {
165             super(context);
166         }
167 
168         @Override
getPaddingTop()169         public int getPaddingTop() {
170             return 0;
171         }
172 
173         @Override
getPaddingBottom()174         public int getPaddingBottom() {
175             return 0;
176         }
177 
178         @Override
getPaddingStart()179         public int getPaddingStart() {
180             return 0;
181         }
182 
183         @Override
getPaddingEnd()184         public int getPaddingEnd() {
185             return 0;
186         }
187 
188         @Override
canScrollHorizontally()189         public boolean canScrollHorizontally() {
190             return false;
191         }
192 
193         @Override
canScrollVertically()194         public boolean canScrollVertically() {
195             return false;
196         }
197     }
198 
PagedRecyclerView(@onNull Context context)199     public PagedRecyclerView(@NonNull Context context) {
200         this(context, null, 0);
201     }
202 
PagedRecyclerView(@onNull Context context, @Nullable AttributeSet attrs)203     public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
204         this(context, attrs, 0);
205     }
206 
PagedRecyclerView(@onNull Context context, @Nullable AttributeSet attrs, int defStyle)207     public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
208         super(context, attrs, defStyle);
209 
210         mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
211         mListener = this::updateCarUxRestrictions;
212 
213         init(context, attrs, defStyle);
214     }
215 
init(Context context, AttributeSet attrs, int defStyleAttr)216     private void init(Context context, AttributeSet attrs, int defStyleAttr) {
217         TypedArray a = context.obtainStyledAttributes(
218                 attrs, R.styleable.PagedRecyclerView, defStyleAttr,
219                 R.style.PagedRecyclerView);
220 
221         mScrollBarEnabled = a.getBoolean(R.styleable.PagedRecyclerView_scrollBarEnabled,
222                 /* defValue= */true);
223         mFullyInitialized = false;
224 
225         if (!mScrollBarEnabled) {
226             a.recycle();
227             mFullyInitialized = true;
228             return;
229         }
230 
231         mNestedRecyclerView = new RecyclerView(context, attrs,
232                 R.style.PagedRecyclerView_NestedRecyclerView);
233 
234         super.setLayoutManager(new PagedRecyclerViewLayoutManager(context));
235         super.setAdapter(new PagedRecyclerViewAdapter());
236         super.setNestedScrollingEnabled(false);
237         super.setClipToPadding(false);
238 
239         // Gutter
240         mGutter = a.getInt(R.styleable.PagedRecyclerView_gutter, Gutter.BOTH);
241         mGutterSize = getResources().getDimensionPixelSize(R.dimen.car_scroll_bar_margin);
242 
243         int carMargin = getResources().getDimensionPixelSize(R.dimen.car_scroll_bar_margin);
244         mScrollBarContainerWidth = a.getDimensionPixelSize(
245                 R.styleable.PagedRecyclerView_scrollBarContainerWidth, carMargin);
246 
247         mScrollBarPosition = a.getInt(R.styleable.PagedRecyclerView_scrollBarPosition,
248                 ScrollBarPosition.START);
249 
250         mScrollBarAboveRecyclerView = a.getBoolean(
251                 R.styleable.PagedRecyclerView_scrollBarAboveRecyclerView, /* defValue= */true);
252 
253         mScrollBarClass = a.getString(R.styleable.PagedRecyclerView_scrollBarCustomClass);
254         a.recycle();
255 
256         // Apply inner RV layout changes after the layout has been calculated for this view.
257         this.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
258             @Override
259             public void onGlobalLayout() {
260                 // View holder layout is still pending.
261                 if (PagedRecyclerView.this.findViewHolderForAdapterPosition(0) == null) return;
262 
263                 PagedRecyclerView.this.getViewTreeObserver().removeOnGlobalLayoutListener(this);
264                 initNestedRecyclerView();
265                 setNestedViewLayout();
266 
267                 createScrollBarFromConfig();
268 
269                 mNestedRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
270                         new OnGlobalLayoutListener() {
271                             @Override
272                             public void onGlobalLayout() {
273                                 mNestedRecyclerView.getViewTreeObserver()
274                                         .removeOnGlobalLayoutListener(this);
275                                 mFullyInitialized = true;
276                             }
277                         });
278             }
279         });
280     }
281 
282     /**
283      * Returns {@code true} if the {@PagedRecyclerView} is fully drawn. Using a global layout
284      * listener may not necessarily signify that this view is fully drawn (i.e. when the
285      * scrollbar is enabled). This is because the inner views (scrollbar and inner recycler view)
286      * are drawn after the outer views are finished.
287      */
fullyInitialized()288     public boolean fullyInitialized() {
289         return mFullyInitialized;
290     }
291 
292     @Override
onAttachedToWindow()293     protected void onAttachedToWindow() {
294         super.onAttachedToWindow();
295         mCarUxRestrictionsUtil.register(mListener);
296     }
297 
298     @Override
onDetachedFromWindow()299     protected void onDetachedFromWindow() {
300         super.onDetachedFromWindow();
301         mCarUxRestrictionsUtil.unregister(mListener);
302     }
303 
updateCarUxRestrictions(CarUxRestrictions carUxRestrictions)304     private void updateCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
305         // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
306         if (!(mAdapter instanceof ItemCap)) {
307             return;
308         }
309 
310         int maxItems = ItemCap.UNLIMITED;
311         if ((carUxRestrictions.getActiveRestrictions()
312                 & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT) != 0) {
313             maxItems = carUxRestrictions.getMaxCumulativeContentItems();
314         }
315 
316         int originalCount = mAdapter.getItemCount();
317         ((ItemCap) mAdapter).setMaxItems(maxItems);
318         int newCount = mAdapter.getItemCount();
319 
320         if (newCount == originalCount) {
321             return;
322         }
323 
324         if (newCount < originalCount) {
325             mAdapter.notifyItemRangeRemoved(
326                     newCount, originalCount - newCount);
327         } else {
328             mAdapter.notifyItemRangeInserted(
329                     originalCount, newCount - originalCount);
330         }
331     }
332 
333     @Override
setClipToPadding(boolean clipToPadding)334     public void setClipToPadding(boolean clipToPadding) {
335         if (mScrollBarEnabled) {
336             mNestedRecyclerView.setClipToPadding(clipToPadding);
337         } else {
338             super.setClipToPadding(clipToPadding);
339         }
340     }
341 
342     @Override
setAdapter(@ullable Adapter adapter)343     public void setAdapter(@Nullable Adapter adapter) {
344         mAdapter = adapter;
345         if (mScrollBarEnabled) {
346             mNestedRecyclerView.setAdapter(adapter);
347         } else {
348             super.setAdapter(adapter);
349         }
350     }
351 
352     @Nullable
353     @Override
getAdapter()354     public Adapter getAdapter() {
355         if (mScrollBarEnabled) {
356             return mNestedRecyclerView.getAdapter();
357         }
358         return super.getAdapter();
359     }
360 
361     @Override
setLayoutManager(@ullable LayoutManager layout)362     public void setLayoutManager(@Nullable LayoutManager layout) {
363         if (mScrollBarEnabled) {
364             mNestedRecyclerView.setLayoutManager(layout);
365         } else {
366             super.setLayoutManager(layout);
367         }
368     }
369 
370     /**
371      * Returns the {@link LayoutManager} for the {@link RecyclerView} displaying the content.
372      *
373      * <p>In cases where the scroll bar is visible and the nested {@link RecyclerView} is
374      * displaying content, {@link #getLayoutManager()} cannot be used because it returns the
375      * {@link LayoutManager} of the outer {@link RecyclerView}. {@link #getLayoutManager()} could
376      * not be overridden to return the effective manager due to interference with accessibility
377      * node tree traversal.
378      */
379     @Nullable
getEffectiveLayoutManager()380     public LayoutManager getEffectiveLayoutManager() {
381         if (mScrollBarEnabled) {
382             return mNestedRecyclerView.getLayoutManager();
383         }
384         return super.getLayoutManager();
385     }
386 
387     @Override
setOnScrollChangeListener(OnScrollChangeListener l)388     public void setOnScrollChangeListener(OnScrollChangeListener l) {
389         if (mScrollBarEnabled) {
390             mNestedRecyclerView.setOnScrollChangeListener(l);
391         } else {
392             super.setOnScrollChangeListener(l);
393         }
394     }
395 
396     @Override
setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled)397     public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
398         if (mScrollBarEnabled) {
399             mNestedRecyclerView.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
400         } else {
401             super.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
402         }
403     }
404 
405     @Override
setFadingEdgeLength(int length)406     public void setFadingEdgeLength(int length) {
407         if (mScrollBarEnabled) {
408             mNestedRecyclerView.setFadingEdgeLength(length);
409         } else {
410             super.setFadingEdgeLength(length);
411         }
412     }
413 
414     @Override
addItemDecoration(@onNull ItemDecoration decor, int index)415     public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
416         if (mScrollBarEnabled) {
417             mNestedRecyclerView.addItemDecoration(decor, index);
418         } else {
419             super.addItemDecoration(decor, index);
420         }
421     }
422 
423     @Override
addItemDecoration(@onNull ItemDecoration decor)424     public void addItemDecoration(@NonNull ItemDecoration decor) {
425         if (mScrollBarEnabled) {
426             mNestedRecyclerView.addItemDecoration(decor);
427         } else {
428             super.addItemDecoration(decor);
429         }
430     }
431 
432     @Override
setItemAnimator(@ullable ItemAnimator animator)433     public void setItemAnimator(@Nullable ItemAnimator animator) {
434         if (mScrollBarEnabled) {
435             mNestedRecyclerView.setItemAnimator(animator);
436         } else {
437             super.setItemAnimator(animator);
438         }
439     }
440 
441     @Override
setPadding(int left, int top, int right, int bottom)442     public void setPadding(int left, int top, int right, int bottom) {
443         if (mScrollBarEnabled) {
444             mNestedRecyclerView.setPadding(left, top, right, bottom);
445             if (mScrollBarUI != null) mScrollBarUI.requestLayout();
446         } else {
447             super.setPadding(left, top, right, bottom);
448         }
449     }
450 
451     @Override
setPaddingRelative(int start, int top, int end, int bottom)452     public void setPaddingRelative(int start, int top, int end, int bottom) {
453         if (mScrollBarEnabled) {
454             mNestedRecyclerView.setPaddingRelative(start, top, end, bottom);
455             if (mScrollBarUI != null) mScrollBarUI.requestLayout();
456         } else {
457             super.setPaddingRelative(start, top, end, bottom);
458         }
459     }
460 
461     @Override
findViewHolderForLayoutPosition(int position)462     public ViewHolder findViewHolderForLayoutPosition(int position) {
463         if (mScrollBarEnabled) {
464             return mNestedRecyclerView.findViewHolderForLayoutPosition(position);
465         } else {
466             return super.findViewHolderForLayoutPosition(position);
467         }
468     }
469 
470     @Override
findContainingViewHolder(View view)471     public ViewHolder findContainingViewHolder(View view) {
472         if (mScrollBarEnabled) {
473             return mNestedRecyclerView.findContainingViewHolder(view);
474         } else {
475             return super.findContainingViewHolder(view);
476         }
477     }
478 
479     @Override
480     @Nullable
findChildViewUnder(float x, float y)481     public View findChildViewUnder(float x, float y) {
482         if (mScrollBarEnabled) {
483             return mNestedRecyclerView.findChildViewUnder(x, y);
484         } else {
485             return super.findChildViewUnder(x, y);
486         }
487     }
488 
489     @Override
addOnScrollListener(@onNull OnScrollListener listener)490     public void addOnScrollListener(@NonNull OnScrollListener listener) {
491         if (mScrollBarEnabled) {
492             mNestedRecyclerView.addOnScrollListener(listener);
493         } else {
494             super.addOnScrollListener(listener);
495         }
496     }
497 
498     @Override
removeOnScrollListener(@onNull OnScrollListener listener)499     public void removeOnScrollListener(@NonNull OnScrollListener listener) {
500         if (mScrollBarEnabled) {
501             mNestedRecyclerView.removeOnScrollListener(listener);
502         } else {
503             super.removeOnScrollListener(listener);
504         }
505     }
506 
507     /**
508      * Calls {@link #layout(int, int, int, int)} for both this RecyclerView and the nested one.
509      */
510     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
layoutBothForTesting(int l, int t, int r, int b)511     public void layoutBothForTesting(int l, int t, int r, int b) {
512         super.layout(l, t, r, b);
513         if (mScrollBarEnabled) {
514             mNestedRecyclerView.layout(l, t, r, b);
515         }
516     }
517 
518     @Override
getPaddingStart()519     public int getPaddingStart() {
520         return mScrollBarEnabled ? mNestedRecyclerView.getPaddingStart() : super.getPaddingStart();
521     }
522 
523     @Override
getPaddingEnd()524     public int getPaddingEnd() {
525         return mScrollBarEnabled ? mNestedRecyclerView.getPaddingEnd() : super.getPaddingEnd();
526     }
527 
528     @Override
getPaddingTop()529     public int getPaddingTop() {
530         return mScrollBarEnabled ? mNestedRecyclerView.getPaddingTop() : super.getPaddingTop();
531     }
532 
533     @Override
getPaddingBottom()534     public int getPaddingBottom() {
535         return mScrollBarEnabled ? mNestedRecyclerView.getPaddingBottom()
536                 : super.getPaddingBottom();
537     }
538 
539     @Override
setVisibility(int visibility)540     public void setVisibility(int visibility) {
541         super.setVisibility(visibility);
542         if (mScrollBarEnabled) {
543             mNestedRecyclerView.setVisibility(visibility);
544         }
545     }
546 
initNestedRecyclerView()547     private void initNestedRecyclerView() {
548         PagedRecyclerViewAdapter.NestedRowViewHolder vh =
549                 (PagedRecyclerViewAdapter.NestedRowViewHolder)
550                         this.findViewHolderForAdapterPosition(0);
551         if (vh == null) {
552             throw new Error("Outer RecyclerView failed to initialize.");
553         }
554 
555         vh.mFrameLayout.addView(mNestedRecyclerView);
556     }
557 
createScrollBarFromConfig()558     private void createScrollBarFromConfig() {
559         if (DEBUG) Log.d(TAG, "createScrollBarFromConfig");
560         final String clsName = mScrollBarClass == null
561                 ? getContext().getString(R.string.config_scrollBarComponent) : mScrollBarClass;
562         if (clsName == null || clsName.length() == 0) {
563             throw andLog("No scroll bar component configured", null);
564         }
565 
566         Class<?> cls;
567         try {
568             cls = getContext().getClassLoader().loadClass(clsName);
569         } catch (Throwable t) {
570             throw andLog("Error loading scroll bar component: " + clsName, t);
571         }
572         try {
573             mScrollBarUI = (ScrollBarUI) cls.newInstance();
574         } catch (Throwable t) {
575             throw andLog("Error creating scroll bar component: " + clsName, t);
576         }
577 
578         mScrollBarUI.initialize(getContext(), mNestedRecyclerView, mScrollBarContainerWidth,
579                 mScrollBarPosition, mScrollBarAboveRecyclerView);
580 
581         mScrollBarUI.setPadding(mScrollBarPaddingStart, mScrollBarPaddingEnd);
582 
583         if (DEBUG) Log.d(TAG, "started " + mScrollBarUI.getClass().getSimpleName());
584     }
585 
586     /**
587      * Sets the scrollbar's padding start (top) and end (bottom).
588      * This padding is applied in addition to the padding of the inner RecyclerView.
589      */
setScrollBarPadding(int paddingStart, int paddingEnd)590     public void setScrollBarPadding(int paddingStart, int paddingEnd) {
591         if (mScrollBarEnabled) {
592             mScrollBarPaddingStart = paddingStart;
593             mScrollBarPaddingEnd = paddingEnd;
594 
595             if (mScrollBarUI != null) {
596                 mScrollBarUI.setPadding(paddingStart, paddingEnd);
597             }
598         }
599     }
600 
601     /**
602      * Set the nested view's layout to the specified value.
603      *
604      * <p>The gutter is the space to the start/end of the list view items and will be equal in size
605      * to the scroll bars. By default, there is a gutter to both the left and right of the list
606      * view items, to account for the scroll bar.
607      */
setNestedViewLayout()608     private void setNestedViewLayout() {
609         int startMargin = 0;
610         int endMargin = 0;
611         if ((mGutter & Gutter.START) != 0) {
612             startMargin = mGutterSize;
613         }
614         if ((mGutter & Gutter.END) != 0) {
615             endMargin = mGutterSize;
616         }
617 
618         MarginLayoutParams layoutParams =
619                 (MarginLayoutParams) mNestedRecyclerView.getLayoutParams();
620 
621         layoutParams.setMarginStart(startMargin);
622         layoutParams.setMarginEnd(endMargin);
623 
624         layoutParams.height = LayoutParams.MATCH_PARENT;
625         layoutParams.width = super.getLayoutManager().getWidth() - startMargin - endMargin;
626         // requestLayout() isn't sufficient because we also need to resolveLayoutParams().
627         mNestedRecyclerView.setLayoutParams(layoutParams);
628 
629         // If there's a gutter, set ClipToPadding to false so that CardView's shadow will still
630         // appear outside of the padding.
631         mNestedRecyclerView.setClipToPadding(startMargin == 0 && endMargin == 0);
632     }
633 
andLog(String msg, Throwable t)634     private RuntimeException andLog(String msg, Throwable t) {
635         Log.e(TAG, msg, t);
636         throw new RuntimeException(msg, t);
637     }
638 
639     @Override
onSaveInstanceState()640     public Parcelable onSaveInstanceState() {
641         Parcelable superState = super.onSaveInstanceState();
642         SavedState ss = new SavedState(superState, getContext());
643         if (mScrollBarEnabled) {
644             mNestedRecyclerView.saveHierarchyState(ss.mNestedRecyclerViewState);
645         }
646         return ss;
647     }
648 
649     @Override
onRestoreInstanceState(Parcelable state)650     public void onRestoreInstanceState(Parcelable state) {
651         if (!(state instanceof SavedState)) {
652             Log.w(TAG, "onRestoreInstanceState called with an unsupported state");
653             super.onRestoreInstanceState(state);
654         } else {
655             SavedState ss = (SavedState) state;
656             super.onRestoreInstanceState(ss.getSuperState());
657             if (mScrollBarEnabled) {
658                 mNestedRecyclerView.restoreHierarchyState(ss.mNestedRecyclerViewState);
659             }
660         }
661     }
662 
663     static class SavedState extends BaseSavedState {
664         SparseArray mNestedRecyclerViewState;
665         Context mContext;
666 
SavedState(Parcelable superState, Context c)667         SavedState(Parcelable superState, Context c) {
668             super(superState);
669             mContext = c;
670             mNestedRecyclerViewState = new SparseArray();
671         }
672 
SavedState(Parcel source, ClassLoader loader)673         private SavedState(Parcel source, ClassLoader loader) {
674             super(source, loader);
675             mNestedRecyclerViewState = source.readSparseArray(loader);
676         }
677 
678         @Override
writeToParcel(Parcel out, int flags)679         public void writeToParcel(Parcel out, int flags) {
680             super.writeToParcel(out, flags);
681             out.writeSparseArray(mNestedRecyclerViewState);
682         }
683 
684         public static final Parcelable.Creator<SavedState> CREATOR =
685                 new Parcelable.Creator<SavedState>() {
686             public SavedState createFromParcel(Parcel in) {
687                 return new SavedState(in, getClass().getClassLoader());
688             }
689 
690             public SavedState[] newArray(int size) {
691                 return new SavedState[size];
692             }
693         };
694     }
695 }
696