Add tabs to car-ui-lib shared library
Bug: 175624230
Test: Manually
Change-Id: I63289d5a843a51fd1bdc704eb27b0e465abc9673
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java
index 288de5b..106b612 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ImageViewListener.java
@@ -23,11 +23,11 @@
import java.util.function.Consumer;
@SuppressWarnings("AndroidJdkLibsChecker")
-final class ImageViewListener extends ImageView {
+public final class ImageViewListener extends ImageView {
private Consumer<Drawable> mImageDrawableListener;
- ImageViewListener(Context context) {
+ public ImageViewListener(Context context) {
super(context);
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java
index 1f45508..477d42f 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabAdapterV1.java
@@ -35,22 +35,12 @@
TabAdapterV1(Context context, Tab clientTab, Runnable onClickListener) {
ImageViewListener imageView = new ImageViewListener(context);
- imageView.setImageDrawableListener(drawable -> {
- mIcon = drawable;
- if (mUpdateListener != null) {
- mUpdateListener.accept(this);
- }
- });
- clientTab.bindIcon(imageView);
+ imageView.setImageDrawableListener(this::setIcon);
+ clientTab.bindIconPublic(imageView);
TextViewListener textView = new TextViewListener(context);
- textView.setTextListener(text -> {
- mText = text;
- if (mUpdateListener != null) {
- mUpdateListener.accept(this);
- }
- });
- clientTab.bindText(textView);
+ textView.setTextListener(this::setTitle);
+ clientTab.bindTextPublic(textView);
mOnClickListener = onClickListener;
mClientTab = clientTab;
@@ -70,11 +60,25 @@
return mText;
}
+ public void setTitle(CharSequence title) {
+ mText = title;
+ if (mUpdateListener != null) {
+ mUpdateListener.accept(this);
+ }
+ }
+
@Override
public Drawable getIcon() {
return mIcon;
}
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ if (mUpdateListener != null) {
+ mUpdateListener.accept(this);
+ }
+ }
+
@Override
public Runnable getOnClickListener() {
return mOnClickListener;
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java
index 1f973d2..f16d8bd 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TabLayout.java
@@ -295,9 +295,25 @@
textView.setText(mText);
}
+ /**
+ * Do not use, this method is here for the shared library adapters, which cannot
+ * call the protected version due to being in a different classloader.
+ */
+ public final void bindTextPublic(TextView textView) {
+ bindText(textView);
+ }
+
/** Set icon drawable. TODO(b/139444064): revise this api.*/
protected void bindIcon(ImageView imageView) {
imageView.setImageDrawable(mIcon);
}
+
+ /**
+ * Do not use, this method is here for the shared library adapters, which cannot
+ * call the protected version due to being in a different classloader.
+ */
+ public final void bindIconPublic(ImageView imageView) {
+ bindIcon(imageView);
+ }
}
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java
index 319e0f4..ca6152a 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/TextViewListener.java
@@ -22,10 +22,10 @@
import java.util.function.Consumer;
@SuppressWarnings("AndroidJdkLibsChecker")
-final class TextViewListener extends TextView {
+public final class TextViewListener extends TextView {
private Consumer<CharSequence> mTextListener;
- TextViewListener(Context context) {
+ public TextViewListener(Context context) {
super(context);
}
diff --git a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java
index 0dcda9a..2d4c62c 100644
--- a/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java
+++ b/car-ui-lib/car-ui-lib/src/main/java/com/android/car/ui/toolbar/ToolbarControllerAdapterV1.java
@@ -19,6 +19,7 @@
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
import android.util.Log;
import android.view.View;
@@ -42,6 +43,7 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
@@ -54,15 +56,10 @@
private static final String TAG = ToolbarControllerAdapterV1.class.getName();
- private ToolbarControllerOEMV1 mOemToolbar;
- private Context mContext;
+ private final ToolbarControllerOEMV1 mOemToolbar;
+ private final Context mContext;
- private final List<TabAdapterV1> mTabs = new ArrayList<>();
- private State mState = State.HOME;
- private CharSequence mTitle = null;
- private CharSequence mSubtitle = null;
- private Drawable mLogo = null;
- private boolean mShowTabsInSubpage = false;
+ private ToolbarAdapterState mAdapterState = new ToolbarAdapterState();
private final Set<OnTabSelectedListener> mOnTabSelectedListeners = new HashSet<>();
private final Set<OnBackListener> mOnBackListeners = new HashSet<>();
private final Set<OnSearchListener> mOnSearchListeners = new HashSet<>();
@@ -113,15 +110,12 @@
@Override
public void setTitle(CharSequence title) {
- mTitle = title;
- if (stateHasLogoTitleOrSubtitle(mState)) {
- mOemToolbar.setTitle(title);
- }
+ update(mAdapterState.copy().setTitle(title).build());
}
@Override
public CharSequence getTitle() {
- return mTitle;
+ return mAdapterState.getTitle();
}
@Override
@@ -131,26 +125,24 @@
@Override
public void setSubtitle(CharSequence subtitle) {
- mSubtitle = subtitle;
- if (stateHasLogoTitleOrSubtitle(mState)) {
- mOemToolbar.setSubtitle(subtitle);
- }
+ update(mAdapterState.copy().setSubtitle(subtitle).build());
}
@Override
public CharSequence getSubtitle() {
- return mSubtitle;
+ return mAdapterState.getSubtitle();
}
@Override
public int getTabCount() {
- return mTabs.size();
+ return mAdapterState.getTabs().size();
}
@Override
public int getTabPosition(Tab tab) {
- for (int i = 0; i < mTabs.size(); i++) {
- if (mTabs.get(i).getClientTab() == tab) {
+ List<TabAdapterV1> tabs = mAdapterState.getTabs();
+ for (int i = 0; i < tabs.size(); i++) {
+ if (tabs.get(i).getClientTab() == tab) {
return i;
}
}
@@ -159,40 +151,64 @@
@Override
public void addTab(Tab clientTab) {
- mTabs.add(new TabAdapterV1(mContext, clientTab, () -> {
+ ToolbarAdapterState.Builder newStateBuilder = mAdapterState.copy();
+ newStateBuilder.addTab(new TabAdapterV1(mContext, clientTab, () -> {
+ List<TabAdapterV1> tabs = mAdapterState.getTabs();
+ int selectedIndex = -1;
+ for (int i = 0; i < tabs.size(); i++) {
+ if (tabs.get(i).getClientTab() == clientTab) {
+ selectedIndex = i;
+ break;
+ }
+ }
+ // We need to update selectedIndex in our state, but don't need to call update(),
+ // as this change originated in the shared library so it already knows about it.
+ mAdapterState = mAdapterState.copy().setSelectedTab(selectedIndex).build();
+
for (OnTabSelectedListener listener : mOnTabSelectedListeners) {
listener.onTabSelected(clientTab);
}
}));
- setState(getState());
+ if (mAdapterState.getSelectedTab() < 0) {
+ newStateBuilder.setSelectedTab(0);
+ }
+ update(newStateBuilder.build());
}
@Override
public void clearAllTabs() {
- mTabs.clear();
- setState(getState());
+ update(mAdapterState.copy()
+ .setTabs(Collections.emptyList())
+ .setSelectedTab(-1)
+ .build());
}
@Override
public Tab getTab(int position) {
- TabAdapterV1 tab = mTabs.get(position);
+ List<TabAdapterV1> tabs = mAdapterState.getTabs();
+ if (position < 0 || position >= tabs.size()) {
+ throw new IllegalArgumentException("Tab position is invalid: " + position);
+ }
+ TabAdapterV1 tab = tabs.get(position);
return tab.getClientTab();
}
@Override
public void selectTab(int position) {
- mOemToolbar.selectTab(position);
+ if (position < 0 || position >= mAdapterState.getTabs().size()) {
+ throw new IllegalArgumentException("Tab position is invalid: " + position);
+ }
+ update(mAdapterState.copy().setSelectedTab(position).build());
}
@Override
public void setShowTabsInSubpage(boolean showTabs) {
- mShowTabsInSubpage = showTabs;
- setState(getState());
+ update(mAdapterState.copy().setShowTabsInSubpage(showTabs).build());
}
@Override
public boolean getShowTabsInSubpage() {
- return mShowTabsInSubpage;
+ return mAdapterState.getShowTabsInSubpage();
}
@Override
@@ -206,10 +222,7 @@
@Override
public void setLogo(Drawable drawable) {
- mLogo = drawable;
- if (stateHasLogoTitleOrSubtitle(mState)) {
- mOemToolbar.setLogo(drawable);
- }
+ update(mAdapterState.copy().setLogo(drawable).build());
}
@Override
@@ -282,9 +295,11 @@
@Override
public List<MenuItem> setMenuItems(int resId) {
- List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(mContext, resId);
- setMenuItemsInternal(menuItems);
- return menuItems;
+ //TODO(b/175624230) MenuItemRenderer cannot be reached from this classloader
+ //List<MenuItem> menuItems = MenuItemRenderer.readMenuItemList(mContext, resId);
+ //setMenuItemsInternal(menuItems);
+ //return menuItems;
+ return Collections.emptyList();
}
private void setMenuItemsInternal(@Nullable List<MenuItem> items) {
@@ -337,49 +352,87 @@
@Override
public void setState(State state) {
- boolean gainingLogoTitleOrSubtitle =
- stateHasLogoTitleOrSubtitle(state) && !stateHasLogoTitleOrSubtitle(mState);
- boolean losingLogoTitleOrSubtitle =
- !stateHasLogoTitleOrSubtitle(state) && stateHasLogoTitleOrSubtitle(mState);
- mState = state;
-
- if (gainingLogoTitleOrSubtitle) {
- mOemToolbar.setLogo(mLogo);
- mOemToolbar.setTitle(mTitle);
- mOemToolbar.setSubtitle(mSubtitle);
- } else if (losingLogoTitleOrSubtitle) {
- mOemToolbar.setLogo(null);
- mOemToolbar.setTitle(null);
- mOemToolbar.setSubtitle(null);
- }
-
- switch (state) {
- case SEARCH:
- mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_SEARCH);
- break;
- case EDIT:
- mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_EDIT);
- break;
- default:
- mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_DISABLED);
- }
-
- mOemToolbar.setBackButtonVisible(state != State.HOME);
-
- if (state == State.HOME || (state == State.SUBPAGE && mShowTabsInSubpage)) {
- mOemToolbar.setTabs(mTabs);
- } else {
- mOemToolbar.setTabs(Collections.emptyList());
- }
+ update(mAdapterState.copy().setState(state).build());
}
- private boolean stateHasLogoTitleOrSubtitle(State state) {
- return state == State.HOME || state == State.SUBPAGE;
+ /**
+ * This method takes a new {@link ToolbarAdapterState} and compares it to the current
+ * {@link #mAdapterState}. It then sends any differences it detects to the shared library
+ * toolbar.
+ *
+ * This is also the core of the logic that adapts from the client's toolbar interface to
+ * the OEM apis toolbar interface. For example, when you are in the HOME state and add tabs,
+ * it will call setTitle(null) on the shared library toolbar. This is because the client
+ * interface
+ */
+ private void update(ToolbarAdapterState newAdapterState) {
+ ToolbarAdapterState oldAdapterState = mAdapterState;
+ mAdapterState = newAdapterState;
+
+ boolean gainingTitleOrSubtitle = newAdapterState.hasTitleOrSubtitle()
+ && !oldAdapterState.hasTitleOrSubtitle();
+ boolean losingTitleOrSubtitle = !newAdapterState.hasTitleOrSubtitle()
+ && oldAdapterState.hasTitleOrSubtitle();
+ boolean gainingLogo = newAdapterState.hasLogo() && !oldAdapterState.hasLogo();
+ boolean losingLogo = !newAdapterState.hasLogo() && oldAdapterState.hasLogo();
+
+ if (gainingTitleOrSubtitle) {
+ mOemToolbar.setTitle(newAdapterState.getTitle());
+ mOemToolbar.setSubtitle(newAdapterState.getSubtitle());
+ } else if (losingTitleOrSubtitle) {
+ mOemToolbar.setTitle(null);
+ mOemToolbar.setSubtitle(null);
+ } else if (newAdapterState.hasTitleOrSubtitle()) {
+ if (!TextUtils.equals(newAdapterState.getTitle(), oldAdapterState.getTitle())) {
+ mOemToolbar.setTitle(newAdapterState.getTitle());
+ }
+ if (!TextUtils.equals(newAdapterState.getSubtitle(), oldAdapterState.getSubtitle())) {
+ mOemToolbar.setSubtitle(newAdapterState.getSubtitle());
+ }
+ }
+
+ if (gainingLogo) {
+ mOemToolbar.setLogo(newAdapterState.getLogo());
+ } else if (losingLogo) {
+ mOemToolbar.setLogo(null);
+ } else if (newAdapterState.hasLogo() && newAdapterState.getLogoDirty()) {
+ mOemToolbar.setLogo(newAdapterState.getLogo());
+ }
+
+ if (newAdapterState.getState() != oldAdapterState.getState()) {
+ switch (newAdapterState.getState()) {
+ case SEARCH:
+ mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_SEARCH);
+ break;
+ case EDIT:
+ mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_EDIT);
+ break;
+ default:
+ mOemToolbar.setSearchMode(ToolbarControllerOEMV1.SEARCH_MODE_DISABLED);
+ }
+ }
+
+ if (oldAdapterState.hasBackButton() != newAdapterState.hasBackButton()) {
+ mOemToolbar.setBackButtonVisible(newAdapterState.hasBackButton());
+ }
+
+ boolean gainingTabs = newAdapterState.hasTabs() && !oldAdapterState.hasTabs();
+ boolean losingTabs = !newAdapterState.hasTabs() && oldAdapterState.hasTabs();
+ if (gainingTabs) {
+ mOemToolbar.setTabs(newAdapterState.getTabs(), newAdapterState.getSelectedTab());
+ } else if (losingTabs) {
+ mOemToolbar.setTabs(Collections.emptyList(), -1);
+ } else if (newAdapterState.hasTabs() && newAdapterState.getTabsDirty()) {
+ mOemToolbar.setTabs(newAdapterState.getTabs(), newAdapterState.getSelectedTab());
+ } else if (newAdapterState.hasTabs()
+ && newAdapterState.getSelectedTab() != oldAdapterState.getSelectedTab()) {
+ mOemToolbar.selectTab(newAdapterState.getSelectedTab());
+ }
}
@Override
public State getState() {
- return mState;
+ return mAdapterState.getState();
}
@Override
@@ -475,4 +528,205 @@
}
return result;
}
+
+ private static class ToolbarAdapterState {
+ private final State mState;
+ private final boolean mShowTabsInSubpage;
+ @NonNull
+ private final List<TabAdapterV1> mTabs;
+ private final int mSelectedTab;
+ private final CharSequence mTitle;
+ private final CharSequence mSubtitle;
+ private final Drawable mLogo;
+ private final boolean mTabsDirty;
+ private final boolean mLogoDirty;
+
+ ToolbarAdapterState() {
+ mState = State.HOME;
+ mShowTabsInSubpage = false;
+ mTabs = Collections.emptyList();
+ mSelectedTab = -1;
+ mTitle = null;
+ mSubtitle = null;
+ mLogo = null;
+ mTabsDirty = false;
+ mLogoDirty = false;
+ }
+
+ private ToolbarAdapterState(Builder builder) {
+ mState = builder.mState;
+ mShowTabsInSubpage = builder.mShowTabsInSubpage;
+ mTabs = builder.mTabs;
+ mSelectedTab = builder.mSelectedTab;
+ mTitle = builder.mTitle;
+ mSubtitle = builder.mSubtitle;
+ mLogo = builder.mLogo;
+ mTabsDirty = builder.mTabsDirty;
+ mLogoDirty = builder.mLogoDirty;
+ }
+
+ public State getState() {
+ return mState;
+ }
+
+ public boolean getShowTabsInSubpage() {
+ return mShowTabsInSubpage;
+ }
+
+ @NonNull
+ public List<TabAdapterV1> getTabs() {
+ return mTabs;
+ }
+
+ public int getSelectedTab() {
+ return mSelectedTab;
+ }
+
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public CharSequence getSubtitle() {
+ return mSubtitle;
+ }
+
+ public Drawable getLogo() {
+ return mLogo;
+ }
+
+ public boolean getTabsDirty() {
+ return mTabsDirty;
+ }
+
+ public boolean getLogoDirty() {
+ return mLogoDirty;
+ }
+
+ private boolean hasLogo() {
+ State state = getState();
+ return (state == State.HOME || state == State.SUBPAGE) && getLogo() != null;
+ }
+
+ private boolean hasTitleOrSubtitle() {
+ State state = getState();
+ return state == State.HOME || state == State.SUBPAGE;
+ }
+
+ private boolean hasTabs() {
+ State state = getState();
+ return (state == State.HOME
+ || state == State.SUBPAGE && getShowTabsInSubpage())
+ && getTabs().size() > 0;
+ }
+
+ private boolean hasBackButton() {
+ return getState() != State.HOME;
+ }
+
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ public static class Builder {
+ private final ToolbarAdapterState mStateClonedFrom;
+ private boolean mWasChanged = false;
+ private State mState;
+ private boolean mShowTabsInSubpage;
+ @NonNull
+ private List<TabAdapterV1> mTabs;
+ private int mSelectedTab;
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+ private Drawable mLogo;
+ private boolean mTabsDirty = false;
+ private boolean mLogoDirty = false;
+
+ Builder(ToolbarAdapterState state) {
+ mStateClonedFrom = state;
+ mState = state.getState();
+ mShowTabsInSubpage = state.getShowTabsInSubpage();
+ mTabs = state.getTabs();
+ mSelectedTab = state.getSelectedTab();
+ mTitle = state.getTitle();
+ mSubtitle = state.getSubtitle();
+ mLogo = state.getLogo();
+ }
+
+ public ToolbarAdapterState build() {
+ if (!mWasChanged) {
+ return mStateClonedFrom;
+ } else {
+ return new ToolbarAdapterState(this);
+ }
+ }
+
+ public Builder setState(State state) {
+ if (state != mState) {
+ mState = state;
+ mWasChanged = true;
+ }
+ return this;
+ }
+
+ public Builder setShowTabsInSubpage(boolean showTabsInSubpage) {
+ if (mShowTabsInSubpage != showTabsInSubpage) {
+ mShowTabsInSubpage = showTabsInSubpage;
+ mWasChanged = true;
+ }
+ return this;
+ }
+
+ public Builder setTabs(
+ @NonNull List<TabAdapterV1> tabs) {
+ if (!Objects.equals(tabs, mTabs)) {
+ mTabs = Collections.unmodifiableList(tabs);
+ mWasChanged = true;
+ mTabsDirty = true;
+ }
+ return this;
+ }
+
+ public Builder addTab(@NonNull TabAdapterV1 tab) {
+ List<TabAdapterV1> newTabs = new ArrayList<>(mTabs);
+ newTabs.add(tab);
+ mTabs = Collections.unmodifiableList(newTabs);
+ mWasChanged = true;
+ mTabsDirty = true;
+ return this;
+ }
+
+ public Builder setSelectedTab(int selectedTab) {
+ if (mSelectedTab != selectedTab) {
+ mSelectedTab = selectedTab;
+ mWasChanged = true;
+ }
+ return this;
+ }
+
+ public Builder setTitle(CharSequence title) {
+ if (!Objects.equals(mTitle, title)) {
+ mTitle = title;
+ mWasChanged = true;
+ }
+ return this;
+ }
+
+ public Builder setSubtitle(CharSequence subtitle) {
+ if (!Objects.equals(mSubtitle, subtitle)) {
+ mSubtitle = subtitle;
+ mWasChanged = true;
+ }
+ return this;
+ }
+
+ public Builder setLogo(Drawable logo) {
+ if (mLogo != logo) {
+ mLogo = logo;
+ mWasChanged = true;
+ mLogoDirty = true;
+ }
+ return this;
+ }
+ }
+ }
}
diff --git a/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java b/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java
index 17efbf9..d2b4719 100644
--- a/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java
+++ b/car-ui-lib/oem-apis/src/main/java/com/android/car/ui/sharedlibrary/oemapis/toolbar/ToolbarControllerOEMV1.java
@@ -16,7 +16,6 @@
package com.android.car.ui.sharedlibrary.oemapis.toolbar;
-import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
@@ -27,9 +26,6 @@
@SuppressWarnings("AndroidJdkLibsChecker")
public interface ToolbarControllerOEMV1 {
- /** Gets the context used by the views of this toolbar */
- Context getContext();
-
/**
* Sets the title of the toolbar to a CharSequence.
*
@@ -59,8 +55,9 @@
* or else the list could be modified from the app when the toolbar wasn't expecting it.
*
* @param tabs Nullable. Must not be mutated. List of tabs to show.
+ * @param selectedTab The index of the tab that is initially selected.
*/
- void setTabs(List<? extends TabOEMV1> tabs);
+ void setTabs(List<? extends TabOEMV1> tabs, int selectedTab);
/**
* Selects a tab added to this toolbar. See
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java
index d28d330..78c1f62 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/BaseLayoutInstaller.java
@@ -64,7 +64,8 @@
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
- ToolbarControllerOEMV1 toolbarController = new ToolbarControllerImpl(baseLayout);
+ ToolbarControllerOEMV1 toolbarController = new ToolbarControllerImpl(
+ baseLayout, sharedLibraryContext);
InsetsUpdater updater = new InsetsUpdater(baseLayout, contentView);
updater.replaceInsetsChangedListenerWith(insetsChangedListener);
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/TabLayout.java b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/TabLayout.java
new file mode 100644
index 0000000..3a7f358
--- /dev/null
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/TabLayout.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2021 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.google.car.ui.sharedlibrary.toolbar;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.ui.sharedlibrary.oemapis.toolbar.TabOEMV1;
+
+import com.google.car.ui.sharedlibrary.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A view that can show tabs via the {@link #setTabs(List, int)} method.
+ */
+public class TabLayout extends LinearLayout {
+ private List<? extends TabOEMV1> mTabs = new ArrayList<>();
+ private TabOEMV1 mSelectedTab;
+ private final Map<TabOEMV1, View> mTabViews = new HashMap<>();
+
+ public TabLayout(Context context) {
+ super(context);
+ }
+
+ public TabLayout(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TabLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Sets a list of tabs to show.
+ *
+ * @param tabs The tabs to show.
+ * @param selectedTab Which tab is selected.
+ */
+ public void setTabs(@NonNull List<? extends TabOEMV1> tabs, int selectedTab) {
+ removeAllViews();
+ mTabViews.clear();
+ mSelectedTab = null;
+ mTabs = tabs;
+
+ if (tabs != null && tabs.size() > 0) {
+ mSelectedTab = tabs.get(selectedTab);
+ for (TabOEMV1 tab : tabs) {
+ View view = LayoutInflater.from(getContext())
+ .inflate(R.layout.toolbar_tab, this, false);
+ addView(view);
+ tab.setUpdateListener(this::bindTab);
+ mTabViews.put(tab, view);
+ bindTab(tab);
+ }
+ }
+ }
+
+ private void bindTab(TabOEMV1 tab) {
+ View view = mTabViews.get(tab);
+ if (view == null) {
+ return;
+ }
+
+ ImageView iconView = view.requireViewById(R.id.car_ui_toolbar_tab_item_icon);
+ TextView textView = view.requireViewById(R.id.car_ui_toolbar_tab_item_text);
+
+ view.setOnClickListener(v -> selectTab(tab));
+ iconView.setImageDrawable(tab.getIcon());
+ textView.setText(tab.getTitle());
+
+ boolean selected = tab == mSelectedTab;
+ view.setActivated(selected);
+ textView.setTextAppearance(selected
+ ? R.style.TextAppearance_CarUi_Widget_Toolbar_Tab_Selected
+ : R.style.TextAppearance_CarUi_Widget_Toolbar_Tab);
+ }
+
+ /**
+ * Selects a particular tab.
+ */
+ private void selectTab(TabOEMV1 tab) {
+ if (mSelectedTab == tab) {
+ return;
+ }
+
+ TabOEMV1 oldSelectedTab = mSelectedTab;
+ mSelectedTab = tab;
+ bindTab(oldSelectedTab);
+ bindTab(mSelectedTab);
+
+ Runnable onClickListener = tab.getOnClickListener();
+ if (onClickListener != null) {
+ onClickListener.run();
+ }
+ }
+
+ /**
+ * Selects a particular tab by its position in the list.
+ */
+ public void selectTab(int position) {
+ selectTab(mTabs.get(position));
+ }
+
+ /**
+ * Returns if this TabLayout has at least one tab.
+ */
+ public boolean hasTabs() {
+ return mTabs != null && mTabs.size() > 0;
+ }
+}
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java
index b96a896..329fd6f 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/java/com/google/car/ui/sharedlibrary/toolbar/ToolbarControllerImpl.java
@@ -30,19 +30,21 @@
import com.google.car.ui.sharedlibrary.R;
+import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@SuppressWarnings("AndroidJdkLibsChecker")
class ToolbarControllerImpl implements ToolbarControllerOEMV1 {
- private final Context mContext;
+ private final Context mSharedLibraryContext;
private final ImageView mBackButtonView;
private final TextView mTitleView;
private final TextView mSubtitleView;
private final ImageView mLogo;
private final ImageView mLogoInNavIconSpace;
private final ProgressBarController mProgressBar;
+ private final TabLayout mTabContainer;
private Runnable mBackListener;
private int mNavButtonMode = ToolbarControllerOEMV1.NAV_BUTTON_MODE_BACK;
@@ -50,9 +52,8 @@
private boolean mBackButtonVisible = false;
private boolean mHasLogo = false;
- ToolbarControllerImpl(View view) {
- mContext = view.getContext();
-
+ ToolbarControllerImpl(View view, Context sharedLibraryContext) {
+ mSharedLibraryContext = sharedLibraryContext;
mBackButtonView = view.requireViewById(R.id.toolbar_nav_icon);
mTitleView = view.requireViewById(R.id.toolbar_title);
mSubtitleView = view.requireViewById(R.id.toolbar_subtitle);
@@ -60,11 +61,7 @@
mLogoInNavIconSpace = view.requireViewById(R.id.toolbar_logo);
mProgressBar = new ProgressBarController(
view.requireViewById(R.id.toolbar_progress_bar));
- }
-
- @Override
- public Context getContext() {
- return mContext;
+ mTabContainer = view.requireViewById(R.id.toolbar_tabs);
}
@Override
@@ -73,8 +70,6 @@
mTitleView.setText(title);
boolean hasTitle = !TextUtils.isEmpty(getTitle());
- setVisible(mTitleView, hasTitle);
-
if (hadTitle != hasTitle) {
update();
}
@@ -91,8 +86,6 @@
mSubtitleView.setText(title);
boolean hasSubtitle = !TextUtils.isEmpty(getSubtitle());
- setVisible(mSubtitleView, hasSubtitle);
-
if (hadSubtitle != hasSubtitle) {
update();
}
@@ -104,13 +97,23 @@
}
@Override
- public void setTabs(List<? extends TabOEMV1> tabs) {
+ public void setTabs(List<? extends TabOEMV1> tabs, int selectedTab) {
+ if (tabs == null) {
+ tabs = Collections.emptyList();
+ }
+ boolean hadTabs = mTabContainer.hasTabs();
+ mTabContainer.setTabs(tabs, selectedTab);
+ boolean hasTabs = mTabContainer.hasTabs();
+
+ if (hadTabs != hasTabs) {
+ update();
+ }
}
@Override
public void selectTab(int position) {
-
+ mTabContainer.selectTab(position);
}
@Override
@@ -254,6 +257,10 @@
setVisible(mBackButtonView, mBackButtonVisible);
setVisible(mLogoInNavIconSpace, mHasLogo && !mBackButtonVisible);
setVisible(mLogo, mHasLogo && mBackButtonVisible);
+ boolean hasTabs = mTabContainer.hasTabs();
+ setVisible(mTabContainer, hasTabs);
+ setVisible(mTitleView, !TextUtils.isEmpty(getTitle()) && !hasTabs);
+ setVisible(mSubtitleView, !TextUtils.isEmpty(getSubtitle()) && !hasTabs);
}
private static void setVisible(View view, boolean visible) {
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml
index 77c56c0..cbcc9a1 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/base_layout_toolbar.xml
@@ -86,7 +86,7 @@
android:textAppearance="?android:attr/textAppearanceSmall"/>
</LinearLayout>
- <LinearLayout
+ <com.google.car.ui.sharedlibrary.toolbar.TabLayout
android:id="@+id/toolbar_tabs"
android:layout_width="wrap_content"
android:layout_height="0dp"
@@ -94,7 +94,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/toolbar_menu_items_container"
app:layout_constraintHorizontal_bias="0.0"
- app:layout_constraintStart_toEndOf="@+id/toolbar_title_logo_container"
+ app:layout_constraintStart_toEndOf="@+id/toolbar_title_logo"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/toolbar_tab.xml b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/toolbar_tab.xml
new file mode 100644
index 0000000..7ca84be
--- /dev/null
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/layout/toolbar_tab.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:gravity="center"
+ android:background="?android:attr/selectableItemBackground">
+ <ImageView
+ android:id="@+id/car_ui_toolbar_tab_item_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:scaleType="fitCenter"
+ android:tint="@color/toolbar_tab_item_selector"
+ android:tintMode="src_in" />
+ <TextView
+ android:id="@+id/car_ui_toolbar_tab_item_text"
+ android:layout_width="135dp"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance.CarUi.Widget.Toolbar.Tab"/>
+</LinearLayout>
diff --git a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml
index b7c39b7..230f314 100644
--- a/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml
+++ b/car-ui-lib/referencedesign/sharedlibrary/src/main/res/values/values-toolbar.xml
@@ -3,4 +3,14 @@
<dimen name="toolbar_margin">112dp</dimen>
<dimen name="toolbar_row_height">96dp</dimen>
<dimen name="toolbar_menu_item_icon_ripple_radius">48dp</dimen>
+
+ <style name="TextAppearance.CarUi.Widget.Toolbar.Tab" parent="TextAppearance.CarUi.Body3">
+ <item name="android:textColor">@color/toolbar_tab_item_selector</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:textFontWeight">400</item>
+ </style>
+
+ <style name="TextAppearance.CarUi.Widget.Toolbar.Tab.Selected">
+ <item name="android:textFontWeight">500</item>
+ </style>
</resources>