Migrate Paged Recycler View to gerrit.

Test: Manual
Change-Id: I3c0803a5c1dfb6fdefeef44acd2d425c372560a7
diff --git a/car-chassis-lib/build.gradle b/car-chassis-lib/build.gradle
index 01b49e4..21b2b0d 100644
--- a/car-chassis-lib/build.gradle
+++ b/car-chassis-lib/build.gradle
@@ -68,4 +68,5 @@
 dependencies {
     implementation 'androidx.annotation:annotation:1.1.0'
     implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    implementation 'androidx.recyclerview:recyclerview:1.0.0'
 }
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_button_ripple_background.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_button_ripple_background.xml
new file mode 100644
index 0000000..b5f107c
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_button_ripple_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<ripple
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/chassis_card_ripple_background" />
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_divider.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_divider.xml
new file mode 100644
index 0000000..bddaae3
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_divider.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <size android:height="0dp" />
+    <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_down.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_down.xml
new file mode 100644
index 0000000..380bf46
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_down.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M14.83,16.42L24,25.59l9.17,-9.17L36,19.25l-12,12 -12,-12z"
+        android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_up.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_up.xml
new file mode 100644
index 0000000..2eff62f
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_up.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48dp"
+        android:height="48dp"
+        android:viewportWidth="48.0"
+        android:viewportHeight="48.0">
+    <path
+        android:pathData="M14.83,30.83L24,21.66l9.17,9.17L36,28 24,16 12,28z"
+        android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_scrollbar_thumb.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_scrollbar_thumb.xml
new file mode 100644
index 0000000..9180f1a
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_scrollbar_thumb.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/chassis_scrollbar_thumb" />
+    <corners android:radius="@dimen/chassis_scrollbar_thumb_radius"/>
+</shape>
diff --git a/car-chassis-lib/res/drawable/divider.xml b/car-chassis-lib/res/drawable/divider.xml
new file mode 100644
index 0000000..164b71a
--- /dev/null
+++ b/car-chassis-lib/res/drawable/divider.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <size android:height="2dp"
+          android:width="2dp"/>
+    <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/car-chassis-lib/res/layout/chassis_paged_recycler_view_item.xml b/car-chassis-lib/res/layout/chassis_paged_recycler_view_item.xml
new file mode 100644
index 0000000..6a35b43
--- /dev/null
+++ b/car-chassis-lib/res/layout/chassis_paged_recycler_view_item.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/nested_recycler_view_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center">
+</FrameLayout>
diff --git a/car-chassis-lib/res/layout/chassis_pagedrecyclerview_scrollbar.xml b/car-chassis-lib/res/layout/chassis_pagedrecyclerview_scrollbar.xml
new file mode 100644
index 0000000..7678940
--- /dev/null
+++ b/car-chassis-lib/res/layout/chassis_pagedrecyclerview_scrollbar.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center">
+
+    <ImageButton
+        android:id="@+id/page_up"
+        android:layout_width="@dimen/chassis_scrollbar_button_size"
+        android:layout_height="@dimen/chassis_scrollbar_button_size"
+        android:background="@drawable/chassis_pagedrecyclerview_button_ripple_background"
+        android:contentDescription="@string/chassis_scrollbar_page_up_button"
+        android:focusable="false"
+        android:hapticFeedbackEnabled="false"
+        android:src="@drawable/chassis_pagedrecyclerview_ic_up"
+        android:scaleType="centerInside" />
+
+    <!-- View height is dynamically calculated during layout. -->
+    <View
+        android:id="@+id/scrollbar_thumb"
+        android:layout_width="@dimen/chassis_scrollbar_thumb_width"
+        android:layout_height="0dp"
+        android:layout_gravity="center_horizontal"
+        android:background="@drawable/chassis_pagedrecyclerview_scrollbar_thumb" />
+
+    <ImageButton
+        android:id="@+id/page_down"
+        android:layout_width="@dimen/chassis_scrollbar_button_size"
+        android:layout_height="@dimen/chassis_scrollbar_button_size"
+        android:background="@drawable/chassis_pagedrecyclerview_button_ripple_background"
+        android:contentDescription="@string/chassis_scrollbar_page_down_button"
+        android:focusable="false"
+        android:hapticFeedbackEnabled="false"
+        android:src="@drawable/chassis_pagedrecyclerview_ic_down"
+        android:scaleType="centerInside" />
+</LinearLayout>
diff --git a/car-chassis-lib/res/values/attrs.xml b/car-chassis-lib/res/values/attrs.xml
index 58c6fd0..4caf70b 100644
--- a/car-chassis-lib/res/values/attrs.xml
+++ b/car-chassis-lib/res/values/attrs.xml
@@ -38,4 +38,28 @@
 
     <!-- Theme attribute to specifying a default style for all chassisToolbars -->
     <attr name="chassisToolbarStyle" format="reference"/>
+
+    <declare-styleable name="PagedRecyclerView">
+        <!-- Whether to enable the chassis_pagedrecyclerview_divider for linear layout or not. -->
+        <attr name="enableDivider" format="boolean" />
+        <!-- Top offset for paged recycler view. -->
+        <attr name="startOffset" format="integer" />
+        <!-- Bottom offset for paged recycler view for linear layout. -->
+        <attr name="endOffset" format="integer" />
+
+        <!-- Number of columns in a grid layout. -->
+        <attr name="numOfColumns" format="integer" />
+
+        <!-- Paged recycler view layout. -->
+        <attr name="layoutStyle" format="enum">
+            <!-- linear layout -->
+            <enum name="linear" value="0" />
+            <!-- grid layout -->
+            <enum name="grid" value="1" />
+        </attr>
+    </declare-styleable>
+
+    <declare-styleable name="PagedRecyclerViewTheme">
+        <attr name="pagedRecyclerViewStyle" format="reference" />
+    </declare-styleable>
 </resources>
diff --git a/car-chassis-lib/res/values/bools.xml b/car-chassis-lib/res/values/bools.xml
new file mode 100644
index 0000000..ff1f439
--- /dev/null
+++ b/car-chassis-lib/res/values/bools.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+    <!-- Whether to display the Scroll Bar or not. Defaults to true. If this is set to false,
+         the PagedRecyclerView will behave exactly like the RecyclerView. -->
+    <bool name="chassis_scrollbar_enable">true</bool>
+
+    <!-- Whether to place the scrollbar z-index above the recycler view. Defaults to
+         true. -->
+    <bool name="chassis_scrollbar_above_recycler_view">true</bool>
+</resources>
diff --git a/car-chassis-lib/res/values/colors.xml b/car-chassis-lib/res/values/colors.xml
index bfd44f0..caf7266 100644
--- a/car-chassis-lib/res/values/colors.xml
+++ b/car-chassis-lib/res/values/colors.xml
@@ -36,4 +36,8 @@
     <color name="chassis_toolbar_search_hint_text_color">#33FFFFFF</color>
     <!-- Toolbar background color -->
     <color name="chassis_toolbar_background_color">#E0000000</color>
+
+    <!--  Paged Recycler View  -->
+    <!-- The color of the scroll bar indicator in the PagedListView. -->
+    <color name="chassis_scrollbar_thumb">#99ffffff</color>
 </resources>
diff --git a/car-chassis-lib/res/values/config.xml b/car-chassis-lib/res/values/config.xml
new file mode 100644
index 0000000..8a0875f
--- /dev/null
+++ b/car-chassis-lib/res/values/config.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <!--
+    Configuration for a default scrollbar for the PagedRecyclerView. This component must inherit
+    abstract class ScrollBar. If the ScrollBar is enabled, the component will be initialized from
+    PagedRecyclerView#createScrollBarFromConfig().
+    -->
+    <string name="chassis_scrollbar_component" translatable="false">
+        com.google.android.apps.automotive.chassis.libraries.CarScrollBar
+    </string>
+
+    <!--
+    Whether to include a gutter to the start, end or both sides of the list view items.
+    The gutter width will be the width of the scrollbar, and by default will be set to
+    both. Values are defined as follows:
+      none = 0
+      start = 1
+      end = 2
+      both = 3
+    -->
+    <integer name="chassis_scrollbar_gutter" translatable="false">1</integer>
+
+    <!--
+    Position of the scrollbar. Default to left. Values are defined as follows:
+      start = 0
+      end = 1
+    -->
+    <integer name="chassis_scrollbar_position" translatable="false">0</integer>
+
+    <!-- Width of the scrollbar container. -->
+    <dimen name="chassis_scrollbar_container_width" translatable="false">@*android:dimen/car_margin</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/res/values/dimens.xml b/car-chassis-lib/res/values/dimens.xml
index 0226617..5565bfb 100644
--- a/car-chassis-lib/res/values/dimens.xml
+++ b/car-chassis-lib/res/values/dimens.xml
@@ -59,4 +59,23 @@
 
     <!-- Internal artifacts. Do not overlay -->
     <item name="wrap_content" format="integer" type="dimen">-2</item>
+
+    <!-- Default Scroll Bar for PagedRecyclerView -->
+    <dimen name="chassis_scrollbar_button_size">76dp</dimen>
+    <dimen name="chassis_scrollbar_thumb_width">6dp</dimen>
+    <dimen name="chassis_scrollbar_separator_margin">16dp</dimen>
+    <dimen name="chassis_scrollbar_margin">20dp</dimen>
+    <dimen name="chassis_scrollbar_thumb_radius">100dp</dimen>
+
+    <item name="chassis_button_disabled_alpha" format="float" type="dimen">0.2</item>
+    <item name="chassis_scroller_milliseconds_per_inch" format="float" type="dimen">150</item>
+    <item name="chassis_scroller_deceleration_time_divisor" format="float" type="dimen">0.45</item>
+    <item name="chassis_scroller_interpolator_factor" format="float" type="dimen">1.8</item>
+
+    <item name="chassis_scrollbar_milliseconds_per_inch" format="float" type="dimen">150.0</item>
+    <item name="chassis_scrollbar_deceleration_times_divisor" format="float" type="dimen">0.45</item>
+    <item name="chassis_scrollbar_decelerate_interpolator_factor" format="float" type="dimen">1.8</item>
+
+    <dimen name="chassis_scrollbar_padding_start">0dp</dimen>
+    <dimen name="chassis_scrollbar_padding_end">0dp</dimen>
 </resources>
diff --git a/car-chassis-lib/res/values/integers.xml b/car-chassis-lib/res/values/integers.xml
new file mode 100644
index 0000000..083fa2e
--- /dev/null
+++ b/car-chassis-lib/res/values/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+    <!-- Default max string length -->
+    <integer name="chassis_default_max_string_length">120</integer>
+
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/res/values/strings.xml b/car-chassis-lib/res/values/strings.xml
index b5cf087..38ea967 100644
--- a/car-chassis-lib/res/values/strings.xml
+++ b/car-chassis-lib/res/values/strings.xml
@@ -16,4 +16,10 @@
 <resources>
     <!-- Search hint, displayed inside the search box [CHAR LIMIT=50] -->
     <string name="chassis_toolbar_default_search_hint">Search&#8230;</string>
+    <!-- CarUxRestrictions Utility -->
+    <string name="chassis_ellipsis" translatable="false">&#8230;</string>
+    <!-- Content description for paged recycler view scroll bar down arrow [CHAR LIMIT=30] -->
+    <string name="chassis_scrollbar_page_down_button">Scroll down</string>
+    <!-- Content description for paged recycler view scroll bar up arrow [CHAR LIMIT=30] -->
+    <string name="chassis_scrollbar_page_up_button">Scroll up</string>
 </resources>
