| /* |
| * Copyright (C) 2018 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 androidx.car.app; |
| |
| import android.app.Dialog; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.os.Bundle; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver; |
| import android.view.Window; |
| import android.view.WindowManager; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.car.R; |
| import androidx.car.widget.DayNightStyle; |
| import androidx.car.widget.ListItem; |
| import androidx.car.widget.ListItemAdapter; |
| import androidx.car.widget.ListItemProvider; |
| import androidx.car.widget.PagedListView; |
| import androidx.car.widget.PagedScrollBarView; |
| import androidx.car.widget.TextListItem; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * A subclass of {@link Dialog} that is tailored for the car environment. This dialog can display a |
| * fixed list of items. There is no affordance for setting titles or any other text. |
| * |
| * <p>Its functionality is similar to if a list has been set on |
| * {@link androidx.appcompat.app.AlertDialog}, but is styled so that it is more appropriate for |
| * displaying in vehicles. |
| * |
| * <p>Note that this dialog cannot be created with an empty list. |
| */ |
| public class CarListDialog extends Dialog { |
| private static final String TAG = "CarListDialog"; |
| |
| private ListItemAdapter mAdapter; |
| private final int mInitialPosition; |
| private PagedListView mList; |
| private PagedScrollBarView mScrollBarView; |
| private final DialogInterface.OnClickListener mOnClickListener; |
| |
| /** Flag for if a touch on the scrim of the dialog will dismiss it. */ |
| private boolean mDismissOnTouchOutside; |
| |
| private final ViewTreeObserver.OnGlobalLayoutListener mLayoutListener = |
| new ViewTreeObserver.OnGlobalLayoutListener() { |
| @Override |
| public void onGlobalLayout() { |
| updateScrollbar(); |
| // Remove this listener because the listener for the scroll state will be |
| // enough to keep the scrollbar in sync. |
| mList.getViewTreeObserver().removeOnGlobalLayoutListener(this); |
| } |
| }; |
| |
| private CarListDialog(Context context, String[] items, int initialPosition, |
| OnClickListener listener) { |
| super(context, getDialogTheme(context)); |
| mInitialPosition = initialPosition; |
| mOnClickListener = listener; |
| initializeAdapter(items); |
| } |
| |
| @Override |
| public void setTitle(CharSequence title) { |
| // Ideally this method should not exist; the list dialog does not support a title. |
| // Unfortunately, this method is defined with the Dialog itself and is public. So, throw |
| // an error if this method is ever called. |
| throw new UnsupportedOperationException("Title is not supported in the CarListDialog"); |
| } |
| |
| /** |
| * @see super#setCanceledOnTouchOutside(boolean) |
| */ |
| @Override |
| public void setCanceledOnTouchOutside(boolean cancel) { |
| super.setCanceledOnTouchOutside(cancel); |
| // Need to override this method to save the value of cancel. |
| mDismissOnTouchOutside = cancel; |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| Window window = getWindow(); |
| window.setContentView(R.layout.car_list_dialog); |
| |
| // Ensure that the dialog takes up the entire window. This is needed because the scrollbar |
| // needs to be drawn off the dialog. |
| WindowManager.LayoutParams layoutParams = window.getAttributes(); |
| layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; |
| layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; |
| window.setAttributes(layoutParams); |
| |
| // The container for this dialog takes up the entire screen. As a result, need to manually |
| // listen for clicks and dismiss the dialog when necessary. |
| window.findViewById(R.id.container).setOnClickListener(v -> handleTouchOutside()); |
| |
| initializeList(); |
| initializeScrollbar(); |
| } |
| |
| @Override |
| protected void onStop() { |
| // Cleanup to ensure that no stray view observers are still attached. |
| if (mList != null) { |
| mList.getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener); |
| } |
| |
| super.onStop(); |
| } |
| |
| private void initializeList() { |
| mList = getWindow().findViewById(R.id.list); |
| mList.setMaxPages(PagedListView.UNLIMITED_PAGES); |
| mList.setAdapter(mAdapter); |
| |
| // The list will start at the 0 position, so no need to scroll. |
| if (mInitialPosition != 0) { |
| mList.snapToPosition(mInitialPosition); |
| } |
| |
| // Ensure that when the list is scrolled, the scrollbar updates to reflect the new position. |
| mList.getRecyclerView().addOnScrollListener(new RecyclerView.OnScrollListener() { |
| @Override |
| public void onScrolled(RecyclerView recyclerView, int dx, int dy) { |
| super.onScrolled(recyclerView, dx, dy); |
| updateScrollbar(); |
| } |
| }); |
| |
| // Update if the scrollbar should be visible after the PagedListView has finished |
| // laying itself out. This is needed because the only way to the state of scrollbar is to |
| // see the items after they have been laid out. |
| mList.getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener); |
| } |
| |
| /** |
| * Initializes the scrollbar that appears off the dialog. This scrollbar is not the one that |
| * usually appears with the PagedListView, but mimics it in functionality. |
| */ |
| private void initializeScrollbar() { |
| mScrollBarView = getWindow().findViewById(R.id.scrollbar); |
| mScrollBarView.setDayNightStyle(DayNightStyle.FORCE_NIGHT); |
| |
| mScrollBarView.setPaginationListener(new PagedScrollBarView.PaginationListener() { |
| @Override |
| public void onPaginate(int direction) { |
| switch (direction) { |
| case PagedScrollBarView.PaginationListener.PAGE_UP: |
| mList.pageUp(); |
| break; |
| case PagedScrollBarView.PaginationListener.PAGE_DOWN: |
| mList.pageDown(); |
| break; |
| default: |
| Log.e(TAG, "Unknown pagination direction (" + direction + ")"); |
| } |
| } |
| |
| @Override |
| public void onAlphaJump() { |
| } |
| }); |
| } |
| |
| /** |
| * Handles if a touch has been detected outside of the dialog. If |
| * {@link #mDismissOnTouchOutside} has been set, then the dialog will be dismissed. |
| */ |
| private void handleTouchOutside() { |
| if (mDismissOnTouchOutside) { |
| dismiss(); |
| } |
| } |
| |
| /** |
| * 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) { |
| 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); |
| } |
| |
| mAdapter = new ListItemAdapter(context, new ListItemProvider.ListProvider(listItems)); |
| } |
| |
| /** |
| * Check if a click listener has been set on this dialog and notify that a click has happened |
| * at the given item position, then dismisses this dialog. If no listener has been set, the |
| * dialog just dismisses. |
| */ |
| private void onItemClick(int position) { |
| if (mOnClickListener != null) { |
| mOnClickListener.onClick(this /* dialog */, position); |
| } |
| dismiss(); |
| } |
| |
| /** |
| * Determines if scrollbar should be visible or not and shows/hides it accordingly. |
| * |
| * <p>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. |
| * |
| * <p>If this is called after an adapter change but before the new layout, the visibility |
| * determination may not be correct. |
| */ |
| private void updateScrollbar() { |
| RecyclerView recyclerView = mList.getRecyclerView(); |
| RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); |
| |
| boolean isAtStart = mList.isAtStart(); |
| boolean isAtEnd = mList.isAtEnd(); |
| |
| if ((isAtStart && isAtEnd)) { |
| mScrollBarView.setVisibility(View.INVISIBLE); |
| return; |
| } |
| |
| mScrollBarView.setVisibility(View.VISIBLE); |
| mScrollBarView.setUpEnabled(!isAtStart); |
| mScrollBarView.setDownEnabled(!isAtEnd); |
| |
| // Assume the list scrolls vertically because we control the list and know the |
| // LayoutManager cannot change. |
| mScrollBarView.setParameters( |
| recyclerView.computeVerticalScrollRange(), |
| recyclerView.computeVerticalScrollOffset(), |
| recyclerView.computeVerticalScrollExtent(), |
| false /* animate */); |
| |
| getWindow().getDecorView().invalidate(); |
| } |
| |
| /** |
| * Returns the style that has been assigned to {@code carDialogTheme} in the |
| * current theme that is inflating this dialog. |
| */ |
| private static int getDialogTheme(Context context) { |
| TypedValue outValue = new TypedValue(); |
| context.getTheme().resolveAttribute(R.attr.carDialogTheme, outValue, true); |
| return outValue.resourceId; |
| } |
| |
| /** |
| * Builder class that can be used to create a {@link CarListDialog} by configuring the |
| * options for the list and behavior of the dialog. |
| */ |
| public static final class Builder { |
| private final Context mContext; |
| private int mInitialPosition; |
| private String[] mItems; |
| private DialogInterface.OnClickListener mOnClickListener; |
| |
| private boolean mCancelable = true; |
| private OnCancelListener mOnCancelListener; |
| private OnDismissListener mOnDismissListener; |
| |
| /** |
| * Creates a new instance of the {@code Builder}. |
| * |
| * @param context The {@code Context} that the dialog is to be created in. |
| */ |
| public Builder(Context context) { |
| mContext = context; |
| } |
| |
| /** |
| * Sets the items that should appear in the list. The dialog will automatically dismiss |
| * itself when an item in the list is clicked on. |
| * |
| * <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 items cannot be {@code null} or empty. Passing an empty list |
| * to this method will throw can exception. |
| * |
| * @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. |
| */ |
| public Builder setItems(@NonNull String[] items, |
| @Nullable OnClickListener onClickListener) { |
| if (items == null || items.length == 0) { |
| throw new IllegalArgumentException("Provided list of items cannot be empty."); |
| } |
| |
| mItems = items; |
| 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. |
| * |
| * @param initialPosition The initial position in the list to display. |
| * @return This {@code Builder} object to allow for chaining of calls. |
| */ |
| public Builder setInitialPosition(int initialPosition) { |
| if (initialPosition < 0) { |
| throw new IllegalArgumentException("Initial position cannot be negative."); |
| } |
| mInitialPosition = initialPosition; |
| return this; |
| } |
| |
| /** |
| * Sets whether the dialog is cancelable or not. Default is {@code true}. |
| * |
| * @return This {@code Builder} object to allow for chaining of calls. |
| */ |
| public Builder setCancelable(boolean cancelable) { |
| mCancelable = cancelable; |
| return this; |
| } |
| |
| /** |
| * Sets the callback that will be called if the dialog is canceled. |
| * |
| * <p>Even in a cancelable dialog, the dialog may be dismissed for reasons other than |
| * being canceled or one of the supplied choices being selected. |
| * If you are interested in listening for all cases where the dialog is dismissed |
| * and not just when it is canceled, see {@link #setOnDismissListener(OnDismissListener)}. |
| * |
| * @param onCancelListener The listener to be invoked when this dialog is canceled. |
| * @return This {@code Builder} object to allow for chaining of calls. |
| * |
| * @see #setCancelable(boolean) |
| * @see #setOnDismissListener(OnDismissListener) |
| */ |
| public Builder setOnCancelListener(OnCancelListener onCancelListener) { |
| mOnCancelListener = onCancelListener; |
| return this; |
| } |
| |
| /** |
| * Sets the callback that will be called when the dialog is dismissed for any reason. |
| * |
| * @return This {@code Builder} object to allow for chaining of calls. |
| */ |
| public Builder setOnDismissListener(OnDismissListener onDismissListener) { |
| mOnDismissListener = onDismissListener; |
| return this; |
| } |
| |
| /** |
| * Creates an {@link CarListDialog} with the arguments supplied to this {@code Builder}. |
| * |
| * <p>If {@link #setItems(String[],DialogInterface.OnClickListener)} is never called, then |
| * calling this method will throw an exception. |
| * |
| * <p>Calling this method does not display the dialog. Utilize this dialog within a |
| * {@link androidx.fragment.app.DialogFragment} to show the dialog. |
| */ |
| public CarListDialog create() { |
| if (mItems == null || mItems.length == 0) { |
| throw new IllegalStateException( |
| "CarListDialog must be created with a non-empty list."); |
| } |
| |
| if (mInitialPosition >= mItems.length) { |
| throw new IllegalStateException("Initial position is greater than the number of " |
| + "items in the list."); |
| } |
| |
| CarListDialog dialog = new CarListDialog( |
| mContext, |
| mItems, |
| mInitialPosition, |
| mOnClickListener); |
| |
| dialog.setCancelable(mCancelable); |
| dialog.setCanceledOnTouchOutside(mCancelable); |
| dialog.setOnCancelListener(mOnCancelListener); |
| dialog.setOnDismissListener(mOnDismissListener); |
| |
| return dialog; |
| } |
| } |
| } |