blob: bc9458daaf208c4cbd430342f8da7438b4516924 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.autofill_assistant.payment;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayout;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.Space;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.ui.widget.ChromeImageView;
import java.util.ArrayList;
import java.util.List;
/**
* A widget that displays a list of choices in a regular grid. It is similar to a RadioGroup, but
* much more customizable, because it allows arbitrary content views rather than only TextViews.
*
* The layout is as follows:
* [radio-button] | content | [edit-button]
* [radio-button] | content | [edit-button]
* ...
* [add-icon] | [add-button] |
*
* A number of custom view attributes control the layout and look of this widget.
* - Edit-buttons are optional on an item-by-item basis.
* - The add-button at the end of the list is optional.
* - Spacing between rows and columns can be customized.
* - The text for the `add' and `edit' buttons can be customized.
*/
public class AssistantChoiceList extends GridLayout {
/**
* Represents a single choice with a radio button, customizable content and an edit button.
*/
private class Item {
final CompoundButton mCompoundButton;
final Callback<Boolean> mOnSelectedListener;
final View mContent;
final View mEditButton;
final View mSpacer;
Item(@Nullable CompoundButton compoundButton, @Nullable Callback onSelectedListener,
View content, @Nullable View editButton, @Nullable View spacer) {
this.mCompoundButton = compoundButton;
this.mOnSelectedListener = onSelectedListener;
this.mContent = content;
this.mEditButton = editButton;
this.mSpacer = spacer;
}
}
/**
* |mCanAddItems| is true if the custom view parameter |add_button_text| was specified.
* If true, the list will have an additional 'add' button at the end.
*/
private final boolean mCanAddItems;
/**
* |mAddButton| and |mAddButtonLabel| are guaranteed to be non-null if |mCanAddItems| is true.
*/
private final ChromeImageView mAddButton;
private final TextView mAddButtonLabel;
private final int mRowSpacing;
private final int mColumnSpacing;
private final int mAddButtonSpacing;
private final List<Item> mItems = new ArrayList<>();
private boolean mAllowMultipleChoices;
private Runnable mAddButtonListener;
public AssistantChoiceList(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.AssistantChoiceList, 0, 0);
mCanAddItems = a.getBoolean(R.styleable.AssistantChoiceList_can_add_items, true);
String addButtonText =
a.hasValue(R.styleable.AssistantChoiceList_add_button_text) && mCanAddItems
? a.getString(R.styleable.AssistantChoiceList_add_button_text)
: null;
mRowSpacing = a.getDimensionPixelSize(R.styleable.AssistantChoiceList_row_spacing, 0);
mColumnSpacing = a.getDimensionPixelSize(R.styleable.AssistantChoiceList_column_spacing, 0);
mAddButtonSpacing = context.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_choicelist_add_button_spacing);
a.recycle();
// One column for the radio buttons, one for the content, one for the edit buttons.
setColumnCount(3);
if (mCanAddItems) {
mAddButton = createAddButtonIcon();
mAddButtonLabel = createAddButtonLabel(addButtonText);
addViewInternal(mAddButton, -1, createRadioButtonLayoutParams());
GridLayout.LayoutParams lp =
new GridLayout.LayoutParams(GridLayout.spec(UNDEFINED), GridLayout.spec(1, 2));
lp.setGravity(Gravity.FILL_HORIZONTAL | Gravity.CENTER_VERTICAL);
lp.width = 0;
addViewInternal(mAddButtonLabel, -1, lp);
// Set margin to 0 because list is currently empty.
updateAddButtonMargins(0);
} else {
mAddButton = null;
mAddButtonLabel = null;
}
}
/**
* Set whether this list allows multiple choices to be selected at the same time. This method
* can only be called when no items have been added, otherwise it will throw an exception.
*/
public void setAllowMultipleChoices(boolean allowMultipleChoices) {
if (!mItems.isEmpty()) {
throw new UnsupportedOperationException(
"Calling #setAllowMultipleChoices is not allowed when items have already been "
+ "added.");
}
mAllowMultipleChoices = allowMultipleChoices;
}
/**
* Children of this container are automatically added as selectable items to the list.
*
* This method is automatically called by layout inflaters and xml files. In code, you usually
* want to call |addItem| directly.
*/
@Override
public void addView(View view, int index, ViewGroup.LayoutParams lp) {
assert index == -1;
addItem(view);
}
public void addItem(View view) {
addItem(view, true);
}
public void addItem(View view, boolean hasEditButton) {
addItem(view, hasEditButton, null, null);
}
/**
* Adds an item to the list. Additional widgets to select and edit the item are created as
* necessary.
*
* @param view The view to add to the list.
* @param hasEditButton Whether an edit button should be offered.
* @param itemSelectedListener Optional listener which is notified when the item is selected or
* deselected.
* @param itemEditedListener Optional listener which is notified when the item is edited.
*/
public void addItem(View view, boolean hasEditButton,
@Nullable Callback<Boolean> itemSelectedListener,
@Nullable Runnable itemEditedListener) {
CompoundButton radioButton =
mAllowMultipleChoices ? new CheckBox(getContext()) : new RadioButton(getContext());
// Insert at end, before the `add' button (if any).
int viewIndex = mCanAddItems ? indexOfChild(mAddButton) : getChildCount();
addViewInternal(radioButton, viewIndex++, createRadioButtonLayoutParams());
addViewInternal(view, viewIndex++, createContentLayoutParams());
ChromeImageView editButton = null;
View spacer = null;
if (hasEditButton) {
editButton = new ChromeImageView(getContext());
editButton.setImageResource(org.chromium.chrome.R.drawable.ic_edit_24dp);
editButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
editButton.setOnClickListener(unusedView -> {
if (itemEditedListener != null) {
itemEditedListener.run();
}
});
addViewInternal(editButton, viewIndex++, createEditButtonLayoutParams());
} else {
spacer = new Space(getContext());
addViewInternal(spacer, viewIndex++, createEditButtonLayoutParams());
}
Item item = new Item(radioButton, itemSelectedListener, view, editButton, spacer);
// When clicking a checkbox, invert its checked value. A radio button will always be
// selected when clicked.
View.OnClickListener clickListener = unusedView
-> setChecked(
view, mAllowMultipleChoices ? !item.mCompoundButton.isChecked() : true);
radioButton.setOnClickListener(clickListener);
view.setOnClickListener(clickListener);
mItems.add(item);
// Need to adjust button margins after first item was inserted.
if (mItems.size() == 1) {
updateAddButtonMargins(mAddButtonSpacing);
}
}
/**
* Removes all items from the list.
*/
public void clearItems() {
for (int i = 0; i < mItems.size(); i++) {
Item item = mItems.get(i);
removeView(item.mContent);
removeView(item.mCompoundButton);
if (item.mEditButton != null) {
removeView(item.mEditButton);
}
if (item.mSpacer != null) {
removeView(item.mSpacer);
}
}
mItems.clear();
updateAddButtonMargins(0);
}
public View getItem(int index) {
if (index >= 0 && index < mItems.size()) {
return mItems.get(index).mContent;
}
return null;
}
public int getItemCount() {
return mItems.size();
}
/**
* Selects the specified item. If this choice list does not allow checking multiple choice, this
* will also deselect all other items.
*
* @param content The content view to select, as specified in |addItem|. Can be null to indicate
* that all items should be de-selected.
*/
public void setCheckedItem(@Nullable View content) {
if (content == null) {
for (Item item : mItems) {
item.mCompoundButton.setChecked(false);
if (item.mOnSelectedListener != null) {
item.mOnSelectedListener.onResult(false);
}
}
return;
}
setChecked(content, true);
}
/**
* Sets whether the specified item is checked or not. If this choice list does not allow
* checking multiple choice and {@code checked} is true, this will also deselect all other
* items.
*
* @param content The content view to (un)select, as specified in |addItem|.
*/
public void setChecked(View content, boolean checked) {
for (Item item : mItems) {
boolean notifyListener = false;
if (item.mContent == content) {
item.mCompoundButton.setChecked(checked);
notifyListener = true;
} else if (checked && !mAllowMultipleChoices) {
item.mCompoundButton.setChecked(false);
notifyListener = true;
}
if (notifyListener && item.mOnSelectedListener != null) {
item.mOnSelectedListener.onResult(item.mCompoundButton.isChecked());
}
}
}
/**
* Allows to change the label of the 'add' button.
*/
public void setAddButtonLabel(String label) {
if (mAddButtonLabel != null) {
mAddButtonLabel.setText(label);
}
}
public void setOnAddButtonClickedListener(Runnable listener) {
mAddButtonListener = listener;
}
/**
* Adds a view to the underlying gridlayout.
*
* This method is used internally to add a view to the actual layout. A single call to |addView|
* will result in multiple calls to |addViewInternal|, because additional widgets are
* automatically generated (e.g., radio-buttons and edit-buttons).
*
* @param view The view to add to the layout.
* @param index The index at which to insert the view into the layout. Note that this - along
* with the column width specified in |lp| - will determine the column in which the view will
* end up in.
* @param lp Additional layout parameters, see |GridLayout.LayoutParams|.
*/
private void addViewInternal(View view, int index, ViewGroup.LayoutParams lp) {
super.addView(view, index, lp);
}
private ChromeImageView createAddButtonIcon() {
ChromeImageView addButtonIcon = new ChromeImageView(getContext());
addButtonIcon.setImageResource(R.drawable.ic_autofill_assistant_add_circle_24dp);
addButtonIcon.setOnClickListener(unusedView -> {
if (mAddButtonListener != null) {
mAddButtonListener.run();
}
});
return addButtonIcon;
}
private TextView createAddButtonLabel(String addButtonText) {
TextView addButtonLabel = new TextView(getContext());
ApiCompatibilityUtils.setTextAppearance(
addButtonLabel, org.chromium.chrome.R.style.TextAppearance_BlueButtonText2);
addButtonLabel.setText(addButtonText);
addButtonLabel.setOnClickListener(unusedView -> {
if (mAddButtonListener != null) {
mAddButtonListener.run();
}
});
return addButtonLabel;
}
private GridLayout.LayoutParams createContentLayoutParams() {
// Set layout params to let content grow to maximum size.
GridLayout.LayoutParams lp =
new GridLayout.LayoutParams(GridLayout.spec(UNDEFINED), GridLayout.spec(1, 1));
lp.setGravity(Gravity.FILL_HORIZONTAL | Gravity.CENTER_VERTICAL);
lp.width = 0;
lp.topMargin = mItems.isEmpty() ? 0 : mRowSpacing;
return lp;
}
private GridLayout.LayoutParams createRadioButtonLayoutParams() {
GridLayout.LayoutParams lp =
new GridLayout.LayoutParams(GridLayout.spec(UNDEFINED), GridLayout.spec(0, 1));
lp.setGravity(Gravity.CENTER);
lp.setMarginEnd(mColumnSpacing);
lp.topMargin = mItems.isEmpty() ? 0 : mRowSpacing;
return lp;
}
private GridLayout.LayoutParams createEditButtonLayoutParams() {
int editButtonSize = getContext().getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_choicelist_edit_button_size);
GridLayout.LayoutParams lp =
new GridLayout.LayoutParams(GridLayout.spec(UNDEFINED), GridLayout.spec(2, 1));
lp.setGravity(Gravity.CENTER_VERTICAL | Gravity.FILL_VERTICAL);
lp.setMarginStart(mColumnSpacing);
lp.width = editButtonSize;
lp.height = editButtonSize;
lp.topMargin = mItems.isEmpty() ? 0 : mRowSpacing;
return lp;
}
/**
* Adjusts the margins of the 'add' button.
*
* For empty lists, the margins should be 0. For non-empty lists, the margins should be equal
* to |mRowSpacing|.
*/
private void updateAddButtonMargins(int marginTop) {
if (!mCanAddItems) {
return;
}
LayoutParams lp = (LayoutParams) mAddButton.getLayoutParams();
lp.setMargins(lp.leftMargin, marginTop, lp.rightMargin, lp.bottomMargin);
mAddButton.setLayoutParams(lp);
lp = (LayoutParams) mAddButtonLabel.getLayoutParams();
lp.setMargins(lp.leftMargin, marginTop, lp.rightMargin, lp.bottomMargin);
mAddButtonLabel.setLayoutParams(lp);
}
}