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