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