Allow setting MenuItems via XML
Test: Manually
Change-Id: I91c4210c942cd061ebbf9c5b05e4afde86751124
diff --git a/car-ui-lib/res/values/attrs.xml b/car-ui-lib/res/values/attrs.xml
index d2ebcf8..163d586 100644
--- a/car-ui-lib/res/values/attrs.xml
+++ b/car-ui-lib/res/values/attrs.xml
@@ -37,6 +37,51 @@
<enum name="close" value="1"/>
<enum name="down" value="2"/>
</attr>
+ <!-- XML resource of MenuItems. See Toolbar.setMenuItems(int) for more information. -->
+ <attr name="menuItems" format="reference"/>
+ </declare-styleable>
+
+ <declare-styleable name="CarUiToolbarMenuItem">
+ <!-- Show/hide the MenuItem -->
+ <attr name="visible" format="boolean"/>
+ <!-- Title -->
+ <attr name="title" format="string"/>
+ <!-- Icon -->
+ <attr name="icon" format="reference"/>
+ <!-- True to tint the icon to a consistent color. Default true, all the other booleans default to false -->
+ <attr name="tinted" format="boolean"/>
+ <!-- Show both the icon and title at the same time -->
+ <attr name="showIconAndTitle" format="boolean"/>
+ <!-- True if this MenuItem should be a switch -->
+ <attr name="checkable" format="boolean"/>
+ <!-- Whether the switch should be checked or not. Setting this implies checkable=true -->
+ <attr name="checked" format="boolean"/>
+ <!-- True if this MenuItem should be activatable, in which case it will visually toggle states when clicked -->
+ <attr name="activatable" format="boolean"/>
+ <!-- Whether the MenuItem starts activated. Setting this implies activatable=true -->
+ <attr name="activated" format="boolean"/>
+ <!-- How to display the MenuItem. "always" means always show it on the toolbar, "never" means never show it on the toolbar and instead show it in the overflow menu -->
+ <attr name="displayBehavior" format="enum">
+ <enum name="always" value="0"/>
+ <enum name="never" value="1"/>
+ </attr>
+ <!-- Ux restrictions required to interact with this MenuItem -->
+ <attr name="uxRestrictions">
+ <!-- Values are copied from android.car.drivingstate.CarUxRestrictions. Note:
+ UX_RESTRICTIONS_BASELINE is not allowed here because it's useless and confusing. -->
+ <flag name="UX_RESTRICTIONS_NO_DIALPAD" value="1"/>
+ <flag name="UX_RESTRICTIONS_NO_FILTERING" value="2"/>
+ <flag name="UX_RESTRICTIONS_LIMIT_STRING_LENGTH" value="4"/>
+ <flag name="UX_RESTRICTIONS_NO_KEYBOARD" value="8"/>
+ <flag name="UX_RESTRICTIONS_NO_VIDEO" value="16"/>
+ <flag name="UX_RESTRICTIONS_LIMIT_CONTENT" value="32"/>
+ <flag name="UX_RESTRICTIONS_NO_SETUP" value="64"/>
+ <flag name="UX_RESTRICTIONS_NO_TEXT_MESSAGE" value="128"/>
+ <flag name="UX_RESTRICTIONS_NO_VOICE_TRANSCRIPTION" value="256"/>
+ <flag name="UX_RESTRICTIONS_FULLY_RESTRICTED" value="511"/>
+ </attr>
+ <!-- The name of a method that takes a MenuItem as an argument in you'r toolbar's Activity. Will be called when the MenuItem is clicked -->
+ <attr name="onClick" format="string"/>
</declare-styleable>
<!-- Theme attribute to specifying a default style for all CarUiToolbars -->
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
index 5b01235..2df4d95 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/CarUiSmoothScroller.java
@@ -25,7 +25,7 @@
import androidx.recyclerview.widget.RecyclerView;
import com.android.car.ui.R;
-import com.android.car.ui.utils.ResourceUtils;
+import com.android.car.ui.utils.CarUiUtils;
/**
* Code drop from {androidx.car.widget.CarUiSmoothScroller}
*
@@ -54,13 +54,13 @@
}
private void init(Context context) {
- mMillisecondsPerInch = ResourceUtils.getFloat(context.getResources(),
+ mMillisecondsPerInch = CarUiUtils.getFloat(context.getResources(),
R.dimen.car_ui_scrollbar_milliseconds_per_inch);
- mDecelerationTimeDivisor = ResourceUtils.getFloat(context.getResources(),
+ mDecelerationTimeDivisor = CarUiUtils.getFloat(context.getResources(),
R.dimen.car_ui_scrollbar_deceleration_times_divisor);
mInterpolator =
new DecelerateInterpolator(
- ResourceUtils.getFloat(context.getResources(),
+ CarUiUtils.getFloat(context.getResources(),
R.dimen.car_ui_scrollbar_decelerate_interpolator_factor));
mDensityDpi = context.getResources().getDisplayMetrics().densityDpi;
mMillisecondsPerPixel = mMillisecondsPerInch / mDensityDpi;
diff --git a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
index bb8b3fe..263e681 100644
--- a/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
+++ b/car-ui-lib/src/com/android/car/ui/recyclerview/DefaultScrollBar.java
@@ -35,7 +35,7 @@
import com.android.car.ui.R;
import com.android.car.ui.recyclerview.CarUiRecyclerView.ScrollBarPosition;
-import com.android.car.ui.utils.ResourceUtils;
+import com.android.car.ui.utils.CarUiUtils;
/**
* The default scroll bar widget for the {@link CarUiRecyclerView}.
@@ -92,7 +92,7 @@
Resources res = rv.getContext().getResources();
- mButtonDisabledAlpha = ResourceUtils.getFloat(res, R.dimen.car_ui_button_disabled_alpha);
+ mButtonDisabledAlpha = CarUiUtils.getFloat(res, R.dimen.car_ui_button_disabled_alpha);
if (scrollBarAboveRecyclerView) {
parent.addView(mScrollView);
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
index 9901d69..9d0c254 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItem.java
@@ -229,7 +229,9 @@
/** Sets the Icon of this MenuItem to a drawable resource. */
public void setIcon(int resId) {
- setIcon(mContext.getDrawable(resId));
+ setIcon(resId == 0
+ ? null
+ : mContext.getDrawable(resId));
}
/** Returns if this is the search MenuItem, which has special behavior when searching */
@@ -300,7 +302,19 @@
* <p>The icon's color and size will be changed to match the other MenuItems.
*/
public Builder setIcon(int resId) {
- mIcon = mContext.getDrawable(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;
}
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
index 3cda47a..c2ef149 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/MenuItemRenderer.java
@@ -15,8 +15,14 @@
*/
package com.android.car.ui.toolbar;
+import android.app.Activity;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Xml;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -25,10 +31,23 @@
import android.widget.TextView;
import android.widget.Toast;
+import androidx.annotation.XmlRes;
+
import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
import com.android.car.ui.utils.CarUxRestrictionsUtil;
import com.android.car.ui.uxr.DrawableStateView;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
class MenuItemRenderer implements MenuItem.Listener {
private static final int[] RESTRICTED_STATE = new int[] {R.attr.state_ux_restricted};
@@ -194,4 +213,102 @@
private boolean isRestricted() {
return CarUxRestrictionsUtil.isRestricted(mMenuItem.getUxRestrictions(), mUxRestrictions);
}
+
+ static List<MenuItem> readMenuItemList(Context c, @XmlRes int resId) {
+ if (resId == 0) {
+ return Collections.emptyList();
+ }
+
+ try (XmlResourceParser parser = c.getResources().getXml(resId)) {
+ AttributeSet attrs = Xml.asAttributeSet(parser);
+ List<MenuItem> menuItems = new ArrayList<>();
+
+ parser.next();
+ parser.next();
+ parser.require(XmlPullParser.START_TAG, null, "MenuItems");
+ while (parser.next() != XmlPullParser.END_TAG) {
+ menuItems.add(readMenuItem(c, parser, attrs));
+ }
+
+ return menuItems;
+ } catch (XmlPullParserException | IOException e) {
+ throw new RuntimeException("Unable to parse Menu Items", e);
+ }
+ }
+
+ private static MenuItem readMenuItem(Context c, XmlResourceParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+
+ parser.require(XmlPullParser.START_TAG, null, "MenuItem");
+
+ TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CarUiToolbarMenuItem);
+ try {
+ String title = a.getString(R.styleable.CarUiToolbarMenuItem_title);
+ Drawable icon = a.getDrawable(R.styleable.CarUiToolbarMenuItem_icon);
+ boolean tinted = a.getBoolean(R.styleable.CarUiToolbarMenuItem_tinted, true);
+ boolean visible = a.getBoolean(R.styleable.CarUiToolbarMenuItem_visible, true);
+ boolean showIconAndTitle = a.getBoolean(
+ R.styleable.CarUiToolbarMenuItem_showIconAndTitle, false);
+ boolean checkable = a.getBoolean(R.styleable.CarUiToolbarMenuItem_checkable, false);
+ boolean checked = a.getBoolean(R.styleable.CarUiToolbarMenuItem_checked, false);
+ boolean checkedExists = a.hasValue(R.styleable.CarUiToolbarMenuItem_checked);
+ boolean activatable = a.getBoolean(R.styleable.CarUiToolbarMenuItem_activatable, false);
+ boolean activated = a.getBoolean(R.styleable.CarUiToolbarMenuItem_activated, false);
+ boolean activatedExists = a.hasValue(R.styleable.CarUiToolbarMenuItem_activated);
+ int displayBehaviorInt = a.getInt(R.styleable.CarUiToolbarMenuItem_displayBehavior, 0);
+ int uxRestrictions = a.getInt(R.styleable.CarUiToolbarMenuItem_uxRestrictions, 0);
+ String onClickMethod = a.getString(R.styleable.CarUiToolbarMenuItem_onClick);
+ MenuItem.OnClickListener onClickListener = null;
+
+ if (onClickMethod != null) {
+ Activity activity = CarUiUtils.getActivity(c);
+ if (activity == null) {
+ throw new RuntimeException("Couldn't find an activity for the MenuItem");
+ }
+
+ try {
+ Method m = activity.getClass().getMethod(onClickMethod, MenuItem.class);
+ onClickListener = i -> {
+ try {
+ m.invoke(activity, i);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException("Couldn't call the MenuItem's listener", e);
+ }
+ };
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("OnClick method "
+ + onClickMethod + "(MenuItem) not found in your activity", e);
+ }
+ }
+
+ MenuItem.DisplayBehavior displayBehavior = displayBehaviorInt == 0
+ ? MenuItem.DisplayBehavior.ALWAYS
+ : MenuItem.DisplayBehavior.NEVER;
+
+ parser.next();
+ parser.require(XmlPullParser.END_TAG, null, "MenuItem");
+
+ MenuItem.Builder builder = new MenuItem.Builder(c)
+ .setTitle(title)
+ .setIcon(icon)
+ .setOnClickListener(onClickListener)
+ .setUxRestrictions(uxRestrictions)
+ .setTinted(tinted)
+ .setVisible(visible)
+ .setShowIconAndTitle(showIconAndTitle)
+ .setDisplayBehavior(displayBehavior);
+
+ if (checkable || checkedExists) {
+ builder.setChecked(checked);
+ }
+
+ if (activatable || activatedExists) {
+ builder.setActivated(activated);
+ }
+
+ return builder.build();
+ } finally {
+ a.recycle();
+ }
+ }
}
diff --git a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
index 01e7c97..3a6a2d8 100644
--- a/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
+++ b/car-ui-lib/src/com/android/car/ui/toolbar/Toolbar.java
@@ -19,7 +19,6 @@
import android.app.AlertDialog;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
-import android.content.ContextWrapper;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
@@ -39,8 +38,10 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
+import androidx.annotation.XmlRes;
import com.android.car.ui.R;
+import com.android.car.ui.utils.CarUiUtils;
import com.android.car.ui.utils.CarUxRestrictionsUtil;
import java.util.ArrayList;
@@ -184,6 +185,7 @@
mTitle.setText(a.getString(R.styleable.CarUiToolbar_title));
setLogo(a.getResourceId(R.styleable.CarUiToolbar_logo, 0));
setBackgroundShown(a.getBoolean(R.styleable.CarUiToolbar_showBackground, true));
+ setMenuItems(a.getResourceId(R.styleable.CarUiToolbar_menuItems, 0));
mShowMenuItemsWhileSearching = a.getBoolean(
R.styleable.CarUiToolbar_showMenuItemsWhileSearching, false);
String searchHint = a.getString(R.styleable.CarUiToolbar_searchHint);
@@ -593,6 +595,38 @@
setState(mState);
}
+ /**
+ * Sets the {@link MenuItem Menuitems} to display to a list defined in XML.
+ *
+ * The XML file must have one <MenuItems> tag, with a variable number of <MenuItem>
+ * child tags. See CarUiToolbarMenuItem in CarUi's attrs.xml for a list of available attributes.
+ *
+ * Example:
+ * <pre>
+ * <MenuItems>
+ * <MenuItem
+ * app:title="Foo"/>
+ * <MenuItem
+ * app:title="Bar"
+ * app:icon="@drawable/ic_tracklist"
+ * app:onClick="xmlMenuItemClicked"/>
+ * <MenuItem
+ * app:title="Bar"
+ * app:checkable="true"
+ * app:uxRestrictions="FULLY_RESTRICTED"
+ * app:onClick="xmlMenuItemClicked"/>
+ * </MenuItems>
+ * </pre>
+ *
+ * @see #setMenuItems(List)
+ * @return The MenuItems that were loaded from XML.
+ */
+ public List<MenuItem> setMenuItems(@XmlRes int resId) {
+ List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(mContext, resId);
+ setMenuItems(menuItems);
+ return menuItems;
+ }
+
/** Gets the {@link MenuItem MenuItems} currently displayed */
@NonNull
public List<MenuItem> getMenuItems() {
@@ -655,17 +689,6 @@
mSearchView.setSearchQuery(query);
}
- private Activity getActivity() {
- Context context = getContext();
- while (context instanceof ContextWrapper) {
- if (context instanceof Activity) {
- return (Activity) context;
- }
- context = ((ContextWrapper) context).getBaseContext();
- }
- return null;
- }
-
/**
* Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar
* for the desired state.
@@ -685,7 +708,7 @@
}
if (!absorbed) {
- Activity activity = getActivity();
+ Activity activity = CarUiUtils.getActivity(getContext());
if (activity != null) {
activity.onBackPressed();
}
diff --git a/car-ui-lib/src/com/android/car/ui/utils/ResourceUtils.java b/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
similarity index 62%
rename from car-ui-lib/src/com/android/car/ui/utils/ResourceUtils.java
rename to car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
index 9835d69..fcbe695 100644
--- a/car-ui-lib/src/com/android/car/ui/utils/ResourceUtils.java
+++ b/car-ui-lib/src/com/android/car/ui/utils/CarUiUtils.java
@@ -15,17 +15,21 @@
*/
package com.android.car.ui.utils;
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
import android.content.res.Resources;
import android.util.TypedValue;
import androidx.annotation.DimenRes;
+import androidx.annotation.Nullable;
/**
- * Collection of resource utility methods
+ * Collection of utility methods
*/
-public final class ResourceUtils {
+public final class CarUiUtils {
/** This is a utility class */
- private ResourceUtils() {}
+ private CarUiUtils() {}
/**
* Reads a float value from a dimens resource. This is necessary as {@link Resources#getFloat}
@@ -39,4 +43,21 @@
res.getValue(resId, outValue, true);
return outValue.getFloat();
}
+
+ /**
+ * Gets the {@link Activity} for a certain {@link Context}.
+ *
+ * <p>It is possible the Context is not associated with an Activity, in which case
+ * this method will return null.
+ */
+ @Nullable
+ public static Activity getActivity(Context context) {
+ while (context instanceof ContextWrapper) {
+ if (context instanceof Activity) {
+ return (Activity) context;
+ }
+ context = ((ContextWrapper) context).getBaseContext();
+ }
+ return null;
+ }
}
diff --git a/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
new file mode 100644
index 0000000..119afea
--- /dev/null
+++ b/car-ui-lib/tests/paintbooth/res/xml/menuitems.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 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.
+ -->
+<MenuItems xmlns:app="http://schemas.android.com/apk/res-auto">
+ <MenuItem
+ app:title="@string/preferences_screen_title"/>
+ <MenuItem
+ app:title="Bar"
+ app:icon="@drawable/ic_tracklist"
+ app:onClick="xmlMenuItemClicked"/>
+ <MenuItem
+ app:title="Bar"
+ app:checkable="true"
+ app:uxRestrictions="UX_RESTRICTIONS_FULLY_RESTRICTED"
+ app:onClick="xmlMenuItemClicked"/>
+</MenuItems>
diff --git a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
index 8b60d4d..cf55a5f 100644
--- a/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
+++ b/car-ui-lib/tests/paintbooth/src/com/android/car/ui/paintbooth/toolbar/ToolbarActivity.java
@@ -71,6 +71,11 @@
mButtons.add(Pair.create("Change title", v ->
toolbar.setTitle(toolbar.getTitle() + " X")));
+ mButtons.add(Pair.create("MenuItem: Set to XML source", v -> {
+ mMenuItems.clear();
+ toolbar.setMenuItems(R.xml.menuitems);
+ }));
+
mButtons.add(Pair.create("MenuItem: Add Icon", v -> {
mMenuItems.add(MenuItem.Builder.createSettings(this, i ->
Toast.makeText(this, "Clicked", Toast.LENGTH_SHORT).show()));
@@ -234,6 +239,11 @@
prv.setAdapter(mAdapter);
}
+ public void xmlMenuItemClicked(MenuItem item) {
+ Toast.makeText(this, "Xml item clicked! " + item.getTitle(),
+ Toast.LENGTH_SHORT).show();
+ }
+
private void getMenuItem(MenuItem.OnClickListener listener) {
if (mMenuItems.size() == 1) {
listener.onClick(mMenuItems.get(0));