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