diff --git a/car-chassis-lib/res/values/styles.xml b/car-chassis-lib/res/values/styles.xml
index da3be41..6d5308f 100644
--- a/car-chassis-lib/res/values/styles.xml
+++ b/car-chassis-lib/res/values/styles.xml
@@ -94,4 +94,8 @@
         <item name="android:letterSpacing">@dimen/chassis_letter_spacing_body3</item>
     </style>
 
+    <style name="PagedRecyclerView">
+    </style>
+    <style name="PagedRecyclerView.NestedRecyclerView">
+    </style>
 </resources>
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarScrollBar.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarScrollBar.java
new file mode 100644
index 0000000..0ec40d4
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarScrollBar.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.annotation.IntRange;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+import com.android.car.chassis.pagedrecyclerview.PagedRecyclerView.ScrollBarPosition;
+
+/**
+ * The default scroll bar widget for the {@link PagedRecyclerView}.
+ *
+ * <p>Inspired by {@link androidx.car.widget.PagedListView}. Most pagination and scrolling logic has
+ * been ported from the PLV with minor updates.
+ */
+class CarScrollBar implements ScrollBar {
+    private float mButtonDisabledAlpha;
+    private static final String TAG = "CarScrollBar";
+    private PagedSnapHelper mSnapHelper;
+
+    private ImageView mUpButton;
+    private View mScrollView;
+    private View mScrollThumb;
+    private ImageView mDownButton;
+    private int mPaddingStart;
+    private int mPaddingEnd;
+
+    private int mSeparatingMargin;
+
+    private RecyclerView mRecyclerView;
+
+    /** The amount of space that the scroll thumb is allowed to roam over. */
+    private int mScrollThumbTrackHeight;
+
+    private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator();
+
+    private final int mRowsPerPage = -1;
+    private final Handler mHandler = new Handler();
+
+    private OrientationHelper mOrientationHelper;
+
+    @Override
+    public void initialize(
+            RecyclerView rv,
+            int scrollBarContainerWidth,
+            @ScrollBarPosition int scrollBarPosition,
+            boolean scrollBarAboveRecyclerView) {
+
+        this.mRecyclerView = rv;
+
+        LayoutInflater inflater =
+                (LayoutInflater) rv.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        FrameLayout parent = (FrameLayout) getRecyclerView().getParent();
+
+        mScrollView = inflater.inflate(R.layout.chassis_pagedrecyclerview_scrollbar, parent, false);
+        mScrollView.setLayoutParams(
+                new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+
+        Resources res = rv.getContext().getResources();
+        mButtonDisabledAlpha = res.getFloat(R.dimen.chassis_button_disabled_alpha);
+
+        if (scrollBarAboveRecyclerView) {
+            parent.addView(mScrollView);
+        } else {
+            parent.addView(mScrollView, /* index= */ 0);
+        }
+
+        setScrollBarContainerWidth(scrollBarContainerWidth);
+        setScrollBarPosition(scrollBarPosition);
+
+        getRecyclerView().addOnScrollListener(mRecyclerViewOnScrollListener);
+        getRecyclerView().getRecycledViewPool().setMaxRecycledViews(0, 12);
+
+        mSeparatingMargin = res.getDimensionPixelSize(R.dimen.chassis_scrollbar_separator_margin);
+
+        mUpButton = mScrollView.findViewById(R.id.page_up);
+        PaginateButtonClickListener upButtonClickListener =
+                new PaginateButtonClickListener(PaginationListener.PAGE_UP);
+        mUpButton.setOnClickListener(upButtonClickListener);
+
+        mDownButton = mScrollView.findViewById(R.id.page_down);
+        PaginateButtonClickListener downButtonClickListener =
+                new PaginateButtonClickListener(PaginationListener.PAGE_DOWN);
+        mDownButton.setOnClickListener(downButtonClickListener);
+
+        mScrollThumb = mScrollView.findViewById(R.id.scrollbar_thumb);
+
+        mSnapHelper = new PagedSnapHelper(rv.getContext());
+        getRecyclerView().setOnFlingListener(null);
+        mSnapHelper.attachToRecyclerView(getRecyclerView());
+
+        mScrollView.addOnLayoutChangeListener(
+                (View v,
+                        int left,
+                        int top,
+                        int right,
+                        int bottom,
+                        int oldLeft,
+                        int oldTop,
+                        int oldRight,
+                        int oldBottom) -> {
+                    int width = right - left;
+
+                    OrientationHelper orientationHelper =
+                            getOrientationHelper(getRecyclerView().getLayoutManager());
+
+                    // This value will keep track of the top of the current view being laid out.
+                    int layoutTop = orientationHelper.getStartAfterPadding() + mPaddingStart;
+
+                    // Lay out the up button at the top of the view.
+                    layoutViewCenteredFromTop(mUpButton, layoutTop, width);
+                    layoutTop = mUpButton.getBottom();
+
+                    // Lay out the scroll thumb
+                    layoutTop += mSeparatingMargin;
+                    layoutViewCenteredFromTop(mScrollThumb, layoutTop, width);
+
+                    // Lay out the bottom button at the bottom of the view.
+                    int downBottom = orientationHelper.getEndAfterPadding() - mPaddingEnd;
+                    layoutViewCenteredFromBottom(mDownButton, downBottom, width);
+
+                    mHandler.post(this::calculateScrollThumbTrackHeight);
+                    mHandler.post(() -> updatePaginationButtons(/* animate= */ false));
+                });
+    }
+
+    public RecyclerView getRecyclerView() {
+        return mRecyclerView;
+    }
+
+    @Override
+    public void requestLayout() {
+        mScrollView.requestLayout();
+    }
+
+    /**
+     * Sets the width of the container that holds the scrollbar. The scrollbar will be centered
+     * within
+     * this width.
+     *
+     * @param width The width of the scrollbar container.
+     */
+    private void setScrollBarContainerWidth(int width) {
+        ViewGroup.LayoutParams layoutParams = mScrollView.getLayoutParams();
+        layoutParams.width = width;
+        mScrollView.requestLayout();
+    }
+
+    @Override
+    public void setPadding(int paddingStart, int paddingEnd) {
+        this.mPaddingStart = paddingStart;
+        this.mPaddingEnd = paddingEnd;
+        requestLayout();
+    }
+
+    /**
+     * Sets the position of the scrollbar.
+     *
+     * @param position Enum value of the scrollbar position. 0 for Start and 1 for end.
+     */
+    private void setScrollBarPosition(@ScrollBarPosition int position) {
+        FrameLayout.LayoutParams layoutParams =
+                (FrameLayout.LayoutParams) mScrollView.getLayoutParams();
+        if (position == ScrollBarPosition.START) {
+            layoutParams.gravity = Gravity.LEFT;
+        } else {
+            layoutParams.gravity = Gravity.RIGHT;
+        }
+
+        mScrollView.requestLayout();
+    }
+
+    /**
+     * Sets whether or not the up button on the scroll bar is clickable.
+     *
+     * @param enabled {@code true} if the up button is enabled.
+     */
+    private void setUpEnabled(boolean enabled) {
+        mUpButton.setEnabled(enabled);
+        mUpButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
+    }
+
+    /**
+     * Sets whether or not the down button on the scroll bar is clickable.
+     *
+     * @param enabled {@code true} if the down button is enabled.
+     */
+    private void setDownEnabled(boolean enabled) {
+        mDownButton.setEnabled(enabled);
+        mDownButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
+    }
+
+    /**
+     * Returns whether or not the down button on the scroll bar is clickable.
+     *
+     * @return {@code true} if the down button is enabled. {@code false} otherwise.
+     */
+    private boolean isDownEnabled() {
+        return mDownButton.isEnabled();
+    }
+
+    /** Listener for when the list should paginate. */
+    interface PaginationListener {
+        int PAGE_UP = 0;
+        int PAGE_DOWN = 1;
+
+        /** Called when the linked view should be paged in the given direction */
+        void onPaginate(int direction);
+    }
+
+    /**
+     * Calculate the amount of space that the scroll bar thumb is allowed to roam. The thumb is
+     * allowed to take up the space between the down bottom and the up or alpha jump button,
+     * depending
+     * on if the latter is visible.
+     */
+    private void calculateScrollThumbTrackHeight() {
+        // Subtracting (2 * mSeparatingMargin) for the top/bottom margin above and below the
+        // scroll bar thumb.
+        mScrollThumbTrackHeight = mDownButton.getTop() - (2 * mSeparatingMargin);
+
+        // If there's an alpha jump button, then the thumb is laid out starting from below that.
+        mScrollThumbTrackHeight -= mUpButton.getBottom();
+    }
+
+    /**
+     * Lays out the given View starting from the given {@code top} value downwards and centered
+     * within
+     * the given {@code availableWidth}.
+     *
+     * @param view The view to lay out.
+     * @param top The top value to start laying out from. This value will be the resulting top value
+     * of the view.
+     * @param availableWidth The width in which to center the given view.
+     */
+    private static void layoutViewCenteredFromTop(View view, int top, int availableWidth) {
+        int viewWidth = view.getMeasuredWidth();
+        int viewLeft = (availableWidth - viewWidth) / 2;
+        view.layout(viewLeft, top, viewLeft + viewWidth, top + view.getMeasuredHeight());
+    }
+
+    /**
+     * Lays out the given View starting from the given {@code bottom} value upwards and centered
+     * within the given {@code availableSpace}.
+     *
+     * @param view The view to lay out.
+     * @param bottom The bottom value to start laying out from. This value will be the resulting
+     * bottom value of the view.
+     * @param availableWidth The width in which to center the given view.
+     */
+    private static void layoutViewCenteredFromBottom(View view, int bottom, int availableWidth) {
+        int viewWidth = view.getMeasuredWidth();
+        int viewLeft = (availableWidth - viewWidth) / 2;
+        view.layout(viewLeft, bottom - view.getMeasuredHeight(), viewLeft + viewWidth, bottom);
+    }
+
+    /**
+     * Sets the range, offset and extent of the scroll bar. The range represents the size of a
+     * container for the scrollbar thumb; offset is the distance from the start of the container to
+     * where the thumb should be; and finally, extent is the size of the thumb.
+     *
+     * <p>These values can be expressed in arbitrary units, so long as they share the same units.
+     * The
+     * values should also be positive.
+     *
+     * @param range The range of the scrollbar's thumb
+     * @param offset The offset of the scrollbar's thumb
+     * @param extent The extent of the scrollbar's thumb
+     * @param animate Whether or not the thumb should animate from its current position to the
+     * position specified by the given range, offset and extent.
+     */
+    private void setParameters(
+            @IntRange(from = 0) int range,
+            @IntRange(from = 0) int offset,
+            @IntRange(from = 0) int extent,
+            boolean animate) {
+        // Not laid out yet, so values cannot be calculated.
+        if (!mScrollView.isLaidOut()) {
+            return;
+        }
+
+        // If the scroll bars aren't visible, then no need to update.
+        if (mScrollView.getVisibility() == View.GONE || range == 0) {
+            return;
+        }
+
+        int thumbLength = calculateScrollThumbLength(range, extent);
+        int thumbOffset = calculateScrollThumbOffset(range, offset, thumbLength);
+
+        // Sets the size of the thumb and request a redraw if needed.
+        ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
+
+        if (lp.height != thumbLength) {
+            lp.height = thumbLength;
+            mScrollThumb.requestLayout();
+        }
+
+        moveY(mScrollThumb, thumbOffset, animate);
+    }
+
+    /**
+     * Calculates and returns how big the scroll bar thumb should be based on the given range and
+     * extent.
+     *
+     * @param range The total amount of space the scroll bar is allowed to roam over.
+     * @param extent The amount of space that the scroll bar takes up relative to the range.
+     * @return The height of the scroll bar thumb in pixels.
+     */
+    private int calculateScrollThumbLength(int range, int extent) {
+        // Scale the length by the available space that the thumb can fill.
+        return Math.round(((float) extent / range) * mScrollThumbTrackHeight);
+    }
+
+    /**
+     * Calculates and returns how much the scroll thumb should be offset from the top of where it
+     * has
+     * been laid out.
+     *
+     * @param range The total amount of space the scroll bar is allowed to roam over.
+     * @param offset The amount the scroll bar should be offset, expressed in the same units as the
+     * given range.
+     * @param thumbLength The current length of the thumb in pixels.
+     * @return The amount the thumb should be offset in pixels.
+     */
+    private int calculateScrollThumbOffset(int range, int offset, int thumbLength) {
+        // Ensure that if the user has reached the bottom of the list, then the scroll bar is
+        // aligned to the bottom as well. Otherwise, scale the offset appropriately.
+        // This offset will be a value relative to the parent of this scrollbar, so start by where
+        // the top of mScrollThumb is.
+        return mScrollThumb.getTop()
+                + (isDownEnabled()
+                ? Math.round(((float) offset / range) * mScrollThumbTrackHeight)
+                : mScrollThumbTrackHeight - thumbLength);
+    }
+
+    /** Moves the given view to the specified 'y' position. */
+    private void moveY(final View view, float newPosition, boolean animate) {
+        final int duration = animate ? 200 : 0;
+        view.animate()
+                .y(newPosition)
+                .setDuration(duration)
+                .setInterpolator(mPaginationInterpolator)
+                .start();
+    }
+
+    private class PaginateButtonClickListener implements View.OnClickListener {
+        private final int mPaginateDirection;
+        private PaginationListener mPaginationListener;
+
+        PaginateButtonClickListener(int paginateDirection) {
+            this.mPaginateDirection = paginateDirection;
+        }
+
+        @Override
+        public void onClick(View v) {
+            if (mPaginationListener != null) {
+                mPaginationListener.onPaginate(mPaginateDirection);
+            }
+            if (mPaginateDirection == PaginationListener.PAGE_DOWN) {
+                pageDown();
+            } else if (mPaginateDirection == PaginationListener.PAGE_UP) {
+                pageUp();
+            }
+        }
+    }
+
+    private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
+            new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                    updatePaginationButtons(false);
+                }
+            };
+
+    /** Returns the page the given position is on, starting with page 0. */
+    int getPage(int position) {
+        if (mRowsPerPage == -1) {
+            return -1;
+        }
+        if (mRowsPerPage == 0) {
+            return 0;
+        }
+        return position / mRowsPerPage;
+    }
+
+    private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
+        if (mOrientationHelper == null || mOrientationHelper.getLayoutManager() != layoutManager) {
+            // PagedRecyclerView is assumed to be a list that always vertically scrolls.
+            mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
+        }
+        return mOrientationHelper;
+    }
+
+    /**
+     * Scrolls the contents of the RecyclerView up a page. A page is defined as the height of the
+     * {@code PagedRecyclerView}.
+     *
+     * <p>The resulting first item in the list will be snapped to so that it is completely visible.
+     * If
+     * this is not possible due to the first item being taller than the containing {@code
+     * PagedRecyclerView}, then the snapping will not occur.
+     */
+    void pageUp() {
+        int currentOffset = getRecyclerView().computeVerticalScrollOffset();
+        if (getRecyclerView().getLayoutManager() == null
+                || getRecyclerView().getChildCount() == 0
+                || currentOffset == 0) {
+            return;
+        }
+
+        // Use OrientationHelper to calculate scroll distance in order to match snapping behavior.
+        OrientationHelper orientationHelper =
+                getOrientationHelper(getRecyclerView().getLayoutManager());
+        int screenSize = orientationHelper.getTotalSpace();
+
+        int scrollDistance = screenSize;
+        // The iteration order matters. In case where there are 2 items longer than screen size, we
+        // want to focus on upcoming view.
+        for (int i = 0; i < getRecyclerView().getChildCount(); i++) {
+            /*
+             * We treat child View longer than screen size differently:
+             * 1) When it enters screen, next pageUp will align its bottom with parent bottom;
+             * 2) When it leaves screen, next pageUp will align its top with parent top.
+             */
+            View child = getRecyclerView().getChildAt(i);
+            if (child.getHeight() > screenSize) {
+                if (orientationHelper.getDecoratedEnd(child) < screenSize) {
+                    // Child view bottom is entering screen. Align its bottom with parent bottom.
+                    scrollDistance = screenSize - orientationHelper.getDecoratedEnd(child);
+                } else if (-screenSize < orientationHelper.getDecoratedStart(child)
+                        && orientationHelper.getDecoratedStart(child) < 0) {
+                    // Child view top is about to enter screen - its distance to parent top
+                    // is less than a full scroll. Align child top with parent top.
+                    scrollDistance = Math.abs(orientationHelper.getDecoratedStart(child));
+                }
+                // There can be two items that are longer than the screen. We stop at the first one.
+                // This is affected by the iteration order.
+                break;
+            }
+        }
+        // Distance should always be positive. Negate its value to scroll up.
+        getRecyclerView().smoothScrollBy(0, -scrollDistance);
+    }
+
+    /**
+     * Scrolls the contents of the RecyclerView down a page. A page is defined as the height of the
+     * {@code PagedRecyclerView}.
+     *
+     * <p>This method will attempt to bring the last item in the list as the first item. If the
+     * current first item in the list is taller than the {@code PagedRecyclerView}, then it will be
+     * scrolled the length of a page, but not snapped to.
+     */
+    void pageDown() {
+        if (getRecyclerView().getLayoutManager() == null
+                || getRecyclerView().getChildCount() == 0) {
+            return;
+        }
+
+        OrientationHelper orientationHelper =
+                getOrientationHelper(getRecyclerView().getLayoutManager());
+        int screenSize = orientationHelper.getTotalSpace();
+        int scrollDistance = screenSize;
+
+        // If the last item is partially visible, page down should bring it to the top.
+        View lastChild = getRecyclerView().getChildAt(getRecyclerView().getChildCount() - 1);
+        if (getRecyclerView()
+                .getLayoutManager()
+                .isViewPartiallyVisible(
+                        lastChild, /* completelyVisible= */ false, /* acceptEndPointInclusion= */
+                        false)) {
+            scrollDistance = orientationHelper.getDecoratedStart(lastChild);
+            if (scrollDistance < 0) {
+                // Scroll value can be negative if the child is longer than the screen size and the
+                // visible area of the screen does not show the start of the child.
+                // Scroll to the next screen if the start value is negative
+                scrollDistance = screenSize;
+            }
+        }
+
+        // The iteration order matters. In case where there are 2 items longer than screen size, we
+        // want to focus on upcoming view (the one at the bottom of screen).
+        for (int i = getRecyclerView().getChildCount() - 1; i >= 0; i--) {
+            /* We treat child View longer than screen size differently:
+             * 1) When it enters screen, next pageDown will align its top with parent top;
+             * 2) When it leaves screen, next pageDown will align its bottom with parent bottom.
+             */
+            View child = getRecyclerView().getChildAt(i);
+            if (child.getHeight() > screenSize) {
+                if (orientationHelper.getDecoratedStart(child) > 0) {
+                    // Child view top is entering screen. Align its top with parent top.
+                    scrollDistance = orientationHelper.getDecoratedStart(child);
+                } else if (screenSize < orientationHelper.getDecoratedEnd(child)
+                        && orientationHelper.getDecoratedEnd(child) < 2 * screenSize) {
+                    // Child view bottom is about to enter screen - its distance to parent bottom
+                    // is less than a full scroll. Align child bottom with parent bottom.
+                    scrollDistance = orientationHelper.getDecoratedEnd(child) - screenSize;
+                }
+                // There can be two items that are longer than the screen. We stop at the first one.
+                // This is affected by the iteration order.
+                break;
+            }
+        }
+
+        getRecyclerView().smoothScrollBy(0, scrollDistance);
+    }
+
+    /**
+     * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
+     * being called as a result of adapter changes, it should be called after the new layout has
+     * been
+     * calculated because the method of determining scrollbar visibility uses the current layout.
+     * If
+     * this is called after an adapter change but before the new layout, the visibility
+     * determination
+     * may not be correct.
+     *
+     * @param animate {@code true} if the scrollbar should animate to its new position. {@code
+     * false}
+     * if no animation is used
+     */
+    private void updatePaginationButtons(boolean animate) {
+
+        boolean isAtStart = isAtStart();
+        boolean isAtEnd = isAtEnd();
+        RecyclerView.LayoutManager layoutManager = getRecyclerView().getLayoutManager();
+
+        if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
+            mScrollView.setVisibility(View.INVISIBLE);
+        } else {
+            mScrollView.setVisibility(View.VISIBLE);
+        }
+        setUpEnabled(!isAtStart);
+        setDownEnabled(!isAtEnd);
+
+        if (layoutManager == null) {
+            return;
+        }
+
+        if (layoutManager.canScrollVertically()) {
+            setParameters(
+                    getRecyclerView().computeVerticalScrollRange(),
+                    getRecyclerView().computeVerticalScrollOffset(),
+                    getRecyclerView().computeVerticalScrollExtent(),
+                    animate);
+        } else {
+            setParameters(
+                    getRecyclerView().computeHorizontalScrollRange(),
+                    getRecyclerView().computeHorizontalScrollOffset(),
+                    getRecyclerView().computeHorizontalScrollExtent(),
+                    animate);
+        }
+
+        mScrollView.invalidate();
+    }
+
+    /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+    boolean isAtStart() {
+        return mSnapHelper.isAtStart(getRecyclerView().getLayoutManager());
+    }
+
+    /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+    boolean isAtEnd() {
+        return mSnapHelper.isAtEnd(getRecyclerView().getLayoutManager());
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarUxRestrictionsUtil.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarUxRestrictionsUtil.java
new file mode 100644
index 0000000..7e204bd
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarUxRestrictionsUtil.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview;
+
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_LIMIT_STRING_LENGTH;
+
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo;
+import android.car.drivingstate.CarUxRestrictionsManager;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.chassis.R;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Utility class to access Car Restriction Manager.
+ *
+ * <p>This class must be a singleton because only one listener can be registered with {@link
+ * CarUxRestrictionsManager} at a time, as documented in {@link
+ * CarUxRestrictionsManager#registerListener}.
+ */
+public class CarUxRestrictionsUtil {
+    private static final String TAG = "CarUxRestrictionsUtil";
+
+    private final Car mCarApi;
+    private CarUxRestrictionsManager mCarUxRestrictionsManager;
+    @NonNull
+    private CarUxRestrictions mCarUxRestrictions = getDefaultRestrictions();
+
+    private Set<OnUxRestrictionsChangedListener> mObservers;
+    private static CarUxRestrictionsUtil sInstance = null;
+
+    private CarUxRestrictionsUtil(Context context) {
+        CarUxRestrictionsManager.OnUxRestrictionsChangedListener listener =
+                (carUxRestrictions) -> {
+                    if (carUxRestrictions == null) {
+                        this.mCarUxRestrictions = getDefaultRestrictions();
+                    } else {
+                        this.mCarUxRestrictions = carUxRestrictions;
+                    }
+
+                    for (OnUxRestrictionsChangedListener observer : mObservers) {
+                        observer.onRestrictionsChanged(this.mCarUxRestrictions);
+                    }
+                };
+
+        mCarApi = Car.createCar(context);
+        mObservers = Collections.newSetFromMap(new WeakHashMap<>());
+
+        try {
+            mCarUxRestrictionsManager =
+                    (CarUxRestrictionsManager) mCarApi.getCarManager(
+                            Car.CAR_UX_RESTRICTION_SERVICE);
+            mCarUxRestrictionsManager.registerListener(listener);
+            listener.onUxRestrictionsChanged(
+                    mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
+        } catch (CarNotConnectedException | NullPointerException e) {
+            Log.e(TAG, "Car not connected", e);
+            // mCarUxRestrictions will be the default
+        }
+    }
+
+    @NonNull
+    private static CarUxRestrictions getDefaultRestrictions() {
+        return new CarUxRestrictions.Builder(
+                true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0)
+                .build();
+    }
+
+    /** Listener interface used to update clients on UxRestrictions changes */
+    public interface OnUxRestrictionsChangedListener {
+        /** Called when CarUxRestrictions changes */
+        void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions);
+    }
+
+    /** Returns the singleton sInstance of this class */
+    @NonNull
+    public static CarUxRestrictionsUtil getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new CarUxRestrictionsUtil(context);
+        }
+
+        return sInstance;
+    }
+
+    /**
+     * Registers a listener on this class for updates to CarUxRestrictions. Multiple listeners may
+     * be
+     * registered.
+     */
+    public void register(OnUxRestrictionsChangedListener listener) {
+        mObservers.add(listener);
+        listener.onRestrictionsChanged(mCarUxRestrictions);
+    }
+
+    /** Unregisters a registered listener */
+    public void unregister(OnUxRestrictionsChangedListener listener) {
+        mObservers.remove(listener);
+    }
+
+    /**
+     * Returns whether any of the given flags is blocked by the current restrictions. If null is
+     * given, the method returns true for safety.
+     */
+    public static boolean isRestricted(
+            @CarUxRestrictionsInfo int restrictionFlags, @Nullable CarUxRestrictions uxr) {
+        return (uxr == null) || ((uxr.getActiveRestrictions() & restrictionFlags) != 0);
+    }
+
+    /**
+     * Complies the input string with the given UX restrictions. Returns the original string if
+     * already compliant, otherwise a shortened ellipsized string.
+     */
+    public static String complyString(Context context, String str, CarUxRestrictions uxr) {
+
+        if (isRestricted(UX_RESTRICTIONS_LIMIT_STRING_LENGTH, uxr)) {
+            int maxLength =
+                    uxr == null
+                            ? context.getResources().getInteger(
+                            R.integer.chassis_default_max_string_length)
+                            : uxr.getMaxRestrictedStringLength();
+
+            if (str.length() > maxLength) {
+                return str.substring(0, maxLength) + context.getString(R.string.chassis_ellipsis);
+            }
+        }
+
+        return str;
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerView.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerView.java
new file mode 100644
index 0000000..7f76e16
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerView.java
@@ -0,0 +1,811 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+import com.android.car.chassis.pagedrecyclerview.decorations.grid.GridDividerItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.grid.GridOffsetItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.linear.LinearDividerItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration.OffsetPosition;
+
+import java.lang.annotation.Retention;
+
+/**
+ * View that extends a {@link RecyclerView} and creates a nested {@code RecyclerView} which could
+ * potentially include a scrollbar that has page up and down arrows. Interaction with this view is
+ * similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
+ */
+public final class PagedRecyclerView extends RecyclerView {
+
+    private static final boolean DEBUG = false;
+    private static final String TAG = "PagedRecyclerView";
+
+    private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
+    private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener;
+
+    private boolean mScrollBarEnabled;
+    private int mScrollBarContainerWidth;
+    @ScrollBarPosition
+    private int mScrollBarPosition;
+    private boolean mScrollBarAboveRecyclerView;
+    private String mScrollBarClass;
+    private boolean mFullyInitialized;
+    private float mScrollBarPaddingStart;
+    private float mScrollBarPaddingEnd;
+    private Context mContext;
+
+    @Gutter
+    private int mGutter;
+    private int mGutterSize;
+    private RecyclerView mNestedRecyclerView;
+    private Adapter<?> mAdapter;
+    private ScrollBar mScrollBar;
+
+    private GridOffsetItemDecoration mOffsetItemDecoration;
+    private GridDividerItemDecoration mDividerItemDecoration;
+    @PagedRecyclerViewLayout
+    int mPagedRecyclerViewLayout;
+    private int mNumOfColumns;
+
+    /**
+     * The possible values for @{link #setGutter}. The default value is actually {@link
+     * PagedRecyclerView.Gutter#BOTH}.
+     */
+    @IntDef({
+            Gutter.NONE,
+            Gutter.START,
+            Gutter.END,
+            Gutter.BOTH,
+    })
+    @Retention(SOURCE)
+    public @interface Gutter {
+        /**
+         * No gutter on either side of the list items. The items will span the full width of the
+         * RecyclerView
+         */
+        int NONE = 0;
+
+        /** Include a gutter only on the start side (that is, the same side as the scroll bar). */
+        int START = 1;
+
+        /** Include a gutter only on the end side (that is, the opposite side of the scroll bar). */
+        int END = 2;
+
+        /** Include a gutter on both sides of the list items. This is the default behaviour. */
+        int BOTH = 3;
+    }
+
+    /**
+     * The possible values for setScrollbarPosition. The default value is actually {@link
+     * PagedRecyclerView.ScrollBarPosition#START}.
+     */
+    @IntDef({
+            ScrollBarPosition.START,
+            ScrollBarPosition.END,
+    })
+    @Retention(SOURCE)
+    public @interface ScrollBarPosition {
+        /** Position the scrollbar to the left of the screen. This is default. */
+        int START = 0;
+
+        /** Position scrollbar to the right of the screen. */
+        int END = 2;
+    }
+
+    /**
+     * The possible values for setScrollbarPosition. The default value is actually {@link
+     * PagedRecyclerViewLayout#LINEAR}.
+     */
+    @IntDef({
+            PagedRecyclerViewLayout.LINEAR,
+            PagedRecyclerViewLayout.GRID,
+    })
+    @Retention(SOURCE)
+    public @interface PagedRecyclerViewLayout {
+        /** Position the scrollbar to the left of the screen. This is default. */
+        int LINEAR = 0;
+
+        /** Position scrollbar to the right of the screen. */
+        int GRID = 2;
+    }
+
+    /**
+     * Interface for a {@link RecyclerView.Adapter} to cap the number of items.
+     *
+     * <p>NOTE: it is still up to the adapter to use maxItems in {@link
+     * RecyclerView.Adapter#getItemCount()}.
+     *
+     * <p>the recommended way would be with:
+     *
+     * <pre>{@code
+     * {@literal@}Override
+     * public int getItemCount() {
+     *   return Math.min(super.getItemCount(), mMaxItems);
+     * }
+     * }</pre>
+     */
+    public interface ItemCap {
+        /** A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit. */
+        int UNLIMITED = -1;
+
+        /**
+         * Sets the maximum number of items available in the adapter. A value less than '0' means
+         * the
+         * list should not be capped.
+         */
+        void setMaxItems(int maxItems);
+    }
+
+    /**
+     * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
+     * inner
+     * recycler view within its bounds, this layout manager should always have 0 padding.
+     */
+    private static class PagedRecyclerViewLayoutManager extends LinearLayoutManager {
+        PagedRecyclerViewLayoutManager(Context context) {
+            super(context);
+        }
+
+        @Override
+        public int getPaddingTop() {
+            return 0;
+        }
+
+        @Override
+        public int getPaddingBottom() {
+            return 0;
+        }
+
+        @Override
+        public int getPaddingStart() {
+            return 0;
+        }
+
+        @Override
+        public int getPaddingEnd() {
+            return 0;
+        }
+
+        @Override
+        public boolean canScrollHorizontally() {
+            return false;
+        }
+
+        @Override
+        public boolean canScrollVertically() {
+            return false;
+        }
+    }
+
+    /**
+     * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
+     * inner
+     * recycler view within its bounds, this layout manager should always have 0 padding.
+     */
+    private static class GridPagedRecyclerViewLayoutManager extends GridLayoutManager {
+        GridPagedRecyclerViewLayoutManager(Context context, int numOfColumns) {
+            super(context, numOfColumns);
+        }
+
+        @Override
+        public int getPaddingTop() {
+            return 0;
+        }
+
+        @Override
+        public int getPaddingBottom() {
+            return 0;
+        }
+
+        @Override
+        public int getPaddingStart() {
+            return 0;
+        }
+
+        @Override
+        public int getPaddingEnd() {
+            return 0;
+        }
+    }
+
+    public PagedRecyclerView(@NonNull Context context) {
+        this(context, null, 0);
+    }
+
+    public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
+        mListener = this::updateCarUxRestrictions;
+
+        init(context, attrs, defStyle);
+    }
+
+    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+        TypedArray a =
+                context.obtainStyledAttributes(
+                        attrs, R.styleable.PagedRecyclerView, defStyleAttr,
+                        R.style.PagedRecyclerView);
+
+        mScrollBarEnabled = context.getResources().getBoolean(R.bool.chassis_scrollbar_enable);
+        mFullyInitialized = false;
+
+        if (!mScrollBarEnabled) {
+            a.recycle();
+            mFullyInitialized = true;
+            return;
+        }
+
+        mNestedRecyclerView =
+                new RecyclerView(context, attrs, R.style.PagedRecyclerView_NestedRecyclerView);
+
+        mScrollBarPaddingStart =
+                context.getResources().getDimension(R.dimen.chassis_scrollbar_padding_start);
+        mScrollBarPaddingEnd =
+                context.getResources().getDimension(R.dimen.chassis_scrollbar_padding_end);
+
+        mPagedRecyclerViewLayout =
+                a.getInt(R.styleable.PagedRecyclerView_layoutStyle, PagedRecyclerViewLayout.LINEAR);
+        mNumOfColumns = a.getInt(R.styleable.PagedRecyclerView_numOfColumns, /* defValue= */ 2);
+        boolean enableDivider =
+                a.getBoolean(R.styleable.PagedRecyclerView_enableDivider, /* defValue= */ true);
+
+        if (mPagedRecyclerViewLayout == PagedRecyclerViewLayout.LINEAR) {
+
+            int linearTopOffset =
+                    a.getInteger(R.styleable.PagedRecyclerView_startOffset, /* defValue= */ 0);
+            int linearBottomOffset =
+                    a.getInteger(R.styleable.PagedRecyclerView_endOffset, /* defValue= */ 0);
+
+            if (enableDivider) {
+                RecyclerView.ItemDecoration dividerItemDecoration =
+                        new LinearDividerItemDecoration(
+                                context.getDrawable(R.drawable.chassis_pagedrecyclerview_divider));
+                super.addItemDecoration(dividerItemDecoration);
+            }
+            RecyclerView.ItemDecoration topOffsetItemDecoration =
+                    new LinearOffsetItemDecoration(linearTopOffset, OffsetPosition.START);
+            super.addItemDecoration(topOffsetItemDecoration);
+
+            RecyclerView.ItemDecoration bottomOffsetItemDecoration =
+                    new LinearOffsetItemDecoration(linearBottomOffset, OffsetPosition.END);
+            super.addItemDecoration(bottomOffsetItemDecoration);
+        } else {
+
+            int gridTopOffset =
+                    a.getInteger(R.styleable.PagedRecyclerView_startOffset, /* defValue= */ 0);
+            int gridBottomOffset =
+                    a.getInteger(R.styleable.PagedRecyclerView_endOffset, /* defValue= */ 0);
+
+            if (enableDivider) {
+                mDividerItemDecoration =
+                        new GridDividerItemDecoration(
+                                context.getDrawable(R.drawable.divider),
+                                context.getDrawable(R.drawable.divider),
+                                mNumOfColumns);
+                super.addItemDecoration(mDividerItemDecoration);
+            }
+
+            mOffsetItemDecoration =
+                    new GridOffsetItemDecoration(gridTopOffset, mNumOfColumns,
+                            OffsetPosition.START);
+            super.addItemDecoration(mOffsetItemDecoration);
+
+            GridOffsetItemDecoration bottomOffsetItemDecoration =
+                    new GridOffsetItemDecoration(gridBottomOffset, mNumOfColumns,
+                            OffsetPosition.END);
+            super.addItemDecoration(bottomOffsetItemDecoration);
+        }
+
+        super.setLayoutManager(new PagedRecyclerViewLayoutManager(context));
+        super.setAdapter(new PagedRecyclerViewAdapter());
+        super.setNestedScrollingEnabled(false);
+        super.setClipToPadding(false);
+
+        // Gutter
+        mGutter = context.getResources().getInteger(R.integer.chassis_scrollbar_gutter);
+        mGutterSize = getResources().getDimensionPixelSize(R.dimen.chassis_scrollbar_margin);
+
+        mScrollBarContainerWidth =
+                (int) context.getResources().getDimension(
+                        R.dimen.chassis_scrollbar_container_width);
+
+        mScrollBarPosition = context.getResources().getInteger(
+                R.integer.chassis_scrollbar_position);
+
+        mScrollBarAboveRecyclerView =
+                context.getResources().getBoolean(R.bool.chassis_scrollbar_above_recycler_view);
+        mScrollBarClass = context.getResources().getString(R.string.chassis_scrollbar_component);
+        a.recycle();
+        this.mContext = context;
+        // Apply inner RV layout changes after the layout has been calculated for this view.
+        this.getViewTreeObserver()
+                .addOnGlobalLayoutListener(
+                        new OnGlobalLayoutListener() {
+                            @Override
+                            public void onGlobalLayout() {
+                                // View holder layout is still pending.
+                                if (PagedRecyclerView.this.findViewHolderForAdapterPosition(0)
+                                        == null) {
+                                    return;
+                                }
+
+                                PagedRecyclerView.this.getViewTreeObserver()
+                                        .removeOnGlobalLayoutListener(this);
+                                initNestedRecyclerView();
+                                setNestedViewLayout();
+
+                                mNestedRecyclerView
+                                        .getViewTreeObserver()
+                                        .addOnGlobalLayoutListener(
+                                                new OnGlobalLayoutListener() {
+                                                    @Override
+                                                    public void onGlobalLayout() {
+                                                        mNestedRecyclerView
+                                                                .getViewTreeObserver()
+                                                                .removeOnGlobalLayoutListener(this);
+                                                        ViewGroup.LayoutParams params =
+                                                                getLayoutParams();
+                                                        params.height = getMeasuredHeight();
+                                                        setLayoutParams(params);
+                                                        createScrollBarFromConfig();
+                                                        mFullyInitialized = true;
+                                                    }
+                                                });
+                            }
+                        });
+    }
+
+    /**
+     * Returns {@code true} if the {@PagedRecyclerView} is fully drawn. Using a global layout
+     * mListener
+     * may not necessarily signify that this view is fully drawn (i.e. when the scrollbar is
+     * enabled).
+     * This is because the inner views (scrollbar and inner recycler view) are drawn after the
+     * outer
+     * views are finished.
+     */
+    public boolean fullyInitialized() {
+        return mFullyInitialized;
+    }
+
+    /** Sets the number of columns in which grid needs to be divided. */
+    public void setNumOfColumns(int numberOfColumns) {
+        mNumOfColumns = numberOfColumns;
+        mOffsetItemDecoration.setNumOfColumns(mNumOfColumns);
+        mDividerItemDecoration.setNumOfColumns(mNumOfColumns);
+    }
+
+    /**
+     * Returns the {@link LayoutManager} for the {@link RecyclerView} displaying the content.
+     *
+     * <p>In cases where the scroll bar is visible and the nested {@link RecyclerView} is displaying
+     * content, {@link #getLayoutManager()} cannot be used because it returns the {@link
+     * LayoutManager} of the outer {@link RecyclerView}. {@link #getLayoutManager()} could not be
+     * overridden to return the effective manager due to interference with accessibility node tree
+     * traversal.
+     */
+    @Nullable
+    public LayoutManager getEffectiveLayoutManager() {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.getLayoutManager();
+        }
+        return super.getLayoutManager();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mCarUxRestrictionsUtil.register(mListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mCarUxRestrictionsUtil.unregister(mListener);
+    }
+
+    private void updateCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
+        // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
+        if (!(mAdapter instanceof ItemCap)) {
+            return;
+        }
+
+        int maxItems = ItemCap.UNLIMITED;
+        if ((carUxRestrictions.getActiveRestrictions()
+                & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT)
+                != 0) {
+            maxItems = carUxRestrictions.getMaxCumulativeContentItems();
+        }
+
+        int originalCount = mAdapter.getItemCount();
+        ((ItemCap) mAdapter).setMaxItems(maxItems);
+        int newCount = mAdapter.getItemCount();
+
+        if (newCount == originalCount) {
+            return;
+        }
+
+        if (newCount < originalCount) {
+            mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
+        } else {
+            mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
+        }
+    }
+
+    @Override
+    public void setClipToPadding(boolean clipToPadding) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setClipToPadding(clipToPadding);
+        } else {
+            super.setClipToPadding(clipToPadding);
+        }
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public void setAdapter(@Nullable Adapter adapter) {
+
+        if (mPagedRecyclerViewLayout == PagedRecyclerViewLayout.LINEAR) {
+            mNestedRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
+        } else {
+            mNestedRecyclerView.setLayoutManager(
+                    new GridPagedRecyclerViewLayoutManager(mContext, mNumOfColumns));
+            setNumOfColumns(mNumOfColumns);
+        }
+
+        this.mAdapter = adapter;
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setAdapter(adapter);
+        } else {
+            super.setAdapter(adapter);
+        }
+    }
+
+    @Nullable
+    @Override
+    public Adapter<?> getAdapter() {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.getAdapter();
+        }
+        return super.getAdapter();
+    }
+
+    @Override
+    public void setLayoutManager(@Nullable LayoutManager layout) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setLayoutManager(layout);
+        } else {
+            super.setLayoutManager(layout);
+        }
+    }
+
+    @Override
+    public void setOnScrollChangeListener(OnScrollChangeListener l) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setOnScrollChangeListener(l);
+        } else {
+            super.setOnScrollChangeListener(l);
+        }
+    }
+
+    @Override
+    public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
+        } else {
+            super.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
+        }
+    }
+
+    @Override
+    public void setFadingEdgeLength(int length) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setFadingEdgeLength(length);
+        } else {
+            super.setFadingEdgeLength(length);
+        }
+    }
+
+    @Override
+    public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.addItemDecoration(decor, index);
+        } else {
+            super.addItemDecoration(decor, index);
+        }
+    }
+
+    @Override
+    public void addItemDecoration(@NonNull ItemDecoration decor) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.addItemDecoration(decor);
+        } else {
+            super.addItemDecoration(decor);
+        }
+    }
+
+    @Override
+    public void setItemAnimator(@Nullable ItemAnimator animator) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setItemAnimator(animator);
+        } else {
+            super.setItemAnimator(animator);
+        }
+    }
+
+    @Override
+    public void setPadding(int left, int top, int right, int bottom) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setPadding(left, top, right, bottom);
+            if (mScrollBar != null) {
+                mScrollBar.requestLayout();
+            }
+        } else {
+            super.setPadding(left, top, right, bottom);
+        }
+    }
+
+    @Override
+    public void setPaddingRelative(int start, int top, int end, int bottom) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setPaddingRelative(start, top, end, bottom);
+            if (mScrollBar != null) {
+                mScrollBar.requestLayout();
+            }
+        } else {
+            super.setPaddingRelative(start, top, end, bottom);
+        }
+    }
+
+    @Override
+    public ViewHolder findViewHolderForLayoutPosition(int position) {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.findViewHolderForLayoutPosition(position);
+        } else {
+            return super.findViewHolderForLayoutPosition(position);
+        }
+    }
+
+    @Override
+    public ViewHolder findContainingViewHolder(View view) {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.findContainingViewHolder(view);
+        } else {
+            return super.findContainingViewHolder(view);
+        }
+    }
+
+    @Override
+    @Nullable
+    public View findChildViewUnder(float x, float y) {
+        if (mScrollBarEnabled) {
+            return mNestedRecyclerView.findChildViewUnder(x, y);
+        } else {
+            return super.findChildViewUnder(x, y);
+        }
+    }
+
+    @Override
+    public void addOnScrollListener(@NonNull OnScrollListener listener) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.addOnScrollListener(listener);
+        } else {
+            super.addOnScrollListener(listener);
+        }
+    }
+
+    @Override
+    public void removeOnScrollListener(@NonNull OnScrollListener listener) {
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.removeOnScrollListener(listener);
+        } else {
+            super.removeOnScrollListener(listener);
+        }
+    }
+
+    @Override
+    public int getPaddingStart() {
+        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingStart() : super.getPaddingStart();
+    }
+
+    @Override
+    public int getPaddingEnd() {
+        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingEnd() : super.getPaddingEnd();
+    }
+
+    @Override
+    public int getPaddingTop() {
+        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingTop() : super.getPaddingTop();
+    }
+
+    @Override
+    public int getPaddingBottom() {
+        return mScrollBarEnabled ? mNestedRecyclerView.getPaddingBottom()
+                : super.getPaddingBottom();
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.setVisibility(visibility);
+        }
+    }
+
+    private void initNestedRecyclerView() {
+        PagedRecyclerViewAdapter.NestedRowViewHolder vh =
+                (PagedRecyclerViewAdapter.NestedRowViewHolder)
+                        this.findViewHolderForAdapterPosition(0);
+        if (vh == null) {
+            throw new Error("Outer RecyclerView failed to initialize.");
+        }
+
+        vh.frameLayout.addView(mNestedRecyclerView);
+    }
+
+    private void createScrollBarFromConfig() {
+        if (DEBUG) {
+            Log.d(TAG, "createScrollBarFromConfig");
+        }
+
+        Class<?> cls;
+        try {
+            cls = getContext().getClassLoader().loadClass(mScrollBarClass);
+        } catch (Throwable t) {
+            throw andLog("Error loading scroll bar component: " + mScrollBarClass, t);
+        }
+        try {
+            mScrollBar = (ScrollBar) cls.getDeclaredConstructor().newInstance();
+        } catch (Throwable t) {
+            throw andLog("Error creating scroll bar component: " + mScrollBarClass, t);
+        }
+
+        mScrollBar.initialize(
+                mNestedRecyclerView, mScrollBarContainerWidth, mScrollBarPosition,
+                mScrollBarAboveRecyclerView);
+
+        mScrollBar.setPadding((int) mScrollBarPaddingStart, (int) mScrollBarPaddingEnd);
+
+        if (DEBUG) {
+            Log.d(TAG, "started " + mScrollBar.getClass().getSimpleName());
+        }
+    }
+
+    /**
+     * Set the nested view's layout to the specified value.
+     *
+     * <p>The mGutter is the space to the start/end of the list view items and will be equal in size
+     * to
+     * the scroll bars. By default, there is a mGutter to both the left and right of the list view
+     * items, to account for the scroll bar.
+     */
+    private void setNestedViewLayout() {
+        int startMargin = 0;
+        int endMargin = 0;
+        if ((mGutter & Gutter.START) != 0) {
+            startMargin = mGutterSize;
+        }
+        if ((mGutter & Gutter.END) != 0) {
+            endMargin = mGutterSize;
+        }
+
+        MarginLayoutParams layoutParams =
+                (MarginLayoutParams) mNestedRecyclerView.getLayoutParams();
+
+        layoutParams.setMarginStart(startMargin);
+        layoutParams.setMarginEnd(endMargin);
+
+        layoutParams.height = LayoutParams.MATCH_PARENT;
+        layoutParams.width = super.getLayoutManager().getWidth() - startMargin - endMargin;
+        // requestLayout() isn't sufficient because we also need to resolveLayoutParams().
+        mNestedRecyclerView.setLayoutParams(layoutParams);
+
+        // If there's a mGutter, set ClipToPadding to false so that CardView's shadow will still
+        // appear outside of the padding.
+        mNestedRecyclerView.setClipToPadding(startMargin == 0 && endMargin == 0);
+    }
+
+    private static RuntimeException andLog(String msg, Throwable t) {
+        Log.e(TAG, msg, t);
+        throw new RuntimeException(msg, t);
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState, getContext());
+        if (mScrollBarEnabled) {
+            mNestedRecyclerView.saveHierarchyState(ss.mNestedRecyclerViewState);
+        }
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            Log.w(TAG, "onRestoreInstanceState called with an unsupported state");
+            super.onRestoreInstanceState(state);
+        } else {
+            SavedState ss = (SavedState) state;
+            super.onRestoreInstanceState(ss.getSuperState());
+            if (mScrollBarEnabled) {
+                mNestedRecyclerView.restoreHierarchyState(ss.mNestedRecyclerViewState);
+            }
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        SparseArray<Parcelable> mNestedRecyclerViewState;
+        Context mContext;
+
+        SavedState(Parcelable superState, Context c) {
+            super(superState);
+            mContext = c;
+            mNestedRecyclerViewState = new SparseArray<>();
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            mNestedRecyclerViewState = in.readSparseArray(mContext.getClassLoader());
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeSparseArray(mNestedRecyclerViewState);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+                    @Override
+                    public SavedState createFromParcel(Parcel in) {
+                        return new SavedState(in);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerViewAdapter.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerViewAdapter.java
new file mode 100644
index 0000000..f3167ea
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerViewAdapter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+
+/** The adapter for the parent recyclerview in {@link PagedRecyclerView} widget. */
+final class PagedRecyclerViewAdapter
+        extends RecyclerView.Adapter<PagedRecyclerViewAdapter.NestedRowViewHolder> {
+
+    @Override
+    public PagedRecyclerViewAdapter.NestedRowViewHolder onCreateViewHolder(
+            ViewGroup parent, int viewType) {
+        View v =
+                LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.chassis_paged_recycler_view_item, parent, false);
+        return new NestedRowViewHolder(v);
+    }
+
+    // Replace the contents of a view (invoked by the layout manager). Intentionally left empty
+    // since this adapter is an empty shell for the nested recyclerview.
+    @Override
+    public void onBindViewHolder(NestedRowViewHolder holder, int position) {
+    }
+
+    // Return the size of your dataset (invoked by the layout manager)
+    @Override
+    public int getItemCount() {
+        return 1;
+    }
+
+    /** The viewholder class for the parent recyclerview. */
+    static class NestedRowViewHolder extends RecyclerView.ViewHolder {
+        public FrameLayout frameLayout;
+
+        NestedRowViewHolder(View view) {
+            super(view);
+            frameLayout = view.findViewById(R.id.nested_recycler_view_layout);
+        }
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSmoothScroller.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSmoothScroller.java
new file mode 100644
index 0000000..910e370
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSmoothScroller.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+
+/**
+ * Code drop from {androidx.car.widget.PagedSmoothScroller}
+ *
+ * <p>Custom {@link LinearSmoothScroller} that has:
+ *
+ * <ul>
+ * <li>Custom control over the speed of scrolls.
+ * <li>Scrolling that snaps to start of a child view.
+ * </ul>
+ */
+public class PagedSmoothScroller extends LinearSmoothScroller {
+    private float mMillisecondsPerInch;
+    private float mDecelerationTimeDivisor;
+
+    private Interpolator mInterpolator;
+
+    public PagedSmoothScroller(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context context) {
+        mMillisecondsPerInch =
+                context.getResources().getFloat(R.dimen.chassis_scrollbar_milliseconds_per_inch);
+        mDecelerationTimeDivisor =
+                context.getResources().getFloat(
+                        R.dimen.chassis_scrollbar_deceleration_times_divisor);
+        mInterpolator =
+                new DecelerateInterpolator(
+                        context
+                                .getResources()
+                                .getFloat(
+                                        R.dimen.chassis_scrollbar_decelerate_interpolator_factor));
+    }
+
+    @Override
+    protected int getVerticalSnapPreference() {
+        // Returning SNAP_TO_START will ensure that if the top (start) row is partially visible it
+        // will be scrolled downward (END) to make the row fully visible.
+        return SNAP_TO_START;
+    }
+
+    @Override
+    protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+        int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START);
+
+        if (dy == 0) {
+            return;
+        }
+
+        final int time = calculateTimeForDeceleration(dy);
+        if (time > 0) {
+            action.update(0, -dy, time, mInterpolator);
+        }
+    }
+
+    @Override
+    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+        return mMillisecondsPerInch / displayMetrics.densityDpi;
+    }
+
+    @Override
+    protected int calculateTimeForDeceleration(int dx) {
+        return (int) Math.ceil(calculateTimeForScrolling(dx) / mDecelerationTimeDivisor);
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSnapHelper.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSnapHelper.java
new file mode 100644
index 0000000..0d31a76
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSnapHelper.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearSnapHelper;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.LayoutManager;
+
+/**
+ * Inspired by {@link androidx.car.widget.PagedSnapHelper}
+ *
+ * <p>Extension of a {@link LinearSnapHelper} that will snap to the start of the target child view
+ * to the start of the attached {@link RecyclerView}. The start of the view is defined as the top if
+ * the RecyclerView is scrolling vertically; it is defined as the left (or right if RTL) if the
+ * RecyclerView is scrolling horizontally.
+ */
+public class PagedSnapHelper extends LinearSnapHelper {
+
+    private final Context mContext;
+    private RecyclerView mRecyclerView;
+
+    public PagedSnapHelper(Context context) {
+        this.mContext = context;
+    }
+
+    // Orientation helpers are lazily created per LayoutManager.
+    @Nullable
+    private OrientationHelper mVerticalHelper;
+    @Nullable
+    private OrientationHelper mHorizontalHelper;
+
+    @Override
+    public int[] calculateDistanceToFinalSnap(
+            @NonNull LayoutManager layoutManager, @NonNull View targetView) {
+        int[] out = new int[2];
+        if (layoutManager.canScrollHorizontally()) {
+            out[0] = distanceToTopMargin(targetView, getHorizontalHelper(layoutManager));
+        } else {
+            out[0] = 0;
+        }
+
+        if (layoutManager.canScrollVertically()) {
+            out[1] = distanceToTopMargin(targetView, getVerticalHelper(layoutManager));
+        } else {
+            out[1] = 0;
+        }
+        return out;
+    }
+
+    @Override
+    public View findSnapView(LayoutManager layoutManager) {
+        OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+
+        if (mRecyclerView.computeVerticalScrollRange() - mRecyclerView.computeVerticalScrollOffset()
+                <= orientationHelper.getTotalSpace()
+                + mRecyclerView.getPaddingTop()
+                + mRecyclerView.getPaddingBottom()) {
+            return null;
+        }
+
+        if (layoutManager.canScrollVertically()) {
+            return findTopView(layoutManager, getVerticalHelper(layoutManager));
+        } else if (layoutManager.canScrollHorizontally()) {
+            return findTopView(layoutManager, getHorizontalHelper(layoutManager));
+        }
+        return null;
+    }
+
+    private static int distanceToTopMargin(@NonNull View targetView, OrientationHelper helper) {
+        final int childTop = helper.getDecoratedStart(targetView);
+        final int containerTop = helper.getStartAfterPadding();
+        return childTop - containerTop;
+    }
+
+    /**
+     * Finds the view to snap to. The view to snap to is the child of the LayoutManager that is
+     * closest to the start of the RecyclerView. The "start" depends on if the LayoutManager is
+     * scrolling horizontally or vertically. If it is horizontally scrolling, then the start is the
+     * view on the left (right if RTL). Otherwise, it is the top-most view.
+     *
+     * @param layoutManager The current {@link RecyclerView.LayoutManager} for the attached
+     * RecyclerView.
+     * @return The View closest to the start of the RecyclerView.
+     */
+    private static View findTopView(LayoutManager layoutManager, OrientationHelper helper) {
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return null;
+        }
+
+        View closestChild = null;
+        int absClosest = Integer.MAX_VALUE;
+
+        for (int i = 0; i < childCount; i++) {
+            View child = layoutManager.getChildAt(i);
+            if (child == null) {
+                continue;
+            }
+            int absDistance = Math.abs(distanceToTopMargin(child, helper));
+
+            /** if child top is closer than previous closest, set it as closest */
+            if (absDistance < absClosest) {
+                absClosest = absDistance;
+                closestChild = child;
+            }
+        }
+        return closestChild;
+    }
+
+    /**
+     * Returns the percentage of the given view that is visible, relative to its containing
+     * RecyclerView.
+     *
+     * @param view The View to get the percentage visible of.
+     * @param helper An {@link OrientationHelper} to aid with calculation.
+     * @return A float indicating the percentage of the given view that is visible.
+     */
+    private static float getPercentageVisible(View view, OrientationHelper helper) {
+
+        int start = helper.getStartAfterPadding();
+        int end = helper.getEndAfterPadding();
+
+        int viewHeight = helper.getDecoratedMeasurement(view);
+
+        int viewStart = helper.getDecoratedStart(view);
+        int viewEnd = helper.getDecoratedEnd(view);
+
+        if (viewEnd < start) {
+            // The is outside of the bounds of the recyclerView.
+            return 0f;
+        } else if (viewStart >= start && viewEnd <= end) {
+            // The view is within the bounds of the RecyclerView, so it's fully visible.
+            return 1.f;
+        } else if (viewStart <= start && viewEnd >= end) {
+            // The view is larger than the height of the RecyclerView.
+            return 1.f - ((float) (Math.abs(viewStart) + Math.abs(viewEnd)) / viewHeight);
+        } else if (viewStart < start) {
+            // The view is above the start of the RecyclerView, so subtract the start offset
+            // from the total height.
+            return 1.f - ((float) Math.abs(viewStart) / helper.getDecoratedMeasurement(view));
+        } else {
+            // The view is below the end of the RecyclerView, so subtract the end offset from the
+            // total height.
+            return 1.f - ((float) Math.abs(viewEnd) / helper.getDecoratedMeasurement(view));
+        }
+    }
+
+    @Override
+    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
+        this.mRecyclerView = recyclerView;
+        super.attachToRecyclerView(recyclerView);
+    }
+
+    /**
+     * Returns a scroller specific to this {@code PagedSnapHelper}. This scroller is used for all
+     * smooth scrolling operations, including flings.
+     *
+     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+     * {@link
+     * RecyclerView}.
+     * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
+     */
+    @Override
+    protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
+        return new PagedSmoothScroller(mContext);
+    }
+
+    /**
+     * Calculate the estimated scroll distance in each direction given velocities on both axes. This
+     * method will clamp the maximum scroll distance so that a single fling will never scroll more
+     * than one page.
+     *
+     * @param velocityX Fling velocity on the horizontal axis.
+     * @param velocityY Fling velocity on the vertical axis.
+     * @return An array holding the calculated distances in x and y directions respectively.
+     */
+    @Override
+    public int[] calculateScrollDistance(int velocityX, int velocityY) {
+        int[] outDist = super.calculateScrollDistance(velocityX, velocityY);
+
+        if (mRecyclerView == null) {
+            return outDist;
+        }
+
+        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+        if (layoutManager == null || layoutManager.getChildCount() == 0) {
+            return outDist;
+        }
+
+        int lastChildPosition = isAtEnd(layoutManager) ? 0 : layoutManager.getChildCount() - 1;
+
+        OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+        View lastChild = layoutManager.getChildAt(lastChildPosition);
+        float percentageVisible = getPercentageVisible(lastChild, orientationHelper);
+
+        int maxDistance = layoutManager.getHeight();
+        if (percentageVisible > 0.f) {
+            // The max and min distance is the total height of the RecyclerView minus the height of
+            // the last child. This ensures that each scroll will never scroll more than a single
+            // page on the RecyclerView. That is, the max scroll will make the last child the
+            // first child and vice versa when scrolling the opposite way.
+            maxDistance -= layoutManager.getDecoratedMeasuredHeight(lastChild);
+        }
+
+        int minDistance = -maxDistance;
+
+        outDist[0] = clamp(outDist[0], minDistance, maxDistance);
+        outDist[1] = clamp(outDist[1], minDistance, maxDistance);
+
+        return outDist;
+    }
+
+    /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+    boolean isAtStart(RecyclerView.LayoutManager layoutManager) {
+        if (layoutManager == null || layoutManager.getChildCount() == 0) {
+            return true;
+        }
+
+        View firstChild = layoutManager.getChildAt(0);
+        OrientationHelper orientationHelper =
+                layoutManager.canScrollVertically()
+                        ? getVerticalHelper(layoutManager)
+                        : getHorizontalHelper(layoutManager);
+
+        // Check that the first child is completely visible and is the first item in the list.
+        return orientationHelper.getDecoratedStart(firstChild)
+                >= orientationHelper.getStartAfterPadding()
+                && layoutManager.getPosition(firstChild) == 0;
+    }
+
+    /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+    public boolean isAtEnd(RecyclerView.LayoutManager layoutManager) {
+        if (layoutManager == null || layoutManager.getChildCount() == 0) {
+            return true;
+        }
+
+        int childCount = layoutManager.getChildCount();
+        OrientationHelper orientationHelper =
+                layoutManager.canScrollVertically()
+                        ? getVerticalHelper(layoutManager)
+                        : getHorizontalHelper(layoutManager);
+
+        View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
+
+        // The list has reached the bottom if the last child that is visible is the last item
+        // in the list and it's fully shown.
+        return layoutManager.getPosition(lastVisibleChild) == (layoutManager.getItemCount() - 1)
+                && layoutManager.getDecoratedBottom(lastVisibleChild)
+                <= orientationHelper.getEndAfterPadding();
+    }
+
+    /**
+     * Returns an {@link OrientationHelper} that corresponds to the current scroll direction of the
+     * given {@link RecyclerView.LayoutManager}.
+     */
+    @NonNull
+    private OrientationHelper getOrientationHelper(
+            @NonNull RecyclerView.LayoutManager layoutManager) {
+        return layoutManager.canScrollVertically()
+                ? getVerticalHelper(layoutManager)
+                : getHorizontalHelper(layoutManager);
+    }
+
+    @NonNull
+    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
+        if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
+            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
+        }
+        return mVerticalHelper;
+    }
+
+    @NonNull
+    private OrientationHelper getHorizontalHelper(
+            @NonNull RecyclerView.LayoutManager layoutManager) {
+        if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
+            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
+        }
+        return mHorizontalHelper;
+    }
+
+    /**
+     * Ensures that the given value falls between the range given by the min and max values. This
+     * method does not check that the min value is greater than or equal to the max value. If the
+     * parameters are not well-formed, this method's behavior is undefined.
+     *
+     * @param value The value to clamp.
+     * @param min The minimum value the given value can be.
+     * @param max The maximum value the given value can be.
+     * @return A number that falls between {@code min} or {@code max} or one of those values if the
+     * given value is less than or greater than {@code min} and {@code max} respectively.
+     */
+    private static int clamp(int value, int min, int max) {
+        return Math.max(min, Math.min(max, value));
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/ScrollBar.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/ScrollBar.java
new file mode 100644
index 0000000..40a673b
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/ScrollBar.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.pagedrecyclerview.PagedRecyclerView.ScrollBarPosition;
+
+/**
+ * An abstract class that defines required contract for a custom scroll bar for the {@link
+ * PagedRecyclerView}. All custom scroll bar must inherit from this class.
+ */
+public interface ScrollBar {
+    /**
+     * The concrete class should implement this method to initialize configuration of a scrollbar
+     * view.
+     */
+    void initialize(
+            RecyclerView recyclerView,
+            int scrollBarContainerWidth,
+            @ScrollBarPosition int scrollBarPosition,
+            boolean scrollBarAboveRecyclerView);
+
+    /**
+     * Requests layout of the scrollbar. Should be called when there's been a change that will
+     * affect
+     * the size of the scrollbar view.
+     */
+    void requestLayout();
+
+    /** Sets the padding of the scrollbar, relative to the padding of the RecyclerView. */
+    void setPadding(int padddingStart, int paddingEnd);
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridDividerItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridDividerItemDecoration.java
new file mode 100644
index 0000000..adf9f28
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridDividerItemDecoration.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview.decorations.grid;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/** Adds interior dividers to a RecyclerView with a GridLayoutManager. */
+public class GridDividerItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final Drawable mHorizontalDivider;
+    private final Drawable mVerticalDivider;
+    private int mNumColumns;
+
+    /**
+     * Sole constructor. Takes in {@link Drawable} objects to be used as horizontal and vertical
+     * dividers.
+     *
+     * @param horizontalDivider A divider {@code Drawable} to be drawn on the rows of the grid of
+     * the
+     * RecyclerView
+     * @param verticalDivider A divider {@code Drawable} to be drawn on the columns of the grid of
+     * the
+     * RecyclerView
+     * @param numColumns The number of columns in the grid of the RecyclerView
+     */
+    public GridDividerItemDecoration(
+            Drawable horizontalDivider, Drawable verticalDivider, int numColumns) {
+        this.mHorizontalDivider = horizontalDivider;
+        this.mVerticalDivider = verticalDivider;
+        this.mNumColumns = numColumns;
+    }
+
+    /**
+     * Draws horizontal and/or vertical dividers onto the parent RecyclerView.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
+        drawVerticalDividers(canvas, parent);
+        drawHorizontalDividers(canvas, parent);
+    }
+
+    /**
+     * Determines the size and location of offsets between items in the parent RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        outRect.set(
+                0, 0, mHorizontalDivider.getIntrinsicWidth(),
+                mHorizontalDivider.getIntrinsicHeight());
+    }
+
+    /**
+     * Adds horizontal dividers to a RecyclerView with a GridLayoutManager or its subclass.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     */
+    private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) {
+        int childCount = parent.getChildCount();
+        int rowCount = childCount / mNumColumns;
+        int lastRowChildCount = childCount % mNumColumns;
+
+        for (int i = 1; i < mNumColumns; i++) {
+            int lastRowChildIndex;
+            if (i < lastRowChildCount) {
+                lastRowChildIndex = i + (rowCount * mNumColumns);
+            } else {
+                lastRowChildIndex = i + ((rowCount - 1) * mNumColumns);
+            }
+
+            View firstRowChild = parent.getChildAt(i);
+            View lastRowChild = parent.getChildAt(lastRowChildIndex);
+
+            int dividerTop = firstRowChild.getTop();
+            int dividerRight = firstRowChild.getLeft();
+            int dividerLeft = dividerRight - mHorizontalDivider.getIntrinsicWidth();
+            int dividerBottom = lastRowChild.getBottom();
+
+            mHorizontalDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
+            mHorizontalDivider.draw(canvas);
+        }
+    }
+
+    public void setNumOfColumns(int numberOfColumns) {
+        mNumColumns = numberOfColumns;
+    }
+
+    /**
+     * Adds vertical dividers to a RecyclerView with a GridLayoutManager or its subclass.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     */
+    private void drawVerticalDividers(Canvas canvas, RecyclerView parent) {
+        double childCount = parent.getChildCount();
+        double rowCount = Math.ceil(childCount / mNumColumns);
+        int rightmostChildIndex;
+        for (int i = 1; i <= rowCount; i++) {
+            // we dont want the divider on top of first row.
+            if (i == 1) {
+                continue;
+            }
+            if (i == rowCount) {
+                rightmostChildIndex = ((i - 1) * mNumColumns) - 1;
+            } else {
+                rightmostChildIndex = (i * mNumColumns) - 1;
+            }
+
+            View leftmostChild = parent.getChildAt(mNumColumns * (i - 1));
+            View rightmostChild = parent.getChildAt(rightmostChildIndex);
+
+            // draws on top of each row.
+            int dividerLeft = leftmostChild.getLeft();
+            int dividerBottom = leftmostChild.getTop();
+            int dividerTop = dividerBottom - mVerticalDivider.getIntrinsicHeight();
+            int dividerRight = rightmostChild.getRight();
+
+            mVerticalDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
+            mVerticalDivider.draw(canvas);
+        }
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridOffsetItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridOffsetItemDecoration.java
new file mode 100644
index 0000000..e290763
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridOffsetItemDecoration.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview.decorations.grid;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+
+/** Adds an offset to the top of a RecyclerView with a GridLayoutManager or its subclass. */
+public class GridOffsetItemDecoration extends RecyclerView.ItemDecoration {
+
+    private int mOffsetPx;
+    private Drawable mOffsetDrawable;
+    private int mNumColumns;
+    @OffsetPosition
+    private final int mOffsetPosition;
+
+    /** The possible values for setScrollbarPosition. */
+    @IntDef({
+            OffsetPosition.START,
+            OffsetPosition.END,
+    })
+    @Retention(SOURCE)
+    public @interface OffsetPosition {
+        /** Position the offset to the start of the screen. */
+        int START = 0;
+
+        /** Position offset to the end of the screen. */
+        int END = 1;
+    }
+
+    /**
+     * Constructor that takes in the size of the offset to be added to the top of the RecyclerView.
+     *
+     * @param offsetPx The size of the offset to be added to the top of the RecyclerView in pixels
+     * @param numColumns The number of columns in the grid of the RecyclerView
+     * @param offsetPosition Position where offset needs to be applied.
+     */
+    public GridOffsetItemDecoration(int offsetPx, int numColumns, int offsetPosition) {
+        this.mOffsetPx = offsetPx;
+        this.mNumColumns = numColumns;
+        this.mOffsetPosition = offsetPosition;
+    }
+
+    /**
+     * Constructor that takes in a {@link Drawable} to be drawn at the top of the RecyclerView.
+     *
+     * @param offsetDrawable The {@code Drawable} to be added to the top of the RecyclerView
+     * @param numColumns The number of columns in the grid of the RecyclerView
+     */
+    public GridOffsetItemDecoration(Drawable offsetDrawable, int numColumns, int offsetPosition) {
+        this.mOffsetDrawable = offsetDrawable;
+        this.mNumColumns = numColumns;
+        this.mOffsetPosition = offsetPosition;
+    }
+
+    public void setNumOfColumns(int numberOfColumns) {
+        mNumColumns = numberOfColumns;
+    }
+
+    /**
+     * Determines the size and the location of the offset to be added to the top of the
+     * RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        super.getItemOffsets(outRect, view, parent, state);
+
+        if (mOffsetPosition == OffsetPosition.START) {
+            boolean childIsInTopRow = parent.getChildAdapterPosition(view) < mNumColumns;
+            if (childIsInTopRow) {
+                if (mOffsetPx > 0) {
+                    outRect.top = mOffsetPx;
+                } else if (mOffsetDrawable != null) {
+                    outRect.top = mOffsetDrawable.getIntrinsicHeight();
+                }
+            }
+            return;
+        }
+
+        int childCount = state.getItemCount();
+        int lastRowChildCount = getLastRowChildCount(childCount);
+
+        boolean childIsInBottomRow =
+                parent.getChildAdapterPosition(view) >= childCount - lastRowChildCount;
+        if (childIsInBottomRow) {
+            if (mOffsetPx > 0) {
+                outRect.bottom = mOffsetPx;
+            } else if (mOffsetDrawable != null) {
+                outRect.bottom = mOffsetDrawable.getIntrinsicHeight();
+            }
+        }
+    }
+
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        super.onDraw(c, parent, state);
+        if (mOffsetDrawable == null) {
+            return;
+        }
+
+        int parentLeft = parent.getPaddingLeft();
+        int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+        if (mOffsetPosition == OffsetPosition.START) {
+
+            int parentTop = parent.getPaddingTop();
+            int offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+
+            mOffsetDrawable.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom);
+            mOffsetDrawable.draw(c);
+            return;
+        }
+
+        int childCount = state.getItemCount();
+        int lastRowChildCount = getLastRowChildCount(childCount);
+
+        int offsetDrawableTop = 0;
+        int offsetDrawableBottom = 0;
+
+        for (int i = childCount - lastRowChildCount; i < childCount; i++) {
+            View child = parent.getChildAt(i);
+            offsetDrawableTop = child.getBottom();
+            offsetDrawableBottom = offsetDrawableTop + mOffsetDrawable.getIntrinsicHeight();
+        }
+
+        mOffsetDrawable.setBounds(parentLeft, offsetDrawableTop, parentRight, offsetDrawableBottom);
+        mOffsetDrawable.draw(c);
+    }
+
+    private int getLastRowChildCount(int itemCount) {
+        int lastRowChildCount = itemCount % mNumColumns;
+        if (lastRowChildCount == 0) {
+            lastRowChildCount = mNumColumns;
+        }
+
+        return lastRowChildCount;
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearDividerItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearDividerItemDecoration.java
new file mode 100644
index 0000000..b007cd3
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearDividerItemDecoration.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview.decorations.linear;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/** Adds interior dividers to a RecyclerView with a LinearLayoutManager or its subclass. */
+public class LinearDividerItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final Drawable mDivider;
+    private int mOrientation;
+
+    /**
+     * Sole constructor. Takes in a {@link Drawable} to be used as the interior
+     * chassis_pagedrecyclerview_divider.
+     *
+     * @param divider A chassis_pagedrecyclerview_divider {@code Drawable} to be drawn on the
+     * RecyclerView
+     */
+    public LinearDividerItemDecoration(Drawable divider) {
+        this.mDivider = divider;
+    }
+
+    /**
+     * Draws horizontal or vertical dividers onto the parent RecyclerView.
+     *
+     * @param canvas The {@link Canvas} onto which dividers will be drawn
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            drawHorizontalDividers(canvas, parent);
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            drawVerticalDividers(canvas, parent);
+        }
+    }
+
+    /**
+     * Determines the size and location of offsets between items in the parent RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        super.getItemOffsets(outRect, view, parent, state);
+
+        if (parent.getChildAdapterPosition(view) == 0) {
+            return;
+        }
+
+        mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation();
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            outRect.left = mDivider.getIntrinsicWidth();
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            outRect.top = mDivider.getIntrinsicHeight();
+        }
+    }
+
+    /**
+     * Adds dividers to a RecyclerView with a LinearLayoutManager or its subclass oriented
+     * horizontally.
+     *
+     * @param canvas The {@link Canvas} onto which horizontal dividers will be drawn
+     * @param parent The RecyclerView onto which horizontal dividers are being added
+     */
+    private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) {
+        int parentTop = parent.getPaddingTop();
+        int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            View child = parent.getChildAt(i);
+
+            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
+
+            int parentLeft = child.getRight() + params.rightMargin;
+            int parentRight = parentLeft + mDivider.getIntrinsicWidth();
+
+            mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom);
+            mDivider.draw(canvas);
+        }
+    }
+
+    /**
+     * Adds dividers to a RecyclerView with a LinearLayoutManager or its subclass oriented
+     * vertically.
+     *
+     * @param canvas The {@link Canvas} onto which vertical dividers will be drawn
+     * @param parent The RecyclerView onto which vertical dividers are being added
+     */
+    private void drawVerticalDividers(Canvas canvas, RecyclerView parent) {
+        int parentLeft = parent.getPaddingLeft();
+        int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount - 1; i++) {
+            View child = parent.getChildAt(i);
+
+            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
+
+            int parentTop = child.getBottom() + params.bottomMargin;
+            int parentBottom = parentTop + mDivider.getIntrinsicHeight();
+
+            mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom);
+            mDivider.draw(canvas);
+        }
+    }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearOffsetItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearOffsetItemDecoration.java
new file mode 100644
index 0000000..f011843
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearOffsetItemDecoration.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.car.chassis.pagedrecyclerview.decorations.linear;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Adds an offset to the start of a RecyclerView using a LinearLayoutManager or its subclass.
+ *
+ * <p>If the RecyclerView.LayoutManager is oriented vertically, the offset will be added to the top
+ * of the RecyclerView. If the LayoutManager is oriented horizontally, the offset will be added to
+ * the left of the RecyclerView.
+ */
+public class LinearOffsetItemDecoration extends RecyclerView.ItemDecoration {
+
+    private int mOffsetPx;
+    private Drawable mOffsetDrawable;
+    private int mOrientation;
+    @OffsetPosition
+    private int mOffsetPosition;
+
+    /** The possible values for setScrollbarPosition. */
+    @IntDef({
+            OffsetPosition.START,
+            OffsetPosition.END,
+    })
+    @Retention(SOURCE)
+    public @interface OffsetPosition {
+        /** Position the offset to the start of the screen. */
+        int START = 0;
+
+        /** Position offset to the end of the screen. */
+        int END = 1;
+    }
+
+    /**
+     * Constructor that takes in the size of the offset to be added to the start of the
+     * RecyclerView.
+     *
+     * @param offsetPx The size of the offset to be added to the start of the RecyclerView in pixels
+     * @param offsetPosition Position where offset needs to be applied.
+     */
+    public LinearOffsetItemDecoration(int offsetPx, int offsetPosition) {
+        this.mOffsetPx = offsetPx;
+        this.mOffsetPosition = offsetPosition;
+    }
+
+    /**
+     * Constructor that takes in a {@link Drawable} to be drawn at the start of the RecyclerView.
+     *
+     * @param offsetDrawable The {@code Drawable} to be added to the start of the RecyclerView
+     */
+    public LinearOffsetItemDecoration(Drawable offsetDrawable) {
+        this.mOffsetDrawable = offsetDrawable;
+    }
+
+    /**
+     * Determines the size and location of the offset to be added to the start of the RecyclerView.
+     *
+     * @param outRect The {@link Rect} of offsets to be added around the child view
+     * @param view The child view to be decorated with an offset
+     * @param parent The RecyclerView onto which dividers are being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void getItemOffsets(
+            Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        super.getItemOffsets(outRect, view, parent, state);
+
+        if (mOffsetPosition == OffsetPosition.START && parent.getChildAdapterPosition(view) > 0) {
+            return;
+        }
+
+        int itemCount = state.getItemCount();
+        if (mOffsetPosition == OffsetPosition.END
+                && parent.getChildAdapterPosition(view) != itemCount - 1) {
+            return;
+        }
+
+        mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation();
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            if (mOffsetPx > 0) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.left = mOffsetPx;
+                } else {
+                    outRect.right = mOffsetPx;
+                }
+            } else if (mOffsetDrawable != null) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.left = mOffsetDrawable.getIntrinsicWidth();
+                } else {
+                    outRect.right = mOffsetDrawable.getIntrinsicWidth();
+                }
+            }
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            if (mOffsetPx > 0) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.top = mOffsetPx;
+                } else {
+                    outRect.bottom = mOffsetPx;
+                }
+            } else if (mOffsetDrawable != null) {
+                if (mOffsetPosition == OffsetPosition.START) {
+                    outRect.top = mOffsetDrawable.getIntrinsicHeight();
+                } else {
+                    outRect.bottom = mOffsetDrawable.getIntrinsicHeight();
+                }
+            }
+        }
+    }
+
+    /**
+     * Draws horizontal or vertical offset onto the start of the parent RecyclerView.
+     *
+     * @param c The {@link Canvas} onto which an offset will be drawn
+     * @param parent The RecyclerView onto which an offset is being added
+     * @param state The current RecyclerView.State of the RecyclerView
+     */
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        super.onDraw(c, parent, state);
+        if (mOffsetDrawable == null) {
+            return;
+        }
+
+        if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+            drawOffsetHorizontal(c, parent);
+        } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+            drawOffsetVertical(c, parent);
+        }
+    }
+
+    private void drawOffsetHorizontal(Canvas canvas, RecyclerView parent) {
+        int parentTop = parent.getPaddingTop();
+        int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+        int parentLeft = 0;
+        int offsetDrawableRight = 0;
+
+        if (mOffsetPosition == OffsetPosition.START) {
+            parentLeft = parent.getPaddingLeft();
+            offsetDrawableRight = parentLeft + mOffsetDrawable.getIntrinsicWidth();
+        } else {
+            View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+            RecyclerView.LayoutParams lastChildLayoutParams =
+                    (RecyclerView.LayoutParams) lastChild.getLayoutParams();
+            parentLeft = lastChild.getRight() + lastChildLayoutParams.rightMargin;
+            offsetDrawableRight = parentLeft + mOffsetDrawable.getIntrinsicWidth();
+        }
+
+        mOffsetDrawable.setBounds(parentLeft, parentTop, offsetDrawableRight, parentBottom);
+        mOffsetDrawable.draw(canvas);
+    }
+
+    private void drawOffsetVertical(Canvas canvas, RecyclerView parent) {
+        int parentLeft = parent.getPaddingLeft();
+        int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+        int parentTop = 0;
+        int offsetDrawableBottom = 0;
+
+        if (mOffsetPosition == OffsetPosition.START) {
+            parentTop = parent.getPaddingTop();
+            offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+        } else {
+            View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+            RecyclerView.LayoutParams lastChildLayoutParams =
+                    (RecyclerView.LayoutParams) lastChild.getLayoutParams();
+            parentTop = lastChild.getBottom() + lastChildLayoutParams.bottomMargin;
+            offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+        }
+
+        mOffsetDrawable.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom);
+        mOffsetDrawable.draw(canvas);
+    }
+}