blob: 24dd58b135900c0f92f7a6813c6bc5ce93307974 [file] [log] [blame]
/*
* Copyright 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.widget;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.car.R;
import androidx.car.utils.CarUxRestrictionsUtils;
/**
* Class to build a list item with {@link SeekBar}.
*
* <p>An item supports primary action and supplemental action(s).
*
* <p>An item visually composes of 3 parts; each part may contain multiple views.
* <ul>
* <li>{@code Primary Action}: represented by an icon of following types.
* <ul>
* <li>Primary Icon - icon size could be large or small.
* <li>No Icon - no icon is shown.
* <li>Empty Icon - {@code Seekbar} offsets start space as if there was an icon.
* </ul>
* <li>{@code Seekbar}: with optional {@code Text}.
* <li>{@code Supplemental Action}: presented by an icon of following types; aligned to
* the end of item.
* <ul>
* <li>Supplemental Icon.
* <li>Supplemental Empty Icon - {@code Seekbar} offsets end space as if there was an icon.
* </ul>
* </ul>
*
* {@code SeekbarListItem} binds data to {@link ViewHolder} based on components selected.
*
* <p>When conflicting methods are called (e.g. setting primary action to both primary icon and
* no icon), the last called method wins.
*/
public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
@Retention(SOURCE)
@IntDef({
PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_ICON,
PRIMARY_ACTION_TYPE_SMALL_ICON})
private @interface PrimaryActionType {}
private static final int PRIMARY_ACTION_TYPE_NO_ICON = 0;
private static final int PRIMARY_ACTION_TYPE_EMPTY_ICON = 1;
private static final int PRIMARY_ACTION_TYPE_SMALL_ICON = 2;
@Retention(SOURCE)
@IntDef({SUPPLEMENTAL_ACTION_NO_ACTION, SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON,
SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON,
SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER})
private @interface SupplementalActionType {}
private static final int SUPPLEMENTAL_ACTION_NO_ACTION = 0;
private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON = 1;
private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON = 2;
private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER = 3;
private final Context mContext;
private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
@PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
private Drawable mPrimaryActionIconDrawable;
private String mText;
private int mProgress;
private int mMax;
private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
@SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
private Drawable mSupplementalIconDrawable;
private View.OnClickListener mSupplementalIconOnClickListener;
private boolean mShowSupplementalIconDivider;
/**
* Creates a {@link SeekbarListItem.ViewHolder}.
*/
public static ViewHolder createViewHolder(View itemView) {
return new ViewHolder(itemView);
}
/**
* Creates a SeekbarListItem.
*
* @param context context
* @param max the upper range of the SeekBar.
* @param progress the current progress of the specified value.
* @param listener listener to receive notification of changes to progress level.
* @param text displays a text on top of the SeekBar.
*/
public SeekbarListItem(Context context, int max, int progress,
SeekBar.OnSeekBarChangeListener listener, String text) {
mContext = context;
mMax = max;
mProgress = progress;
mOnSeekBarChangeListener = listener;
mText = text;
markDirty();
}
/**
* Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
*/
@Override
public int getViewType() {
return ListItemAdapter.LIST_ITEM_TYPE_SEEKBAR;
}
/**
* Calculates the layout params for views in {@link ViewHolder}.
*/
@Override
protected void resolveDirtyState() {
mBinders.clear();
// Create binders that adjust layout params of each view.
setItemLayoutHeight();
setPrimaryAction();
setSeekBarAndText();
setSupplementalAction();
}
/**
* Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params.
*/
@Override
protected void onBind(ViewHolder viewHolder) {
// Hide all subviews then apply view binders to adjust subviews.
hideSubViews(viewHolder);
for (ViewBinder binder : mBinders) {
binder.bind(viewHolder);
}
}
private void hideSubViews(ViewHolder vh) {
View[] subviews = new View[] {
vh.getPrimaryIcon(),
// SeekBar is always visible.
vh.getText(),
vh.getSupplementalIcon(), vh.getSupplementalIconDivider(),
};
for (View v : subviews) {
v.setVisibility(View.GONE);
}
}
private void setItemLayoutHeight() {
int minHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.car_single_line_list_item_height);
mBinders.add(vh -> {
vh.itemView.setMinimumHeight(minHeight);
vh.getContainerLayout().setMinimumHeight(minHeight);
ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
vh.itemView.requestLayout();
});
}
private void setPrimaryAction() {
setPrimaryActionLayout();
setPrimaryActionContent();
}
private void setSeekBarAndText() {
setSeekBarAndTextContent();
setSeekBarAndTextLayout();
}
private void setSupplementalAction() {
setSupplementalActionLayout();
setSupplementalActionContent();
}
private void setPrimaryActionLayout() {
switch (mPrimaryActionType) {
case PRIMARY_ACTION_TYPE_NO_ICON:
case PRIMARY_ACTION_TYPE_EMPTY_ICON:
// Do nothing.
break;
case PRIMARY_ACTION_TYPE_SMALL_ICON:
int startMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.car_keyline_1);
int iconSize = mContext.getResources().getDimensionPixelSize(
R.dimen.car_primary_icon_size);
mBinders.add(vh -> {
RelativeLayout.LayoutParams layoutParams =
(RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
// Icon size.
layoutParams.height = layoutParams.width = iconSize;
// Start margin.
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_START);
layoutParams.setMarginStart(startMargin);
layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
vh.getPrimaryIcon().requestLayout();
});
break;
default:
throw new IllegalStateException("Unknown primary action type.");
}
}
private void setPrimaryActionContent() {
switch (mPrimaryActionType) {
case PRIMARY_ACTION_TYPE_NO_ICON:
case PRIMARY_ACTION_TYPE_EMPTY_ICON:
// Do nothing.
break;
case PRIMARY_ACTION_TYPE_SMALL_ICON:
mBinders.add(vh -> {
vh.getPrimaryIcon().setVisibility(View.VISIBLE);
vh.getPrimaryIcon().setImageDrawable(mPrimaryActionIconDrawable);
});
break;
default:
throw new IllegalStateException("Unknown primary action type.");
}
}
private void setSeekBarAndTextContent() {
mBinders.add(vh -> {
vh.getSeekBar().setMax(mMax);
vh.getSeekBar().setProgress(mProgress);
vh.getSeekBar().setOnSeekBarChangeListener(mOnSeekBarChangeListener);
if (!TextUtils.isEmpty(mText)) {
vh.getText().setVisibility(View.VISIBLE);
vh.getText().setText(mText);
vh.getText().setTextAppearance(getTitleTextAppearance());
}
});
}
private void setSeekBarAndTextLayout() {
mBinders.add(vh -> {
// SeekBar is below text with a gap.
ViewGroup.MarginLayoutParams seekBarLayoutParams =
(ViewGroup.MarginLayoutParams) vh.getSeekBar().getLayoutParams();
seekBarLayoutParams.topMargin = TextUtils.isEmpty(mText)
? 0
: mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_1);
vh.getSeekBar().requestLayout();
// Set start and end margin of text and seek bar.
setViewStartMargin(vh.getSeekBarContainer());
setViewEndMargin(vh.getSeekBarContainer());
RelativeLayout.LayoutParams containerLayoutParams =
(RelativeLayout.LayoutParams) vh.getSeekBarContainer().getLayoutParams();
containerLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
});
}
// Helper method to set start margin of seekbar/text.
private void setViewStartMargin(View v) {
int startMarginResId;
switch (mPrimaryActionType) {
case PRIMARY_ACTION_TYPE_NO_ICON:
startMarginResId = R.dimen.car_keyline_1;
break;
case PRIMARY_ACTION_TYPE_EMPTY_ICON:
case PRIMARY_ACTION_TYPE_SMALL_ICON:
startMarginResId = R.dimen.car_keyline_3;
break;
default:
throw new IllegalStateException("Unknown primary action type.");
}
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) v.getLayoutParams();
layoutParams.setMarginStart(
mContext.getResources().getDimensionPixelSize(startMarginResId));
v.requestLayout();
}
// Helper method to set end margin of seekbar/text.
private void setViewEndMargin(View v) {
RelativeLayout.LayoutParams layoutParams =
(RelativeLayout.LayoutParams) v.getLayoutParams();
int endMargin = 0;
switch (mSupplementalActionType) {
case SUPPLEMENTAL_ACTION_NO_ACTION:
// Aligned to parent end with margin.
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
layoutParams.removeRule(RelativeLayout.START_OF);
layoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize(
R.dimen.car_keyline_1));
break;
case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON:
// Align to start of divider with padding.
layoutParams.addRule(RelativeLayout.START_OF, R.id.supplemental_icon_divider);
layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END);
layoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize(
R.dimen.car_padding_4));
break;
case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER:
// Align to parent end with a margin as if the icon and an optional divider were
// present. We do this by setting
// Add divider padding to icon, and width of divider.
endMargin += mContext.getResources().getDimensionPixelSize(
R.dimen.car_padding_4);
endMargin += mContext.getResources().getDimensionPixelSize(
R.dimen.car_vertical_line_divider_width);
// Fall through.
case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON:
// Add view padding, width of icon, and icon end margin.
endMargin += mContext.getResources().getDimensionPixelSize(
R.dimen.car_padding_4);
endMargin += mContext.getResources().getDimensionPixelSize(
R.dimen.car_primary_icon_size);
endMargin += mContext.getResources().getDimensionPixelSize(
R.dimen.car_keyline_1);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
layoutParams.removeRule(RelativeLayout.START_OF);
layoutParams.setMarginEnd(endMargin);
break;
default:
throw new IllegalStateException("Unknown supplemental action type.");
}
v.requestLayout();
}
private void setSupplementalActionLayout() {
int keyline1 = mContext.getResources().getDimensionPixelSize(R.dimen.car_keyline_1);
int padding4 = mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_4);
mBinders.add(vh -> {
RelativeLayout.LayoutParams iconLayoutParams =
(RelativeLayout.LayoutParams) vh.getSupplementalIcon().getLayoutParams();
// Align to parent end with margin.
iconLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
iconLayoutParams.setMarginEnd(keyline1);
iconLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
vh.getSupplementalIcon().requestLayout();
// Divider aligns to the start of supplemental icon with margin.
RelativeLayout.LayoutParams dividerLayoutParams =
(RelativeLayout.LayoutParams) vh.getSupplementalIconDivider()
.getLayoutParams();
dividerLayoutParams.addRule(RelativeLayout.START_OF, R.id.supplemental_icon);
dividerLayoutParams.setMarginEnd(padding4);
dividerLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
vh.getSupplementalIconDivider().requestLayout();
});
}
private void setSupplementalActionContent() {
switch (mSupplementalActionType) {
case SUPPLEMENTAL_ACTION_NO_ACTION:
case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER:
case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON:
break;
case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON:
mBinders.add(vh -> {
vh.getSupplementalIcon().setVisibility(View.VISIBLE);
if (mShowSupplementalIconDivider) {
vh.getSupplementalIconDivider().setVisibility(View.VISIBLE);
}
vh.getSupplementalIcon().setImageDrawable(mSupplementalIconDrawable);
vh.getSupplementalIcon().setOnClickListener(
mSupplementalIconOnClickListener);
vh.getSupplementalIcon().setClickable(
mSupplementalIconOnClickListener != null);
});
break;
default:
throw new IllegalStateException("Unknown supplemental action type.");
}
}
/**
* Sets {@code Primary Action} to be represented by an icon.
*
* @param iconResId the resource identifier of the drawable.
*/
public void setPrimaryActionIcon(@DrawableRes int iconResId) {
setPrimaryActionIcon(mContext.getDrawable(iconResId));
}
/**
* Sets {@code Primary Action} to be represented by an icon.
*
* @param drawable the Drawable to set, or null to clear the content.
*/
public void setPrimaryActionIcon(Drawable drawable) {
mPrimaryActionType = PRIMARY_ACTION_TYPE_SMALL_ICON;
mPrimaryActionIconDrawable = drawable;
markDirty();
}
/**
* Sets {@code Primary Action} to be empty icon.
*
* {@code Seekbar} would have a start margin as if {@code Primary Action} were set as icon.
*/
public void setPrimaryActionEmptyIcon() {
mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
markDirty();
}
/**
* Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
*/
public void setSupplementalIcon(@DrawableRes int iconResId,
boolean showSupplementalIconDivider) {
setSupplementalIcon(mContext.getDrawable(iconResId), showSupplementalIconDivider, null);
}
/**
* Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
*/
public void setSupplementalIcon(@DrawableRes int iconResId, boolean showSupplementalIconDivider,
@Nullable View.OnClickListener listener) {
setSupplementalIcon(mContext.getDrawable(iconResId), showSupplementalIconDivider, listener);
}
/**
* Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
*/
public void setSupplementalIcon(Drawable drawable, boolean showSupplementalIconDivider) {
setSupplementalIcon(drawable, showSupplementalIconDivider, null);
}
/**
* Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
*/
public void setSupplementalIcon(Drawable drawable, boolean showSupplementalIconDivider,
@Nullable View.OnClickListener listener) {
mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
mSupplementalIconDrawable = drawable;
mShowSupplementalIconDivider = showSupplementalIconDivider;
mSupplementalIconOnClickListener = listener;
markDirty();
}
/**
* Sets {@code Supplemental Action} to be empty icon.
*
* {@code Seekbar} would have an end margin as if {@code Supplemental Action} were set.
*/
public void setSupplementalEmptyIcon(boolean seekbarOffsetDividerWidth) {
mSupplementalActionType = seekbarOffsetDividerWidth
? SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER
: SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON;
markDirty();
}
/**
* Holds views of SeekbarListItem.
*/
public static class ViewHolder extends ListItem.ViewHolder {
private RelativeLayout mContainerLayout;
private ImageView mPrimaryIcon;
private LinearLayout mSeekBarContainer;
private TextView mText;
private SeekBar mSeekBar;
private View mSupplementalIconDivider;
private ImageView mSupplementalIcon;
public ViewHolder(View itemView) {
super(itemView);
mContainerLayout = itemView.findViewById(R.id.container);
mPrimaryIcon = itemView.findViewById(R.id.primary_icon);
mSeekBarContainer = itemView.findViewById(R.id.seek_bar_container);
mText = itemView.findViewById(R.id.text);
mSeekBar = itemView.findViewById(R.id.seek_bar);
mSupplementalIcon = itemView.findViewById(R.id.supplemental_icon);
mSupplementalIconDivider = itemView.findViewById(R.id.supplemental_icon_divider);
}
@Override
void complyWithUxRestrictions(CarUxRestrictions restrictions) {
CarUxRestrictionsUtils.comply(itemView.getContext(), restrictions, getText());
}
public RelativeLayout getContainerLayout() {
return mContainerLayout;
}
public ImageView getPrimaryIcon() {
return mPrimaryIcon;
}
public LinearLayout getSeekBarContainer() {
return mSeekBarContainer;
}
public TextView getText() {
return mText;
}
public SeekBar getSeekBar() {
return mSeekBar;
}
public ImageView getSupplementalIcon() {
return mSupplementalIcon;
}
public View getSupplementalIconDivider() {
return mSupplementalIconDivider;
}
}
}