1/* 2 * Copyright (C) 2014 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 17package android.support.v7.app; 18 19import android.content.Context; 20import android.content.res.Configuration; 21import android.content.res.Resources; 22import android.graphics.drawable.Drawable; 23import android.support.annotation.Nullable; 24import android.support.v4.view.ViewCompat; 25import android.support.v7.appcompat.R; 26import android.support.v7.view.WindowCallbackWrapper; 27import android.support.v7.view.menu.ListMenuPresenter; 28import android.support.v7.view.menu.MenuBuilder; 29import android.support.v7.view.menu.MenuPresenter; 30import android.support.v7.widget.DecorToolbar; 31import android.support.v7.widget.Toolbar; 32import android.support.v7.widget.ToolbarWidgetWrapper; 33import android.util.TypedValue; 34import android.view.ContextThemeWrapper; 35import android.view.KeyCharacterMap; 36import android.view.KeyEvent; 37import android.view.LayoutInflater; 38import android.view.Menu; 39import android.view.MenuItem; 40import android.view.View; 41import android.view.ViewGroup; 42import android.view.Window; 43import android.widget.SpinnerAdapter; 44 45import java.util.ArrayList; 46 47class ToolbarActionBar extends ActionBar { 48 private DecorToolbar mDecorToolbar; 49 private boolean mToolbarMenuPrepared; 50 private Window.Callback mWindowCallback; 51 private boolean mMenuCallbackSet; 52 53 private boolean mLastMenuVisibility; 54 private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners = new ArrayList<>(); 55 56 private ListMenuPresenter mListMenuPresenter; 57 58 private final Runnable mMenuInvalidator = new Runnable() { 59 @Override 60 public void run() { 61 populateOptionsMenu(); 62 } 63 }; 64 65 private final Toolbar.OnMenuItemClickListener mMenuClicker = 66 new Toolbar.OnMenuItemClickListener() { 67 @Override 68 public boolean onMenuItemClick(MenuItem item) { 69 return mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, item); 70 } 71 }; 72 73 public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback callback) { 74 mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false); 75 mWindowCallback = new ToolbarCallbackWrapper(callback); 76 mDecorToolbar.setWindowCallback(mWindowCallback); 77 toolbar.setOnMenuItemClickListener(mMenuClicker); 78 mDecorToolbar.setWindowTitle(title); 79 } 80 81 public Window.Callback getWrappedWindowCallback() { 82 return mWindowCallback; 83 } 84 85 @Override 86 public void setCustomView(View view) { 87 setCustomView(view, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 88 } 89 90 @Override 91 public void setCustomView(View view, LayoutParams layoutParams) { 92 if (view != null) { 93 view.setLayoutParams(layoutParams); 94 } 95 mDecorToolbar.setCustomView(view); 96 } 97 98 @Override 99 public void setCustomView(int resId) { 100 final LayoutInflater inflater = LayoutInflater.from(mDecorToolbar.getContext()); 101 setCustomView(inflater.inflate(resId, mDecorToolbar.getViewGroup(), false)); 102 } 103 104 @Override 105 public void setIcon(int resId) { 106 mDecorToolbar.setIcon(resId); 107 } 108 109 @Override 110 public void setIcon(Drawable icon) { 111 mDecorToolbar.setIcon(icon); 112 } 113 114 @Override 115 public void setLogo(int resId) { 116 mDecorToolbar.setLogo(resId); 117 } 118 119 @Override 120 public void setLogo(Drawable logo) { 121 mDecorToolbar.setLogo(logo); 122 } 123 124 @Override 125 public void setStackedBackgroundDrawable(Drawable d) { 126 // This space for rent (do nothing) 127 } 128 129 @Override 130 public void setSplitBackgroundDrawable(Drawable d) { 131 // This space for rent (do nothing) 132 } 133 134 @Override 135 public void setHomeButtonEnabled(boolean enabled) { 136 // If the nav button on a Toolbar is present, it's enabled. No-op. 137 } 138 139 @Override 140 public void setElevation(float elevation) { 141 ViewCompat.setElevation(mDecorToolbar.getViewGroup(), elevation); 142 } 143 144 @Override 145 public float getElevation() { 146 return ViewCompat.getElevation(mDecorToolbar.getViewGroup()); 147 } 148 149 @Override 150 public Context getThemedContext() { 151 return mDecorToolbar.getContext(); 152 } 153 154 @Override 155 public boolean isTitleTruncated() { 156 return super.isTitleTruncated(); 157 } 158 159 @Override 160 public void setHomeAsUpIndicator(Drawable indicator) { 161 mDecorToolbar.setNavigationIcon(indicator); 162 } 163 164 @Override 165 public void setHomeAsUpIndicator(int resId) { 166 mDecorToolbar.setNavigationIcon(resId); 167 } 168 169 @Override 170 public void setHomeActionContentDescription(CharSequence description) { 171 mDecorToolbar.setNavigationContentDescription(description); 172 } 173 174 @Override 175 public void setDefaultDisplayHomeAsUpEnabled(boolean enabled) { 176 // Do nothing 177 } 178 179 @Override 180 public void setHomeActionContentDescription(int resId) { 181 mDecorToolbar.setNavigationContentDescription(resId); 182 } 183 184 @Override 185 public void setShowHideAnimationEnabled(boolean enabled) { 186 // This space for rent; no-op. 187 } 188 189 @Override 190 public void onConfigurationChanged(Configuration config) { 191 super.onConfigurationChanged(config); 192 } 193 194 @Override 195 public void setListNavigationCallbacks(SpinnerAdapter adapter, OnNavigationListener callback) { 196 mDecorToolbar.setDropdownParams(adapter, new NavItemSelectedListener(callback)); 197 } 198 199 @Override 200 public void setSelectedNavigationItem(int position) { 201 switch (mDecorToolbar.getNavigationMode()) { 202 case NAVIGATION_MODE_LIST: 203 mDecorToolbar.setDropdownSelectedPosition(position); 204 break; 205 default: 206 throw new IllegalStateException( 207 "setSelectedNavigationIndex not valid for current navigation mode"); 208 } 209 } 210 211 @Override 212 public int getSelectedNavigationIndex() { 213 return -1; 214 } 215 216 @Override 217 public int getNavigationItemCount() { 218 return 0; 219 } 220 221 @Override 222 public void setTitle(CharSequence title) { 223 mDecorToolbar.setTitle(title); 224 } 225 226 @Override 227 public void setTitle(int resId) { 228 mDecorToolbar.setTitle(resId != 0 ? mDecorToolbar.getContext().getText(resId) : null); 229 } 230 231 @Override 232 public void setWindowTitle(CharSequence title) { 233 mDecorToolbar.setWindowTitle(title); 234 } 235 236 @Override 237 public boolean requestFocus() { 238 final ViewGroup viewGroup = mDecorToolbar.getViewGroup(); 239 if (viewGroup != null && !viewGroup.hasFocus()) { 240 viewGroup.requestFocus(); 241 return true; 242 } 243 return false; 244 } 245 246 @Override 247 public void setSubtitle(CharSequence subtitle) { 248 mDecorToolbar.setSubtitle(subtitle); 249 } 250 251 @Override 252 public void setSubtitle(int resId) { 253 mDecorToolbar.setSubtitle(resId != 0 ? mDecorToolbar.getContext().getText(resId) : null); 254 } 255 256 @Override 257 public void setDisplayOptions(@DisplayOptions int options) { 258 setDisplayOptions(options, 0xffffffff); 259 } 260 261 @Override 262 public void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask) { 263 final int currentOptions = mDecorToolbar.getDisplayOptions(); 264 mDecorToolbar.setDisplayOptions(options & mask | currentOptions & ~mask); 265 } 266 267 @Override 268 public void setDisplayUseLogoEnabled(boolean useLogo) { 269 setDisplayOptions(useLogo ? DISPLAY_USE_LOGO : 0, DISPLAY_USE_LOGO); 270 } 271 272 @Override 273 public void setDisplayShowHomeEnabled(boolean showHome) { 274 setDisplayOptions(showHome ? DISPLAY_SHOW_HOME : 0, DISPLAY_SHOW_HOME); 275 } 276 277 @Override 278 public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) { 279 setDisplayOptions(showHomeAsUp ? DISPLAY_HOME_AS_UP : 0, DISPLAY_HOME_AS_UP); 280 } 281 282 @Override 283 public void setDisplayShowTitleEnabled(boolean showTitle) { 284 setDisplayOptions(showTitle ? DISPLAY_SHOW_TITLE : 0, DISPLAY_SHOW_TITLE); 285 } 286 287 @Override 288 public void setDisplayShowCustomEnabled(boolean showCustom) { 289 setDisplayOptions(showCustom ? DISPLAY_SHOW_CUSTOM : 0, DISPLAY_SHOW_CUSTOM); 290 } 291 292 @Override 293 public void setBackgroundDrawable(@Nullable Drawable d) { 294 mDecorToolbar.setBackgroundDrawable(d); 295 } 296 297 @Override 298 public View getCustomView() { 299 return mDecorToolbar.getCustomView(); 300 } 301 302 @Override 303 public CharSequence getTitle() { 304 return mDecorToolbar.getTitle(); 305 } 306 307 @Override 308 public CharSequence getSubtitle() { 309 return mDecorToolbar.getSubtitle(); 310 } 311 312 @Override 313 public int getNavigationMode() { 314 return NAVIGATION_MODE_STANDARD; 315 } 316 317 @Override 318 public void setNavigationMode(@NavigationMode int mode) { 319 if (mode == ActionBar.NAVIGATION_MODE_TABS) { 320 throw new IllegalArgumentException("Tabs not supported in this configuration"); 321 } 322 mDecorToolbar.setNavigationMode(mode); 323 } 324 325 @Override 326 public int getDisplayOptions() { 327 return mDecorToolbar.getDisplayOptions(); 328 } 329 330 @Override 331 public Tab newTab() { 332 throw new UnsupportedOperationException( 333 "Tabs are not supported in toolbar action bars"); 334 } 335 336 @Override 337 public void addTab(Tab tab) { 338 throw new UnsupportedOperationException( 339 "Tabs are not supported in toolbar action bars"); 340 } 341 342 @Override 343 public void addTab(Tab tab, boolean setSelected) { 344 throw new UnsupportedOperationException( 345 "Tabs are not supported in toolbar action bars"); 346 } 347 348 @Override 349 public void addTab(Tab tab, int position) { 350 throw new UnsupportedOperationException( 351 "Tabs are not supported in toolbar action bars"); 352 } 353 354 @Override 355 public void addTab(Tab tab, int position, boolean setSelected) { 356 throw new UnsupportedOperationException( 357 "Tabs are not supported in toolbar action bars"); 358 } 359 360 @Override 361 public void removeTab(Tab tab) { 362 throw new UnsupportedOperationException( 363 "Tabs are not supported in toolbar action bars"); 364 } 365 366 @Override 367 public void removeTabAt(int position) { 368 throw new UnsupportedOperationException( 369 "Tabs are not supported in toolbar action bars"); 370 } 371 372 @Override 373 public void removeAllTabs() { 374 throw new UnsupportedOperationException( 375 "Tabs are not supported in toolbar action bars"); 376 } 377 378 @Override 379 public void selectTab(Tab tab) { 380 throw new UnsupportedOperationException( 381 "Tabs are not supported in toolbar action bars"); 382 } 383 384 @Override 385 public Tab getSelectedTab() { 386 throw new UnsupportedOperationException( 387 "Tabs are not supported in toolbar action bars"); 388 } 389 390 @Override 391 public Tab getTabAt(int index) { 392 throw new UnsupportedOperationException( 393 "Tabs are not supported in toolbar action bars"); 394 } 395 396 @Override 397 public int getTabCount() { 398 return 0; 399 } 400 401 @Override 402 public int getHeight() { 403 return mDecorToolbar.getHeight(); 404 } 405 406 @Override 407 public void show() { 408 // TODO: Consider a better transition for this. 409 // Right now use no automatic transition so that the app can supply one if desired. 410 mDecorToolbar.setVisibility(View.VISIBLE); 411 } 412 413 @Override 414 public void hide() { 415 // TODO: Consider a better transition for this. 416 // Right now use no automatic transition so that the app can supply one if desired. 417 mDecorToolbar.setVisibility(View.GONE); 418 } 419 420 @Override 421 public boolean isShowing() { 422 return mDecorToolbar.getVisibility() == View.VISIBLE; 423 } 424 425 @Override 426 public boolean openOptionsMenu() { 427 return mDecorToolbar.showOverflowMenu(); 428 } 429 430 @Override 431 public boolean invalidateOptionsMenu() { 432 mDecorToolbar.getViewGroup().removeCallbacks(mMenuInvalidator); 433 ViewCompat.postOnAnimation(mDecorToolbar.getViewGroup(), mMenuInvalidator); 434 return true; 435 } 436 437 @Override 438 public boolean collapseActionView() { 439 if (mDecorToolbar.hasExpandedActionView()) { 440 mDecorToolbar.collapseActionView(); 441 return true; 442 } 443 return false; 444 } 445 446 void populateOptionsMenu() { 447 final Menu menu = getMenu(); 448 final MenuBuilder mb = menu instanceof MenuBuilder ? (MenuBuilder) menu : null; 449 if (mb != null) { 450 mb.stopDispatchingItemsChanged(); 451 } 452 try { 453 menu.clear(); 454 if (!mWindowCallback.onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, menu) || 455 !mWindowCallback.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, null, menu)) { 456 menu.clear(); 457 } 458 } finally { 459 if (mb != null) { 460 mb.startDispatchingItemsChanged(); 461 } 462 } 463 } 464 465 @Override 466 public boolean onMenuKeyEvent(KeyEvent event) { 467 if (event.getAction() == KeyEvent.ACTION_UP) { 468 openOptionsMenu(); 469 } 470 return true; 471 } 472 473 @Override 474 public boolean onKeyShortcut(int keyCode, KeyEvent ev) { 475 Menu menu = getMenu(); 476 if (menu != null) { 477 final KeyCharacterMap kmap = KeyCharacterMap.load( 478 ev != null ? ev.getDeviceId() : KeyCharacterMap.VIRTUAL_KEYBOARD); 479 menu.setQwertyMode(kmap.getKeyboardType() != KeyCharacterMap.NUMERIC); 480 menu.performShortcut(keyCode, ev, 0); 481 } 482 // This action bar always returns true for handling keyboard shortcuts. 483 // This will block the window from preparing a temporary panel to handle 484 // keyboard shortcuts. 485 return true; 486 } 487 488 @Override 489 void onDestroy() { 490 // Remove any invalidation callbacks 491 mDecorToolbar.getViewGroup().removeCallbacks(mMenuInvalidator); 492 } 493 494 public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) { 495 mMenuVisibilityListeners.add(listener); 496 } 497 498 public void removeOnMenuVisibilityListener(OnMenuVisibilityListener listener) { 499 mMenuVisibilityListeners.remove(listener); 500 } 501 502 public void dispatchMenuVisibilityChanged(boolean isVisible) { 503 if (isVisible == mLastMenuVisibility) { 504 return; 505 } 506 mLastMenuVisibility = isVisible; 507 508 final int count = mMenuVisibilityListeners.size(); 509 for (int i = 0; i < count; i++) { 510 mMenuVisibilityListeners.get(i).onMenuVisibilityChanged(isVisible); 511 } 512 } 513 514 private View getListMenuView(Menu menu) { 515 ensureListMenuPresenter(menu); 516 517 if (menu == null || mListMenuPresenter == null) { 518 return null; 519 } 520 521 if (mListMenuPresenter.getAdapter().getCount() > 0) { 522 return (View) mListMenuPresenter.getMenuView(mDecorToolbar.getViewGroup()); 523 } 524 return null; 525 } 526 527 private void ensureListMenuPresenter(Menu menu) { 528 if (mListMenuPresenter == null && (menu instanceof MenuBuilder)) { 529 MenuBuilder mb = (MenuBuilder) menu; 530 531 Context context = mDecorToolbar.getContext(); 532 final TypedValue outValue = new TypedValue(); 533 final Resources.Theme widgetTheme = context.getResources().newTheme(); 534 widgetTheme.setTo(context.getTheme()); 535 536 // First apply the actionBarPopupTheme 537 widgetTheme.resolveAttribute(R.attr.actionBarPopupTheme, outValue, true); 538 if (outValue.resourceId != 0) { 539 widgetTheme.applyStyle(outValue.resourceId, true); 540 } 541 542 // Apply the panelMenuListTheme 543 widgetTheme.resolveAttribute(R.attr.panelMenuListTheme, outValue, true); 544 if (outValue.resourceId != 0) { 545 widgetTheme.applyStyle(outValue.resourceId, true); 546 } else { 547 widgetTheme.applyStyle(R.style.Theme_AppCompat_CompactMenu, true); 548 } 549 550 context = new ContextThemeWrapper(context, 0); 551 context.getTheme().setTo(widgetTheme); 552 553 // Finally create the list menu presenter 554 mListMenuPresenter = new ListMenuPresenter(context, R.layout.abc_list_menu_item_layout); 555 mListMenuPresenter.setCallback(new PanelMenuPresenterCallback()); 556 mb.addMenuPresenter(mListMenuPresenter); 557 } 558 } 559 560 private class ToolbarCallbackWrapper extends WindowCallbackWrapper { 561 public ToolbarCallbackWrapper(Window.Callback wrapped) { 562 super(wrapped); 563 } 564 565 @Override 566 public boolean onPreparePanel(int featureId, View view, Menu menu) { 567 final boolean result = super.onPreparePanel(featureId, view, menu); 568 if (result && !mToolbarMenuPrepared) { 569 mDecorToolbar.setMenuPrepared(); 570 mToolbarMenuPrepared = true; 571 } 572 return result; 573 } 574 575 @Override 576 public View onCreatePanelView(int featureId) { 577 switch (featureId) { 578 case Window.FEATURE_OPTIONS_PANEL: 579 final Menu menu = mDecorToolbar.getMenu(); 580 if (onPreparePanel(featureId, null, menu) && onMenuOpened(featureId, menu)) { 581 return getListMenuView(menu); 582 } 583 break; 584 } 585 return super.onCreatePanelView(featureId); 586 } 587 } 588 589 private Menu getMenu() { 590 if (!mMenuCallbackSet) { 591 mDecorToolbar.setMenuCallbacks(new ActionMenuPresenterCallback(), 592 new MenuBuilderCallback()); 593 mMenuCallbackSet = true; 594 } 595 return mDecorToolbar.getMenu(); 596 } 597 598 private final class ActionMenuPresenterCallback implements MenuPresenter.Callback { 599 private boolean mClosingActionMenu; 600 601 @Override 602 public boolean onOpenSubMenu(MenuBuilder subMenu) { 603 if (mWindowCallback != null) { 604 mWindowCallback.onMenuOpened(AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR, subMenu); 605 return true; 606 } 607 return false; 608 } 609 610 @Override 611 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 612 if (mClosingActionMenu) { 613 return; 614 } 615 616 mClosingActionMenu = true; 617 mDecorToolbar.dismissPopupMenus(); 618 if (mWindowCallback != null) { 619 mWindowCallback.onPanelClosed(AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR, menu); 620 } 621 mClosingActionMenu = false; 622 } 623 } 624 625 private final class PanelMenuPresenterCallback implements MenuPresenter.Callback { 626 @Override 627 public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { 628 if (mWindowCallback != null) { 629 mWindowCallback.onPanelClosed(Window.FEATURE_OPTIONS_PANEL, menu); 630 } 631 } 632 633 @Override 634 public boolean onOpenSubMenu(MenuBuilder subMenu) { 635 if (subMenu == null && mWindowCallback != null) { 636 mWindowCallback.onMenuOpened(Window.FEATURE_OPTIONS_PANEL, subMenu); 637 } 638 return true; 639 } 640 } 641 642 private final class MenuBuilderCallback implements MenuBuilder.Callback { 643 644 @Override 645 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { 646 return false; 647 } 648 649 @Override 650 public void onMenuModeChange(MenuBuilder menu) { 651 if (mWindowCallback != null) { 652 if (mDecorToolbar.isOverflowMenuShowing()) { 653 mWindowCallback.onPanelClosed(AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR, menu); 654 } else if (mWindowCallback.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, 655 null, menu)) { 656 mWindowCallback.onMenuOpened(AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR, menu); 657 } 658 } 659 } 660 } 661} 662