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