1 /* 2 * Copyright (C) 2015 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 17 package com.android.internal.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.ValueAnimator; 24 import android.content.Context; 25 import android.content.res.TypedArray; 26 import android.graphics.Color; 27 import android.graphics.Point; 28 import android.graphics.Rect; 29 import android.graphics.Region; 30 import android.graphics.drawable.AnimatedVectorDrawable; 31 import android.graphics.drawable.ColorDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.text.TextUtils; 34 import android.util.Size; 35 import android.util.TypedValue; 36 import android.view.ContextThemeWrapper; 37 import android.view.Gravity; 38 import android.view.LayoutInflater; 39 import android.view.Menu; 40 import android.view.MenuItem; 41 import android.view.MotionEvent; 42 import android.view.View; 43 import android.view.View.MeasureSpec; 44 import android.view.View.OnLayoutChangeListener; 45 import android.view.ViewConfiguration; 46 import android.view.ViewGroup; 47 import android.view.ViewTreeObserver; 48 import android.view.Window; 49 import android.view.WindowManager; 50 import android.view.animation.Animation; 51 import android.view.animation.AnimationSet; 52 import android.view.animation.AnimationUtils; 53 import android.view.animation.Interpolator; 54 import android.view.animation.Transformation; 55 import android.widget.ArrayAdapter; 56 import android.widget.ImageButton; 57 import android.widget.ImageView; 58 import android.widget.LinearLayout; 59 import android.widget.ListView; 60 import android.widget.PopupWindow; 61 import android.widget.TextView; 62 63 import com.android.internal.R; 64 import com.android.internal.util.Preconditions; 65 66 import java.util.ArrayList; 67 import java.util.LinkedList; 68 import java.util.List; 69 import java.util.Objects; 70 71 /** 72 * A floating toolbar for showing contextual menu items. 73 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the 74 * the remaining menu items in a vertical overflow view when the overflow button is clicked. 75 * The horizontal toolbar morphs into the vertical overflow view. 76 */ 77 public final class FloatingToolbar { 78 79 // This class is responsible for the public API of the floating toolbar. 80 // It delegates rendering operations to the FloatingToolbarPopup. 81 82 public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar"; 83 84 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER = 85 item -> false; 86 87 private final Context mContext; 88 private final Window mWindow; 89 private final FloatingToolbarPopup mPopup; 90 91 private final Rect mContentRect = new Rect(); 92 private final Rect mPreviousContentRect = new Rect(); 93 94 private Menu mMenu; 95 private List<MenuItem> mShowingMenuItems = new ArrayList<>(); 96 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 97 98 private int mSuggestedWidth; 99 private boolean mWidthChanged = true; 100 101 private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() { 102 103 private final Rect mNewRect = new Rect(); 104 private final Rect mOldRect = new Rect(); 105 106 @Override 107 public void onLayoutChange( 108 View view, 109 int newLeft, int newRight, int newTop, int newBottom, 110 int oldLeft, int oldRight, int oldTop, int oldBottom) { 111 mNewRect.set(newLeft, newRight, newTop, newBottom); 112 mOldRect.set(oldLeft, oldRight, oldTop, oldBottom); 113 if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) { 114 mWidthChanged = true; 115 updateLayout(); 116 } 117 } 118 }; 119 120 /** 121 * Initializes a floating toolbar. 122 */ 123 public FloatingToolbar(Context context, Window window) { 124 mContext = applyDefaultTheme(Preconditions.checkNotNull(context)); 125 mWindow = Preconditions.checkNotNull(window); 126 mPopup = new FloatingToolbarPopup(mContext, window.getDecorView()); 127 } 128 129 /** 130 * Sets the menu to be shown in this floating toolbar. 131 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 132 * toolbar. 133 */ 134 public FloatingToolbar setMenu(Menu menu) { 135 mMenu = Preconditions.checkNotNull(menu); 136 return this; 137 } 138 139 /** 140 * Sets the custom listener for invocation of menu items in this floating toolbar. 141 */ 142 public FloatingToolbar setOnMenuItemClickListener( 143 MenuItem.OnMenuItemClickListener menuItemClickListener) { 144 if (menuItemClickListener != null) { 145 mMenuItemClickListener = menuItemClickListener; 146 } else { 147 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 148 } 149 return this; 150 } 151 152 /** 153 * Sets the content rectangle. This is the area of the interesting content that this toolbar 154 * should avoid obstructing. 155 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 156 * toolbar. 157 */ 158 public FloatingToolbar setContentRect(Rect rect) { 159 mContentRect.set(Preconditions.checkNotNull(rect)); 160 return this; 161 } 162 163 /** 164 * Sets the suggested width of this floating toolbar. 165 * The actual width will be about this size but there are no guarantees that it will be exactly 166 * the suggested width. 167 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 168 * toolbar. 169 */ 170 public FloatingToolbar setSuggestedWidth(int suggestedWidth) { 171 // Check if there's been a substantial width spec change. 172 int difference = Math.abs(suggestedWidth - mSuggestedWidth); 173 mWidthChanged = difference > (mSuggestedWidth * 0.2); 174 175 mSuggestedWidth = suggestedWidth; 176 return this; 177 } 178 179 /** 180 * Shows this floating toolbar. 181 */ 182 public FloatingToolbar show() { 183 registerOrientationHandler(); 184 doShow(); 185 return this; 186 } 187 188 /** 189 * Updates this floating toolbar to reflect recent position and view updates. 190 * NOTE: This method is a no-op if the toolbar isn't showing. 191 */ 192 public FloatingToolbar updateLayout() { 193 if (mPopup.isShowing()) { 194 doShow(); 195 } 196 return this; 197 } 198 199 /** 200 * Dismisses this floating toolbar. 201 */ 202 public void dismiss() { 203 unregisterOrientationHandler(); 204 mPopup.dismiss(); 205 } 206 207 /** 208 * Hides this floating toolbar. This is a no-op if the toolbar is not showing. 209 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar. 210 */ 211 public void hide() { 212 mPopup.hide(); 213 } 214 215 /** 216 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise. 217 */ 218 public boolean isShowing() { 219 return mPopup.isShowing(); 220 } 221 222 /** 223 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise. 224 */ 225 public boolean isHidden() { 226 return mPopup.isHidden(); 227 } 228 229 private void doShow() { 230 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); 231 tidy(menuItems); 232 if (!isCurrentlyShowing(menuItems) || mWidthChanged) { 233 mPopup.dismiss(); 234 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth); 235 mShowingMenuItems = menuItems; 236 } 237 if (!mPopup.isShowing()) { 238 mPopup.show(mContentRect); 239 } else if (!mPreviousContentRect.equals(mContentRect)) { 240 mPopup.updateCoordinates(mContentRect); 241 } 242 mWidthChanged = false; 243 mPreviousContentRect.set(mContentRect); 244 } 245 246 /** 247 * Returns true if this floating toolbar is currently showing the specified menu items. 248 */ 249 private boolean isCurrentlyShowing(List<MenuItem> menuItems) { 250 if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) { 251 return false; 252 } 253 254 final int size = menuItems.size(); 255 for (int i = 0; i < size; i++) { 256 final MenuItem menuItem = menuItems.get(i); 257 final MenuItem showingItem = mShowingMenuItems.get(i); 258 if (menuItem.getItemId() != showingItem.getItemId() 259 || !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle()) 260 || !Objects.equals(menuItem.getIcon(), showingItem.getIcon()) 261 || menuItem.getGroupId() != showingItem.getGroupId()) { 262 return false; 263 } 264 } 265 266 return true; 267 } 268 269 /** 270 * Returns the visible and enabled menu items in the specified menu. 271 * This method is recursive. 272 */ 273 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) { 274 List<MenuItem> menuItems = new ArrayList<>(); 275 for (int i = 0; (menu != null) && (i < menu.size()); i++) { 276 MenuItem menuItem = menu.getItem(i); 277 if (menuItem.isVisible() && menuItem.isEnabled()) { 278 Menu subMenu = menuItem.getSubMenu(); 279 if (subMenu != null) { 280 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu)); 281 } else { 282 menuItems.add(menuItem); 283 } 284 } 285 } 286 return menuItems; 287 } 288 289 /** 290 * Update the list of menu items to conform to certain requirements. 291 */ 292 private void tidy(List<MenuItem> menuItems) { 293 int assistItemIndex = -1; 294 Drawable assistItemDrawable = null; 295 296 final int size = menuItems.size(); 297 for (int i = 0; i < size; i++) { 298 final MenuItem menuItem = menuItems.get(i); 299 300 if (menuItem.getItemId() == android.R.id.textAssist) { 301 assistItemIndex = i; 302 assistItemDrawable = menuItem.getIcon(); 303 } 304 305 // Remove icons for all menu items with text. 306 if (!TextUtils.isEmpty(menuItem.getTitle())) { 307 menuItem.setIcon(null); 308 } 309 } 310 if (assistItemIndex > -1) { 311 final MenuItem assistMenuItem = menuItems.remove(assistItemIndex); 312 // Ensure the assist menu item preserves its icon. 313 assistMenuItem.setIcon(assistItemDrawable); 314 // Ensure the assist menu item is always the first item. 315 menuItems.add(0, assistMenuItem); 316 } 317 } 318 319 private void registerOrientationHandler() { 320 unregisterOrientationHandler(); 321 mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler); 322 } 323 324 private void unregisterOrientationHandler() { 325 mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler); 326 } 327 328 329 /** 330 * A popup window used by the floating toolbar. 331 * 332 * This class is responsible for the rendering/animation of the floating toolbar. 333 * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button 334 * to transition between panels. 335 */ 336 private static final class FloatingToolbarPopup { 337 338 /* Minimum and maximum number of items allowed in the overflow. */ 339 private static final int MIN_OVERFLOW_SIZE = 2; 340 private static final int MAX_OVERFLOW_SIZE = 4; 341 342 private final Context mContext; 343 private final View mParent; // Parent for the popup window. 344 private final PopupWindow mPopupWindow; 345 346 /* Margins between the popup window and it's content. */ 347 private final int mMarginHorizontal; 348 private final int mMarginVertical; 349 350 /* View components */ 351 private final ViewGroup mContentContainer; // holds all contents. 352 private final ViewGroup mMainPanel; // holds menu items that are initially displayed. 353 private final OverflowPanel mOverflowPanel; // holds menu items hidden in the overflow. 354 private final ImageButton mOverflowButton; // opens/closes the overflow. 355 /* overflow button drawables. */ 356 private final Drawable mArrow; 357 private final Drawable mOverflow; 358 private final AnimatedVectorDrawable mToArrow; 359 private final AnimatedVectorDrawable mToOverflow; 360 361 private final OverflowPanelViewHelper mOverflowPanelViewHelper; 362 363 /* Animation interpolators. */ 364 private final Interpolator mLogAccelerateInterpolator; 365 private final Interpolator mFastOutSlowInInterpolator; 366 private final Interpolator mLinearOutSlowInInterpolator; 367 private final Interpolator mFastOutLinearInInterpolator; 368 369 /* Animations. */ 370 private final AnimatorSet mShowAnimation; 371 private final AnimatorSet mDismissAnimation; 372 private final AnimatorSet mHideAnimation; 373 private final AnimationSet mOpenOverflowAnimation; 374 private final AnimationSet mCloseOverflowAnimation; 375 private final Animation.AnimationListener mOverflowAnimationListener; 376 377 private final Rect mViewPortOnScreen = new Rect(); // portion of screen we can draw in. 378 private final Point mCoordsOnWindow = new Point(); // popup window coordinates. 379 /* Temporary data holders. Reset values before using. */ 380 private final int[] mTmpCoords = new int[2]; 381 382 private final Region mTouchableRegion = new Region(); 383 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = 384 info -> { 385 info.contentInsets.setEmpty(); 386 info.visibleInsets.setEmpty(); 387 info.touchableRegion.set(mTouchableRegion); 388 info.setTouchableInsets( 389 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 390 }; 391 392 private final int mLineHeight; 393 private final int mIconTextSpacing; 394 395 /** 396 * @see OverflowPanelViewHelper#preparePopupContent(). 397 */ 398 private final Runnable mPreparePopupContentRTLHelper = new Runnable() { 399 @Override 400 public void run() { 401 setPanelsStatesAtRestingPosition(); 402 setContentAreaAsTouchableSurface(); 403 mContentContainer.setAlpha(1); 404 } 405 }; 406 407 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing. 408 private boolean mHidden; // tracks whether this popup is hidden or hiding. 409 410 /* Calculated sizes for panels and overflow button. */ 411 private final Size mOverflowButtonSize; 412 private Size mOverflowPanelSize; // Should be null when there is no overflow. 413 private Size mMainPanelSize; 414 415 /* Item click listeners */ 416 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener; 417 private final View.OnClickListener mMenuItemButtonOnClickListener = 418 new View.OnClickListener() { 419 @Override 420 public void onClick(View v) { 421 if (v.getTag() instanceof MenuItem) { 422 if (mOnMenuItemClickListener != null) { 423 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag()); 424 } 425 } 426 } 427 }; 428 429 private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards. 430 private boolean mIsOverflowOpen; 431 432 private int mTransitionDurationScale; // Used to scale the toolbar transition duration. 433 434 /** 435 * Initializes a new floating toolbar popup. 436 * 437 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token 438 * from. 439 */ 440 public FloatingToolbarPopup(Context context, View parent) { 441 mParent = Preconditions.checkNotNull(parent); 442 mContext = Preconditions.checkNotNull(context); 443 mContentContainer = createContentContainer(context); 444 mPopupWindow = createPopupWindow(mContentContainer); 445 mMarginHorizontal = parent.getResources() 446 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); 447 mMarginVertical = parent.getResources() 448 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin); 449 mLineHeight = context.getResources() 450 .getDimensionPixelSize(R.dimen.floating_toolbar_height); 451 mIconTextSpacing = context.getResources() 452 .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_side_padding); 453 454 // Interpolators 455 mLogAccelerateInterpolator = new LogAccelerateInterpolator(); 456 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator( 457 mContext, android.R.interpolator.fast_out_slow_in); 458 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( 459 mContext, android.R.interpolator.linear_out_slow_in); 460 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( 461 mContext, android.R.interpolator.fast_out_linear_in); 462 463 // Drawables. Needed for views. 464 mArrow = mContext.getResources() 465 .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme()); 466 mArrow.setAutoMirrored(true); 467 mOverflow = mContext.getResources() 468 .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme()); 469 mOverflow.setAutoMirrored(true); 470 mToArrow = (AnimatedVectorDrawable) mContext.getResources() 471 .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme()); 472 mToArrow.setAutoMirrored(true); 473 mToOverflow = (AnimatedVectorDrawable) mContext.getResources() 474 .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme()); 475 mToOverflow.setAutoMirrored(true); 476 477 // Views 478 mOverflowButton = createOverflowButton(); 479 mOverflowButtonSize = measure(mOverflowButton); 480 mMainPanel = createMainPanel(); 481 mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext); 482 mOverflowPanel = createOverflowPanel(); 483 484 // Animation. Need views. 485 mOverflowAnimationListener = createOverflowAnimationListener(); 486 mOpenOverflowAnimation = new AnimationSet(true); 487 mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener); 488 mCloseOverflowAnimation = new AnimationSet(true); 489 mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener); 490 mShowAnimation = createEnterAnimation(mContentContainer); 491 mDismissAnimation = createExitAnimation( 492 mContentContainer, 493 150, // startDelay 494 new AnimatorListenerAdapter() { 495 @Override 496 public void onAnimationEnd(Animator animation) { 497 mPopupWindow.dismiss(); 498 mContentContainer.removeAllViews(); 499 } 500 }); 501 mHideAnimation = createExitAnimation( 502 mContentContainer, 503 0, // startDelay 504 new AnimatorListenerAdapter() { 505 @Override 506 public void onAnimationEnd(Animator animation) { 507 mPopupWindow.dismiss(); 508 } 509 }); 510 } 511 512 /** 513 * Lays out buttons for the specified menu items. 514 * Requires a subsequent call to {@link #show()} to show the items. 515 */ 516 public void layoutMenuItems( 517 List<MenuItem> menuItems, 518 MenuItem.OnMenuItemClickListener menuItemClickListener, 519 int suggestedWidth) { 520 mOnMenuItemClickListener = menuItemClickListener; 521 cancelOverflowAnimations(); 522 clearPanels(); 523 menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth)); 524 if (!menuItems.isEmpty()) { 525 // Add remaining items to the overflow. 526 layoutOverflowPanelItems(menuItems); 527 } 528 updatePopupSize(); 529 } 530 531 /** 532 * Shows this popup at the specified coordinates. 533 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. 534 */ 535 public void show(Rect contentRectOnScreen) { 536 Preconditions.checkNotNull(contentRectOnScreen); 537 538 if (isShowing()) { 539 return; 540 } 541 542 mHidden = false; 543 mDismissed = false; 544 cancelDismissAndHideAnimations(); 545 cancelOverflowAnimations(); 546 547 refreshCoordinatesAndOverflowDirection(contentRectOnScreen); 548 preparePopupContent(); 549 // We need to specify the position in window coordinates. 550 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can 551 // specify the popup position in screen coordinates. 552 mPopupWindow.showAtLocation( 553 mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y); 554 setTouchableSurfaceInsetsComputer(); 555 runShowAnimation(); 556 } 557 558 /** 559 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op. 560 */ 561 public void dismiss() { 562 if (mDismissed) { 563 return; 564 } 565 566 mHidden = false; 567 mDismissed = true; 568 mHideAnimation.cancel(); 569 570 runDismissAnimation(); 571 setZeroTouchableSurface(); 572 } 573 574 /** 575 * Hides this popup. This is a no-op if this popup is not showing. 576 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup. 577 */ 578 public void hide() { 579 if (!isShowing()) { 580 return; 581 } 582 583 mHidden = true; 584 runHideAnimation(); 585 setZeroTouchableSurface(); 586 } 587 588 /** 589 * Returns {@code true} if this popup is currently showing. {@code false} otherwise. 590 */ 591 public boolean isShowing() { 592 return !mDismissed && !mHidden; 593 } 594 595 /** 596 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise. 597 */ 598 public boolean isHidden() { 599 return mHidden; 600 } 601 602 /** 603 * Updates the coordinates of this popup. 604 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. 605 * This is a no-op if this popup is not showing. 606 */ 607 public void updateCoordinates(Rect contentRectOnScreen) { 608 Preconditions.checkNotNull(contentRectOnScreen); 609 610 if (!isShowing() || !mPopupWindow.isShowing()) { 611 return; 612 } 613 614 cancelOverflowAnimations(); 615 refreshCoordinatesAndOverflowDirection(contentRectOnScreen); 616 preparePopupContent(); 617 // We need to specify the position in window coordinates. 618 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can 619 // specify the popup position in screen coordinates. 620 mPopupWindow.update( 621 mCoordsOnWindow.x, mCoordsOnWindow.y, 622 mPopupWindow.getWidth(), mPopupWindow.getHeight()); 623 } 624 625 private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) { 626 refreshViewPort(); 627 628 // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in 629 // landscape. 630 final int x = Math.min( 631 contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2, 632 mViewPortOnScreen.right - mPopupWindow.getWidth()); 633 634 final int y; 635 636 final int availableHeightAboveContent = 637 contentRectOnScreen.top - mViewPortOnScreen.top; 638 final int availableHeightBelowContent = 639 mViewPortOnScreen.bottom - contentRectOnScreen.bottom; 640 641 final int margin = 2 * mMarginVertical; 642 final int toolbarHeightWithVerticalMargin = mLineHeight + margin; 643 644 if (!hasOverflow()) { 645 if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) { 646 // There is enough space at the top of the content. 647 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin; 648 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) { 649 // There is enough space at the bottom of the content. 650 y = contentRectOnScreen.bottom; 651 } else if (availableHeightBelowContent >= mLineHeight) { 652 // Just enough space to fit the toolbar with no vertical margins. 653 y = contentRectOnScreen.bottom - mMarginVertical; 654 } else { 655 // Not enough space. Prefer to position as high as possible. 656 y = Math.max( 657 mViewPortOnScreen.top, 658 contentRectOnScreen.top - toolbarHeightWithVerticalMargin); 659 } 660 } else { 661 // Has an overflow. 662 final int minimumOverflowHeightWithMargin = 663 calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin; 664 final int availableHeightThroughContentDown = mViewPortOnScreen.bottom - 665 contentRectOnScreen.top + toolbarHeightWithVerticalMargin; 666 final int availableHeightThroughContentUp = contentRectOnScreen.bottom - 667 mViewPortOnScreen.top + toolbarHeightWithVerticalMargin; 668 669 if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) { 670 // There is enough space at the top of the content rect for the overflow. 671 // Position above and open upwards. 672 updateOverflowHeight(availableHeightAboveContent - margin); 673 y = contentRectOnScreen.top - mPopupWindow.getHeight(); 674 mOpenOverflowUpwards = true; 675 } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin 676 && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) { 677 // There is enough space at the top of the content rect for the main panel 678 // but not the overflow. 679 // Position above but open downwards. 680 updateOverflowHeight(availableHeightThroughContentDown - margin); 681 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin; 682 mOpenOverflowUpwards = false; 683 } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) { 684 // There is enough space at the bottom of the content rect for the overflow. 685 // Position below and open downwards. 686 updateOverflowHeight(availableHeightBelowContent - margin); 687 y = contentRectOnScreen.bottom; 688 mOpenOverflowUpwards = false; 689 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin 690 && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) { 691 // There is enough space at the bottom of the content rect for the main panel 692 // but not the overflow. 693 // Position below but open upwards. 694 updateOverflowHeight(availableHeightThroughContentUp - margin); 695 y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin - 696 mPopupWindow.getHeight(); 697 mOpenOverflowUpwards = true; 698 } else { 699 // Not enough space. 700 // Position at the top of the view port and open downwards. 701 updateOverflowHeight(mViewPortOnScreen.height() - margin); 702 y = mViewPortOnScreen.top; 703 mOpenOverflowUpwards = false; 704 } 705 } 706 707 // We later specify the location of PopupWindow relative to the attached window. 708 // The idea here is that 1) we can get the location of a View in both window coordinates 709 // and screen coordiantes, where the offset between them should be equal to the window 710 // origin, and 2) we can use an arbitrary for this calculation while calculating the 711 // location of the rootview is supposed to be least expensive. 712 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid 713 // the following calculation. 714 mParent.getRootView().getLocationOnScreen(mTmpCoords); 715 int rootViewLeftOnScreen = mTmpCoords[0]; 716 int rootViewTopOnScreen = mTmpCoords[1]; 717 mParent.getRootView().getLocationInWindow(mTmpCoords); 718 int rootViewLeftOnWindow = mTmpCoords[0]; 719 int rootViewTopOnWindow = mTmpCoords[1]; 720 int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow; 721 int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow; 722 mCoordsOnWindow.set( 723 Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen)); 724 } 725 726 /** 727 * Performs the "show" animation on the floating popup. 728 */ 729 private void runShowAnimation() { 730 mShowAnimation.start(); 731 } 732 733 /** 734 * Performs the "dismiss" animation on the floating popup. 735 */ 736 private void runDismissAnimation() { 737 mDismissAnimation.start(); 738 } 739 740 /** 741 * Performs the "hide" animation on the floating popup. 742 */ 743 private void runHideAnimation() { 744 mHideAnimation.start(); 745 } 746 747 private void cancelDismissAndHideAnimations() { 748 mDismissAnimation.cancel(); 749 mHideAnimation.cancel(); 750 } 751 752 private void cancelOverflowAnimations() { 753 mContentContainer.clearAnimation(); 754 mMainPanel.animate().cancel(); 755 mOverflowPanel.animate().cancel(); 756 mToArrow.stop(); 757 mToOverflow.stop(); 758 } 759 760 private void openOverflow() { 761 final int targetWidth = mOverflowPanelSize.getWidth(); 762 final int targetHeight = mOverflowPanelSize.getHeight(); 763 final int startWidth = mContentContainer.getWidth(); 764 final int startHeight = mContentContainer.getHeight(); 765 final float startY = mContentContainer.getY(); 766 final float left = mContentContainer.getX(); 767 final float right = left + mContentContainer.getWidth(); 768 Animation widthAnimation = new Animation() { 769 @Override 770 protected void applyTransformation(float interpolatedTime, Transformation t) { 771 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); 772 setWidth(mContentContainer, startWidth + deltaWidth); 773 if (isInRTLMode()) { 774 mContentContainer.setX(left); 775 776 // Lock the panels in place. 777 mMainPanel.setX(0); 778 mOverflowPanel.setX(0); 779 } else { 780 mContentContainer.setX(right - mContentContainer.getWidth()); 781 782 // Offset the panels' positions so they look like they're locked in place 783 // on the screen. 784 mMainPanel.setX(mContentContainer.getWidth() - startWidth); 785 mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth); 786 } 787 } 788 }; 789 Animation heightAnimation = new Animation() { 790 @Override 791 protected void applyTransformation(float interpolatedTime, Transformation t) { 792 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); 793 setHeight(mContentContainer, startHeight + deltaHeight); 794 if (mOpenOverflowUpwards) { 795 mContentContainer.setY( 796 startY - (mContentContainer.getHeight() - startHeight)); 797 positionContentYCoordinatesIfOpeningOverflowUpwards(); 798 } 799 } 800 }; 801 final float overflowButtonStartX = mOverflowButton.getX(); 802 final float overflowButtonTargetX = isInRTLMode() ? 803 overflowButtonStartX + targetWidth - mOverflowButton.getWidth() : 804 overflowButtonStartX - targetWidth + mOverflowButton.getWidth(); 805 Animation overflowButtonAnimation = new Animation() { 806 @Override 807 protected void applyTransformation(float interpolatedTime, Transformation t) { 808 float overflowButtonX = overflowButtonStartX 809 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX); 810 float deltaContainerWidth = isInRTLMode() ? 811 0 : 812 mContentContainer.getWidth() - startWidth; 813 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth; 814 mOverflowButton.setX(actualOverflowButtonX); 815 } 816 }; 817 widthAnimation.setInterpolator(mLogAccelerateInterpolator); 818 widthAnimation.setDuration(getAdjustedDuration(250)); 819 heightAnimation.setInterpolator(mFastOutSlowInInterpolator); 820 heightAnimation.setDuration(getAdjustedDuration(250)); 821 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator); 822 overflowButtonAnimation.setDuration(getAdjustedDuration(250)); 823 mOpenOverflowAnimation.getAnimations().clear(); 824 mOpenOverflowAnimation.getAnimations().clear(); 825 mOpenOverflowAnimation.addAnimation(widthAnimation); 826 mOpenOverflowAnimation.addAnimation(heightAnimation); 827 mOpenOverflowAnimation.addAnimation(overflowButtonAnimation); 828 mContentContainer.startAnimation(mOpenOverflowAnimation); 829 mIsOverflowOpen = true; 830 mMainPanel.animate() 831 .alpha(0).withLayer() 832 .setInterpolator(mLinearOutSlowInInterpolator) 833 .setDuration(250) 834 .start(); 835 mOverflowPanel.setAlpha(1); // fadeIn in 0ms. 836 } 837 838 private void closeOverflow() { 839 final int targetWidth = mMainPanelSize.getWidth(); 840 final int startWidth = mContentContainer.getWidth(); 841 final float left = mContentContainer.getX(); 842 final float right = left + mContentContainer.getWidth(); 843 Animation widthAnimation = new Animation() { 844 @Override 845 protected void applyTransformation(float interpolatedTime, Transformation t) { 846 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); 847 setWidth(mContentContainer, startWidth + deltaWidth); 848 if (isInRTLMode()) { 849 mContentContainer.setX(left); 850 851 // Lock the panels in place. 852 mMainPanel.setX(0); 853 mOverflowPanel.setX(0); 854 } else { 855 mContentContainer.setX(right - mContentContainer.getWidth()); 856 857 // Offset the panels' positions so they look like they're locked in place 858 // on the screen. 859 mMainPanel.setX(mContentContainer.getWidth() - targetWidth); 860 mOverflowPanel.setX(mContentContainer.getWidth() - startWidth); 861 } 862 } 863 }; 864 final int targetHeight = mMainPanelSize.getHeight(); 865 final int startHeight = mContentContainer.getHeight(); 866 final float bottom = mContentContainer.getY() + mContentContainer.getHeight(); 867 Animation heightAnimation = new Animation() { 868 @Override 869 protected void applyTransformation(float interpolatedTime, Transformation t) { 870 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); 871 setHeight(mContentContainer, startHeight + deltaHeight); 872 if (mOpenOverflowUpwards) { 873 mContentContainer.setY(bottom - mContentContainer.getHeight()); 874 positionContentYCoordinatesIfOpeningOverflowUpwards(); 875 } 876 } 877 }; 878 final float overflowButtonStartX = mOverflowButton.getX(); 879 final float overflowButtonTargetX = isInRTLMode() ? 880 overflowButtonStartX - startWidth + mOverflowButton.getWidth() : 881 overflowButtonStartX + startWidth - mOverflowButton.getWidth(); 882 Animation overflowButtonAnimation = new Animation() { 883 @Override 884 protected void applyTransformation(float interpolatedTime, Transformation t) { 885 float overflowButtonX = overflowButtonStartX 886 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX); 887 float deltaContainerWidth = isInRTLMode() ? 888 0 : 889 mContentContainer.getWidth() - startWidth; 890 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth; 891 mOverflowButton.setX(actualOverflowButtonX); 892 } 893 }; 894 widthAnimation.setInterpolator(mFastOutSlowInInterpolator); 895 widthAnimation.setDuration(getAdjustedDuration(250)); 896 heightAnimation.setInterpolator(mLogAccelerateInterpolator); 897 heightAnimation.setDuration(getAdjustedDuration(250)); 898 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator); 899 overflowButtonAnimation.setDuration(getAdjustedDuration(250)); 900 mCloseOverflowAnimation.getAnimations().clear(); 901 mCloseOverflowAnimation.addAnimation(widthAnimation); 902 mCloseOverflowAnimation.addAnimation(heightAnimation); 903 mCloseOverflowAnimation.addAnimation(overflowButtonAnimation); 904 mContentContainer.startAnimation(mCloseOverflowAnimation); 905 mIsOverflowOpen = false; 906 mMainPanel.animate() 907 .alpha(1).withLayer() 908 .setInterpolator(mFastOutLinearInInterpolator) 909 .setDuration(100) 910 .start(); 911 mOverflowPanel.animate() 912 .alpha(0).withLayer() 913 .setInterpolator(mLinearOutSlowInInterpolator) 914 .setDuration(150) 915 .start(); 916 } 917 918 /** 919 * Defines the position of the floating toolbar popup panels when transition animation has 920 * stopped. 921 */ 922 private void setPanelsStatesAtRestingPosition() { 923 mOverflowButton.setEnabled(true); 924 mOverflowPanel.awakenScrollBars(); 925 926 if (mIsOverflowOpen) { 927 // Set open state. 928 final Size containerSize = mOverflowPanelSize; 929 setSize(mContentContainer, containerSize); 930 mMainPanel.setAlpha(0); 931 mMainPanel.setVisibility(View.INVISIBLE); 932 mOverflowPanel.setAlpha(1); 933 mOverflowPanel.setVisibility(View.VISIBLE); 934 mOverflowButton.setImageDrawable(mArrow); 935 mOverflowButton.setContentDescription(mContext.getString( 936 R.string.floating_toolbar_close_overflow_description)); 937 938 // Update x-coordinates depending on RTL state. 939 if (isInRTLMode()) { 940 mContentContainer.setX(mMarginHorizontal); // align left 941 mMainPanel.setX(0); // align left 942 mOverflowButton.setX( // align right 943 containerSize.getWidth() - mOverflowButtonSize.getWidth()); 944 mOverflowPanel.setX(0); // align left 945 } else { 946 mContentContainer.setX( // align right 947 mPopupWindow.getWidth() - 948 containerSize.getWidth() - mMarginHorizontal); 949 mMainPanel.setX(-mContentContainer.getX()); // align right 950 mOverflowButton.setX(0); // align left 951 mOverflowPanel.setX(0); // align left 952 } 953 954 // Update y-coordinates depending on overflow's open direction. 955 if (mOpenOverflowUpwards) { 956 mContentContainer.setY(mMarginVertical); // align top 957 mMainPanel.setY( // align bottom 958 containerSize.getHeight() - mContentContainer.getHeight()); 959 mOverflowButton.setY( // align bottom 960 containerSize.getHeight() - mOverflowButtonSize.getHeight()); 961 mOverflowPanel.setY(0); // align top 962 } else { 963 // opens downwards. 964 mContentContainer.setY(mMarginVertical); // align top 965 mMainPanel.setY(0); // align top 966 mOverflowButton.setY(0); // align top 967 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom 968 } 969 } else { 970 // Overflow not open. Set closed state. 971 final Size containerSize = mMainPanelSize; 972 setSize(mContentContainer, containerSize); 973 mMainPanel.setAlpha(1); 974 mMainPanel.setVisibility(View.VISIBLE); 975 mOverflowPanel.setAlpha(0); 976 mOverflowPanel.setVisibility(View.INVISIBLE); 977 mOverflowButton.setImageDrawable(mOverflow); 978 mOverflowButton.setContentDescription(mContext.getString( 979 R.string.floating_toolbar_open_overflow_description)); 980 981 if (hasOverflow()) { 982 // Update x-coordinates depending on RTL state. 983 if (isInRTLMode()) { 984 mContentContainer.setX(mMarginHorizontal); // align left 985 mMainPanel.setX(0); // align left 986 mOverflowButton.setX(0); // align left 987 mOverflowPanel.setX(0); // align left 988 } else { 989 mContentContainer.setX( // align right 990 mPopupWindow.getWidth() - 991 containerSize.getWidth() - mMarginHorizontal); 992 mMainPanel.setX(0); // align left 993 mOverflowButton.setX( // align right 994 containerSize.getWidth() - mOverflowButtonSize.getWidth()); 995 mOverflowPanel.setX( // align right 996 containerSize.getWidth() - mOverflowPanelSize.getWidth()); 997 } 998 999 // Update y-coordinates depending on overflow's open direction. 1000 if (mOpenOverflowUpwards) { 1001 mContentContainer.setY( // align bottom 1002 mMarginVertical + 1003 mOverflowPanelSize.getHeight() - containerSize.getHeight()); 1004 mMainPanel.setY(0); // align top 1005 mOverflowButton.setY(0); // align top 1006 mOverflowPanel.setY( // align bottom 1007 containerSize.getHeight() - mOverflowPanelSize.getHeight()); 1008 } else { 1009 // opens downwards. 1010 mContentContainer.setY(mMarginVertical); // align top 1011 mMainPanel.setY(0); // align top 1012 mOverflowButton.setY(0); // align top 1013 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom 1014 } 1015 } else { 1016 // No overflow. 1017 mContentContainer.setX(mMarginHorizontal); // align left 1018 mContentContainer.setY(mMarginVertical); // align top 1019 mMainPanel.setX(0); // align left 1020 mMainPanel.setY(0); // align top 1021 } 1022 } 1023 } 1024 1025 private void updateOverflowHeight(int suggestedHeight) { 1026 if (hasOverflow()) { 1027 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) / 1028 mLineHeight; 1029 final int newHeight = calculateOverflowHeight(maxItemSize); 1030 if (mOverflowPanelSize.getHeight() != newHeight) { 1031 mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight); 1032 } 1033 setSize(mOverflowPanel, mOverflowPanelSize); 1034 if (mIsOverflowOpen) { 1035 setSize(mContentContainer, mOverflowPanelSize); 1036 if (mOpenOverflowUpwards) { 1037 final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight; 1038 mContentContainer.setY(mContentContainer.getY() + deltaHeight); 1039 mOverflowButton.setY(mOverflowButton.getY() - deltaHeight); 1040 } 1041 } else { 1042 setSize(mContentContainer, mMainPanelSize); 1043 } 1044 updatePopupSize(); 1045 } 1046 } 1047 1048 private void updatePopupSize() { 1049 int width = 0; 1050 int height = 0; 1051 if (mMainPanelSize != null) { 1052 width = Math.max(width, mMainPanelSize.getWidth()); 1053 height = Math.max(height, mMainPanelSize.getHeight()); 1054 } 1055 if (mOverflowPanelSize != null) { 1056 width = Math.max(width, mOverflowPanelSize.getWidth()); 1057 height = Math.max(height, mOverflowPanelSize.getHeight()); 1058 } 1059 mPopupWindow.setWidth(width + mMarginHorizontal * 2); 1060 mPopupWindow.setHeight(height + mMarginVertical * 2); 1061 maybeComputeTransitionDurationScale(); 1062 } 1063 1064 private void refreshViewPort() { 1065 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen); 1066 } 1067 1068 private int getAdjustedToolbarWidth(int suggestedWidth) { 1069 int width = suggestedWidth; 1070 refreshViewPort(); 1071 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources() 1072 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); 1073 if (width <= 0) { 1074 width = mParent.getResources() 1075 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width); 1076 } 1077 return Math.min(width, maximumWidth); 1078 } 1079 1080 /** 1081 * Sets the touchable region of this popup to be zero. This means that all touch events on 1082 * this popup will go through to the surface behind it. 1083 */ 1084 private void setZeroTouchableSurface() { 1085 mTouchableRegion.setEmpty(); 1086 } 1087 1088 /** 1089 * Sets the touchable region of this popup to be the area occupied by its content. 1090 */ 1091 private void setContentAreaAsTouchableSurface() { 1092 Preconditions.checkNotNull(mMainPanelSize); 1093 final int width; 1094 final int height; 1095 if (mIsOverflowOpen) { 1096 Preconditions.checkNotNull(mOverflowPanelSize); 1097 width = mOverflowPanelSize.getWidth(); 1098 height = mOverflowPanelSize.getHeight(); 1099 } else { 1100 width = mMainPanelSize.getWidth(); 1101 height = mMainPanelSize.getHeight(); 1102 } 1103 mTouchableRegion.set( 1104 (int) mContentContainer.getX(), 1105 (int) mContentContainer.getY(), 1106 (int) mContentContainer.getX() + width, 1107 (int) mContentContainer.getY() + height); 1108 } 1109 1110 /** 1111 * Make the touchable area of this popup be the area specified by mTouchableRegion. 1112 * This should be called after the popup window has been dismissed (dismiss/hide) 1113 * and is probably being re-shown with a new content root view. 1114 */ 1115 private void setTouchableSurfaceInsetsComputer() { 1116 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView() 1117 .getRootView() 1118 .getViewTreeObserver(); 1119 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer); 1120 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer); 1121 } 1122 1123 private boolean isInRTLMode() { 1124 return mContext.getApplicationInfo().hasRtlSupport() 1125 && mContext.getResources().getConfiguration().getLayoutDirection() 1126 == View.LAYOUT_DIRECTION_RTL; 1127 } 1128 1129 private boolean hasOverflow() { 1130 return mOverflowPanelSize != null; 1131 } 1132 1133 /** 1134 * Fits as many menu items in the main panel and returns a list of the menu items that 1135 * were not fit in. 1136 * 1137 * @return The menu items that are not included in this main panel. 1138 */ 1139 public List<MenuItem> layoutMainPanelItems( 1140 List<MenuItem> menuItems, final int toolbarWidth) { 1141 Preconditions.checkNotNull(menuItems); 1142 1143 int availableWidth = toolbarWidth; 1144 1145 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>(); 1146 // add the overflow menu items to the end of the remainingMenuItems list. 1147 final LinkedList<MenuItem> overflowMenuItems = new LinkedList(); 1148 for (MenuItem menuItem : menuItems) { 1149 if (menuItem.requiresOverflow()) { 1150 overflowMenuItems.add(menuItem); 1151 } else { 1152 remainingMenuItems.add(menuItem); 1153 } 1154 } 1155 remainingMenuItems.addAll(overflowMenuItems); 1156 1157 mMainPanel.removeAllViews(); 1158 mMainPanel.setPaddingRelative(0, 0, 0, 0); 1159 1160 int lastGroupId = -1; 1161 boolean isFirstItem = true; 1162 while (!remainingMenuItems.isEmpty()) { 1163 final MenuItem menuItem = remainingMenuItems.peek(); 1164 1165 // if this is the first item, regardless of requiresOverflow(), it should be 1166 // displayed on the main panel. Otherwise all items including this one will be 1167 // overflow items, and should be displayed in overflow panel. 1168 if(!isFirstItem && menuItem.requiresOverflow()) { 1169 break; 1170 } 1171 1172 View menuItemButton = createMenuItemButton(mContext, menuItem, mIconTextSpacing); 1173 1174 // Adding additional start padding for the first button to even out button spacing. 1175 if (isFirstItem) { 1176 menuItemButton.setPaddingRelative( 1177 (int) (1.5 * menuItemButton.getPaddingStart()), 1178 menuItemButton.getPaddingTop(), 1179 menuItemButton.getPaddingEnd(), 1180 menuItemButton.getPaddingBottom()); 1181 } 1182 1183 // Adding additional end padding for the last button to even out button spacing. 1184 boolean isLastItem = remainingMenuItems.size() == 1; 1185 if (isLastItem) { 1186 menuItemButton.setPaddingRelative( 1187 menuItemButton.getPaddingStart(), 1188 menuItemButton.getPaddingTop(), 1189 (int) (1.5 * menuItemButton.getPaddingEnd()), 1190 menuItemButton.getPaddingBottom()); 1191 } 1192 1193 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1194 final int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); 1195 1196 final boolean isNewGroup = !isFirstItem && lastGroupId != menuItem.getGroupId(); 1197 final int extraPadding = isNewGroup ? menuItemButton.getPaddingEnd() * 2 : 0; 1198 1199 // Check if we can fit an item while reserving space for the overflowButton. 1200 boolean canFitWithOverflow = 1201 menuItemButtonWidth <= 1202 availableWidth - mOverflowButtonSize.getWidth() - extraPadding; 1203 boolean canFitNoOverflow = 1204 isLastItem && menuItemButtonWidth <= availableWidth - extraPadding; 1205 if (canFitWithOverflow || canFitNoOverflow) { 1206 if (isNewGroup) { 1207 final View divider = createDivider(mContext); 1208 final int dividerWidth = divider.getLayoutParams().width; 1209 1210 // Add extra padding to the end of the previous button. 1211 // Half of the extra padding (less borderWidth) goes to the previous button. 1212 View previousButton = mMainPanel.getChildAt(mMainPanel.getChildCount() - 1); 1213 final int prevPaddingEnd = previousButton.getPaddingEnd() 1214 + extraPadding / 2 - dividerWidth; 1215 previousButton.setPaddingRelative( 1216 previousButton.getPaddingStart(), 1217 previousButton.getPaddingTop(), 1218 prevPaddingEnd, 1219 previousButton.getPaddingBottom()); 1220 final ViewGroup.LayoutParams prevParams = previousButton.getLayoutParams(); 1221 prevParams.width += extraPadding / 2 - dividerWidth; 1222 previousButton.setLayoutParams(prevParams); 1223 1224 // Add extra padding to the start of this button. 1225 // Other half of the extra padding goes to this button. 1226 final int paddingStart = menuItemButton.getPaddingStart() 1227 + extraPadding / 2; 1228 menuItemButton.setPaddingRelative( 1229 paddingStart, 1230 menuItemButton.getPaddingTop(), 1231 menuItemButton.getPaddingEnd(), 1232 menuItemButton.getPaddingBottom()); 1233 1234 // Include a divider. 1235 mMainPanel.addView(divider); 1236 } 1237 1238 setButtonTagAndClickListener(menuItemButton, menuItem); 1239 // Set tooltips for main panel items, but not overflow items (b/35726766). 1240 menuItemButton.setTooltipText(menuItem.getTooltipText()); 1241 mMainPanel.addView(menuItemButton); 1242 final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams(); 1243 params.width = menuItemButtonWidth + extraPadding / 2; 1244 menuItemButton.setLayoutParams(params); 1245 availableWidth -= menuItemButtonWidth + extraPadding; 1246 remainingMenuItems.pop(); 1247 } else { 1248 break; 1249 } 1250 lastGroupId = menuItem.getGroupId(); 1251 isFirstItem = false; 1252 } 1253 1254 if (!remainingMenuItems.isEmpty()) { 1255 // Reserve space for overflowButton. 1256 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0); 1257 } 1258 1259 mMainPanelSize = measure(mMainPanel); 1260 return remainingMenuItems; 1261 } 1262 1263 private void layoutOverflowPanelItems(List<MenuItem> menuItems) { 1264 ArrayAdapter<MenuItem> overflowPanelAdapter = 1265 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter(); 1266 overflowPanelAdapter.clear(); 1267 final int size = menuItems.size(); 1268 for (int i = 0; i < size; i++) { 1269 overflowPanelAdapter.add(menuItems.get(i)); 1270 } 1271 mOverflowPanel.setAdapter(overflowPanelAdapter); 1272 if (mOpenOverflowUpwards) { 1273 mOverflowPanel.setY(0); 1274 } else { 1275 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); 1276 } 1277 1278 int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth()); 1279 int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE); 1280 mOverflowPanelSize = new Size(width, height); 1281 setSize(mOverflowPanel, mOverflowPanelSize); 1282 } 1283 1284 /** 1285 * Resets the content container and appropriately position it's panels. 1286 */ 1287 private void preparePopupContent() { 1288 mContentContainer.removeAllViews(); 1289 1290 // Add views in the specified order so they stack up as expected. 1291 // Order: overflowPanel, mainPanel, overflowButton. 1292 if (hasOverflow()) { 1293 mContentContainer.addView(mOverflowPanel); 1294 } 1295 mContentContainer.addView(mMainPanel); 1296 if (hasOverflow()) { 1297 mContentContainer.addView(mOverflowButton); 1298 } 1299 setPanelsStatesAtRestingPosition(); 1300 setContentAreaAsTouchableSurface(); 1301 1302 // The positioning of contents in RTL is wrong when the view is first rendered. 1303 // Hide the view and post a runnable to recalculate positions and render the view. 1304 // TODO: Investigate why this happens and fix. 1305 if (isInRTLMode()) { 1306 mContentContainer.setAlpha(0); 1307 mContentContainer.post(mPreparePopupContentRTLHelper); 1308 } 1309 } 1310 1311 /** 1312 * Clears out the panels and their container. Resets their calculated sizes. 1313 */ 1314 private void clearPanels() { 1315 mOverflowPanelSize = null; 1316 mMainPanelSize = null; 1317 mIsOverflowOpen = false; 1318 mMainPanel.removeAllViews(); 1319 ArrayAdapter<MenuItem> overflowPanelAdapter = 1320 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter(); 1321 overflowPanelAdapter.clear(); 1322 mOverflowPanel.setAdapter(overflowPanelAdapter); 1323 mContentContainer.removeAllViews(); 1324 } 1325 1326 private void positionContentYCoordinatesIfOpeningOverflowUpwards() { 1327 if (mOpenOverflowUpwards) { 1328 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight()); 1329 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight()); 1330 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight()); 1331 } 1332 } 1333 1334 private int getOverflowWidth() { 1335 int overflowWidth = 0; 1336 final int count = mOverflowPanel.getAdapter().getCount(); 1337 for (int i = 0; i < count; i++) { 1338 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i); 1339 overflowWidth = 1340 Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth); 1341 } 1342 return overflowWidth; 1343 } 1344 1345 private int calculateOverflowHeight(int maxItemSize) { 1346 // Maximum of 4 items, minimum of 2 if the overflow has to scroll. 1347 int actualSize = Math.min( 1348 MAX_OVERFLOW_SIZE, 1349 Math.min( 1350 Math.max(MIN_OVERFLOW_SIZE, maxItemSize), 1351 mOverflowPanel.getCount())); 1352 int extension = 0; 1353 if (actualSize < mOverflowPanel.getCount()) { 1354 // The overflow will require scrolling to get to all the items. 1355 // Extend the height so that part of the hidden items is displayed. 1356 extension = (int) (mLineHeight * 0.5f); 1357 } 1358 return actualSize * mLineHeight 1359 + mOverflowButtonSize.getHeight() 1360 + extension; 1361 } 1362 1363 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) { 1364 menuItemButton.setTag(menuItem); 1365 menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener); 1366 } 1367 1368 /** 1369 * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.* 1370 * animations. See comment about this in the code. 1371 */ 1372 private int getAdjustedDuration(int originalDuration) { 1373 if (mTransitionDurationScale < 150) { 1374 // For smaller transition, decrease the time. 1375 return Math.max(originalDuration - 50, 0); 1376 } else if (mTransitionDurationScale > 300) { 1377 // For bigger transition, increase the time. 1378 return originalDuration + 50; 1379 } 1380 1381 // Scale the animation duration with getDurationScale(). This allows 1382 // android.view.animation.* animations to scale just like android.animation.* animations 1383 // when animator duration scale is adjusted in "Developer Options". 1384 // For this reason, do not use this method for android.animation.* animations. 1385 return (int) (originalDuration * ValueAnimator.getDurationScale()); 1386 } 1387 1388 private void maybeComputeTransitionDurationScale() { 1389 if (mMainPanelSize != null && mOverflowPanelSize != null) { 1390 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth(); 1391 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight(); 1392 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) / 1393 mContentContainer.getContext().getResources().getDisplayMetrics().density); 1394 } 1395 } 1396 1397 private ViewGroup createMainPanel() { 1398 ViewGroup mainPanel = new LinearLayout(mContext) { 1399 @Override 1400 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1401 if (isOverflowAnimating()) { 1402 // Update widthMeasureSpec to make sure that this view is not clipped 1403 // as we offset it's coordinates with respect to it's parent. 1404 widthMeasureSpec = MeasureSpec.makeMeasureSpec( 1405 mMainPanelSize.getWidth(), 1406 MeasureSpec.EXACTLY); 1407 } 1408 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1409 } 1410 1411 @Override 1412 public boolean onInterceptTouchEvent(MotionEvent ev) { 1413 // Intercept the touch event while the overflow is animating. 1414 return isOverflowAnimating(); 1415 } 1416 }; 1417 return mainPanel; 1418 } 1419 1420 private ImageButton createOverflowButton() { 1421 final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext) 1422 .inflate(R.layout.floating_popup_overflow_button, null); 1423 overflowButton.setImageDrawable(mOverflow); 1424 overflowButton.setOnClickListener(v -> { 1425 if (mIsOverflowOpen) { 1426 overflowButton.setImageDrawable(mToOverflow); 1427 mToOverflow.start(); 1428 closeOverflow(); 1429 } else { 1430 overflowButton.setImageDrawable(mToArrow); 1431 mToArrow.start(); 1432 openOverflow(); 1433 } 1434 }); 1435 return overflowButton; 1436 } 1437 1438 private OverflowPanel createOverflowPanel() { 1439 final OverflowPanel overflowPanel = new OverflowPanel(this); 1440 overflowPanel.setLayoutParams(new ViewGroup.LayoutParams( 1441 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 1442 overflowPanel.setDivider(null); 1443 overflowPanel.setDividerHeight(0); 1444 1445 final ArrayAdapter adapter = 1446 new ArrayAdapter<MenuItem>(mContext, 0) { 1447 @Override 1448 public View getView(int position, View convertView, ViewGroup parent) { 1449 return mOverflowPanelViewHelper.getView( 1450 getItem(position), mOverflowPanelSize.getWidth(), convertView); 1451 } 1452 }; 1453 overflowPanel.setAdapter(adapter); 1454 1455 overflowPanel.setOnItemClickListener((parent, view, position, id) -> { 1456 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position); 1457 if (mOnMenuItemClickListener != null) { 1458 mOnMenuItemClickListener.onMenuItemClick(menuItem); 1459 } 1460 }); 1461 1462 return overflowPanel; 1463 } 1464 1465 private boolean isOverflowAnimating() { 1466 final boolean overflowOpening = mOpenOverflowAnimation.hasStarted() 1467 && !mOpenOverflowAnimation.hasEnded(); 1468 final boolean overflowClosing = mCloseOverflowAnimation.hasStarted() 1469 && !mCloseOverflowAnimation.hasEnded(); 1470 return overflowOpening || overflowClosing; 1471 } 1472 1473 private Animation.AnimationListener createOverflowAnimationListener() { 1474 Animation.AnimationListener listener = new Animation.AnimationListener() { 1475 @Override 1476 public void onAnimationStart(Animation animation) { 1477 // Disable the overflow button while it's animating. 1478 // It will be re-enabled when the animation stops. 1479 mOverflowButton.setEnabled(false); 1480 // Ensure both panels have visibility turned on when the overflow animation 1481 // starts. 1482 mMainPanel.setVisibility(View.VISIBLE); 1483 mOverflowPanel.setVisibility(View.VISIBLE); 1484 } 1485 1486 @Override 1487 public void onAnimationEnd(Animation animation) { 1488 // Posting this because it seems like this is called before the animation 1489 // actually ends. 1490 mContentContainer.post(() -> { 1491 setPanelsStatesAtRestingPosition(); 1492 setContentAreaAsTouchableSurface(); 1493 }); 1494 } 1495 1496 @Override 1497 public void onAnimationRepeat(Animation animation) { 1498 } 1499 }; 1500 return listener; 1501 } 1502 1503 private static Size measure(View view) { 1504 Preconditions.checkState(view.getParent() == null); 1505 view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1506 return new Size(view.getMeasuredWidth(), view.getMeasuredHeight()); 1507 } 1508 1509 private static void setSize(View view, int width, int height) { 1510 view.setMinimumWidth(width); 1511 view.setMinimumHeight(height); 1512 ViewGroup.LayoutParams params = view.getLayoutParams(); 1513 params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params; 1514 params.width = width; 1515 params.height = height; 1516 view.setLayoutParams(params); 1517 } 1518 1519 private static void setSize(View view, Size size) { 1520 setSize(view, size.getWidth(), size.getHeight()); 1521 } 1522 1523 private static void setWidth(View view, int width) { 1524 ViewGroup.LayoutParams params = view.getLayoutParams(); 1525 setSize(view, width, params.height); 1526 } 1527 1528 private static void setHeight(View view, int height) { 1529 ViewGroup.LayoutParams params = view.getLayoutParams(); 1530 setSize(view, params.width, height); 1531 } 1532 1533 /** 1534 * A custom ListView for the overflow panel. 1535 */ 1536 private static final class OverflowPanel extends ListView { 1537 1538 private final FloatingToolbarPopup mPopup; 1539 1540 OverflowPanel(FloatingToolbarPopup popup) { 1541 super(Preconditions.checkNotNull(popup).mContext); 1542 this.mPopup = popup; 1543 setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3); 1544 setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); 1545 } 1546 1547 @Override 1548 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1549 // Update heightMeasureSpec to make sure that this view is not clipped 1550 // as we offset it's coordinates with respect to it's parent. 1551 int height = mPopup.mOverflowPanelSize.getHeight() 1552 - mPopup.mOverflowButtonSize.getHeight(); 1553 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 1554 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1555 } 1556 1557 @Override 1558 public boolean dispatchTouchEvent(MotionEvent ev) { 1559 if (mPopup.isOverflowAnimating()) { 1560 // Eat the touch event. 1561 return true; 1562 } 1563 return super.dispatchTouchEvent(ev); 1564 } 1565 1566 @Override 1567 protected boolean awakenScrollBars() { 1568 return super.awakenScrollBars(); 1569 } 1570 } 1571 1572 /** 1573 * A custom interpolator used for various floating toolbar animations. 1574 */ 1575 private static final class LogAccelerateInterpolator implements Interpolator { 1576 1577 private static final int BASE = 100; 1578 private static final float LOGS_SCALE = 1f / computeLog(1, BASE); 1579 1580 private static float computeLog(float t, int base) { 1581 return (float) (1 - Math.pow(base, -t)); 1582 } 1583 1584 @Override 1585 public float getInterpolation(float t) { 1586 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE; 1587 } 1588 } 1589 1590 /** 1591 * A helper for generating views for the overflow panel. 1592 */ 1593 private static final class OverflowPanelViewHelper { 1594 1595 private final View mCalculator; 1596 private final int mIconTextSpacing; 1597 private final int mSidePadding; 1598 1599 private final Context mContext; 1600 1601 public OverflowPanelViewHelper(Context context) { 1602 mContext = Preconditions.checkNotNull(context); 1603 mIconTextSpacing = context.getResources() 1604 .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_side_padding); 1605 mSidePadding = context.getResources() 1606 .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding); 1607 mCalculator = createMenuButton(null); 1608 } 1609 1610 public View getView(MenuItem menuItem, int minimumWidth, View convertView) { 1611 Preconditions.checkNotNull(menuItem); 1612 if (convertView != null) { 1613 updateMenuItemButton(convertView, menuItem, mIconTextSpacing); 1614 } else { 1615 convertView = createMenuButton(menuItem); 1616 } 1617 convertView.setMinimumWidth(minimumWidth); 1618 return convertView; 1619 } 1620 1621 public int calculateWidth(MenuItem menuItem) { 1622 updateMenuItemButton(mCalculator, menuItem, mIconTextSpacing); 1623 mCalculator.measure( 1624 View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 1625 return mCalculator.getMeasuredWidth(); 1626 } 1627 1628 private View createMenuButton(MenuItem menuItem) { 1629 View button = createMenuItemButton(mContext, menuItem, mIconTextSpacing); 1630 button.setPadding(mSidePadding, 0, mSidePadding, 0); 1631 return button; 1632 } 1633 } 1634 } 1635 1636 /** 1637 * Creates and returns a menu button for the specified menu item. 1638 */ 1639 private static View createMenuItemButton( 1640 Context context, MenuItem menuItem, int iconTextSpacing) { 1641 final View menuItemButton = LayoutInflater.from(context) 1642 .inflate(R.layout.floating_popup_menu_button, null); 1643 if (menuItem != null) { 1644 updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing); 1645 } 1646 return menuItemButton; 1647 } 1648 1649 /** 1650 * Updates the specified menu item button with the specified menu item data. 1651 */ 1652 private static void updateMenuItemButton( 1653 View menuItemButton, MenuItem menuItem, int iconTextSpacing) { 1654 final TextView buttonText = (TextView) menuItemButton.findViewById( 1655 R.id.floating_toolbar_menu_item_text); 1656 if (TextUtils.isEmpty(menuItem.getTitle())) { 1657 buttonText.setVisibility(View.GONE); 1658 } else { 1659 buttonText.setVisibility(View.VISIBLE); 1660 buttonText.setText(menuItem.getTitle()); 1661 } 1662 final ImageView buttonIcon = (ImageView) menuItemButton 1663 .findViewById(R.id.floating_toolbar_menu_item_image); 1664 if (menuItem.getIcon() == null) { 1665 buttonIcon.setVisibility(View.GONE); 1666 if (buttonText != null) { 1667 buttonText.setPaddingRelative(0, 0, 0, 0); 1668 } 1669 } else { 1670 buttonIcon.setVisibility(View.VISIBLE); 1671 buttonIcon.setImageDrawable(menuItem.getIcon()); 1672 if (buttonText != null) { 1673 buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0); 1674 } 1675 } 1676 final CharSequence contentDescription = menuItem.getContentDescription(); 1677 if (TextUtils.isEmpty(contentDescription)) { 1678 menuItemButton.setContentDescription(menuItem.getTitle()); 1679 } else { 1680 menuItemButton.setContentDescription(contentDescription); 1681 } 1682 } 1683 1684 private static ViewGroup createContentContainer(Context context) { 1685 ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context) 1686 .inflate(R.layout.floating_popup_container, null); 1687 contentContainer.setLayoutParams(new ViewGroup.LayoutParams( 1688 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 1689 contentContainer.setTag(FLOATING_TOOLBAR_TAG); 1690 return contentContainer; 1691 } 1692 1693 private static PopupWindow createPopupWindow(ViewGroup content) { 1694 ViewGroup popupContentHolder = new LinearLayout(content.getContext()); 1695 PopupWindow popupWindow = new PopupWindow(popupContentHolder); 1696 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false) 1697 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects. 1698 popupWindow.setClippingEnabled(false); 1699 popupWindow.setWindowLayoutType( 1700 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); 1701 popupWindow.setAnimationStyle(0); 1702 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 1703 content.setLayoutParams(new ViewGroup.LayoutParams( 1704 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 1705 popupContentHolder.addView(content); 1706 return popupWindow; 1707 } 1708 1709 private static View createDivider(Context context) { 1710 // TODO: Inflate this instead. 1711 View divider = new View(context); 1712 1713 int _1dp = (int) TypedValue.applyDimension( 1714 TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); 1715 LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( 1716 _1dp, ViewGroup.LayoutParams.MATCH_PARENT); 1717 params.setMarginsRelative(0, _1dp * 10, 0, _1dp * 10); 1718 divider.setLayoutParams(params); 1719 1720 TypedArray a = context.obtainStyledAttributes( 1721 new TypedValue().data, new int[] { R.attr.floatingToolbarDividerColor }); 1722 divider.setBackgroundColor(a.getColor(0, 0)); 1723 a.recycle(); 1724 1725 divider.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 1726 divider.setEnabled(false); 1727 divider.setFocusable(false); 1728 divider.setContentDescription(null); 1729 1730 return divider; 1731 } 1732 1733 /** 1734 * Creates an "appear" animation for the specified view. 1735 * 1736 * @param view The view to animate 1737 */ 1738 private static AnimatorSet createEnterAnimation(View view) { 1739 AnimatorSet animation = new AnimatorSet(); 1740 animation.playTogether( 1741 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150)); 1742 return animation; 1743 } 1744 1745 /** 1746 * Creates a "disappear" animation for the specified view. 1747 * 1748 * @param view The view to animate 1749 * @param startDelay The start delay of the animation 1750 * @param listener The animation listener 1751 */ 1752 private static AnimatorSet createExitAnimation( 1753 View view, int startDelay, Animator.AnimatorListener listener) { 1754 AnimatorSet animation = new AnimatorSet(); 1755 animation.playTogether( 1756 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100)); 1757 animation.setStartDelay(startDelay); 1758 animation.addListener(listener); 1759 return animation; 1760 } 1761 1762 /** 1763 * Returns a re-themed context with controlled look and feel for views. 1764 */ 1765 private static Context applyDefaultTheme(Context originalContext) { 1766 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme}); 1767 boolean isLightTheme = a.getBoolean(0, true); 1768 int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material; 1769 a.recycle(); 1770 return new ContextThemeWrapper(originalContext, themeId); 1771 } 1772 } 1773