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…</string>
+ <!-- CarUxRestrictions Utility -->
+ <string name="chassis_ellipsis" translatable="false">…</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);
+ }
+}