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