Implement CarUiListItem specific layout manager.

Bug: 143234489
Test: Manual test

Change-Id: I06d4899ba00a2e3533f94c968609c59634716bba
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java
new file mode 100644
index 0000000..c825052
--- /dev/null
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiListItemLayoutManager.java
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+package com.android.car.ui.recyclerview;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.ui.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link LinearLayoutManager} implementation which provides a fixed scrollbar when used with
+ * viewholders that vary in height. This layout manager is only compatible with {@link
+ * CarUiListItemAdapter}.
+ */
+public class CarUiListItemLayoutManager extends LinearLayoutManager {
+
+    private final int mContentItemSize;
+    private final int mHeaderItemSize;
+
+    private List<Integer> mListItemHeights;
+    private CarUiListItemAdapter mAdapter;
+
+    public CarUiListItemLayoutManager(@NonNull Context context) {
+        super(context);
+        mContentItemSize = (int) context.getResources().getDimension(
+                R.dimen.car_ui_list_item_height);
+        mHeaderItemSize = (int) context.getResources().getDimension(
+                R.dimen.car_ui_header_list_item_height);
+
+        setSmoothScrollbarEnabled(false);
+    }
+
+    @Override
+    public void onAttachedToWindow(RecyclerView recyclerView) {
+        super.onAttachedToWindow(recyclerView);
+        populateHeightMap(recyclerView.getAdapter());
+    }
+
+    @Override
+    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
+        super.onAdapterChanged(oldAdapter, newAdapter);
+        populateHeightMap(newAdapter);
+    }
+
+    @Override
+    public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, int itemCount) {
+        super.onItemsAdded(recyclerView, positionStart, itemCount);
+        populateHeightMap(recyclerView.getAdapter());
+    }
+
+    @Override
+    public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart,
+            int itemCount) {
+        super.onItemsRemoved(recyclerView, positionStart, itemCount);
+        populateHeightMap(recyclerView.getAdapter());
+    }
+
+    @Override
+    public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart,
+            int itemCount) {
+        super.onItemsUpdated(recyclerView, positionStart, itemCount);
+        populateHeightMap(recyclerView.getAdapter());
+    }
+
+    @Override
+    public void onItemsChanged(@NonNull RecyclerView recyclerView) {
+        super.onItemsChanged(recyclerView);
+        populateHeightMap(recyclerView.getAdapter());
+    }
+
+    @Override
+    public int computeVerticalScrollExtent(RecyclerView.State state) {
+        final int count = getChildCount();
+        return count > 0 ? mContentItemSize * 3 : 0;
+    }
+
+    @Override
+    public int computeVerticalScrollRange(RecyclerView.State state) {
+        return Math.max(mListItemHeights.get(mListItemHeights.size() - 1), 0);
+    }
+
+    @Override
+    public int computeVerticalScrollOffset(RecyclerView.State state) {
+        if (getChildCount() <= 0) {
+            return 0;
+        }
+
+        int firstPos = findFirstVisibleItemPosition();
+        if (firstPos == RecyclerView.NO_POSITION) {
+            return 0;
+        }
+
+        View view = findViewByPosition(firstPos);
+        if (view == null) {
+            return 0;
+        }
+
+        final int top = getDecoratedTop(view);
+        final int height = getDecoratedMeasuredHeight(view);
+
+        int heightOfScreen;
+        if (height <= 0) {
+            heightOfScreen = 0;
+        } else {
+            CarUiListItem item = mAdapter.getItems().get(firstPos);
+            if (item instanceof CarUiContentListItem) {
+                heightOfScreen = Math.abs(mContentItemSize * top / height);
+            } else if (item instanceof CarUiHeaderListItem) {
+                heightOfScreen = Math.abs(mHeaderItemSize * top / height);
+            } else {
+                throw new IllegalStateException("Unknown list item type.");
+            }
+        }
+
+        return (mListItemHeights.get(firstPos)) + heightOfScreen;
+    }
+
+    /**
+     * Populates an internal list of cumulative heights at each position for the list to be laid out
+     * for the adapter parameter.
+     *
+     * @param adapter the action type for the item.
+     */
+    private void populateHeightMap(RecyclerView.Adapter adapter) {
+        if (!(adapter instanceof CarUiListItemAdapter)) {
+            throw new IllegalStateException(
+                    "Cannot use CarUiListItemLayoutManager with an adapter that is not of type "
+                            + "CarUiListItemAdapter");
+        }
+
+        mAdapter = (CarUiListItemAdapter) adapter;
+        List<CarUiListItem> itemList = mAdapter.getItems();
+        mListItemHeights = new ArrayList<>();
+
+        int cumulativeHeight = 0;
+        mListItemHeights.add(cumulativeHeight);
+        for (CarUiListItem item : itemList) {
+            if (item instanceof CarUiContentListItem) {
+                cumulativeHeight += mContentItemSize;
+                mListItemHeights.add(cumulativeHeight);
+            } else if (item instanceof CarUiHeaderListItem) {
+                cumulativeHeight += mHeaderItemSize;
+                mListItemHeights.add(cumulativeHeight);
+            } else {
+                throw new IllegalStateException("Unknown CarUiListItem type");
+            }
+        }
+    }
+}
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
index 636b0ba..3b5cf7a 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/caruirecyclerview/CarUiListItemActivity.java
@@ -26,6 +26,7 @@
 import com.android.car.ui.recyclerview.CarUiHeaderListItem;
 import com.android.car.ui.recyclerview.CarUiListItem;
 import com.android.car.ui.recyclerview.CarUiListItemAdapter;
+import com.android.car.ui.recyclerview.CarUiListItemLayoutManager;
 import com.android.car.ui.recyclerview.CarUiRecyclerView;
 
 import java.util.ArrayList;
@@ -45,6 +46,7 @@
 
         CarUiListItemAdapter adapter = new CarUiListItemAdapter(generateDummyData());
         recyclerView.setAdapter(adapter);
+        recyclerView.setLayoutManager(new CarUiListItemLayoutManager(this));
     }
 
     private ArrayList<CarUiListItem> generateDummyData() {