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