FloatingToolbar.java revision 5047132887112df4235beea8ff42a28f1653e202
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 17package com.android.internal.widget; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ObjectAnimator; 23import android.content.Context; 24import android.graphics.Color; 25import android.graphics.Point; 26import android.graphics.Rect; 27import android.graphics.Region; 28import android.graphics.drawable.ColorDrawable; 29import android.text.TextUtils; 30import android.util.Size; 31import android.view.Gravity; 32import android.view.LayoutInflater; 33import android.view.Menu; 34import android.view.MenuItem; 35import android.view.View; 36import android.view.View.MeasureSpec; 37import android.view.ViewGroup; 38import android.view.ViewTreeObserver; 39import android.view.Window; 40import android.view.WindowManager; 41import android.view.animation.Animation; 42import android.view.animation.AnimationSet; 43import android.view.animation.Transformation; 44import android.widget.AdapterView; 45import android.widget.ArrayAdapter; 46import android.widget.Button; 47import android.widget.ImageButton; 48import android.widget.ImageView; 49import android.widget.LinearLayout; 50import android.widget.ListView; 51import android.widget.PopupWindow; 52import android.widget.TextView; 53 54import java.util.ArrayList; 55import java.util.LinkedList; 56import java.util.List; 57 58import com.android.internal.R; 59import com.android.internal.util.Preconditions; 60 61/** 62 * A floating toolbar for showing contextual menu items. 63 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the 64 * the remaining menu items in a vertical overflow view when the overflow button is clicked. 65 * The horizontal toolbar morphs into the vertical overflow view. 66 */ 67public final class FloatingToolbar { 68 69 // This class is responsible for the public API of the floating toolbar. 70 // It delegates rendering operations to the FloatingToolbarPopup. 71 72 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER = 73 new MenuItem.OnMenuItemClickListener() { 74 @Override 75 public boolean onMenuItemClick(MenuItem item) { 76 return false; 77 } 78 }; 79 80 private final Context mContext; 81 private final FloatingToolbarPopup mPopup; 82 83 private final Rect mContentRect = new Rect(); 84 85 private Menu mMenu; 86 private List<Object> mShowingMenuItems = new ArrayList<Object>(); 87 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 88 89 private int mSuggestedWidth; 90 private boolean mWidthChanged = true; 91 92 /** 93 * Initializes a floating toolbar. 94 */ 95 public FloatingToolbar(Context context, Window window) { 96 mContext = Preconditions.checkNotNull(context); 97 mPopup = new FloatingToolbarPopup(window.getDecorView()); 98 } 99 100 /** 101 * Sets the menu to be shown in this floating toolbar. 102 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 103 * toolbar. 104 */ 105 public FloatingToolbar setMenu(Menu menu) { 106 mMenu = Preconditions.checkNotNull(menu); 107 return this; 108 } 109 110 /** 111 * Sets the custom listener for invocation of menu items in this floating toolbar. 112 */ 113 public FloatingToolbar setOnMenuItemClickListener( 114 MenuItem.OnMenuItemClickListener menuItemClickListener) { 115 if (menuItemClickListener != null) { 116 mMenuItemClickListener = menuItemClickListener; 117 } else { 118 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 119 } 120 return this; 121 } 122 123 /** 124 * Sets the content rectangle. This is the area of the interesting content that this toolbar 125 * should avoid obstructing. 126 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 127 * toolbar. 128 */ 129 public FloatingToolbar setContentRect(Rect rect) { 130 mContentRect.set(Preconditions.checkNotNull(rect)); 131 return this; 132 } 133 134 /** 135 * Sets the suggested width of this floating toolbar. 136 * The actual width will be about this size but there are no guarantees that it will be exactly 137 * the suggested width. 138 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 139 * toolbar. 140 */ 141 public FloatingToolbar setSuggestedWidth(int suggestedWidth) { 142 // Check if there's been a substantial width spec change. 143 int difference = Math.abs(suggestedWidth - mSuggestedWidth); 144 mWidthChanged = difference > (mSuggestedWidth * 0.2); 145 146 mSuggestedWidth = suggestedWidth; 147 return this; 148 } 149 150 /** 151 * Shows this floating toolbar. 152 */ 153 public FloatingToolbar show() { 154 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); 155 if (!isCurrentlyShowing(menuItems) || mWidthChanged) { 156 mPopup.dismiss(); 157 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth); 158 mShowingMenuItems = getShowingMenuItemsReferences(menuItems); 159 } 160 mPopup.updateCoordinates(mContentRect); 161 if (!mPopup.isShowing()) { 162 mPopup.show(mContentRect); 163 } 164 mWidthChanged = false; 165 return this; 166 } 167 168 /** 169 * Updates this floating toolbar to reflect recent position and view updates. 170 * NOTE: This method is a no-op if the toolbar isn't showing. 171 */ 172 public FloatingToolbar updateLayout() { 173 if (mPopup.isShowing()) { 174 // show() performs all the logic we need here. 175 show(); 176 } 177 return this; 178 } 179 180 /** 181 * Dismisses this floating toolbar. 182 */ 183 public void dismiss() { 184 mPopup.dismiss(); 185 } 186 187 /** 188 * Hides this floating toolbar. This is a no-op if the toolbar is not showing. 189 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar. 190 */ 191 public void hide() { 192 mPopup.hide(); 193 } 194 195 /** 196 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise. 197 */ 198 public boolean isShowing() { 199 return mPopup.isShowing(); 200 } 201 202 /** 203 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise. 204 */ 205 public boolean isHidden() { 206 return mPopup.isHidden(); 207 } 208 209 /** 210 * Returns true if this floating toolbar is currently showing the specified menu items. 211 */ 212 private boolean isCurrentlyShowing(List<MenuItem> menuItems) { 213 return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems)); 214 } 215 216 /** 217 * Returns the visible and enabled menu items in the specified menu. 218 * This method is recursive. 219 */ 220 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) { 221 List<MenuItem> menuItems = new ArrayList<MenuItem>(); 222 for (int i = 0; (menu != null) && (i < menu.size()); i++) { 223 MenuItem menuItem = menu.getItem(i); 224 if (menuItem.isVisible() && menuItem.isEnabled()) { 225 Menu subMenu = menuItem.getSubMenu(); 226 if (subMenu != null) { 227 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu)); 228 } else { 229 menuItems.add(menuItem); 230 } 231 } 232 } 233 return menuItems; 234 } 235 236 private List<Object> getShowingMenuItemsReferences(List<MenuItem> menuItems) { 237 List<Object> references = new ArrayList<Object>(); 238 for (MenuItem menuItem : menuItems) { 239 if (isIconOnlyMenuItem(menuItem)) { 240 references.add(menuItem.getIcon()); 241 } else { 242 references.add(menuItem.getTitle()); 243 } 244 } 245 return references; 246 } 247 248 249 /** 250 * A popup window used by the floating toolbar. 251 * 252 * This class is responsible for the rendering/animation of the floating toolbar. 253 * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time. 254 * It delegates specific panel functionality to the appropriate panel. 255 */ 256 private static final class FloatingToolbarPopup { 257 258 public static final int OVERFLOW_DIRECTION_UP = 0; 259 public static final int OVERFLOW_DIRECTION_DOWN = 1; 260 261 private final View mParent; 262 private final PopupWindow mPopupWindow; 263 private final ViewGroup mContentContainer; 264 private final int mMarginHorizontal; 265 private final int mMarginVertical; 266 267 private final Animation.AnimationListener mOnOverflowOpened = 268 new Animation.AnimationListener() { 269 @Override 270 public void onAnimationStart(Animation animation) {} 271 272 @Override 273 public void onAnimationEnd(Animation animation) { 274 setOverflowPanelAsContent(); 275 mOverflowPanel.fadeIn(true); 276 } 277 278 @Override 279 public void onAnimationRepeat(Animation animation) {} 280 }; 281 private final Animation.AnimationListener mOnOverflowClosed = 282 new Animation.AnimationListener() { 283 @Override 284 public void onAnimationStart(Animation animation) {} 285 286 @Override 287 public void onAnimationEnd(Animation animation) { 288 setMainPanelAsContent(); 289 mMainPanel.fadeIn(true); 290 } 291 292 @Override 293 public void onAnimationRepeat(Animation animation) { 294 } 295 }; 296 private final AnimatorSet mShowAnimation; 297 private final AnimatorSet mDismissAnimation; 298 private final AnimatorSet mHideAnimation; 299 private final AnimationSet mOpenOverflowAnimation = new AnimationSet(true) { 300 @Override 301 public void cancel() { 302 if (hasStarted() && !hasEnded()) { 303 super.cancel(); 304 setOverflowPanelAsContent(); 305 } 306 } 307 }; 308 private final AnimationSet mCloseOverflowAnimation = new AnimationSet(true) { 309 @Override 310 public void cancel() { 311 if (hasStarted() && !hasEnded()) { 312 super.cancel(); 313 setMainPanelAsContent(); 314 } 315 } 316 }; 317 318 private final Runnable mOpenOverflow = new Runnable() { 319 @Override 320 public void run() { 321 openOverflow(); 322 } 323 }; 324 private final Runnable mCloseOverflow = new Runnable() { 325 @Override 326 public void run() { 327 closeOverflow(); 328 } 329 }; 330 331 private final Point mCoords = new Point(); 332 333 private final Region mTouchableRegion = new Region(); 334 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = 335 new ViewTreeObserver.OnComputeInternalInsetsListener() { 336 public void onComputeInternalInsets( 337 ViewTreeObserver.InternalInsetsInfo info) { 338 info.contentInsets.setEmpty(); 339 info.visibleInsets.setEmpty(); 340 info.touchableRegion.set(mTouchableRegion); 341 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo 342 .TOUCHABLE_INSETS_REGION); 343 } 344 }; 345 346 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing. 347 private boolean mHidden; // tracks whether this popup is hidden or hiding. 348 349 private FloatingToolbarOverflowPanel mOverflowPanel; 350 private FloatingToolbarMainPanel mMainPanel; 351 private int mOverflowDirection; 352 353 /** 354 * Initializes a new floating toolbar popup. 355 * 356 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token 357 * from. 358 */ 359 public FloatingToolbarPopup(View parent) { 360 mParent = Preconditions.checkNotNull(parent); 361 mContentContainer = createContentContainer(parent.getContext()); 362 mPopupWindow = createPopupWindow(mContentContainer); 363 mShowAnimation = createGrowFadeInFromBottom(mContentContainer); 364 mDismissAnimation = createShrinkFadeOutFromBottomAnimation( 365 mContentContainer, 366 150, // startDelay 367 new AnimatorListenerAdapter() { 368 @Override 369 public void onAnimationEnd(Animator animation) { 370 mPopupWindow.dismiss(); 371 mContentContainer.removeAllViews(); 372 } 373 }); 374 mHideAnimation = createShrinkFadeOutFromBottomAnimation( 375 mContentContainer, 376 0, // startDelay 377 new AnimatorListenerAdapter() { 378 @Override 379 public void onAnimationEnd(Animator animation) { 380 mPopupWindow.dismiss(); 381 } 382 }); 383 mMarginHorizontal = parent.getResources() 384 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); 385 mMarginVertical = parent.getResources() 386 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin); 387 } 388 389 /** 390 * Lays out buttons for the specified menu items. 391 */ 392 public void layoutMenuItems(List<MenuItem> menuItems, 393 MenuItem.OnMenuItemClickListener menuItemClickListener, int suggestedWidth) { 394 Preconditions.checkNotNull(menuItems); 395 396 mContentContainer.removeAllViews(); 397 if (mMainPanel == null) { 398 mMainPanel = new FloatingToolbarMainPanel(mParent.getContext(), mOpenOverflow); 399 } 400 List<MenuItem> overflowMenuItems = 401 mMainPanel.layoutMenuItems(menuItems, suggestedWidth); 402 mMainPanel.setOnMenuItemClickListener(menuItemClickListener); 403 if (!overflowMenuItems.isEmpty()) { 404 if (mOverflowPanel == null) { 405 mOverflowPanel = 406 new FloatingToolbarOverflowPanel(mParent.getContext(), mCloseOverflow); 407 } 408 mOverflowPanel.setMenuItems(overflowMenuItems); 409 mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener); 410 } 411 updatePopupSize(); 412 } 413 414 /** 415 * Shows this popup at the specified coordinates. 416 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. 417 */ 418 public void show(Rect contentRect) { 419 Preconditions.checkNotNull(contentRect); 420 421 if (isShowing()) { 422 return; 423 } 424 425 mHidden = false; 426 mDismissed = false; 427 cancelDismissAndHideAnimations(); 428 cancelOverflowAnimations(); 429 430 // Make sure a panel is set as the content. 431 if (mContentContainer.getChildCount() == 0) { 432 setMainPanelAsContent(); 433 // If we're yet to show the popup, set the container visibility to zero. 434 // The "show" animation will make this visible. 435 mContentContainer.setAlpha(0); 436 } 437 updateOverflowHeight(contentRect.top - (mMarginVertical * 2)); 438 refreshCoordinatesAndOverflowDirection(contentRect); 439 preparePopupContent(); 440 mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, mCoords.x, mCoords.y); 441 setTouchableSurfaceInsetsComputer(); 442 runShowAnimation(); 443 } 444 445 /** 446 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op. 447 */ 448 public void dismiss() { 449 if (mDismissed) { 450 return; 451 } 452 453 mHidden = false; 454 mDismissed = true; 455 mHideAnimation.cancel(); 456 runDismissAnimation(); 457 setZeroTouchableSurface(); 458 } 459 460 /** 461 * Hides this popup. This is a no-op if this popup is not showing. 462 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup. 463 */ 464 public void hide() { 465 if (!isShowing()) { 466 return; 467 } 468 469 mHidden = true; 470 runHideAnimation(); 471 setZeroTouchableSurface(); 472 } 473 474 /** 475 * Returns {@code true} if this popup is currently showing. {@code false} otherwise. 476 */ 477 public boolean isShowing() { 478 return !mDismissed && !mHidden; 479 } 480 481 /** 482 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise. 483 */ 484 public boolean isHidden() { 485 return mHidden; 486 } 487 488 /** 489 * Updates the coordinates of this popup. 490 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. 491 * This is a no-op if this popup is not showing. 492 */ 493 public void updateCoordinates(Rect contentRect) { 494 Preconditions.checkNotNull(contentRect); 495 496 if (!isShowing() || !mPopupWindow.isShowing()) { 497 return; 498 } 499 500 cancelOverflowAnimations(); 501 refreshCoordinatesAndOverflowDirection(contentRect); 502 preparePopupContent(); 503 mPopupWindow.update(mCoords.x, mCoords.y, getWidth(), getHeight()); 504 } 505 506 /** 507 * Returns the width of this popup. 508 */ 509 public int getWidth() { 510 return mPopupWindow.getWidth(); 511 } 512 513 /** 514 * Returns the height of this popup. 515 */ 516 public int getHeight() { 517 return mPopupWindow.getHeight(); 518 } 519 520 /** 521 * Returns the context this popup is running in. 522 */ 523 public Context getContext() { 524 return mContentContainer.getContext(); 525 } 526 527 private void refreshCoordinatesAndOverflowDirection(Rect contentRect) { 528 int x = contentRect.centerX() - getWidth() / 2; 529 int y; 530 if (contentRect.top > getHeight()) { 531 y = contentRect.top - getHeight(); 532 mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_UP; 533 } else if (contentRect.top > getToolbarHeightWithVerticalMargin()) { 534 y = contentRect.top - getToolbarHeightWithVerticalMargin(); 535 mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN; 536 } else { 537 y = contentRect.bottom; 538 mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN; 539 } 540 mCoords.set(x, y); 541 if (mOverflowPanel != null) { 542 mOverflowPanel.setOverflowDirection(mOverflowDirection); 543 } 544 } 545 546 private int getToolbarHeightWithVerticalMargin() { 547 return getEstimatedToolbarHeight(mParent.getContext()) + mMarginVertical * 2; 548 } 549 550 /** 551 * Performs the "show" animation on the floating popup. 552 */ 553 private void runShowAnimation() { 554 mShowAnimation.start(); 555 } 556 557 /** 558 * Performs the "dismiss" animation on the floating popup. 559 */ 560 private void runDismissAnimation() { 561 mDismissAnimation.start(); 562 } 563 564 /** 565 * Performs the "hide" animation on the floating popup. 566 */ 567 private void runHideAnimation() { 568 mHideAnimation.start(); 569 } 570 571 private void cancelDismissAndHideAnimations() { 572 mDismissAnimation.cancel(); 573 mHideAnimation.cancel(); 574 } 575 576 private void cancelOverflowAnimations() { 577 mOpenOverflowAnimation.cancel(); 578 mCloseOverflowAnimation.cancel(); 579 } 580 581 /** 582 * Opens the floating toolbar overflow. 583 * This method should not be called if menu items have not been laid out with 584 * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}. 585 * 586 * @throws IllegalStateException if called when menu items have not been laid out. 587 */ 588 private void openOverflow() { 589 Preconditions.checkState(mMainPanel != null); 590 Preconditions.checkState(mOverflowPanel != null); 591 592 mMainPanel.fadeOut(true); 593 Size overflowPanelSize = mOverflowPanel.measure(); 594 final int targetWidth = overflowPanelSize.getWidth(); 595 final int targetHeight = overflowPanelSize.getHeight(); 596 final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP); 597 final int startWidth = mContentContainer.getWidth(); 598 final int startHeight = mContentContainer.getHeight(); 599 final float startY = mContentContainer.getY(); 600 final float right = mContentContainer.getX() + mContentContainer.getWidth(); 601 Animation widthAnimation = new Animation() { 602 @Override 603 protected void applyTransformation(float interpolatedTime, Transformation t) { 604 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); 605 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); 606 params.width = startWidth + deltaWidth; 607 mContentContainer.setLayoutParams(params); 608 mContentContainer.setX(right - mContentContainer.getWidth()); 609 } 610 }; 611 Animation heightAnimation = new Animation() { 612 @Override 613 protected void applyTransformation(float interpolatedTime, Transformation t) { 614 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); 615 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); 616 params.height = startHeight + deltaHeight; 617 mContentContainer.setLayoutParams(params); 618 if (morphUpwards) { 619 float y = startY - (mContentContainer.getHeight() - startHeight); 620 mContentContainer.setY(y); 621 } 622 } 623 }; 624 widthAnimation.setDuration(240); 625 heightAnimation.setDuration(180); 626 heightAnimation.setStartOffset(60); 627 mOpenOverflowAnimation.getAnimations().clear(); 628 mOpenOverflowAnimation.setAnimationListener(mOnOverflowOpened); 629 mOpenOverflowAnimation.addAnimation(widthAnimation); 630 mOpenOverflowAnimation.addAnimation(heightAnimation); 631 mContentContainer.startAnimation(mOpenOverflowAnimation); 632 } 633 634 /** 635 * Opens the floating toolbar overflow. 636 * This method should not be called if menu items have not been laid out with 637 * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}. 638 * 639 * @throws IllegalStateException if called when menu items have not been laid out. 640 */ 641 private void closeOverflow() { 642 Preconditions.checkState(mMainPanel != null); 643 Preconditions.checkState(mOverflowPanel != null); 644 645 mOverflowPanel.fadeOut(true); 646 Size mainPanelSize = mMainPanel.measure(); 647 final int targetWidth = mainPanelSize.getWidth(); 648 final int targetHeight = mainPanelSize.getHeight(); 649 final int startWidth = mContentContainer.getWidth(); 650 final int startHeight = mContentContainer.getHeight(); 651 final float right = mContentContainer.getX() + mContentContainer.getWidth(); 652 final float bottom = mContentContainer.getY() + mContentContainer.getHeight(); 653 final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP); 654 Animation widthAnimation = new Animation() { 655 @Override 656 protected void applyTransformation(float interpolatedTime, Transformation t) { 657 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); 658 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); 659 params.width = startWidth + deltaWidth; 660 mContentContainer.setLayoutParams(params); 661 mContentContainer.setX(right - mContentContainer.getWidth()); 662 } 663 }; 664 Animation heightAnimation = new Animation() { 665 @Override 666 protected void applyTransformation(float interpolatedTime, Transformation t) { 667 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); 668 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); 669 params.height = startHeight + deltaHeight; 670 mContentContainer.setLayoutParams(params); 671 if (morphedUpwards) { 672 mContentContainer.setY(bottom - mContentContainer.getHeight()); 673 } 674 } 675 }; 676 widthAnimation.setDuration(150); 677 widthAnimation.setStartOffset(150); 678 heightAnimation.setDuration(210); 679 mCloseOverflowAnimation.getAnimations().clear(); 680 mCloseOverflowAnimation.setAnimationListener(mOnOverflowClosed); 681 mCloseOverflowAnimation.addAnimation(widthAnimation); 682 mCloseOverflowAnimation.addAnimation(heightAnimation); 683 mContentContainer.startAnimation(mCloseOverflowAnimation); 684 } 685 686 /** 687 * Prepares the content container for show and update calls. 688 */ 689 private void preparePopupContent() { 690 // Reset visibility. 691 if (mMainPanel != null) { 692 mMainPanel.fadeIn(false); 693 } 694 if (mOverflowPanel != null) { 695 mOverflowPanel.fadeIn(false); 696 } 697 698 // Reset position. 699 if (isMainPanelContent()) { 700 positionMainPanel(); 701 } 702 if (isOverflowPanelContent()) { 703 positionOverflowPanel(); 704 } 705 } 706 707 private boolean isMainPanelContent() { 708 return mMainPanel != null 709 && mContentContainer.getChildAt(0) == mMainPanel.getView(); 710 } 711 712 private boolean isOverflowPanelContent() { 713 return mOverflowPanel != null 714 && mContentContainer.getChildAt(0) == mOverflowPanel.getView(); 715 } 716 717 /** 718 * Sets the current content to be the main view panel. 719 */ 720 private void setMainPanelAsContent() { 721 // This should never be called if the main panel has not been initialized. 722 Preconditions.checkNotNull(mMainPanel); 723 mContentContainer.removeAllViews(); 724 Size mainPanelSize = mMainPanel.measure(); 725 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); 726 params.width = mainPanelSize.getWidth(); 727 params.height = mainPanelSize.getHeight(); 728 mContentContainer.setLayoutParams(params); 729 mContentContainer.addView(mMainPanel.getView()); 730 setContentAreaAsTouchableSurface(); 731 } 732 733 /** 734 * Sets the current content to be the overflow view panel. 735 */ 736 private void setOverflowPanelAsContent() { 737 // This should never be called if the overflow panel has not been initialized. 738 Preconditions.checkNotNull(mOverflowPanel); 739 mContentContainer.removeAllViews(); 740 Size overflowPanelSize = mOverflowPanel.measure(); 741 ViewGroup.LayoutParams params = mContentContainer.getLayoutParams(); 742 params.width = overflowPanelSize.getWidth(); 743 params.height = overflowPanelSize.getHeight(); 744 mContentContainer.setLayoutParams(params); 745 mContentContainer.addView(mOverflowPanel.getView()); 746 setContentAreaAsTouchableSurface(); 747 } 748 749 /** 750 * Places the main view panel at the appropriate resting coordinates. 751 */ 752 private void positionMainPanel() { 753 Preconditions.checkNotNull(mMainPanel); 754 float x = mPopupWindow.getWidth() 755 - (mMainPanel.getView().getMeasuredWidth() + mMarginHorizontal); 756 mContentContainer.setX(x); 757 758 float y = mMarginVertical; 759 if (mOverflowDirection == OVERFLOW_DIRECTION_UP) { 760 y = getHeight() 761 - (mMainPanel.getView().getMeasuredHeight() + mMarginVertical); 762 } 763 mContentContainer.setY(y); 764 setContentAreaAsTouchableSurface(); 765 } 766 767 /** 768 * Places the main view panel at the appropriate resting coordinates. 769 */ 770 private void positionOverflowPanel() { 771 Preconditions.checkNotNull(mOverflowPanel); 772 float x = mPopupWindow.getWidth() 773 - (mOverflowPanel.getView().getMeasuredWidth() + mMarginHorizontal); 774 mContentContainer.setX(x); 775 mContentContainer.setY(mMarginVertical); 776 setContentAreaAsTouchableSurface(); 777 } 778 779 private void updateOverflowHeight(int height) { 780 if (mOverflowPanel != null) { 781 mOverflowPanel.setSuggestedHeight(height); 782 783 // Re-measure the popup and it's contents. 784 boolean mainPanelContent = isMainPanelContent(); 785 boolean overflowPanelContent = isOverflowPanelContent(); 786 mContentContainer.removeAllViews(); // required to update popup size. 787 updatePopupSize(); 788 // Reset the appropriate content. 789 if (mainPanelContent) { 790 setMainPanelAsContent(); 791 } 792 if (overflowPanelContent) { 793 setOverflowPanelAsContent(); 794 } 795 } 796 } 797 798 private void updatePopupSize() { 799 int width = 0; 800 int height = 0; 801 if (mMainPanel != null) { 802 Size mainPanelSize = mMainPanel.measure(); 803 width = mainPanelSize.getWidth(); 804 height = mainPanelSize.getHeight(); 805 } 806 if (mOverflowPanel != null) { 807 Size overflowPanelSize = mOverflowPanel.measure(); 808 width = Math.max(width, overflowPanelSize.getWidth()); 809 height = Math.max(height, overflowPanelSize.getHeight()); 810 } 811 mPopupWindow.setWidth(width + mMarginHorizontal * 2); 812 mPopupWindow.setHeight(height + mMarginVertical * 2); 813 } 814 815 /** 816 * Sets the touchable region of this popup to be zero. This means that all touch events on 817 * this popup will go through to the surface behind it. 818 */ 819 private void setZeroTouchableSurface() { 820 mTouchableRegion.setEmpty(); 821 } 822 823 /** 824 * Sets the touchable region of this popup to be the area occupied by its content. 825 */ 826 private void setContentAreaAsTouchableSurface() { 827 if (!mPopupWindow.isShowing()) { 828 mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 829 } 830 int width = mContentContainer.getMeasuredWidth(); 831 int height = mContentContainer.getMeasuredHeight(); 832 mTouchableRegion.set( 833 (int) mContentContainer.getX(), 834 (int) mContentContainer.getY(), 835 (int) mContentContainer.getX() + width, 836 (int) mContentContainer.getY() + height); 837 } 838 839 /** 840 * Make the touchable area of this popup be the area specified by mTouchableRegion. 841 * This should be called after the popup window has been dismissed (dismiss/hide) 842 * and is probably being re-shown with a new content root view. 843 */ 844 private void setTouchableSurfaceInsetsComputer() { 845 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView() 846 .getRootView() 847 .getViewTreeObserver(); 848 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer); 849 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer); 850 } 851 } 852 853 /** 854 * A widget that holds the primary menu items in the floating toolbar. 855 */ 856 private static final class FloatingToolbarMainPanel { 857 858 private final Context mContext; 859 private final ViewGroup mContentView; 860 private final View.OnClickListener mMenuItemButtonOnClickListener = 861 new View.OnClickListener() { 862 @Override 863 public void onClick(View v) { 864 if (v.getTag() instanceof MenuItem) { 865 if (mOnMenuItemClickListener != null) { 866 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag()); 867 } 868 } 869 } 870 }; 871 private final ViewFader viewFader; 872 private final Runnable mOpenOverflow; 873 874 private View mOpenOverflowButton; 875 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener; 876 877 /** 878 * Initializes a floating toolbar popup main view panel. 879 * 880 * @param context 881 * @param openOverflow The code that opens the toolbar popup overflow. 882 */ 883 public FloatingToolbarMainPanel(Context context, Runnable openOverflow) { 884 mContext = Preconditions.checkNotNull(context); 885 mContentView = new LinearLayout(context); 886 viewFader = new ViewFader(mContentView); 887 mOpenOverflow = Preconditions.checkNotNull(openOverflow); 888 } 889 890 /** 891 * Fits as many menu items in the main panel and returns a list of the menu items that 892 * were not fit in. 893 * 894 * @return The menu items that are not included in this main panel. 895 */ 896 public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int suggestedWidth) { 897 Preconditions.checkNotNull(menuItems); 898 899 final int toolbarWidth = getAdjustedToolbarWidth(mContext, suggestedWidth) 900 // Reserve space for the "open overflow" button. 901 - getEstimatedOpenOverflowButtonWidth(mContext); 902 903 int availableWidth = toolbarWidth; 904 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems); 905 906 mContentView.removeAllViews(); 907 908 boolean isFirstItem = true; 909 while (!remainingMenuItems.isEmpty()) { 910 final MenuItem menuItem = remainingMenuItems.peek(); 911 View menuItemButton = createMenuItemButton(mContext, menuItem); 912 913 // Adding additional start padding for the first button to even out button spacing. 914 if (isFirstItem) { 915 menuItemButton.setPaddingRelative( 916 (int) (1.5 * menuItemButton.getPaddingStart()), 917 menuItemButton.getPaddingTop(), 918 menuItemButton.getPaddingEnd(), 919 menuItemButton.getPaddingBottom()); 920 isFirstItem = false; 921 } 922 923 // Adding additional end padding for the last button to even out button spacing. 924 if (remainingMenuItems.size() == 1) { 925 menuItemButton.setPaddingRelative( 926 menuItemButton.getPaddingStart(), 927 menuItemButton.getPaddingTop(), 928 (int) (1.5 * menuItemButton.getPaddingEnd()), 929 menuItemButton.getPaddingBottom()); 930 } 931 932 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 933 int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); 934 if (menuItemButtonWidth <= availableWidth) { 935 setButtonTagAndClickListener(menuItemButton, menuItem); 936 mContentView.addView(menuItemButton); 937 ViewGroup.LayoutParams params = menuItemButton.getLayoutParams(); 938 params.width = menuItemButtonWidth; 939 menuItemButton.setLayoutParams(params); 940 availableWidth -= menuItemButtonWidth; 941 remainingMenuItems.pop(); 942 } else { 943 if (mOpenOverflowButton == null) { 944 mOpenOverflowButton = LayoutInflater.from(mContext) 945 .inflate(R.layout.floating_popup_open_overflow_button, null); 946 mOpenOverflowButton.setOnClickListener(new View.OnClickListener() { 947 @Override 948 public void onClick(View v) { 949 if (mOpenOverflowButton != null) { 950 mOpenOverflow.run(); 951 } 952 } 953 }); 954 } 955 mContentView.addView(mOpenOverflowButton); 956 break; 957 } 958 } 959 return remainingMenuItems; 960 } 961 962 public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) { 963 mOnMenuItemClickListener = listener; 964 } 965 966 public View getView() { 967 return mContentView; 968 } 969 970 public void fadeIn(boolean animate) { 971 viewFader.fadeIn(animate); 972 } 973 974 public void fadeOut(boolean animate) { 975 viewFader.fadeOut(animate); 976 } 977 978 /** 979 * Returns how big this panel's view should be. 980 * This method should only be called when the view has not been attached to a parent 981 * otherwise it will throw an illegal state. 982 */ 983 public Size measure() throws IllegalStateException { 984 Preconditions.checkState(mContentView.getParent() == null); 985 mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 986 return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight()); 987 } 988 989 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) { 990 View button = menuItemButton; 991 if (isIconOnlyMenuItem(menuItem)) { 992 button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button); 993 } 994 button.setTag(menuItem); 995 button.setOnClickListener(mMenuItemButtonOnClickListener); 996 } 997 } 998 999 1000 /** 1001 * A widget that holds the overflow items in the floating toolbar. 1002 */ 1003 private static final class FloatingToolbarOverflowPanel { 1004 1005 private final LinearLayout mContentView; 1006 private final ViewGroup mBackButtonContainer; 1007 private final View mBackButton; 1008 private final ListView mListView; 1009 private final TextView mListViewItemWidthCalculator; 1010 private final ViewFader mViewFader; 1011 private final Runnable mCloseOverflow; 1012 1013 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener; 1014 private int mOverflowWidth = 0; 1015 private int mSuggestedHeight; 1016 1017 /** 1018 * Initializes a floating toolbar popup overflow view panel. 1019 * 1020 * @param context 1021 * @param closeOverflow The code that closes the toolbar popup's overflow. 1022 */ 1023 public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) { 1024 mCloseOverflow = Preconditions.checkNotNull(closeOverflow); 1025 mSuggestedHeight = getScreenHeight(context); 1026 1027 mContentView = new LinearLayout(context); 1028 mContentView.setOrientation(LinearLayout.VERTICAL); 1029 mViewFader = new ViewFader(mContentView); 1030 1031 mBackButton = LayoutInflater.from(context) 1032 .inflate(R.layout.floating_popup_close_overflow_button, null); 1033 mBackButton.setOnClickListener(new View.OnClickListener() { 1034 @Override 1035 public void onClick(View v) { 1036 mCloseOverflow.run(); 1037 } 1038 }); 1039 mBackButtonContainer = new LinearLayout(context); 1040 mBackButtonContainer.addView(mBackButton); 1041 1042 mListView = createOverflowListView(); 1043 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1044 @Override 1045 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1046 MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position); 1047 if (mOnMenuItemClickListener != null) { 1048 mOnMenuItemClickListener.onMenuItemClick(menuItem); 1049 } 1050 } 1051 }); 1052 1053 mContentView.addView(mListView); 1054 mContentView.addView(mBackButtonContainer); 1055 1056 mListViewItemWidthCalculator = createOverflowMenuItemButton(context); 1057 mListViewItemWidthCalculator.setLayoutParams(new ViewGroup.LayoutParams( 1058 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 1059 } 1060 1061 /** 1062 * Sets the menu items to be displayed in the overflow. 1063 */ 1064 public void setMenuItems(List<MenuItem> menuItems) { 1065 ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter(); 1066 overflowListViewAdapter.clear(); 1067 overflowListViewAdapter.addAll(menuItems); 1068 setListViewHeight(); 1069 setOverflowWidth(); 1070 } 1071 1072 public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) { 1073 mOnMenuItemClickListener = listener; 1074 } 1075 1076 /** 1077 * Notifies the overflow of the current direction in which the overflow will be opened. 1078 * 1079 * @param overflowDirection {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP} 1080 * or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}. 1081 */ 1082 public void setOverflowDirection(int overflowDirection) { 1083 mContentView.removeView(mBackButtonContainer); 1084 int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0; 1085 mContentView.addView(mBackButtonContainer, index); 1086 } 1087 1088 public void setSuggestedHeight(int height) { 1089 mSuggestedHeight = height; 1090 setListViewHeight(); 1091 } 1092 1093 /** 1094 * Returns the content view of the overflow. 1095 */ 1096 public View getView() { 1097 return mContentView; 1098 } 1099 1100 public void fadeIn(boolean animate) { 1101 mViewFader.fadeIn(animate); 1102 } 1103 1104 public void fadeOut(boolean animate) { 1105 mViewFader.fadeOut(animate); 1106 } 1107 1108 /** 1109 * Returns how big this panel's view should be. 1110 * This method should only be called when the view has not been attached to a parent. 1111 * 1112 * @throws IllegalStateException 1113 */ 1114 public Size measure() { 1115 Preconditions.checkState(mContentView.getParent() == null); 1116 mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1117 return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight()); 1118 } 1119 1120 private void setListViewHeight() { 1121 int itemHeight = getEstimatedToolbarHeight(mContentView.getContext()); 1122 int height = mListView.getAdapter().getCount() * itemHeight; 1123 int maxHeight = mContentView.getContext().getResources(). 1124 getDimensionPixelSize(R.dimen.floating_toolbar_maximum_overflow_height); 1125 int minHeight = mContentView.getContext().getResources(). 1126 getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height); 1127 int availableHeight = mSuggestedHeight - (mSuggestedHeight % itemHeight) 1128 - itemHeight; // reserve space for the back button. 1129 ViewGroup.LayoutParams params = mListView.getLayoutParams(); 1130 if (availableHeight >= minHeight) { 1131 params.height = Math.min(Math.min(availableHeight, maxHeight), height); 1132 } else { 1133 params.height = Math.min(maxHeight, height); 1134 } 1135 mListView.setLayoutParams(params); 1136 } 1137 1138 private int setOverflowWidth() { 1139 for (int i = 0; i < mListView.getAdapter().getCount(); i++) { 1140 MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(i); 1141 Preconditions.checkNotNull(menuItem); 1142 mListViewItemWidthCalculator.setText(menuItem.getTitle()); 1143 mListViewItemWidthCalculator.measure( 1144 MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1145 mOverflowWidth = Math.max( 1146 mListViewItemWidthCalculator.getMeasuredWidth(), mOverflowWidth); 1147 } 1148 return mOverflowWidth; 1149 } 1150 1151 private ListView createOverflowListView() { 1152 final Context context = mContentView.getContext(); 1153 final ListView overflowListView = new ListView(context); 1154 overflowListView.setLayoutParams(new ViewGroup.LayoutParams( 1155 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 1156 overflowListView.setDivider(null); 1157 overflowListView.setDividerHeight(0); 1158 1159 final int viewTypeCount = 2; 1160 final int stringLabelViewType = 0; 1161 final int iconOnlyViewType = 1; 1162 final ArrayAdapter overflowListViewAdapter = 1163 new ArrayAdapter<MenuItem>(context, 0) { 1164 @Override 1165 public int getViewTypeCount() { 1166 return viewTypeCount; 1167 } 1168 1169 @Override 1170 public int getItemViewType(int position) { 1171 if (isIconOnlyMenuItem(getItem(position))) { 1172 return iconOnlyViewType; 1173 } 1174 return stringLabelViewType; 1175 } 1176 1177 @Override 1178 public View getView(int position, View convertView, ViewGroup parent) { 1179 if (getItemViewType(position) == iconOnlyViewType) { 1180 return getIconOnlyView(position, convertView); 1181 } 1182 return getStringTitleView(position, convertView); 1183 } 1184 1185 private View getStringTitleView(int position, View convertView) { 1186 TextView menuButton; 1187 if (convertView != null) { 1188 menuButton = (TextView) convertView; 1189 } else { 1190 menuButton = createOverflowMenuItemButton(context); 1191 } 1192 MenuItem menuItem = getItem(position); 1193 menuButton.setText(menuItem.getTitle()); 1194 menuButton.setContentDescription(menuItem.getTitle()); 1195 menuButton.setMinimumWidth(mOverflowWidth); 1196 return menuButton; 1197 } 1198 1199 private View getIconOnlyView(int position, View convertView) { 1200 View menuButton; 1201 if (convertView != null) { 1202 menuButton = convertView; 1203 } else { 1204 menuButton = LayoutInflater.from(context).inflate( 1205 R.layout.floating_popup_overflow_image_list_item, null); 1206 } 1207 MenuItem menuItem = getItem(position); 1208 ((ImageView) menuButton 1209 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1210 .setImageDrawable(menuItem.getIcon()); 1211 menuButton.setMinimumWidth(mOverflowWidth); 1212 return menuButton; 1213 } 1214 }; 1215 overflowListView.setAdapter(overflowListViewAdapter); 1216 return overflowListView; 1217 } 1218 } 1219 1220 1221 /** 1222 * A helper for fading in or out a view. 1223 */ 1224 private static final class ViewFader { 1225 1226 private static final int FADE_OUT_DURATION = 250; 1227 private static final int FADE_IN_DURATION = 150; 1228 1229 private final View mView; 1230 private final ObjectAnimator mFadeOutAnimation; 1231 private final ObjectAnimator mFadeInAnimation; 1232 1233 private ViewFader(View view) { 1234 mView = Preconditions.checkNotNull(view); 1235 mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0) 1236 .setDuration(FADE_OUT_DURATION); 1237 mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1) 1238 .setDuration(FADE_IN_DURATION); 1239 } 1240 1241 public void fadeIn(boolean animate) { 1242 cancelFadeAnimations(); 1243 if (animate) { 1244 mFadeInAnimation.start(); 1245 } else { 1246 mView.setAlpha(1); 1247 } 1248 } 1249 1250 public void fadeOut(boolean animate) { 1251 cancelFadeAnimations(); 1252 if (animate) { 1253 mFadeOutAnimation.start(); 1254 } else { 1255 mView.setAlpha(0); 1256 } 1257 } 1258 1259 private void cancelFadeAnimations() { 1260 mFadeInAnimation.cancel(); 1261 mFadeOutAnimation.cancel(); 1262 } 1263 } 1264 1265 /** 1266 * @return {@code true} if the menu item does not not have a string title but has an icon. 1267 * {@code false} otherwise. 1268 */ 1269 private static boolean isIconOnlyMenuItem(MenuItem menuItem) { 1270 if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) { 1271 return true; 1272 } 1273 return false; 1274 } 1275 1276 /** 1277 * Creates and returns a menu button for the specified menu item. 1278 */ 1279 private static View createMenuItemButton(Context context, MenuItem menuItem) { 1280 if (isIconOnlyMenuItem(menuItem)) { 1281 View imageMenuItemButton = LayoutInflater.from(context) 1282 .inflate(R.layout.floating_popup_menu_image_button, null); 1283 ((ImageButton) imageMenuItemButton 1284 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1285 .setImageDrawable(menuItem.getIcon()); 1286 return imageMenuItemButton; 1287 } 1288 1289 Button menuItemButton = (Button) LayoutInflater.from(context) 1290 .inflate(R.layout.floating_popup_menu_button, null); 1291 menuItemButton.setText(menuItem.getTitle()); 1292 menuItemButton.setContentDescription(menuItem.getTitle()); 1293 return menuItemButton; 1294 } 1295 1296 /** 1297 * Creates and returns a styled floating toolbar overflow list view item. 1298 */ 1299 private static TextView createOverflowMenuItemButton(Context context) { 1300 return (TextView) LayoutInflater.from(context) 1301 .inflate(R.layout.floating_popup_overflow_list_item, null); 1302 } 1303 1304 private static ViewGroup createContentContainer(Context context) { 1305 return (ViewGroup) LayoutInflater.from(context) 1306 .inflate(R.layout.floating_popup_container, null); 1307 } 1308 1309 private static PopupWindow createPopupWindow(View content) { 1310 ViewGroup popupContentHolder = new LinearLayout(content.getContext()); 1311 PopupWindow popupWindow = new PopupWindow(popupContentHolder); 1312 popupWindow.setWindowLayoutType( 1313 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); 1314 popupWindow.setAnimationStyle(0); 1315 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 1316 content.setLayoutParams(new ViewGroup.LayoutParams( 1317 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 1318 popupContentHolder.addView(content); 1319 return popupWindow; 1320 } 1321 1322 /** 1323 * Creates a "grow and fade in from the bottom" animation for the specified view. 1324 * 1325 * @param view The view to animate 1326 */ 1327 private static AnimatorSet createGrowFadeInFromBottom(View view) { 1328 AnimatorSet growFadeInFromBottomAnimation = new AnimatorSet(); 1329 growFadeInFromBottomAnimation.playTogether( 1330 ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125), 1331 ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125), 1332 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75)); 1333 growFadeInFromBottomAnimation.setStartDelay(50); 1334 return growFadeInFromBottomAnimation; 1335 } 1336 1337 /** 1338 * Creates a "shrink and fade out from bottom" animation for the specified view. 1339 * 1340 * @param view The view to animate 1341 * @param startDelay The start delay of the animation 1342 * @param listener The animation listener 1343 */ 1344 private static AnimatorSet createShrinkFadeOutFromBottomAnimation( 1345 View view, int startDelay, Animator.AnimatorListener listener) { 1346 AnimatorSet shrinkFadeOutFromBottomAnimation = new AnimatorSet(); 1347 shrinkFadeOutFromBottomAnimation.playTogether( 1348 ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125), 1349 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75)); 1350 shrinkFadeOutFromBottomAnimation.setStartDelay(startDelay); 1351 shrinkFadeOutFromBottomAnimation.addListener(listener); 1352 return shrinkFadeOutFromBottomAnimation; 1353 } 1354 1355 private static int getEstimatedToolbarHeight(Context context) { 1356 return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height); 1357 } 1358 1359 private static int getEstimatedOpenOverflowButtonWidth(Context context) { 1360 return context.getResources() 1361 .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width); 1362 } 1363 1364 private static int getAdjustedToolbarWidth(Context context, int width) { 1365 int maximumWidth = getScreenWidth(context) - 2 * context.getResources() 1366 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); 1367 1368 if (width <= 0 || width > maximumWidth) { 1369 int defaultWidth = context.getResources() 1370 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width); 1371 width = Math.min(defaultWidth, maximumWidth); 1372 } 1373 return width; 1374 } 1375 1376 /** 1377 * Returns the device's screen width. 1378 */ 1379 private static int getScreenWidth(Context context) { 1380 return context.getResources().getDisplayMetrics().widthPixels; 1381 } 1382 1383 /** 1384 * Returns the device's screen height. 1385 */ 1386 private static int getScreenHeight(Context context) { 1387 return context.getResources().getDisplayMetrics().heightPixels; 1388 } 1389} 1390