Add support for sections in CarListDialog.
A section has a title and list of items associated with it. To pass this
information to the CarListDialog, abstract it with a struct class.
Fixes: 77823219
Test: Test on sample application with sections.
Test: ./gradlew car:connectedCheck
Change-Id: I1f9dcf622d52d4fdd23987bbeecbc9be21139db0
diff --git a/car/api/current.txt b/car/api/current.txt
index c02a34d..0425614 100644
--- a/car/api/current.txt
+++ b/car/api/current.txt
@@ -28,12 +28,20 @@
method public androidx.car.app.CarListDialog.Builder setCancelable(boolean);
method public androidx.car.app.CarListDialog.Builder setInitialPosition(int);
method public androidx.car.app.CarListDialog.Builder setItems(java.lang.String[], android.content.DialogInterface.OnClickListener);
+ method public androidx.car.app.CarListDialog.Builder setItems(androidx.car.app.CarListDialog.DialogSubSection[], android.content.DialogInterface.OnClickListener);
method public androidx.car.app.CarListDialog.Builder setOnCancelListener(android.content.DialogInterface.OnCancelListener);
method public androidx.car.app.CarListDialog.Builder setOnDismissListener(android.content.DialogInterface.OnDismissListener);
method public androidx.car.app.CarListDialog.Builder setTitle(int);
method public androidx.car.app.CarListDialog.Builder setTitle(java.lang.CharSequence);
}
+ public static class CarListDialog.DialogSubSection {
+ ctor public CarListDialog.DialogSubSection(java.lang.String, java.lang.String[]);
+ method public int getItemCount();
+ method public java.lang.String[] getItems();
+ method public java.lang.String getTitle();
+ }
+
}
package androidx.car.drawer {
diff --git a/car/src/main/java/androidx/car/app/CarListDialog.java b/car/src/main/java/androidx/car/app/CarListDialog.java
index 6722ff0..6767890 100644
--- a/car/src/main/java/androidx/car/app/CarListDialog.java
+++ b/car/src/main/java/androidx/car/app/CarListDialog.java
@@ -41,6 +41,7 @@
import androidx.car.widget.ListItemProvider;
import androidx.car.widget.PagedListView;
import androidx.car.widget.PagedScrollBarView;
+import androidx.car.widget.SubheaderListItem;
import androidx.car.widget.TextListItem;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -97,7 +98,12 @@
mTitle = builder.mTitle;
mTitleElevation =
context.getResources().getDimension(R.dimen.car_list_dialog_title_elevation);
- initializeAdapter(builder.mItems);
+
+ if (builder.mSections != null) {
+ initializeWithSections(builder.mSections);
+ } else {
+ initializeWithItems(builder.mItems);
+ }
}
@Override
@@ -210,6 +216,7 @@
mList = getWindow().findViewById(R.id.list);
mList.setMaxPages(PagedListView.UNLIMITED_PAGES);
mList.setAdapter(mAdapter);
+ mList.setDividerVisibilityManager(mAdapter);
// The list will start at the 0 position, so no need to scroll.
if (mInitialPosition != 0) {
@@ -274,22 +281,57 @@
* Initializes {@link #mAdapter} to display the items in the given array. It utilizes the
* {@link TextListItem} but only populates the title field with the the values in the array.
*/
- private void initializeAdapter(String[] items) {
+ private void initializeWithItems(String[] items) {
Context context = getContext();
List<ListItem> listItems = new ArrayList<>();
for (int i = 0; i < items.length; i++) {
- TextListItem item = new TextListItem(getContext());
- item.setTitle(items[i]);
-
- // Save the position to pass to onItemClick().
- final int position = i;
- item.setOnClickListener(v -> onItemClick(position));
-
- listItems.add(item);
+ listItems.add(createItem(/* text= */ items[i], /* position= */ i));
}
mAdapter = new ListItemAdapter(context, new ListItemProvider.ListProvider(listItems));
+
+ }
+
+ /**
+ * Initializes the {@link #mAdapter} to display the sections in the given array. It utilizes
+ * the {@link SubheaderListItem} to display the section title and {@link TextListItem} to
+ * display the individual items of a section.
+ */
+ private void initializeWithSections(DialogSubSection[] sections) {
+ Context context = getContext();
+ List<ListItem> listItems = new ArrayList<>();
+
+ for (DialogSubSection section : sections) {
+ SubheaderListItem header = new SubheaderListItem(getContext(), section.getTitle());
+ header.setHideDivider(true);
+
+ listItems.add(header);
+
+ String[] items = section.getItems();
+ // Now initialize all the items associated with this subsection.
+ for (int i = 0, length = items.length; i < length; i++) {
+ listItems.add(createItem(/* text= */ items[i], /* position= */ i));
+ }
+ }
+
+ mAdapter = new ListItemAdapter(context, new ListItemProvider.ListProvider(listItems));
+ }
+
+ /**
+ * Creates the {@link TextListItem} that represents an item in the {@code CarListDialog}.
+ *
+ * @param text The text to display as the title in {@code TextListItem}.
+ * @param position The position of the item in the list.
+ */
+ private TextListItem createItem(String text, int position) {
+ TextListItem item = new TextListItem(getContext());
+ item.setTitle(text);
+
+ // Save the position to pass to onItemClick().
+ item.setOnClickListener(v -> onItemClick(position));
+
+ return item;
}
/**
@@ -299,7 +341,7 @@
*/
private void onItemClick(int position) {
if (mOnClickListener != null) {
- mOnClickListener.onClick(this /* dialog */, position);
+ mOnClickListener.onClick(/* dialog= */ this, position);
}
dismiss();
}
@@ -316,7 +358,6 @@
*/
private void updateScrollbar() {
RecyclerView recyclerView = mList.getRecyclerView();
- RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
boolean isAtStart = mList.isAtStart();
boolean isAtEnd = mList.isAtEnd();
@@ -336,7 +377,7 @@
recyclerView.computeVerticalScrollRange(),
recyclerView.computeVerticalScrollOffset(),
recyclerView.computeVerticalScrollExtent(),
- false /* animate */);
+ /* animate= */ false);
getWindow().getDecorView().invalidate();
}
@@ -352,6 +393,60 @@
}
/**
+ * A struct that holds data for a section. A section is a combination of the section title and
+ * the list of items associated with that section.
+ */
+ public static class DialogSubSection {
+ private final String mTitle;
+ private final String[] mItems;
+
+ /**
+ * Creates a subsection.
+ *
+ * @param title The title of the section. Must be non-empty.
+ * @param items A list of items associated with this section. This list cannot be
+ * {@code null} or empty.
+ */
+ public DialogSubSection(@NonNull String title, @NonNull String[] items) {
+ if (TextUtils.isEmpty(title)) {
+ throw new IllegalArgumentException("Title cannot be empty.");
+ }
+
+ if (items == null || items.length == 0) {
+ throw new IllegalArgumentException("Items cannot be empty.");
+ }
+
+ mTitle = title;
+ mItems = items;
+ }
+
+ /** Returns the section title. */
+ @NonNull
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /** Returns the section items. */
+ @NonNull
+ public String[] getItems() {
+ return mItems;
+ }
+
+ /**
+ * Returns the total number of items related to this section. The length of the section is
+ * defined as the number of items plus an entry for the title of the section.
+ *
+ * <p>This value will always be greater than 0 due to the fact that the title must always
+ * be specified and the number of items passed to this {@code DialogSubSection} should
+ * always be greater than 0.
+ */
+ public int getItemCount() {
+ // Adding 1 to the length for account for the title.
+ return mItems.length + 1;
+ }
+ }
+
+ /**
* Builder class that can be used to create a {@link CarListDialog} by configuring the
* options for the list and behavior of the dialog.
*/
@@ -361,6 +456,7 @@
private CharSequence mTitle;
private int mInitialPosition;
private String[] mItems;
+ private DialogSubSection[] mSections;
private DialogInterface.OnClickListener mOnClickListener;
private boolean mCancelable = true;
@@ -382,6 +478,7 @@
* @param titleId The resource id of the string to be used as the title.
* @return This {@code Builder} object to allow for chaining of calls.
*/
+ @NonNull
public Builder setTitle(@StringRes int titleId) {
mTitle = mContext.getString(titleId);
return this;
@@ -393,6 +490,7 @@
* @param title The string to be used as the title.
* @return This {@code Builder} object to allow for chaining of calls.
*/
+ @NonNull
public Builder setTitle(CharSequence title) {
mTitle = title;
return this;
@@ -411,10 +509,15 @@
* <p>The provided list of items cannot be {@code null} or empty. Passing an empty list
* to this method will throw can exception.
*
+ * <p>If both this method and {@link #setItems(DialogSubSection[], OnClickListener)} are
+ * called, then the sections will take precedent, and the items set via this method will
+ * be ignored.
+ *
* @param items The items that will appear in the list.
* @param onClickListener The listener that will be notified of a click.
* @return This {@code Builder} object to allow for chaining of calls.
*/
+ @NonNull
public Builder setItems(@NonNull String[] items,
@Nullable OnClickListener onClickListener) {
if (items == null || items.length == 0) {
@@ -427,15 +530,56 @@
}
/**
+ * Sets the items that should appear in the list, divided into sections. Each section has a
+ * title and a list of items associated with it. The dialog will automatically dismiss
+ * itself when an item in the list is clicked on; the title of the section is not
+ * clickable.
+ *
+ * <p>If a {@link DialogInterface.OnClickListener} is given, then it will be notified
+ * of the click. The dialog will still be dismissed afterwards. The {@code which}
+ * parameter of the {@link DialogInterface.OnClickListener#onClick(DialogInterface, int)}
+ * method will be the position of the item. This position maps to the index of the item in
+ * the given list.
+ *
+ * <p>The provided list of sections cannot be {@code null} or empty. The list of items
+ * within a section also cannot be empty. Passing an empty list to this method will
+ * throw can exception.
+ *
+ * <p>If both this method and {@link #setItems(String[], OnClickListener)} are called, then
+ * the sections will take precedent, and the items set via the other method will be
+ * ignored.
+ *
+ * @param sections The sections that will appear in the list.
+ * @param onClickListener The listener that will be notified of a click.
+ * @return This {@code Builder} object to allow for chaining of calls.
+ */
+ @NonNull
+ public Builder setItems(@NonNull DialogSubSection[] sections,
+ @Nullable OnClickListener onClickListener) {
+ if (sections == null || sections.length == 0) {
+ throw new IllegalArgumentException("Provided list of sections cannot be empty.");
+ }
+
+ mSections = sections;
+ mOnClickListener = onClickListener;
+ return this;
+ }
+
+ /**
* Sets the initial position in the list that the {@code CarListDialog} will start at. When
* the dialog is created, the list will animate to the given position.
*
* <p>The position uses zero-based indexing. So, to scroll to the fifth item in the list,
* a value of four should be passed.
*
+ * <p>If the items in this dialog was set by
+ * {@link #setItems(DialogSubSection[], OnClickListener)}, then note that the title of the
+ * section counts as an item in the list.
+ *
* @param initialPosition The initial position in the list to display.
* @return This {@code Builder} object to allow for chaining of calls.
*/
+ @NonNull
public Builder setInitialPosition(int initialPosition) {
if (initialPosition < 0) {
throw new IllegalArgumentException("Initial position cannot be negative.");
@@ -449,6 +593,7 @@
*
* @return This {@code Builder} object to allow for chaining of calls.
*/
+ @NonNull
public Builder setCancelable(boolean cancelable) {
mCancelable = cancelable;
return this;
@@ -468,6 +613,7 @@
* @see #setCancelable(boolean)
* @see #setOnDismissListener(OnDismissListener)
*/
+ @NonNull
public Builder setOnCancelListener(OnCancelListener onCancelListener) {
mOnCancelListener = onCancelListener;
return this;
@@ -478,6 +624,7 @@
*
* @return This {@code Builder} object to allow for chaining of calls.
*/
+ @NonNull
public Builder setOnDismissListener(OnDismissListener onDismissListener) {
mOnDismissListener = onDismissListener;
return this;
@@ -493,12 +640,28 @@
* {@link androidx.fragment.app.DialogFragment} to show the dialog.
*/
public CarListDialog create() {
- if (mItems == null || mItems.length == 0) {
+ // Check that the dialog was created with a list of either sections or items.
+ if ((mSections == null || mSections.length == 0)
+ && (mItems == null || mItems.length == 0)) {
throw new IllegalStateException(
- "CarListDialog must be created with a non-empty list.");
+ "CarListDialog cannot be created with a non-empty list.");
}
- if (mInitialPosition >= mItems.length) {
+ int numOfItems = 0;
+
+ // Subsections take precedent over items as both cannot be set at the same time.
+ if (mSections != null) {
+ mItems = null;
+
+ // Calculate the total number of items by adding up all the sections.
+ for (DialogSubSection section : mSections) {
+ numOfItems += section.getItemCount();
+ }
+ } else {
+ numOfItems = mItems.length;
+ }
+
+ if (mInitialPosition >= numOfItems) {
throw new IllegalStateException("Initial position is greater than the number of "
+ "items in the list.");
}
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java
index 71aa104..20ebd8c 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/CarListDialogDemo.java
@@ -34,6 +34,7 @@
public class CarListDialogDemo extends FragmentActivity {
private static final String DIALOG_TAG = "list_dialog_tag";
+ private static final int DEFAULT_NUM_OF_SECTIONS = 0;
private static final int DEFAULT_NUM_OF_ITEMS = 4;
private static final int DEFAULT_INITIAL_POSITION = 0;
@@ -42,10 +43,16 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.list_dialog_activity);
+ EditText numOfSectionsEdit = findViewById(R.id.num_of_sections_edit);
EditText numOfItemsEdit = findViewById(R.id.num_of_items_edit);
EditText initialPositionEdit = findViewById(R.id.initial_position_edit);
findViewById(R.id.create_dialog).setOnClickListener(v -> {
+ CharSequence numOfSectionsText = numOfSectionsEdit.getText();
+ int numOfSections = TextUtils.isEmpty(numOfSectionsText)
+ ? DEFAULT_NUM_OF_SECTIONS
+ : Integer.parseInt(numOfSectionsText.toString());
+
CharSequence numOfItemsText = numOfItemsEdit.getText();
int numOfItems = TextUtils.isEmpty(numOfItemsText)
? DEFAULT_NUM_OF_ITEMS
@@ -58,6 +65,7 @@
ListDialogFragment alertDialog = ListDialogFragment.newInstance(
((CheckBox) findViewById(R.id.has_title)).isChecked(),
+ numOfSections,
numOfItems,
initialPosition);
@@ -68,13 +76,15 @@
/** A {@link DialogFragment} that will inflate a {@link CarListDialog}. */
public static class ListDialogFragment extends DialogFragment {
private static final String HAS_TITLE_KEY = "has_title_key";
+ private static final String NUM_OF_SECTIONS_KEY = "num_of_sections_key";
private static final String NUM_OF_ITEMS_KEY = "num_of_items_key";
private static final String INITIAL_POSITION_KEY = "initial_position_key";
- static ListDialogFragment newInstance(boolean hasTitle,
+ static ListDialogFragment newInstance(boolean hasTitle, int numOfSections,
int numOfItems, int initialPosition) {
Bundle args = new Bundle();
args.putBoolean(HAS_TITLE_KEY, hasTitle);
+ args.putInt(NUM_OF_SECTIONS_KEY, numOfSections);
args.putInt(NUM_OF_ITEMS_KEY, numOfItems);
args.putInt(INITIAL_POSITION_KEY, initialPosition);
@@ -87,9 +97,17 @@
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
CarListDialog.Builder builder = new CarListDialog.Builder(getContext())
- .setItems(getItems(), /* onClickListener= */ null)
.setInitialPosition(getArguments().getInt(INITIAL_POSITION_KEY));
+ int numOfSections = getArguments().getInt(NUM_OF_SECTIONS_KEY);
+ int numOfItems = getArguments().getInt(NUM_OF_ITEMS_KEY);
+
+ if (numOfSections != 0) {
+ builder.setItems(createSections(numOfSections, numOfItems), null);
+ } else {
+ builder.setItems(createItems(numOfItems), null);
+ }
+
if (getArguments().getBoolean(HAS_TITLE_KEY)) {
builder.setTitle(getContext().getString(R.string.list_dialog_title));
}
@@ -97,9 +115,20 @@
return builder.create();
}
- private String[] getItems() {
- int numOfItems = getArguments().getInt(NUM_OF_ITEMS_KEY);
+ private CarListDialog.DialogSubSection[] createSections(int numOfSections, int numOfItems) {
+ CarListDialog.DialogSubSection[] items =
+ new CarListDialog.DialogSubSection[numOfSections];
+ for (int i = 0; i < numOfSections; i++) {
+ items[i] = new CarListDialog.DialogSubSection(
+ /* title= */ "Section " + (i + 1),
+ createItems(numOfItems));
+ }
+
+ return items;
+ }
+
+ private String[] createItems(int numOfItems) {
String[] items = new String[numOfItems];
for (int i = 0; i < numOfItems; i++) {
items[i] = "Item " + (i + 1);
diff --git a/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml b/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml
index a1e9bbf..1b67ddc 100644
--- a/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml
+++ b/samples/SupportCarDemos/src/main/res/layout/list_dialog_activity.xml
@@ -35,6 +35,22 @@
android:text="@string/list_dialog_checkbox_title" />
<com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/num_of_sections"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_single_line_list_item_height"
+ android:hint="@string/list_dialog_num_of_sections_hint"
+ app:hintTextAppearance="@style/TextAppearance.Car.Hint">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/num_of_sections_edit"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="number"
+ android:maxLines="1"
+ android:textAppearance="@style/TextAppearance.Car.Body2"/>
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
android:id="@+id/num_of_items"
android:layout_width="match_parent"
android:layout_height="@dimen/car_single_line_list_item_height"
diff --git a/samples/SupportCarDemos/src/main/res/values/strings.xml b/samples/SupportCarDemos/src/main/res/values/strings.xml
index 2a66ffc..a7d6e8f 100644
--- a/samples/SupportCarDemos/src/main/res/values/strings.xml
+++ b/samples/SupportCarDemos/src/main/res/values/strings.xml
@@ -72,6 +72,7 @@
<!-- Strings for CarListDialog Demo. -->
<string name="list_dialog_checkbox_title">Title</string>
<string name="list_dialog_title">Title</string>
+ <string name="list_dialog_num_of_sections_hint">Number of Sections</string>
<string name="list_dialog_num_of_items_hint">Number of Items</string>
<string name="list_dialog_initial_position_hint">Initial Position</string>
<string name="create_list_dialog_button">Create CarListDialog</string>