blob: 6ee35d3e27f14b893ed72cf900a411d48d0ab982 [file] [log] [blame]
/*
* Copyright (C) 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.toolbar;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.VisibleForTesting;
import com.android.car.ui.R;
import com.android.car.ui.utils.CarUxRestrictionsUtil;
import java.lang.ref.WeakReference;
/**
* Represents a button to display in the {@link Toolbar}.
*
* <p>There are currently 3 types of buttons: icon, text, and switch. Using
* {@link Builder#setCheckable()} will ensure that you get a switch, after that
* {@link Builder#setIcon(int)} will ensure an icon, and anything left just requires
* {@link Builder#setTitle(int)}.
*
* <p>Each MenuItem has a {@link DisplayBehavior} that controls if it appears on the {@link Toolbar}
* itself, or it's overflow menu.
*
* <p>If you require a search or settings button, you should use
* {@link Builder#setToSearch()} or
* {@link Builder#setToSettings()}.
*
* <p>Some properties can be changed after the creating a MenuItem, but others require being set
* with a {@link Builder}.
*/
public class MenuItem {
private final Context mContext;
private final boolean mIsCheckable;
private final boolean mIsActivatable;
private final boolean mIsSearch;
private final boolean mShowIconAndTitle;
private final boolean mIsTinted;
@CarUxRestrictions.CarUxRestrictionsInfo
private int mId;
private CarUxRestrictions mCurrentRestrictions;
// This is a WeakReference to allow the Toolbar (and by extension, the whole screen
// the toolbar is on) to be garbage-collected if the MenuItem is held past the
// lifecycle of the toolbar.
private WeakReference<Listener> mListener = new WeakReference<>(null);
private CharSequence mTitle;
private Drawable mIcon;
private OnClickListener mOnClickListener;
private DisplayBehavior mDisplayBehavior;
private int mUxRestrictions;
private boolean mIsEnabled;
private boolean mIsChecked;
private boolean mIsVisible;
private boolean mIsActivated;
private MenuItem(Builder builder) {
mContext = builder.mContext;
mId = builder.mId;
mIsCheckable = builder.mIsCheckable;
mIsActivatable = builder.mIsActivatable;
mTitle = builder.mTitle;
mIcon = builder.mIcon;
mOnClickListener = builder.mOnClickListener;
mDisplayBehavior = builder.mDisplayBehavior;
mIsEnabled = builder.mIsEnabled;
mIsChecked = builder.mIsChecked;
mIsVisible = builder.mIsVisible;
mIsActivated = builder.mIsActivated;
mIsSearch = builder.mIsSearch;
mShowIconAndTitle = builder.mShowIconAndTitle;
mIsTinted = builder.mIsTinted;
mUxRestrictions = builder.mUxRestrictions;
mCurrentRestrictions = CarUxRestrictionsUtil.getInstance(mContext).getCurrentRestrictions();
}
private void update() {
Listener listener = mListener.get();
if (listener != null) {
listener.onMenuItemChanged(this);
}
}
/** Sets the id, which is purely for the client to distinguish MenuItems with. */
public void setId(int id) {
mId = id;
update();
}
/** Gets the id, which is purely for the client to distinguish MenuItems with. */
public int getId() {
return mId;
}
/** Returns whether the MenuItem is enabled */
public boolean isEnabled() {
return mIsEnabled;
}
/** Sets whether the MenuItem is enabled */
public void setEnabled(boolean enabled) {
mIsEnabled = enabled;
update();
}
/** Returns whether the MenuItem is checkable. If it is, it will be displayed as a switch. */
public boolean isCheckable() {
return mIsCheckable;
}
/**
* Returns whether the MenuItem is currently checked. Only valid if {@link #isCheckable()}
* is true.
*/
public boolean isChecked() {
return mIsChecked;
}
/**
* Sets whether or not the MenuItem is checked.
* @throws IllegalStateException When {@link #isCheckable()} is false.
*/
public void setChecked(boolean checked) {
if (!isCheckable()) {
throw new IllegalStateException("Cannot call setChecked() on a non-checkable MenuItem");
}
mIsChecked = checked;
update();
}
public boolean isTinted() {
return mIsTinted;
}
/** Returns whether or not the MenuItem is visible */
public boolean isVisible() {
return mIsVisible;
}
/** Sets whether or not the MenuItem is visible */
public void setVisible(boolean visible) {
mIsVisible = visible;
update();
}
/**
* Returns whether the MenuItem is activatable. If it is, it's every click will toggle
* the MenuItem's View to appear activated or not.
*/
public boolean isActivatable() {
return mIsActivatable;
}
/** Returns whether or not this view is selected. Toggles after every click */
public boolean isActivated() {
return mIsActivated;
}
/** Sets the MenuItem as activated and updates it's View to the activated state */
public void setActivated(boolean activated) {
if (!isActivatable()) {
throw new IllegalStateException(
"Cannot call setActivated() on a non-activatable MenuItem");
}
mIsActivated = activated;
update();
}
/** Gets the title of this MenuItem. */
public CharSequence getTitle() {
return mTitle;
}
/** Sets the title of this MenuItem. */
public void setTitle(CharSequence title) {
mTitle = title;
update();
}
/** Sets the title of this MenuItem to a string resource. */
public void setTitle(int resId) {
setTitle(mContext.getString(resId));
}
/** Sets the UxRestrictions of this MenuItem. */
public void setUxRestrictions(@CarUxRestrictions.CarUxRestrictionsInfo int uxRestrictions) {
if (mUxRestrictions != uxRestrictions) {
mUxRestrictions = uxRestrictions;
update();
}
}
@CarUxRestrictions.CarUxRestrictionsInfo
public int getUxRestrictions() {
return mUxRestrictions;
}
/** Gets the current {@link OnClickListener} */
public OnClickListener getOnClickListener() {
return mOnClickListener;
}
public boolean isShowingIconAndTitle() {
return mShowIconAndTitle;
}
/** Sets the {@link OnClickListener} */
public void setOnClickListener(OnClickListener listener) {
mOnClickListener = listener;
update();
}
/* package */ void setCarUxRestrictions(CarUxRestrictions restrictions) {
boolean wasRestricted = isRestricted();
mCurrentRestrictions = restrictions;
if (isRestricted() != wasRestricted) {
update();
}
}
/* package */ boolean isRestricted() {
return CarUxRestrictionsUtil.isRestricted(mUxRestrictions, mCurrentRestrictions);
}
/** Calls the {@link OnClickListener}. */
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void performClick() {
if (!isEnabled() || !isVisible()) {
return;
}
if (isRestricted()) {
Toast.makeText(mContext,
R.string.car_ui_restricted_while_driving, Toast.LENGTH_LONG).show();
return;
}
if (isActivatable()) {
setActivated(!isActivated());
}
if (isCheckable()) {
setChecked(!isChecked());
}
if (mOnClickListener != null) {
mOnClickListener.onClick(this);
}
}
/** Gets the current {@link DisplayBehavior} */
public DisplayBehavior getDisplayBehavior() {
return mDisplayBehavior;
}
/** Gets the current Icon */
public Drawable getIcon() {
return mIcon;
}
/** Sets the Icon of this MenuItem. */
public void setIcon(Drawable icon) {
mIcon = icon;
update();
}
/** Sets the Icon of this MenuItem to a drawable resource. */
public void setIcon(int resId) {
setIcon(resId == 0
? null
: mContext.getDrawable(resId));
}
/** Returns if this is the search MenuItem, which has special behavior when searching */
boolean isSearch() {
return mIsSearch;
}
/** Builder class */
public static final class Builder {
private final Context mContext;
private String mSearchTitle;
private String mSettingsTitle;
private Drawable mSearchIcon;
private Drawable mSettingsIcon;
private int mId = View.NO_ID;
private CharSequence mTitle;
private Drawable mIcon;
private OnClickListener mOnClickListener;
private DisplayBehavior mDisplayBehavior = DisplayBehavior.ALWAYS;
private boolean mIsTinted = true;
private boolean mShowIconAndTitle = false;
private boolean mIsEnabled = true;
private boolean mIsCheckable = false;
private boolean mIsChecked = false;
private boolean mIsVisible = true;
private boolean mIsActivatable = false;
private boolean mIsActivated = false;
private boolean mIsSearch = false;
private boolean mIsSettings = false;
@CarUxRestrictions.CarUxRestrictionsInfo
private int mUxRestrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
public Builder(Context c) {
// Must use getApplicationContext to avoid leaking activities when the MenuItem
// is held onto for longer than the Activity's lifecycle
mContext = c.getApplicationContext();
}
/** Builds a {@link MenuItem} from the current state of the Builder */
public MenuItem build() {
if (mIsActivatable && (mShowIconAndTitle || mIcon == null)) {
throw new IllegalStateException("Only simple icons can be activatable");
}
if (mIsCheckable && (mShowIconAndTitle || mIsActivatable)) {
throw new IllegalStateException("Unsupported options for a checkable MenuItem");
}
if (mIsSearch && mIsSettings) {
throw new IllegalStateException("Can't have both a search and settings MenuItem");
}
if (mIsActivatable && mDisplayBehavior == DisplayBehavior.NEVER) {
throw new IllegalStateException("Activatable MenuItems not supported as Overflow");
}
if (mIsSearch && (!mSearchTitle.contentEquals(mTitle)
|| !mSearchIcon.equals(mIcon)
|| mIsCheckable
|| mIsActivatable
|| !mIsTinted
|| mShowIconAndTitle
|| mDisplayBehavior != DisplayBehavior.ALWAYS)) {
throw new IllegalStateException("Invalid search MenuItem");
}
if (mIsSettings && (!mSettingsTitle.contentEquals(mTitle)
|| !mSettingsIcon.equals(mIcon)
|| mIsCheckable
|| mIsActivatable
|| !mIsTinted
|| mShowIconAndTitle
|| mDisplayBehavior != DisplayBehavior.ALWAYS)) {
throw new IllegalStateException("Invalid settings MenuItem");
}
return new MenuItem(this);
}
/** Sets the id, which is purely for the client to distinguish MenuItems with. */
public Builder setId(int id) {
mId = id;
return this;
}
/** Sets the title to a string resource id */
public Builder setTitle(int resId) {
setTitle(mContext.getString(resId));
return this;
}
/** Sets the title */
public Builder setTitle(CharSequence title) {
mTitle = title;
return this;
}
/**
* Sets the icon to a drawable resource id.
*
* <p>The icon's color and size will be changed to match the other MenuItems.
*/
public Builder setIcon(int resId) {
mIcon = resId == 0
? null
: mContext.getDrawable(resId);
return this;
}
/**
* Sets the icon to a drawable.
*
* <p>The icon's color and size will be changed to match the other MenuItems.
*/
public Builder setIcon(Drawable icon) {
mIcon = icon;
return this;
}
/**
* Sets whether to tint the icon, true by default.
*
* <p>Try not to use this, it should only be used if the MenuItem is displaying some
* kind of logo or avatar and should be colored.
*/
public Builder setTinted(boolean tinted) {
mIsTinted = tinted;
return this;
}
/** Sets whether the MenuItem is visible or not. Default true. */
public Builder setVisible(boolean visible) {
mIsVisible = visible;
return this;
}
/**
* Makes the MenuItem activatable, which means it will toggle it's visual state after
* every click.
*/
public Builder setActivatable() {
mIsActivatable = true;
return this;
}
/**
* Sets whether or not the MenuItem is selected. If it is,
* {@link View#setSelected(boolean)} will be called on its View.
*/
public Builder setActivated(boolean activated) {
setActivatable();
mIsActivated = activated;
return this;
}
/** Sets the {@link OnClickListener} */
public Builder setOnClickListener(OnClickListener listener) {
mOnClickListener = listener;
return this;
}
/**
* Used to show both the icon and title when displayed on the toolbar. If this
* is false, only the icon while be displayed when the MenuItem is in the toolbar
* and only the title will be displayed when the MenuItem is in the overflow menu.
*
* <p>Defaults to false.
*/
public Builder setShowIconAndTitle(boolean showIconAndTitle) {
mShowIconAndTitle = showIconAndTitle;
return this;
}
/**
* Sets the {@link DisplayBehavior}.
*
* <p>If the DisplayBehavior is {@link DisplayBehavior#NEVER}, the MenuItem must not be
* {@link #setCheckable() checkable}.
*/
public Builder setDisplayBehavior(DisplayBehavior behavior) {
mDisplayBehavior = behavior;
return this;
}
/** Sets whether the MenuItem is enabled or not. Default true. */
public Builder setEnabled(boolean enabled) {
mIsEnabled = enabled;
return this;
}
/**
* Makes the MenuItem checkable, meaning it will be displayed as a
* switch.
*
* <p>The MenuItem is not checkable by default.
*/
public Builder setCheckable() {
mIsCheckable = true;
return this;
}
/**
* Sets whether the MenuItem is checked or not. This will imply {@link #setCheckable()}.
*/
public Builder setChecked(boolean checked) {
setCheckable();
mIsChecked = checked;
return this;
}
/**
* Sets under what {@link android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo}
* the MenuItem should be restricted.
*/
public Builder setUxRestrictions(
@CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
mUxRestrictions = restrictions;
return this;
}
/**
* Creates a search MenuItem.
*
* <p>The advantage of using this over creating your own is getting an OEM-styled search
* icon, and this button will always disappear while searching, even when the
* {@link Toolbar Toolbar's} showMenuItemsWhileSearching is true.
*
* <p>If using this, you should only change the id, visibility, or onClickListener.
*/
public Builder setToSearch() {
mSearchTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_search_title);
mSearchIcon = mContext.getDrawable(R.drawable.car_ui_icon_search);
mIsSearch = true;
setTitle(mSearchTitle);
setIcon(mSearchIcon);
return this;
}
/**
* Creates a settings MenuItem.
*
* <p>The advantage of this over creating your own is getting an OEM-styled settings icon,
* and that the MenuItem will be restricted based on
* {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP}
*
* <p>If using this, you should only change the id, visibility, or onClickListener.
*/
public Builder setToSettings() {
mSettingsTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_settings_title);
mSettingsIcon = mContext.getDrawable(R.drawable.car_ui_icon_settings);
mIsSettings = true;
setTitle(mSettingsTitle);
setIcon(mSettingsIcon);
setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP);
return this;
}
/** @deprecated Use {@link #setToSearch()} instead. */
@Deprecated
public static MenuItem createSearch(Context c, OnClickListener listener) {
return MenuItem.builder(c)
.setToSearch()
.setOnClickListener(listener)
.build();
}
/** @deprecated Use {@link #setToSettings()} instead. */
@Deprecated
public static MenuItem createSettings(Context c, OnClickListener listener) {
return MenuItem.builder(c)
.setToSettings()
.setOnClickListener(listener)
.build();
}
}
/** Get a new {@link Builder}. */
public static Builder builder(Context context) {
return new Builder(context);
}
/**
* OnClickListener for a MenuItem.
*/
public interface OnClickListener {
/** Called when the MenuItem is clicked */
void onClick(MenuItem item);
}
/**
* DisplayBehavior controls how the MenuItem is presented in the Toolbar
*/
public enum DisplayBehavior {
/** Always show the MenuItem on the toolbar instead of the overflow menu */
ALWAYS,
/** Never show the MenuItem in the toolbar, always put it in the overflow menu */
NEVER
}
/** Listener for {@link Toolbar} to update when this MenuItem changes */
interface Listener {
/** Called when the MenuItem is changed. For use only by {@link Toolbar} */
void onMenuItemChanged(MenuItem item);
}
/**
* Sets a listener for changes to this MenuItem. Note that the MenuItem will only hold
* weak references to the Listener, so that the listener is not held if the MenuItem
* outlives the toolbar.
*/
void setListener(Listener listener) {
mListener = new WeakReference<>(listener);
}
}