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