1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.ui.toolbar;
17 
18 import android.content.Context;
19 import android.content.res.TypedArray;
20 import android.graphics.drawable.Drawable;
21 import android.util.AttributeSet;
22 import android.util.Log;
23 import android.view.LayoutInflater;
24 import android.view.MotionEvent;
25 import android.widget.FrameLayout;
26 import android.widget.ProgressBar;
27 
28 import androidx.annotation.DrawableRes;
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.annotation.StringRes;
32 import androidx.annotation.XmlRes;
33 
34 import com.android.car.ui.R;
35 
36 import java.util.List;
37 
38 /**
39  * A toolbar for Android Automotive OS apps.
40  *
41  * <p>This isn't a toolbar in the android framework sense, it's merely a custom view that can be
42  * added to a layout. (You can't call
43  * {@link android.app.Activity#setActionBar(android.widget.Toolbar)} with it)
44  *
45  * <p>The toolbar supports a navigation button, title, tabs, search, and {@link MenuItem MenuItems}
46  */
47 public class Toolbar extends FrameLayout implements ToolbarController {
48 
49     /** Callback that will be issued whenever the height of toolbar is changed. */
50     public interface OnHeightChangedListener {
51         /**
52          * Will be called when the height of the toolbar is changed.
53          *
54          * @param height new height of the toolbar
55          */
onHeightChanged(int height)56         void onHeightChanged(int height);
57     }
58 
59     /** Back button listener */
60     public interface OnBackListener {
61         /**
62          * Invoked when the user clicks on the back button. By default, the toolbar will call
63          * the Activity's {@link android.app.Activity#onBackPressed()}. Returning true from
64          * this method will absorb the back press and prevent that behavior.
65          */
onBack()66         boolean onBack();
67     }
68 
69     /** Tab selection listener */
70     public interface OnTabSelectedListener {
71         /** Called when a {@link TabLayout.Tab} is selected */
onTabSelected(TabLayout.Tab tab)72         void onTabSelected(TabLayout.Tab tab);
73     }
74 
75     /** Search listener */
76     public interface OnSearchListener {
77         /**
78          * Invoked when the user edits a search query.
79          *
80          * <p>This is called for every letter the user types, and also empty strings if the user
81          * erases everything.
82          */
onSearch(String query)83         void onSearch(String query);
84     }
85 
86     /** Search completed listener */
87     public interface OnSearchCompletedListener {
88         /**
89          * Invoked when the user submits a search query by clicking the keyboard's search / done
90          * button.
91          */
onSearchCompleted()92         void onSearchCompleted();
93     }
94 
95     private static final String TAG = "CarUiToolbar";
96 
97     /** Enum of states the toolbar can be in. Controls what elements of the toolbar are displayed */
98     public enum State {
99         /**
100          * In the HOME state, the logo will be displayed if there is one, and no navigation icon
101          * will be displayed. The tab bar will be visible. The title will be displayed if there
102          * is space. MenuItems will be displayed.
103          */
104         HOME,
105         /**
106          * In the SUBPAGE state, the logo will be replaced with a back button, the tab bar won't
107          * be visible. The title and MenuItems will be displayed.
108          */
109         SUBPAGE,
110         /**
111          * In the SEARCH state, only the back button and the search bar will be visible.
112          */
113         SEARCH,
114         /**
115          * In the EDIT state, the search bar will look like a regular text box, but will be
116          * functionally identical to the SEARCH state.
117          */
118         EDIT,
119     }
120 
121     private ToolbarControllerImpl mController;
122     private boolean mEatingTouch = false;
123     private boolean mEatingHover = false;
124 
Toolbar(Context context)125     public Toolbar(Context context) {
126         this(context, null);
127     }
128 
Toolbar(Context context, AttributeSet attrs)129     public Toolbar(Context context, AttributeSet attrs) {
130         this(context, attrs, R.attr.CarUiToolbarStyle);
131     }
132 
Toolbar(Context context, AttributeSet attrs, int defStyleAttr)133     public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) {
134         this(context, attrs, defStyleAttr, 0);
135     }
136 
Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)137     public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
138         super(context, attrs, defStyleAttr, defStyleRes);
139 
140         LayoutInflater inflater = (LayoutInflater) context
141                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
142         inflater.inflate(getToolbarLayout(), this, true);
143 
144         mController = new ToolbarControllerImpl(this);
145 
146         TypedArray a = context.obtainStyledAttributes(
147                 attrs, R.styleable.CarUiToolbar, defStyleAttr, defStyleRes);
148 
149         try {
150             setShowTabsInSubpage(a.getBoolean(R.styleable.CarUiToolbar_showTabsInSubpage, false));
151             setTitle(a.getString(R.styleable.CarUiToolbar_title));
152             setLogo(a.getResourceId(R.styleable.CarUiToolbar_logo, 0));
153             setBackgroundShown(a.getBoolean(R.styleable.CarUiToolbar_showBackground, true));
154             setMenuItems(a.getResourceId(R.styleable.CarUiToolbar_menuItems, 0));
155             String searchHint = a.getString(R.styleable.CarUiToolbar_searchHint);
156             if (searchHint != null) {
157                 setSearchHint(searchHint);
158             }
159 
160             switch (a.getInt(R.styleable.CarUiToolbar_state, 0)) {
161                 case 0:
162                     setState(State.HOME);
163                     break;
164                 case 1:
165                     setState(State.SUBPAGE);
166                     break;
167                 case 2:
168                     setState(State.SEARCH);
169                     break;
170                 default:
171                     if (Log.isLoggable(TAG, Log.WARN)) {
172                         Log.w(TAG, "Unknown initial state");
173                     }
174                     break;
175             }
176 
177             switch (a.getInt(R.styleable.CarUiToolbar_navButtonMode, 0)) {
178                 case 0:
179                     setNavButtonMode(NavButtonMode.BACK);
180                     break;
181                 case 1:
182                     setNavButtonMode(NavButtonMode.CLOSE);
183                     break;
184                 case 2:
185                     setNavButtonMode(NavButtonMode.DOWN);
186                     break;
187                 default:
188                     if (Log.isLoggable(TAG, Log.WARN)) {
189                         Log.w(TAG, "Unknown navigation button style");
190                     }
191                     break;
192             }
193         } finally {
194             a.recycle();
195         }
196     }
197 
198     /**
199      * Override this in a subclass to allow for different toolbar layouts within a single app.
200      *
201      * <p>Non-system apps should not use this, as customising the layout isn't possible with RROs
202      */
getToolbarLayout()203     protected int getToolbarLayout() {
204         if (getContext().getResources().getBoolean(
205                 R.bool.car_ui_toolbar_tabs_on_second_row)) {
206             return R.layout.car_ui_toolbar_two_row;
207         }
208 
209         return R.layout.car_ui_toolbar;
210     }
211 
212     /**
213      * Returns {@code true} if a two row layout in enabled for the toolbar.
214      */
isTabsInSecondRow()215     public boolean isTabsInSecondRow() {
216         return mController.isTabsInSecondRow();
217     }
218 
219     /**
220      * Sets the title of the toolbar to a string resource.
221      *
222      * <p>The title may not always be shown, for example with one row layout with tabs.
223      */
setTitle(@tringRes int title)224     public void setTitle(@StringRes int title) {
225         mController.setTitle(title);
226     }
227 
228     /**
229      * Sets the title of the toolbar to a CharSequence.
230      *
231      * <p>The title may not always be shown, for example with one row layout with tabs.
232      */
setTitle(CharSequence title)233     public void setTitle(CharSequence title) {
234         mController.setTitle(title);
235     }
236 
getTitle()237     public CharSequence getTitle() {
238         return mController.getTitle();
239     }
240 
241     /**
242      * Gets the {@link TabLayout} for this toolbar.
243      */
getTabLayout()244     public TabLayout getTabLayout() {
245         return mController.getTabLayout();
246     }
247 
248     /**
249      * Adds a tab to this toolbar. You can listen for when it is selected via
250      * {@link #registerOnTabSelectedListener(OnTabSelectedListener)}.
251      */
addTab(TabLayout.Tab tab)252     public void addTab(TabLayout.Tab tab) {
253         mController.addTab(tab);
254     }
255 
256     /** Removes all the tabs. */
clearAllTabs()257     public void clearAllTabs() {
258         mController.clearAllTabs();
259     }
260 
261     /**
262      * Gets a tab added to this toolbar. See
263      * {@link #addTab(TabLayout.Tab)}.
264      */
getTab(int position)265     public TabLayout.Tab getTab(int position) {
266         return mController.getTab(position);
267     }
268 
269     /**
270      * Selects a tab added to this toolbar. See
271      * {@link #addTab(TabLayout.Tab)}.
272      */
selectTab(int position)273     public void selectTab(int position) {
274         mController.selectTab(position);
275     }
276 
277     /**
278      * Sets whether or not tabs should also be shown in the SUBPAGE {@link State}.
279      */
setShowTabsInSubpage(boolean showTabs)280     public void setShowTabsInSubpage(boolean showTabs) {
281         mController.setShowTabsInSubpage(showTabs);
282     }
283 
284     /**
285      * Gets whether or not tabs should also be shown in the SUBPAGE {@link State}.
286      */
getShowTabsInSubpage()287     public boolean getShowTabsInSubpage() {
288         return mController.getShowTabsInSubpage();
289     }
290 
291     /**
292      * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
293      * will be displayed next to the title.
294      */
setLogo(@rawableRes int resId)295     public void setLogo(@DrawableRes int resId) {
296         mController.setLogo(resId);
297     }
298 
299     /**
300      * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo
301      * will be displayed next to the title.
302      */
setLogo(Drawable drawable)303     public void setLogo(Drawable drawable) {
304         mController.setLogo(drawable);
305     }
306 
307     /** Sets the hint for the search bar. */
setSearchHint(@tringRes int resId)308     public void setSearchHint(@StringRes int resId) {
309         mController.setSearchHint(resId);
310     }
311 
312     /** Sets the hint for the search bar. */
setSearchHint(CharSequence hint)313     public void setSearchHint(CharSequence hint) {
314         mController.setSearchHint(hint);
315     }
316 
317     /** Gets the search hint */
getSearchHint()318     public CharSequence getSearchHint() {
319         return mController.getSearchHint();
320     }
321 
322     /**
323      * Sets the icon to display in the search box.
324      *
325      * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
326      * a similar place.
327      */
setSearchIcon(@rawableRes int resId)328     public void setSearchIcon(@DrawableRes int resId) {
329         mController.setSearchIcon(resId);
330     }
331 
332     /**
333      * Sets the icon to display in the search box.
334      *
335      * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or
336      * a similar place.
337      */
setSearchIcon(Drawable d)338     public void setSearchIcon(Drawable d) {
339         mController.setSearchIcon(d);
340     }
341 
342     /**
343      * An enum of possible styles the nav button could be in. All styles will still call
344      * {@link OnBackListener#onBack()}.
345      */
346     public enum NavButtonMode {
347         /** A back button */
348         BACK,
349         /** A close button */
350         CLOSE,
351         /** A down button, used to indicate that the page will animate down when navigating away */
352         DOWN
353     }
354 
355     /** Sets the {@link NavButtonMode} */
setNavButtonMode(NavButtonMode style)356     public void setNavButtonMode(NavButtonMode style) {
357         mController.setNavButtonMode(style);
358     }
359 
360     /** Gets the {@link NavButtonMode} */
getNavButtonMode()361     public NavButtonMode getNavButtonMode() {
362         return mController.getNavButtonMode();
363     }
364 
365     /**
366      * setBackground is disallowed, to prevent apps from deviating from the intended style too much.
367      */
368     @Override
setBackground(Drawable d)369     public void setBackground(Drawable d) {
370         throw new UnsupportedOperationException(
371                 "You can not change the background of a CarUi toolbar, use "
372                         + "setBackgroundShown(boolean) or an RRO instead.");
373     }
374 
375     /** Show/hide the background. When hidden, the toolbar is completely transparent. */
setBackgroundShown(boolean shown)376     public void setBackgroundShown(boolean shown) {
377         mController.setBackgroundShown(shown);
378     }
379 
380     /** Returns true is the toolbar background is shown */
getBackgroundShown()381     public boolean getBackgroundShown() {
382         return mController.getBackgroundShown();
383     }
384 
385     /**
386      * Sets the {@link MenuItem Menuitems} to display.
387      */
setMenuItems(@ullable List<MenuItem> items)388     public void setMenuItems(@Nullable List<MenuItem> items) {
389         mController.setMenuItems(items);
390     }
391 
392     /**
393      * Sets the {@link MenuItem Menuitems} to display to a list defined in XML.
394      *
395      * <p>If this method is called twice with the same argument (and {@link #setMenuItems(List)}
396      * wasn't called), nothing will happen the second time, even if the MenuItems were changed.
397      *
398      * <p>The XML file must have one <MenuItems> tag, with a variable number of <MenuItem>
399      * child tags. See CarUiToolbarMenuItem in CarUi's attrs.xml for a list of available attributes.
400      *
401      * Example:
402      * <pre>
403      * <MenuItems>
404      *     <MenuItem
405      *         app:title="Foo"/>
406      *     <MenuItem
407      *         app:title="Bar"
408      *         app:icon="@drawable/ic_tracklist"
409      *         app:onClick="xmlMenuItemClicked"/>
410      *     <MenuItem
411      *         app:title="Bar"
412      *         app:checkable="true"
413      *         app:uxRestrictions="FULLY_RESTRICTED"
414      *         app:onClick="xmlMenuItemClicked"/>
415      * </MenuItems>
416      * </pre>
417      *
418      * @see #setMenuItems(List)
419      * @return The MenuItems that were loaded from XML.
420      */
setMenuItems(@mlRes int resId)421     public List<MenuItem> setMenuItems(@XmlRes int resId) {
422         return mController.setMenuItems(resId);
423     }
424 
425     /** Gets the {@link MenuItem MenuItems} currently displayed */
426     @NonNull
getMenuItems()427     public List<MenuItem> getMenuItems() {
428         return mController.getMenuItems();
429     }
430 
431     /** Gets a {@link MenuItem} by id. */
432     @Nullable
findMenuItemById(int id)433     public MenuItem findMenuItemById(int id) {
434         return mController.findMenuItemById(id);
435     }
436 
437     /** Gets a {@link MenuItem} by id. Will throw an exception if not found. */
438     @NonNull
requireMenuItemById(int id)439     public MenuItem requireMenuItemById(int id) {
440         return mController.requireMenuItemById(id);
441     }
442 
443     /**
444      * Set whether or not to show the {@link MenuItem MenuItems} while searching. Default false.
445      * Even if this is set to true, the {@link MenuItem} created by
446      * {@link MenuItem.Builder#setToSearch()} will still be hidden.
447      */
setShowMenuItemsWhileSearching(boolean showMenuItems)448     public void setShowMenuItemsWhileSearching(boolean showMenuItems) {
449         mController.setShowMenuItemsWhileSearching(showMenuItems);
450     }
451 
452     /** Returns if {@link MenuItem MenuItems} are shown while searching */
getShowMenuItemsWhileSearching()453     public boolean getShowMenuItemsWhileSearching() {
454         return mController.getShowMenuItemsWhileSearching();
455     }
456 
457     /**
458      * Sets the search query.
459      */
setSearchQuery(String query)460     public void setSearchQuery(String query) {
461         mController.setSearchQuery(query);
462     }
463 
464     /**
465      * Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar
466      * for the desired state.
467      */
setState(State state)468     public void setState(State state) {
469         mController.setState(state);
470     }
471 
472     /** Gets the current {@link State} of the toolbar. */
getState()473     public State getState() {
474         return mController.getState();
475     }
476 
477     @Override
onTouchEvent(MotionEvent ev)478     public boolean onTouchEvent(MotionEvent ev) {
479         // Copied from androidx.appcompat.widget.Toolbar
480 
481         // Toolbars always eat touch events, but should still respect the touch event dispatch
482         // contract. If the normal View implementation doesn't want the events, we'll just silently
483         // eat the rest of the gesture without reporting the events to the default implementation
484         // since that's what it expects.
485 
486         final int action = ev.getActionMasked();
487         if (action == MotionEvent.ACTION_DOWN) {
488             mEatingTouch = false;
489         }
490 
491         if (!mEatingTouch) {
492             final boolean handled = super.onTouchEvent(ev);
493             if (action == MotionEvent.ACTION_DOWN && !handled) {
494                 mEatingTouch = true;
495             }
496         }
497 
498         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
499             mEatingTouch = false;
500         }
501 
502         return true;
503     }
504 
505     @Override
onHoverEvent(MotionEvent ev)506     public boolean onHoverEvent(MotionEvent ev) {
507         // Copied from androidx.appcompat.widget.Toolbar
508 
509         // Same deal as onTouchEvent() above. Eat all hover events, but still
510         // respect the touch event dispatch contract.
511 
512         final int action = ev.getActionMasked();
513         if (action == MotionEvent.ACTION_HOVER_ENTER) {
514             mEatingHover = false;
515         }
516 
517         if (!mEatingHover) {
518             final boolean handled = super.onHoverEvent(ev);
519             if (action == MotionEvent.ACTION_HOVER_ENTER && !handled) {
520                 mEatingHover = true;
521             }
522         }
523 
524         if (action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_CANCEL) {
525             mEatingHover = false;
526         }
527 
528         return true;
529     }
530 
531     /**
532      * Registers a new {@link OnHeightChangedListener} to the list of listeners. Register a
533      * {@link com.android.car.ui.recyclerview.CarUiRecyclerView} only if there is a toolbar at
534      * the top and a {@link com.android.car.ui.recyclerview.CarUiRecyclerView} in the view and
535      * nothing else. {@link com.android.car.ui.recyclerview.CarUiRecyclerView} will
536      * automatically adjust its height according to the height of the Toolbar.
537      */
registerToolbarHeightChangeListener( OnHeightChangedListener listener)538     public void registerToolbarHeightChangeListener(
539             OnHeightChangedListener listener) {
540         mController.registerToolbarHeightChangeListener(listener);
541     }
542 
543     /** Unregisters an existing {@link OnHeightChangedListener} from the list of listeners. */
unregisterToolbarHeightChangeListener( OnHeightChangedListener listener)544     public boolean unregisterToolbarHeightChangeListener(
545             OnHeightChangedListener listener) {
546         return mController.unregisterToolbarHeightChangeListener(listener);
547     }
548 
549     /** Registers a new {@link OnTabSelectedListener} to the list of listeners. */
registerOnTabSelectedListener(OnTabSelectedListener listener)550     public void registerOnTabSelectedListener(OnTabSelectedListener listener) {
551         mController.registerOnTabSelectedListener(listener);
552     }
553 
554     /** Unregisters an existing {@link OnTabSelectedListener} from the list of listeners. */
unregisterOnTabSelectedListener(OnTabSelectedListener listener)555     public boolean unregisterOnTabSelectedListener(OnTabSelectedListener listener) {
556         return mController.unregisterOnTabSelectedListener(listener);
557     }
558 
559     /** Registers a new {@link OnSearchListener} to the list of listeners. */
registerOnSearchListener(OnSearchListener listener)560     public void registerOnSearchListener(OnSearchListener listener) {
561         mController.registerOnSearchListener(listener);
562     }
563 
564     /** Unregisters an existing {@link OnSearchListener} from the list of listeners. */
unregisterOnSearchListener(OnSearchListener listener)565     public boolean unregisterOnSearchListener(OnSearchListener listener) {
566         return mController.unregisterOnSearchListener(listener);
567     }
568 
569     /** Registers a new {@link OnSearchCompletedListener} to the list of listeners. */
registerOnSearchCompletedListener(OnSearchCompletedListener listener)570     public void registerOnSearchCompletedListener(OnSearchCompletedListener listener) {
571         mController.registerOnSearchCompletedListener(listener);
572     }
573 
574     /** Unregisters an existing {@link OnSearchCompletedListener} from the list of listeners. */
unregisterOnSearchCompletedListener(OnSearchCompletedListener listener)575     public boolean unregisterOnSearchCompletedListener(OnSearchCompletedListener listener) {
576         return mController.unregisterOnSearchCompletedListener(listener);
577     }
578 
579     /** Registers a new {@link OnBackListener} to the list of listeners. */
registerOnBackListener(OnBackListener listener)580     public void registerOnBackListener(OnBackListener listener) {
581         mController.registerOnBackListener(listener);
582     }
583 
584     /** Unregisters an existing {@link OnBackListener} from the list of listeners. */
unregisterOnBackListener(OnBackListener listener)585     public boolean unregisterOnBackListener(OnBackListener listener) {
586         return mController.unregisterOnBackListener(listener);
587     }
588 
589     /** Shows the progress bar */
showProgressBar()590     public void showProgressBar() {
591         mController.showProgressBar();
592     }
593 
594     /** Hides the progress bar */
hideProgressBar()595     public void hideProgressBar() {
596         mController.hideProgressBar();
597     }
598 
599     /** Returns the progress bar */
getProgressBar()600     public ProgressBar getProgressBar() {
601         return mController.getProgressBar();
602     }
603 }
604