blob: ba51cd36cd21e061c5fc317077bb630f1decd451 [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.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.XmlRes;
import com.android.car.ui.R;
import java.util.List;
/**
* A toolbar for Android Automotive OS apps.
*
* <p>This isn't a toolbar in the android framework sense, it's merely a custom view that can be
* added to a layout. (You can't call
* {@link android.app.Activity#setActionBar(android.widget.Toolbar)} with it)
*
* <p>The toolbar supports a navigation button, title, tabs, search, and {@link MenuItem MenuItems}
*/
public class Toolbar extends FrameLayout implements ToolbarController {
/** Callback that will be issued whenever the height of toolbar is changed. */
public interface OnHeightChangedListener {
/**
* Will be called when the height of the toolbar is changed.
*
* @param height new height of the toolbar
*/
void onHeightChanged(int height);
}
/** Back button listener */
public interface OnBackListener {
/**
* Invoked when the user clicks on the back button. By default, the toolbar will call
* the Activity's {@link android.app.Activity#onBackPressed()}. Returning true from
* this method will absorb the back press and prevent that behavior.
*/
boolean onBack();
}
/** Tab selection listener */
public interface OnTabSelectedListener {
/** Called when a {@link TabLayout.Tab} is selected */
void onTabSelected(TabLayout.Tab tab);
}
/** Search listener */
public interface OnSearchListener {
/**
* Invoked when the user edits a search query.
*
* <p>This is called for every letter the user types, and also empty strings if the user
* erases everything.
*/
void onSearch(String query);
}
/** Search completed listener */
public interface OnSearchCompletedListener {
/**
* Invoked when the user submits a search query by clicking the keyboard's search / done
* button.
*/
void onSearchCompleted();
}
private static final String TAG = "CarUiToolbar";
/** Enum of states the toolbar can be in. Controls what elements of the toolbar are displayed */
public enum State {
/**
* In the HOME state, the logo will be displayed if there is one, and no navigation icon
* will be displayed. The tab bar will be visible. The title will be displayed if there
* is space. MenuItems will be displayed.
*/
HOME,
/**
* In the SUBPAGE state, the logo will be replaced with a back button, the tab bar won't
* be visible. The title and MenuItems will be displayed.
*/
SUBPAGE,
/**
* In the SEARCH state, only the back button and the search bar will be visible.
*/
SEARCH,
/**
* In the EDIT state, the search bar will look like a regular text box, but will be
* functionally identical to the SEARCH state.
*/
EDIT,
}
private ToolbarControllerImpl mController;
private boolean mEatingTouch = false;
private boolean mEatingHover = false;
public Toolbar(Context context) {
this(context, null);
}
public Toolbar(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.CarUiToolbarStyle);
}
public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(getToolbarLayout(), this, true);
mController = new ToolbarControllerImpl(this);
TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.CarUiToolbar, defStyleAttr, defStyleRes);
try {
setShowTabsInSubpage(a.getBoolean(R.styleable.CarUiToolbar_showTabsInSubpage, false));
setTitle(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));
String searchHint = a.getString(R.styleable.CarUiToolbar_searchHint);
if (searchHint != null) {
setSearchHint(searchHint);
}
switch (a.getInt(R.styleable.CarUiToolbar_car_ui_state, 0)) {
case 0:
setState(State.HOME);
break;
case 1:
setState(State.SUBPAGE);
break;
case 2:
setState(State.SEARCH);
break;
default:
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unknown initial state");
}
break;
}
switch (a.getInt(R.styleable.CarUiToolbar_car_ui_navButtonMode, 0)) {
case 0:
setNavButtonMode(NavButtonMode.BACK);
break;
case 1:
setNavButtonMode(NavButtonMode.CLOSE);
break;
case 2:
setNavButtonMode(NavButtonMode.DOWN);
break;
default:
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unknown navigation button style");
}
break;
}
} finally {
a.recycle();
}
}
/**
* Override this in a subclass to allow for different toolbar layouts within a single app.
*
* <p>Non-system apps should not use this, as customising the layout isn't possible with RROs
*/
protected int getToolbarLayout() {
if (getContext().getResources().getBoolean(
R.bool.car_ui_toolbar_tabs_on_second_row)) {
return R.layout.car_ui_toolbar_two_row;
}
return R.layout.car_ui_toolbar;
}
/**
* Returns {@code true} if a two row layout in enabled for the toolbar.
*/
@Override
public boolean isTabsInSecondRow() {
return mController.isTabsInSecondRow();
}
/**
* Sets the title of the toolbar to a string resource.
*
* <p>The title may not always be shown, for example with one row layout with tabs.
*/
@Override
public void setTitle(@StringRes int title) {
mController.setTitle(title);
}
/**
* Sets the title of the toolbar to a CharSequence.
*
* <p>The title may not always be shown, for example with one row layout with tabs.
*/
@Override
public void setTitle(CharSequence title) {
mController.setTitle(title);
}
@Override
public CharSequence getTitle() {
return mController.getTitle();
}
/**
* Sets the subtitle of the toolbar to a string resource.
*
* <p>The title may not always be shown, for example with one row layout with tabs.
*/
@Override
public void setSubtitle(@StringRes int title) {
mController.setSubtitle(title);
}
/**
* Sets the subtitle of the toolbar to a CharSequence.
*
* <p>The title may not always be shown, for example with one row layout with tabs.
*/
@Override
public void setSubtitle(CharSequence title) {
mController.setSubtitle(title);
}
@Override
public CharSequence getSubtitle() {
return mController.getSubtitle();
}
/**
* Gets the {@link TabLayout} for this toolbar.
* @deprecated Use other tab-related functions in the ToolbarController interface.
*/
@Deprecated
@Override
public TabLayout getTabLayout() {
return mController.getTabLayout();
}
/**
* Gets the number of tabs in the toolbar. The tabs can be retrieved using
* {@link #getTab(int)}.
*/
@Override
public int getTabCount() {
return mController.getTabCount();
}
/**
* Gets the index of the tab.
*/
@Override
public int getTabPosition(TabLayout.Tab tab) {
return mController.getTabPosition(tab);
}
/**
* Adds a tab to this toolbar. You can listen for when it is selected via
* {@link #registerOnTabSelectedListener(OnTabSelectedListener)}.
*/
@Override
public void addTab(TabLayout.Tab tab) {
mController.addTab(tab);
}
/** Removes all the tabs. */
@Override
public void clearAllTabs() {
mController.clearAllTabs();
}
/**
* Gets a tab added to this toolbar. See
* {@link #addTab(TabLayout.Tab)}.
*/
@Override
public TabLayout.Tab getTab(int position) {
return mController.getTab(position);
}
/**
* Selects a tab added to this toolbar. See
* {@link #addTab(TabLayout.Tab)}.
*/
@Override
public void selectTab(int position) {
mController.selectTab(position);
}
/**
* Sets whether or not tabs should also be shown in the SUBPAGE {@link State}.
*/
@Override
public void setShowTabsInSubpage(boolean showTabs) {
mController.setShowTabsInSubpage(showTabs);
}
/**
* Gets whether or not tabs should also be shown in the SUBPAGE {@link State}.
*/
@Override
public boolean getShowTabsInSubpage() {
return mController.getShowTabsInSubpage();
}
/**
* Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
* will be displayed next to the title.
*/
@Override
public void setLogo(@DrawableRes int resId) {
mController.setLogo(resId);
}
/**
* Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
* will be displayed next to the title.
*/
@Override
public void setLogo(Drawable drawable) {
mController.setLogo(drawable);
}
/** Sets the hint for the search bar. */
@Override
public void setSearchHint(@StringRes int resId) {
mController.setSearchHint(resId);
}
/** Sets the hint for the search bar. */
@Override
public void setSearchHint(CharSequence hint) {
mController.setSearchHint(hint);
}
/** Gets the search hint */
@Override
public CharSequence getSearchHint() {
return mController.getSearchHint();
}
/**
* Sets the icon to display in the search box.
*
* <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
* a similar place.
*/
@Override
public void setSearchIcon(@DrawableRes int resId) {
mController.setSearchIcon(resId);
}
/**
* Sets the icon to display in the search box.
*
* <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
* a similar place.
*/
@Override
public void setSearchIcon(Drawable d) {
mController.setSearchIcon(d);
}
/**
* An enum of possible styles the nav button could be in. All styles will still call
* {@link OnBackListener#onBack()}.
*/
public enum NavButtonMode {
/** A back button */
BACK,
/** A close button */
CLOSE,
/** A down button, used to indicate that the page will animate down when navigating away */
DOWN
}
/** Sets the {@link NavButtonMode} */
@Override
public void setNavButtonMode(NavButtonMode style) {
mController.setNavButtonMode(style);
}
/** Gets the {@link NavButtonMode} */
@Override
public NavButtonMode getNavButtonMode() {
return mController.getNavButtonMode();
}
/**
* setBackground is disallowed, to prevent apps from deviating from the intended style too much.
*/
@Override
public void setBackground(Drawable d) {
throw new UnsupportedOperationException(
"You can not change the background of a CarUi toolbar, use "
+ "setBackgroundShown(boolean) or an RRO instead.");
}
/** Show/hide the background. When hidden, the toolbar is completely transparent. */
@Override
public void setBackgroundShown(boolean shown) {
mController.setBackgroundShown(shown);
}
/** Returns true is the toolbar background is shown */
@Override
public boolean getBackgroundShown() {
return mController.getBackgroundShown();
}
/**
* Sets the {@link MenuItem Menuitems} to display.
*/
@Override
public void setMenuItems(@Nullable List<MenuItem> items) {
mController.setMenuItems(items);
}
/**
* Sets the {@link MenuItem Menuitems} to display to a list defined in XML.
*
* <p>If this method is called twice with the same argument (and {@link #setMenuItems(List)}
* wasn't called), nothing will happen the second time, even if the MenuItems were changed.
*
* <p>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>
*
* @return The MenuItems that were loaded from XML.
* @see #setMenuItems(List)
*/
@Override
public List<MenuItem> setMenuItems(@XmlRes int resId) {
return mController.setMenuItems(resId);
}
/** Gets the {@link MenuItem MenuItems} currently displayed */
@Override
@NonNull
public List<MenuItem> getMenuItems() {
return mController.getMenuItems();
}
/** Gets a {@link MenuItem} by id. */
@Override
@Nullable
public MenuItem findMenuItemById(int id) {
return mController.findMenuItemById(id);
}
/** Gets a {@link MenuItem} by id. Will throw an exception if not found. */
@Override
@NonNull
public MenuItem requireMenuItemById(int id) {
return mController.requireMenuItemById(id);
}
/**
* Set whether or not to show the {@link MenuItem MenuItems} while searching. Default false.
* Even if this is set to true, the {@link MenuItem} created by
* {@link MenuItem.Builder#setToSearch()} will still be hidden.
*/
@Override
public void setShowMenuItemsWhileSearching(boolean showMenuItems) {
mController.setShowMenuItemsWhileSearching(showMenuItems);
}
/** Returns if {@link MenuItem MenuItems} are shown while searching */
@Override
public boolean getShowMenuItemsWhileSearching() {
return mController.getShowMenuItemsWhileSearching();
}
/**
* Sets the search query.
*/
@Override
public void setSearchQuery(String query) {
mController.setSearchQuery(query);
}
/**
* Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar
* for the desired state.
*/
@Override
public void setState(State state) {
mController.setState(state);
}
/** Gets the current {@link State} of the toolbar. */
@Override
public State getState() {
return mController.getState();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Copied from androidx.appcompat.widget.Toolbar
// Toolbars always eat touch events, but should still respect the touch event dispatch
// contract. If the normal View implementation doesn't want the events, we'll just silently
// eat the rest of the gesture without reporting the events to the default implementation
// since that's what it expects.
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
mEatingTouch = false;
}
if (!mEatingTouch) {
final boolean handled = super.onTouchEvent(ev);
if (action == MotionEvent.ACTION_DOWN && !handled) {
mEatingTouch = true;
}
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mEatingTouch = false;
}
return true;
}
@Override
public boolean onHoverEvent(MotionEvent ev) {
// Copied from androidx.appcompat.widget.Toolbar
// Same deal as onTouchEvent() above. Eat all hover events, but still
// respect the touch event dispatch contract.
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_HOVER_ENTER) {
mEatingHover = false;
}
if (!mEatingHover) {
final boolean handled = super.onHoverEvent(ev);
if (action == MotionEvent.ACTION_HOVER_ENTER && !handled) {
mEatingHover = true;
}
}
if (action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_CANCEL) {
mEatingHover = false;
}
return true;
}
/**
* Registers a new {@link OnHeightChangedListener} to the list of listeners. Register a
* {@link com.android.car.ui.recyclerview.CarUiRecyclerView} only if there is a toolbar at
* the top and a {@link com.android.car.ui.recyclerview.CarUiRecyclerView} in the view and
* nothing else. {@link com.android.car.ui.recyclerview.CarUiRecyclerView} will
* automatically adjust its height according to the height of the Toolbar.
*/
@Override
public void registerToolbarHeightChangeListener(
OnHeightChangedListener listener) {
mController.registerToolbarHeightChangeListener(listener);
}
/** Unregisters an existing {@link OnHeightChangedListener} from the list of listeners. */
@Override
public boolean unregisterToolbarHeightChangeListener(
OnHeightChangedListener listener) {
return mController.unregisterToolbarHeightChangeListener(listener);
}
/** Registers a new {@link OnTabSelectedListener} to the list of listeners. */
@Override
public void registerOnTabSelectedListener(OnTabSelectedListener listener) {
mController.registerOnTabSelectedListener(listener);
}
/** Unregisters an existing {@link OnTabSelectedListener} from the list of listeners. */
@Override
public boolean unregisterOnTabSelectedListener(OnTabSelectedListener listener) {
return mController.unregisterOnTabSelectedListener(listener);
}
/** Registers a new {@link OnSearchListener} to the list of listeners. */
@Override
public void registerOnSearchListener(OnSearchListener listener) {
mController.registerOnSearchListener(listener);
}
/** Unregisters an existing {@link OnSearchListener} from the list of listeners. */
@Override
public boolean unregisterOnSearchListener(OnSearchListener listener) {
return mController.unregisterOnSearchListener(listener);
}
/** Registers a new {@link OnSearchCompletedListener} to the list of listeners. */
@Override
public void registerOnSearchCompletedListener(OnSearchCompletedListener listener) {
mController.registerOnSearchCompletedListener(listener);
}
/** Unregisters an existing {@link OnSearchCompletedListener} from the list of listeners. */
@Override
public boolean unregisterOnSearchCompletedListener(OnSearchCompletedListener listener) {
return mController.unregisterOnSearchCompletedListener(listener);
}
/** Registers a new {@link OnBackListener} to the list of listeners. */
@Override
public void registerOnBackListener(OnBackListener listener) {
mController.registerOnBackListener(listener);
}
/** Unregisters an existing {@link OnBackListener} from the list of listeners. */
@Override
public boolean unregisterOnBackListener(OnBackListener listener) {
return mController.unregisterOnBackListener(listener);
}
/** Returns the progress bar */
@Override
public ProgressBarController getProgressBar() {
return mController.getProgressBar();
}
}