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