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