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