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