FloatingToolbar.java revision 0780f3a0733ea8ff4e8a93ca1263d92e24a6e9bb
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.animation.ValueAnimator; 24import android.content.Context; 25import android.content.res.TypedArray; 26import android.graphics.Color; 27import android.graphics.Point; 28import android.graphics.Rect; 29import android.graphics.Region; 30import android.graphics.drawable.AnimatedVectorDrawable; 31import android.graphics.drawable.ColorDrawable; 32import android.graphics.drawable.Drawable; 33import android.text.TextUtils; 34import android.util.Size; 35import android.view.ContextThemeWrapper; 36import android.view.Gravity; 37import android.view.LayoutInflater; 38import android.view.Menu; 39import android.view.MenuItem; 40import android.view.MotionEvent; 41import android.view.View; 42import android.view.View.MeasureSpec; 43import android.view.View.OnLayoutChangeListener; 44import android.view.ViewConfiguration; 45import android.view.ViewGroup; 46import android.view.ViewTreeObserver; 47import android.view.Window; 48import android.view.WindowManager; 49import android.view.animation.Animation; 50import android.view.animation.AnimationSet; 51import android.view.animation.Transformation; 52import android.view.animation.AnimationUtils; 53import android.view.animation.Interpolator; 54import android.widget.AdapterView; 55import android.widget.ArrayAdapter; 56import android.widget.Button; 57import android.widget.ImageButton; 58import android.widget.ImageView; 59import android.widget.LinearLayout; 60import android.widget.ListView; 61import android.widget.PopupWindow; 62import android.widget.TextView; 63 64import java.util.ArrayList; 65import java.util.LinkedList; 66import java.util.List; 67 68import com.android.internal.R; 69import com.android.internal.util.Preconditions; 70 71/** 72 * A floating toolbar for showing contextual menu items. 73 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the 74 * the remaining menu items in a vertical overflow view when the overflow button is clicked. 75 * The horizontal toolbar morphs into the vertical overflow view. 76 */ 77public final class FloatingToolbar { 78 79 // This class is responsible for the public API of the floating toolbar. 80 // It delegates rendering operations to the FloatingToolbarPopup. 81 82 public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar"; 83 84 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER = 85 new MenuItem.OnMenuItemClickListener() { 86 @Override 87 public boolean onMenuItemClick(MenuItem item) { 88 return false; 89 } 90 }; 91 92 private final Context mContext; 93 private final Window mWindow; 94 private final FloatingToolbarPopup mPopup; 95 96 private final Rect mContentRect = new Rect(); 97 private final Rect mPreviousContentRect = new Rect(); 98 99 private Menu mMenu; 100 private List<Object> mShowingMenuItems = new ArrayList<Object>(); 101 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 102 103 private int mSuggestedWidth; 104 private boolean mWidthChanged = true; 105 106 private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() { 107 108 private final Rect mNewRect = new Rect(); 109 private final Rect mOldRect = new Rect(); 110 111 @Override 112 public void onLayoutChange( 113 View view, 114 int newLeft, int newRight, int newTop, int newBottom, 115 int oldLeft, int oldRight, int oldTop, int oldBottom) { 116 mNewRect.set(newLeft, newRight, newTop, newBottom); 117 mOldRect.set(oldLeft, oldRight, oldTop, oldBottom); 118 if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) { 119 mWidthChanged = true; 120 updateLayout(); 121 } 122 } 123 }; 124 125 /** 126 * Initializes a floating toolbar. 127 */ 128 public FloatingToolbar(Context context, Window window) { 129 mContext = applyDefaultTheme(Preconditions.checkNotNull(context)); 130 mWindow = Preconditions.checkNotNull(window); 131 mPopup = new FloatingToolbarPopup(mContext, window.getDecorView()); 132 } 133 134 /** 135 * Sets the menu to be shown in this floating toolbar. 136 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 137 * toolbar. 138 */ 139 public FloatingToolbar setMenu(Menu menu) { 140 mMenu = Preconditions.checkNotNull(menu); 141 return this; 142 } 143 144 /** 145 * Sets the custom listener for invocation of menu items in this floating toolbar. 146 */ 147 public FloatingToolbar setOnMenuItemClickListener( 148 MenuItem.OnMenuItemClickListener menuItemClickListener) { 149 if (menuItemClickListener != null) { 150 mMenuItemClickListener = menuItemClickListener; 151 } else { 152 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; 153 } 154 return this; 155 } 156 157 /** 158 * Sets the content rectangle. This is the area of the interesting content that this toolbar 159 * should avoid obstructing. 160 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 161 * toolbar. 162 */ 163 public FloatingToolbar setContentRect(Rect rect) { 164 mContentRect.set(Preconditions.checkNotNull(rect)); 165 return this; 166 } 167 168 /** 169 * Sets the suggested width of this floating toolbar. 170 * The actual width will be about this size but there are no guarantees that it will be exactly 171 * the suggested width. 172 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the 173 * toolbar. 174 */ 175 public FloatingToolbar setSuggestedWidth(int suggestedWidth) { 176 // Check if there's been a substantial width spec change. 177 int difference = Math.abs(suggestedWidth - mSuggestedWidth); 178 mWidthChanged = difference > (mSuggestedWidth * 0.2); 179 180 mSuggestedWidth = suggestedWidth; 181 return this; 182 } 183 184 /** 185 * Shows this floating toolbar. 186 */ 187 public FloatingToolbar show() { 188 registerOrientationHandler(); 189 doShow(); 190 return this; 191 } 192 193 /** 194 * Updates this floating toolbar to reflect recent position and view updates. 195 * NOTE: This method is a no-op if the toolbar isn't showing. 196 */ 197 public FloatingToolbar updateLayout() { 198 if (mPopup.isShowing()) { 199 doShow(); 200 } 201 return this; 202 } 203 204 /** 205 * Dismisses this floating toolbar. 206 */ 207 public void dismiss() { 208 unregisterOrientationHandler(); 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 private void doShow() { 235 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); 236 if (!isCurrentlyShowing(menuItems) || mWidthChanged) { 237 mPopup.dismiss(); 238 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth); 239 mShowingMenuItems = getShowingMenuItemsReferences(menuItems); 240 } 241 if (!mPopup.isShowing()) { 242 mPopup.show(mContentRect); 243 } else if (!mPreviousContentRect.equals(mContentRect)) { 244 mPopup.updateCoordinates(mContentRect); 245 } 246 mWidthChanged = false; 247 mPreviousContentRect.set(mContentRect); 248 } 249 250 /** 251 * Returns true if this floating toolbar is currently showing the specified menu items. 252 */ 253 private boolean isCurrentlyShowing(List<MenuItem> menuItems) { 254 return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems)); 255 } 256 257 /** 258 * Returns the visible and enabled menu items in the specified menu. 259 * This method is recursive. 260 */ 261 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) { 262 List<MenuItem> menuItems = new ArrayList<MenuItem>(); 263 for (int i = 0; (menu != null) && (i < menu.size()); i++) { 264 MenuItem menuItem = menu.getItem(i); 265 if (menuItem.isVisible() && menuItem.isEnabled()) { 266 Menu subMenu = menuItem.getSubMenu(); 267 if (subMenu != null) { 268 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu)); 269 } else { 270 menuItems.add(menuItem); 271 } 272 } 273 } 274 return menuItems; 275 } 276 277 private List<Object> getShowingMenuItemsReferences(List<MenuItem> menuItems) { 278 List<Object> references = new ArrayList<Object>(); 279 for (MenuItem menuItem : menuItems) { 280 if (isIconOnlyMenuItem(menuItem)) { 281 references.add(menuItem.getIcon()); 282 } else { 283 references.add(menuItem.getTitle()); 284 } 285 } 286 return references; 287 } 288 289 private void registerOrientationHandler() { 290 unregisterOrientationHandler(); 291 mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler); 292 } 293 294 private void unregisterOrientationHandler() { 295 mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler); 296 } 297 298 299 /** 300 * A popup window used by the floating toolbar. 301 * 302 * This class is responsible for the rendering/animation of the floating toolbar. 303 * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button 304 * to transition between panels. 305 */ 306 private static final class FloatingToolbarPopup { 307 308 /* Minimum and maximum number of items allowed in the overflow. */ 309 private static final int MIN_OVERFLOW_SIZE = 2; 310 private static final int MAX_OVERFLOW_SIZE = 4; 311 312 private final Context mContext; 313 private final View mParent; // Parent for the popup window. 314 private final PopupWindow mPopupWindow; 315 316 /* Margins between the popup window and it's content. */ 317 private final int mMarginHorizontal; 318 private final int mMarginVertical; 319 320 /* View components */ 321 private final ViewGroup mContentContainer; // holds all contents. 322 private final ViewGroup mMainPanel; // holds menu items that are initially displayed. 323 private final OverflowPanel mOverflowPanel; // holds menu items hidden in the overflow. 324 private final ImageButton mOverflowButton; // opens/closes the overflow. 325 /* overflow button drawables. */ 326 private final Drawable mArrow; 327 private final Drawable mOverflow; 328 private final AnimatedVectorDrawable mToArrow; 329 private final AnimatedVectorDrawable mToOverflow; 330 331 private final OverflowPanelViewHelper mOverflowPanelViewHelper; 332 333 /* Animation interpolators. */ 334 private final Interpolator mLogAccelerateInterpolator; 335 private final Interpolator mFastOutSlowInInterpolator; 336 private final Interpolator mLinearOutSlowInInterpolator; 337 private final Interpolator mFastOutLinearInInterpolator; 338 339 /* Animations. */ 340 private final AnimatorSet mShowAnimation; 341 private final AnimatorSet mDismissAnimation; 342 private final AnimatorSet mHideAnimation; 343 private final AnimationSet mOpenOverflowAnimation; 344 private final AnimationSet mCloseOverflowAnimation; 345 private final Animation.AnimationListener mOverflowAnimationListener; 346 347 private final Rect mViewPortOnScreen = new Rect(); // portion of screen we can draw in. 348 private final Point mCoordsOnWindow = new Point(); // popup window coordinates. 349 /* Temporary data holders. Reset values before using. */ 350 private final int[] mTmpCoords = new int[2]; 351 private final Rect mTmpRect = new Rect(); 352 353 private final Region mTouchableRegion = new Region(); 354 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = 355 new ViewTreeObserver.OnComputeInternalInsetsListener() { 356 public void onComputeInternalInsets( 357 ViewTreeObserver.InternalInsetsInfo info) { 358 info.contentInsets.setEmpty(); 359 info.visibleInsets.setEmpty(); 360 info.touchableRegion.set(mTouchableRegion); 361 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo 362 .TOUCHABLE_INSETS_REGION); 363 } 364 }; 365 366 /** 367 * @see OverflowPanelViewHelper#preparePopupContent(). 368 */ 369 private final Runnable mPreparePopupContentRTLHelper = new Runnable() { 370 @Override 371 public void run() { 372 setPanelsStatesAtRestingPosition(); 373 setContentAreaAsTouchableSurface(); 374 mContentContainer.setAlpha(1); 375 } 376 }; 377 378 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing. 379 private boolean mHidden; // tracks whether this popup is hidden or hiding. 380 381 /* Calculated sizes for panels and overflow button. */ 382 private final Size mOverflowButtonSize; 383 private Size mOverflowPanelSize; // Should be null when there is no overflow. 384 private Size mMainPanelSize; 385 386 /* Item click listeners */ 387 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener; 388 private final View.OnClickListener mMenuItemButtonOnClickListener = 389 new View.OnClickListener() { 390 @Override 391 public void onClick(View v) { 392 if (v.getTag() instanceof MenuItem) { 393 if (mOnMenuItemClickListener != null) { 394 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag()); 395 } 396 } 397 } 398 }; 399 400 private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards. 401 private boolean mIsOverflowOpen; 402 403 private int mTransitionDurationScale; // Used to scale the toolbar transition duration. 404 405 /** 406 * Initializes a new floating toolbar popup. 407 * 408 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token 409 * from. 410 */ 411 public FloatingToolbarPopup(Context context, View parent) { 412 mParent = Preconditions.checkNotNull(parent); 413 mContext = Preconditions.checkNotNull(context); 414 mContentContainer = createContentContainer(context); 415 mPopupWindow = createPopupWindow(mContentContainer); 416 mMarginHorizontal = parent.getResources() 417 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); 418 mMarginVertical = parent.getResources() 419 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin); 420 421 // Interpolators 422 mLogAccelerateInterpolator = new LogAccelerateInterpolator(); 423 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator( 424 mContext, android.R.interpolator.fast_out_slow_in); 425 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator( 426 mContext, android.R.interpolator.linear_out_slow_in); 427 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator( 428 mContext, android.R.interpolator.fast_out_linear_in); 429 430 // Drawables. Needed for views. 431 mArrow = mContext.getResources() 432 .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme()); 433 mArrow.setAutoMirrored(true); 434 mOverflow = mContext.getResources() 435 .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme()); 436 mOverflow.setAutoMirrored(true); 437 mToArrow = (AnimatedVectorDrawable) mContext.getResources() 438 .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme()); 439 mToArrow.setAutoMirrored(true); 440 mToOverflow = (AnimatedVectorDrawable) mContext.getResources() 441 .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme()); 442 mToOverflow.setAutoMirrored(true); 443 444 // Views 445 mOverflowButton = createOverflowButton(); 446 mOverflowButtonSize = measure(mOverflowButton); 447 mMainPanel = createMainPanel(); 448 mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext); 449 mOverflowPanel = createOverflowPanel(); 450 451 // Animation. Need views. 452 mOverflowAnimationListener = createOverflowAnimationListener(); 453 mOpenOverflowAnimation = new AnimationSet(true); 454 mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener); 455 mCloseOverflowAnimation = new AnimationSet(true); 456 mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener); 457 mShowAnimation = createEnterAnimation(mContentContainer); 458 mDismissAnimation = createExitAnimation( 459 mContentContainer, 460 150, // startDelay 461 new AnimatorListenerAdapter() { 462 @Override 463 public void onAnimationEnd(Animator animation) { 464 mPopupWindow.dismiss(); 465 mContentContainer.removeAllViews(); 466 } 467 }); 468 mHideAnimation = createExitAnimation( 469 mContentContainer, 470 0, // startDelay 471 new AnimatorListenerAdapter() { 472 @Override 473 public void onAnimationEnd(Animator animation) { 474 mPopupWindow.dismiss(); 475 } 476 }); 477 } 478 479 /** 480 * Lays out buttons for the specified menu items. 481 * Requires a subsequent call to {@link #show()} to show the items. 482 */ 483 public void layoutMenuItems( 484 List<MenuItem> menuItems, 485 MenuItem.OnMenuItemClickListener menuItemClickListener, 486 int suggestedWidth) { 487 mOnMenuItemClickListener = menuItemClickListener; 488 cancelOverflowAnimations(); 489 clearPanels(); 490 menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth)); 491 if (!menuItems.isEmpty()) { 492 // Add remaining items to the overflow. 493 layoutOverflowPanelItems(menuItems); 494 } 495 updatePopupSize(); 496 } 497 498 /** 499 * Shows this popup at the specified coordinates. 500 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. 501 */ 502 public void show(Rect contentRectOnScreen) { 503 Preconditions.checkNotNull(contentRectOnScreen); 504 505 if (isShowing()) { 506 return; 507 } 508 509 mHidden = false; 510 mDismissed = false; 511 cancelDismissAndHideAnimations(); 512 cancelOverflowAnimations(); 513 514 refreshCoordinatesAndOverflowDirection(contentRectOnScreen); 515 preparePopupContent(); 516 // We need to specify the position in window coordinates. 517 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can 518 // specify the popup position in screen coordinates. 519 mPopupWindow.showAtLocation( 520 mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y); 521 setTouchableSurfaceInsetsComputer(); 522 runShowAnimation(); 523 } 524 525 /** 526 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op. 527 */ 528 public void dismiss() { 529 if (mDismissed) { 530 return; 531 } 532 533 mHidden = false; 534 mDismissed = true; 535 mHideAnimation.cancel(); 536 537 runDismissAnimation(); 538 setZeroTouchableSurface(); 539 } 540 541 /** 542 * Hides this popup. This is a no-op if this popup is not showing. 543 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup. 544 */ 545 public void hide() { 546 if (!isShowing()) { 547 return; 548 } 549 550 mHidden = true; 551 runHideAnimation(); 552 setZeroTouchableSurface(); 553 } 554 555 /** 556 * Returns {@code true} if this popup is currently showing. {@code false} otherwise. 557 */ 558 public boolean isShowing() { 559 return !mDismissed && !mHidden; 560 } 561 562 /** 563 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise. 564 */ 565 public boolean isHidden() { 566 return mHidden; 567 } 568 569 /** 570 * Updates the coordinates of this popup. 571 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. 572 * This is a no-op if this popup is not showing. 573 */ 574 public void updateCoordinates(Rect contentRectOnScreen) { 575 Preconditions.checkNotNull(contentRectOnScreen); 576 577 if (!isShowing() || !mPopupWindow.isShowing()) { 578 return; 579 } 580 581 cancelOverflowAnimations(); 582 refreshCoordinatesAndOverflowDirection(contentRectOnScreen); 583 preparePopupContent(); 584 // We need to specify the position in window coordinates. 585 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can 586 // specify the popup position in screen coordinates. 587 mPopupWindow.update( 588 mCoordsOnWindow.x, mCoordsOnWindow.y, 589 mPopupWindow.getWidth(), mPopupWindow.getHeight()); 590 } 591 592 private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) { 593 refreshViewPort(); 594 595 // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in 596 // landscape. 597 final int x = Math.min( 598 contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2, 599 mViewPortOnScreen.right - mPopupWindow.getWidth()); 600 601 final int y; 602 603 final int availableHeightAboveContent = 604 contentRectOnScreen.top - mViewPortOnScreen.top; 605 final int availableHeightBelowContent = 606 mViewPortOnScreen.bottom - contentRectOnScreen.bottom; 607 608 final int margin = 2 * mMarginVertical; 609 final int toolbarHeightWithVerticalMargin = getLineHeight(mContext) + margin; 610 611 if (!hasOverflow()) { 612 if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) { 613 // There is enough space at the top of the content. 614 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin; 615 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) { 616 // There is enough space at the bottom of the content. 617 y = contentRectOnScreen.bottom; 618 } else if (availableHeightBelowContent >= getLineHeight(mContext)) { 619 // Just enough space to fit the toolbar with no vertical margins. 620 y = contentRectOnScreen.bottom - mMarginVertical; 621 } else { 622 // Not enough space. Prefer to position as high as possible. 623 y = Math.max( 624 mViewPortOnScreen.top, 625 contentRectOnScreen.top - toolbarHeightWithVerticalMargin); 626 } 627 } else { 628 // Has an overflow. 629 final int minimumOverflowHeightWithMargin = 630 calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin; 631 final int availableHeightThroughContentDown = mViewPortOnScreen.bottom - 632 contentRectOnScreen.top + toolbarHeightWithVerticalMargin; 633 final int availableHeightThroughContentUp = contentRectOnScreen.bottom - 634 mViewPortOnScreen.top + toolbarHeightWithVerticalMargin; 635 636 if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) { 637 // There is enough space at the top of the content rect for the overflow. 638 // Position above and open upwards. 639 updateOverflowHeight(availableHeightAboveContent - margin); 640 y = contentRectOnScreen.top - mPopupWindow.getHeight(); 641 mOpenOverflowUpwards = true; 642 } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin 643 && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) { 644 // There is enough space at the top of the content rect for the main panel 645 // but not the overflow. 646 // Position above but open downwards. 647 updateOverflowHeight(availableHeightThroughContentDown - margin); 648 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin; 649 mOpenOverflowUpwards = false; 650 } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) { 651 // There is enough space at the bottom of the content rect for the overflow. 652 // Position below and open downwards. 653 updateOverflowHeight(availableHeightBelowContent - margin); 654 y = contentRectOnScreen.bottom; 655 mOpenOverflowUpwards = false; 656 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin 657 && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) { 658 // There is enough space at the bottom of the content rect for the main panel 659 // but not the overflow. 660 // Position below but open upwards. 661 updateOverflowHeight(availableHeightThroughContentUp - margin); 662 y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin - 663 mPopupWindow.getHeight(); 664 mOpenOverflowUpwards = true; 665 } else { 666 // Not enough space. 667 // Position at the top of the view port and open downwards. 668 updateOverflowHeight(mViewPortOnScreen.height() - margin); 669 y = mViewPortOnScreen.top; 670 mOpenOverflowUpwards = false; 671 } 672 } 673 674 // We later specify the location of PopupWindow relative to the attached window. 675 // The idea here is that 1) we can get the location of a View in both window coordinates 676 // and screen coordiantes, where the offset between them should be equal to the window 677 // origin, and 2) we can use an arbitrary for this calculation while calculating the 678 // location of the rootview is supposed to be least expensive. 679 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid 680 // the following calculation. 681 mParent.getRootView().getLocationOnScreen(mTmpCoords); 682 int rootViewLeftOnScreen = mTmpCoords[0]; 683 int rootViewTopOnScreen = mTmpCoords[1]; 684 mParent.getRootView().getLocationInWindow(mTmpCoords); 685 int rootViewLeftOnWindow = mTmpCoords[0]; 686 int rootViewTopOnWindow = mTmpCoords[1]; 687 int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow; 688 int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow; 689 mCoordsOnWindow.set( 690 Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen)); 691 } 692 693 /** 694 * Performs the "show" animation on the floating popup. 695 */ 696 private void runShowAnimation() { 697 mShowAnimation.start(); 698 } 699 700 /** 701 * Performs the "dismiss" animation on the floating popup. 702 */ 703 private void runDismissAnimation() { 704 mDismissAnimation.start(); 705 } 706 707 /** 708 * Performs the "hide" animation on the floating popup. 709 */ 710 private void runHideAnimation() { 711 mHideAnimation.start(); 712 } 713 714 private void cancelDismissAndHideAnimations() { 715 mDismissAnimation.cancel(); 716 mHideAnimation.cancel(); 717 } 718 719 private void cancelOverflowAnimations() { 720 mContentContainer.clearAnimation(); 721 mMainPanel.animate().cancel(); 722 mOverflowPanel.animate().cancel(); 723 mToArrow.stop(); 724 mToOverflow.stop(); 725 } 726 727 private void openOverflow() { 728 final int targetWidth = mOverflowPanelSize.getWidth(); 729 final int targetHeight = mOverflowPanelSize.getHeight(); 730 final int startWidth = mContentContainer.getWidth(); 731 final int startHeight = mContentContainer.getHeight(); 732 final float startY = mContentContainer.getY(); 733 final float left = mContentContainer.getX(); 734 final float right = left + mContentContainer.getWidth(); 735 Animation widthAnimation = new Animation() { 736 @Override 737 protected void applyTransformation(float interpolatedTime, Transformation t) { 738 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); 739 setWidth(mContentContainer, startWidth + deltaWidth); 740 if (isRTL()) { 741 mContentContainer.setX(left); 742 743 // Lock the panels in place. 744 mMainPanel.setX(0); 745 mOverflowPanel.setX(0); 746 } else { 747 mContentContainer.setX(right - mContentContainer.getWidth()); 748 749 // Offset the panels' positions so they look like they're locked in place 750 // on the screen. 751 mMainPanel.setX(mContentContainer.getWidth() - startWidth); 752 mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth); 753 } 754 } 755 }; 756 Animation heightAnimation = new Animation() { 757 @Override 758 protected void applyTransformation(float interpolatedTime, Transformation t) { 759 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); 760 setHeight(mContentContainer, startHeight + deltaHeight); 761 if (mOpenOverflowUpwards) { 762 mContentContainer.setY( 763 startY - (mContentContainer.getHeight() - startHeight)); 764 positionContentYCoordinatesIfOpeningOverflowUpwards(); 765 } 766 } 767 }; 768 final float overflowButtonStartX = mOverflowButton.getX(); 769 final float overflowButtonTargetX = isRTL() ? 770 overflowButtonStartX + targetWidth - mOverflowButton.getWidth() : 771 overflowButtonStartX - targetWidth + mOverflowButton.getWidth(); 772 Animation overflowButtonAnimation = new Animation() { 773 @Override 774 protected void applyTransformation(float interpolatedTime, Transformation t) { 775 float overflowButtonX = overflowButtonStartX 776 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX); 777 float deltaContainerWidth = isRTL() ? 778 0 : 779 mContentContainer.getWidth() - startWidth; 780 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth; 781 mOverflowButton.setX(actualOverflowButtonX); 782 } 783 }; 784 widthAnimation.setInterpolator(mLogAccelerateInterpolator); 785 widthAnimation.setDuration(getAdjustedDuration(250)); 786 heightAnimation.setInterpolator(mFastOutSlowInInterpolator); 787 heightAnimation.setDuration(getAdjustedDuration(250)); 788 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator); 789 overflowButtonAnimation.setDuration(getAdjustedDuration(250)); 790 mOpenOverflowAnimation.getAnimations().clear(); 791 mOpenOverflowAnimation.getAnimations().clear(); 792 mOpenOverflowAnimation.addAnimation(widthAnimation); 793 mOpenOverflowAnimation.addAnimation(heightAnimation); 794 mOpenOverflowAnimation.addAnimation(overflowButtonAnimation); 795 mContentContainer.startAnimation(mOpenOverflowAnimation); 796 mIsOverflowOpen = true; 797 mMainPanel.animate() 798 .alpha(0).withLayer() 799 .setInterpolator(mLinearOutSlowInInterpolator) 800 .setDuration(250) 801 .start(); 802 mOverflowPanel.setAlpha(1); // fadeIn in 0ms. 803 } 804 805 private void closeOverflow() { 806 final int targetWidth = mMainPanelSize.getWidth(); 807 final int startWidth = mContentContainer.getWidth(); 808 final float left = mContentContainer.getX(); 809 final float right = left + mContentContainer.getWidth(); 810 Animation widthAnimation = new Animation() { 811 @Override 812 protected void applyTransformation(float interpolatedTime, Transformation t) { 813 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth)); 814 setWidth(mContentContainer, startWidth + deltaWidth); 815 if (isRTL()) { 816 mContentContainer.setX(left); 817 818 // Lock the panels in place. 819 mMainPanel.setX(0); 820 mOverflowPanel.setX(0); 821 } else { 822 mContentContainer.setX(right - mContentContainer.getWidth()); 823 824 // Offset the panels' positions so they look like they're locked in place 825 // on the screen. 826 mMainPanel.setX(mContentContainer.getWidth() - targetWidth); 827 mOverflowPanel.setX(mContentContainer.getWidth() - startWidth); 828 } 829 } 830 }; 831 final int targetHeight = mMainPanelSize.getHeight(); 832 final int startHeight = mContentContainer.getHeight(); 833 final float bottom = mContentContainer.getY() + mContentContainer.getHeight(); 834 Animation heightAnimation = new Animation() { 835 @Override 836 protected void applyTransformation(float interpolatedTime, Transformation t) { 837 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight)); 838 setHeight(mContentContainer, startHeight + deltaHeight); 839 if (mOpenOverflowUpwards) { 840 mContentContainer.setY(bottom - mContentContainer.getHeight()); 841 positionContentYCoordinatesIfOpeningOverflowUpwards(); 842 } 843 } 844 }; 845 final float overflowButtonStartX = mOverflowButton.getX(); 846 final float overflowButtonTargetX = isRTL() ? 847 overflowButtonStartX - startWidth + mOverflowButton.getWidth() : 848 overflowButtonStartX + startWidth - mOverflowButton.getWidth(); 849 Animation overflowButtonAnimation = new Animation() { 850 @Override 851 protected void applyTransformation(float interpolatedTime, Transformation t) { 852 float overflowButtonX = overflowButtonStartX 853 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX); 854 float deltaContainerWidth = isRTL() ? 855 0 : 856 mContentContainer.getWidth() - startWidth; 857 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth; 858 mOverflowButton.setX(actualOverflowButtonX); 859 } 860 }; 861 widthAnimation.setInterpolator(mFastOutSlowInInterpolator); 862 widthAnimation.setDuration(getAdjustedDuration(250)); 863 heightAnimation.setInterpolator(mLogAccelerateInterpolator); 864 heightAnimation.setDuration(getAdjustedDuration(250)); 865 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator); 866 overflowButtonAnimation.setDuration(getAdjustedDuration(250)); 867 mCloseOverflowAnimation.getAnimations().clear(); 868 mCloseOverflowAnimation.addAnimation(widthAnimation); 869 mCloseOverflowAnimation.addAnimation(heightAnimation); 870 mCloseOverflowAnimation.addAnimation(overflowButtonAnimation); 871 mContentContainer.startAnimation(mCloseOverflowAnimation); 872 mIsOverflowOpen = false; 873 mMainPanel.animate() 874 .alpha(1).withLayer() 875 .setInterpolator(mFastOutLinearInInterpolator) 876 .setDuration(100) 877 .start(); 878 mOverflowPanel.animate() 879 .alpha(0).withLayer() 880 .setInterpolator(mLinearOutSlowInInterpolator) 881 .setDuration(150) 882 .start(); 883 } 884 885 /** 886 * Defines the position of the floating toolbar popup panels when transition animation has 887 * stopped. 888 */ 889 private void setPanelsStatesAtRestingPosition() { 890 mOverflowButton.setEnabled(true); 891 mOverflowPanel.awakenScrollBars(); 892 893 if (mIsOverflowOpen) { 894 // Set open state. 895 final Size containerSize = mOverflowPanelSize; 896 setSize(mContentContainer, containerSize); 897 mMainPanel.setAlpha(0); 898 mMainPanel.setVisibility(View.INVISIBLE); 899 mOverflowPanel.setAlpha(1); 900 mOverflowPanel.setVisibility(View.VISIBLE); 901 mOverflowButton.setImageDrawable(mArrow); 902 903 // Update x-coordinates depending on RTL state. 904 if (isRTL()) { 905 mContentContainer.setX(mMarginHorizontal); // align left 906 mMainPanel.setX(0); // align left 907 mOverflowButton.setX( // align right 908 containerSize.getWidth() - mOverflowButtonSize.getWidth()); 909 mOverflowPanel.setX(0); // align left 910 } else { 911 mContentContainer.setX( // align right 912 mPopupWindow.getWidth() - 913 containerSize.getWidth() - mMarginHorizontal); 914 mMainPanel.setX(-mContentContainer.getX()); // align right 915 mOverflowButton.setX(0); // align left 916 mOverflowPanel.setX(0); // align left 917 } 918 919 // Update y-coordinates depending on overflow's open direction. 920 if (mOpenOverflowUpwards) { 921 mContentContainer.setY(mMarginVertical); // align top 922 mMainPanel.setY( // align bottom 923 containerSize.getHeight() - mContentContainer.getHeight()); 924 mOverflowButton.setY( // align bottom 925 containerSize.getHeight() - mOverflowButtonSize.getHeight()); 926 mOverflowPanel.setY(0); // align top 927 } else { 928 // opens downwards. 929 mContentContainer.setY(mMarginVertical); // align top 930 mMainPanel.setY(0); // align top 931 mOverflowButton.setY(0); // align top 932 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom 933 } 934 } else { 935 // Overflow not open. Set closed state. 936 final Size containerSize = mMainPanelSize; 937 setSize(mContentContainer, containerSize); 938 mMainPanel.setAlpha(1); 939 mMainPanel.setVisibility(View.VISIBLE); 940 mOverflowPanel.setAlpha(0); 941 mOverflowPanel.setVisibility(View.INVISIBLE); 942 mOverflowButton.setImageDrawable(mOverflow); 943 944 if (hasOverflow()) { 945 // Update x-coordinates depending on RTL state. 946 if (isRTL()) { 947 mContentContainer.setX(mMarginHorizontal); // align left 948 mMainPanel.setX(0); // align left 949 mOverflowButton.setX(0); // align left 950 mOverflowPanel.setX(0); // align left 951 } else { 952 mContentContainer.setX( // align right 953 mPopupWindow.getWidth() - 954 containerSize.getWidth() - mMarginHorizontal); 955 mMainPanel.setX(0); // align left 956 mOverflowButton.setX( // align right 957 containerSize.getWidth() - mOverflowButtonSize.getWidth()); 958 mOverflowPanel.setX( // align right 959 containerSize.getWidth() - mOverflowPanelSize.getWidth()); 960 } 961 962 // Update y-coordinates depending on overflow's open direction. 963 if (mOpenOverflowUpwards) { 964 mContentContainer.setY( // align bottom 965 mMarginVertical + 966 mOverflowPanelSize.getHeight() - containerSize.getHeight()); 967 mMainPanel.setY(0); // align top 968 mOverflowButton.setY(0); // align top 969 mOverflowPanel.setY( // align bottom 970 containerSize.getHeight() - mOverflowPanelSize.getHeight()); 971 } else { 972 // opens downwards. 973 mContentContainer.setY(mMarginVertical); // align top 974 mMainPanel.setY(0); // align top 975 mOverflowButton.setY(0); // align top 976 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom 977 } 978 } else { 979 // No overflow. 980 mContentContainer.setX(mMarginHorizontal); // align left 981 mContentContainer.setY(mMarginVertical); // align top 982 mMainPanel.setX(0); // align left 983 mMainPanel.setY(0); // align top 984 } 985 } 986 } 987 988 private void updateOverflowHeight(int suggestedHeight) { 989 if (hasOverflow()) { 990 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) / 991 getLineHeight(mContext); 992 final int newHeight = calculateOverflowHeight(maxItemSize); 993 if (mOverflowPanelSize.getHeight() != newHeight) { 994 mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight); 995 } 996 setSize(mOverflowPanel, mOverflowPanelSize); 997 if (mIsOverflowOpen) { 998 setSize(mContentContainer, mOverflowPanelSize); 999 if (mOpenOverflowUpwards) { 1000 final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight; 1001 mContentContainer.setY(mContentContainer.getY() + deltaHeight); 1002 mOverflowButton.setY(mOverflowButton.getY() - deltaHeight); 1003 } 1004 } else { 1005 setSize(mContentContainer, mMainPanelSize); 1006 } 1007 updatePopupSize(); 1008 } 1009 } 1010 1011 private void updatePopupSize() { 1012 int width = 0; 1013 int height = 0; 1014 if (mMainPanelSize != null) { 1015 width = Math.max(width, mMainPanelSize.getWidth()); 1016 height = Math.max(height, mMainPanelSize.getHeight()); 1017 } 1018 if (mOverflowPanelSize != null) { 1019 width = Math.max(width, mOverflowPanelSize.getWidth()); 1020 height = Math.max(height, mOverflowPanelSize.getHeight()); 1021 } 1022 mPopupWindow.setWidth(width + mMarginHorizontal * 2); 1023 mPopupWindow.setHeight(height + mMarginVertical * 2); 1024 maybeComputeTransitionDurationScale(); 1025 } 1026 1027 private void refreshViewPort() { 1028 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen); 1029 } 1030 1031 private int getAdjustedToolbarWidth(int suggestedWidth) { 1032 int width = suggestedWidth; 1033 refreshViewPort(); 1034 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources() 1035 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); 1036 if (width <= 0) { 1037 width = mParent.getResources() 1038 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width); 1039 } 1040 return Math.min(width, maximumWidth); 1041 } 1042 1043 /** 1044 * Sets the touchable region of this popup to be zero. This means that all touch events on 1045 * this popup will go through to the surface behind it. 1046 */ 1047 private void setZeroTouchableSurface() { 1048 mTouchableRegion.setEmpty(); 1049 } 1050 1051 /** 1052 * Sets the touchable region of this popup to be the area occupied by its content. 1053 */ 1054 private void setContentAreaAsTouchableSurface() { 1055 Preconditions.checkNotNull(mMainPanelSize); 1056 final int width; 1057 final int height; 1058 if (mIsOverflowOpen) { 1059 Preconditions.checkNotNull(mOverflowPanelSize); 1060 width = mOverflowPanelSize.getWidth(); 1061 height = mOverflowPanelSize.getHeight(); 1062 } else { 1063 width = mMainPanelSize.getWidth(); 1064 height = mMainPanelSize.getHeight(); 1065 } 1066 mTouchableRegion.set( 1067 (int) mContentContainer.getX(), 1068 (int) mContentContainer.getY(), 1069 (int) mContentContainer.getX() + width, 1070 (int) mContentContainer.getY() + height); 1071 } 1072 1073 /** 1074 * Make the touchable area of this popup be the area specified by mTouchableRegion. 1075 * This should be called after the popup window has been dismissed (dismiss/hide) 1076 * and is probably being re-shown with a new content root view. 1077 */ 1078 private void setTouchableSurfaceInsetsComputer() { 1079 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView() 1080 .getRootView() 1081 .getViewTreeObserver(); 1082 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer); 1083 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer); 1084 } 1085 1086 private boolean isRTL() { 1087 return mContext.getResources().getConfiguration().getLayoutDirection() 1088 == View.LAYOUT_DIRECTION_RTL; 1089 } 1090 1091 private boolean hasOverflow() { 1092 return mOverflowPanelSize != null; 1093 } 1094 1095 /** 1096 * Fits as many menu items in the main panel and returns a list of the menu items that 1097 * were not fit in. 1098 * 1099 * @return The menu items that are not included in this main panel. 1100 */ 1101 public List<MenuItem> layoutMainPanelItems( 1102 List<MenuItem> menuItems, final int toolbarWidth) { 1103 Preconditions.checkNotNull(menuItems); 1104 1105 int availableWidth = toolbarWidth; 1106 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems); 1107 1108 mMainPanel.removeAllViews(); 1109 mMainPanel.setPaddingRelative(0, 0, 0, 0); 1110 1111 boolean isFirstItem = true; 1112 while (!remainingMenuItems.isEmpty()) { 1113 final MenuItem menuItem = remainingMenuItems.peek(); 1114 View menuItemButton = createMenuItemButton(mContext, menuItem); 1115 1116 // Adding additional start padding for the first button to even out button spacing. 1117 if (isFirstItem) { 1118 menuItemButton.setPaddingRelative( 1119 (int) (1.5 * menuItemButton.getPaddingStart()), 1120 menuItemButton.getPaddingTop(), 1121 menuItemButton.getPaddingEnd(), 1122 menuItemButton.getPaddingBottom()); 1123 isFirstItem = false; 1124 } 1125 1126 // Adding additional end padding for the last button to even out button spacing. 1127 if (remainingMenuItems.size() == 1) { 1128 menuItemButton.setPaddingRelative( 1129 menuItemButton.getPaddingStart(), 1130 menuItemButton.getPaddingTop(), 1131 (int) (1.5 * menuItemButton.getPaddingEnd()), 1132 menuItemButton.getPaddingBottom()); 1133 } 1134 1135 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1136 int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); 1137 // Check if we can fit an item while reserving space for the overflowButton. 1138 boolean canFitWithOverflow = 1139 menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth(); 1140 boolean canFitNoOverflow = 1141 remainingMenuItems.size() == 1 && menuItemButtonWidth <= availableWidth; 1142 if (canFitWithOverflow || canFitNoOverflow) { 1143 setButtonTagAndClickListener(menuItemButton, menuItem); 1144 mMainPanel.addView(menuItemButton); 1145 ViewGroup.LayoutParams params = menuItemButton.getLayoutParams(); 1146 params.width = menuItemButtonWidth; 1147 menuItemButton.setLayoutParams(params); 1148 availableWidth -= menuItemButtonWidth; 1149 remainingMenuItems.pop(); 1150 } else { 1151 // Reserve space for overflowButton. 1152 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0); 1153 break; 1154 } 1155 } 1156 mMainPanelSize = measure(mMainPanel); 1157 return remainingMenuItems; 1158 } 1159 1160 private void layoutOverflowPanelItems(List<MenuItem> menuItems) { 1161 ArrayAdapter<MenuItem> overflowPanelAdapter = 1162 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter(); 1163 overflowPanelAdapter.clear(); 1164 final int size = menuItems.size(); 1165 for (int i = 0; i < size; i++) { 1166 overflowPanelAdapter.add(menuItems.get(i)); 1167 } 1168 mOverflowPanel.setAdapter(overflowPanelAdapter); 1169 if (mOpenOverflowUpwards) { 1170 mOverflowPanel.setY(0); 1171 } else { 1172 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); 1173 } 1174 1175 int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth()); 1176 int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE); 1177 mOverflowPanelSize = new Size(width, height); 1178 setSize(mOverflowPanel, mOverflowPanelSize); 1179 } 1180 1181 /** 1182 * Resets the content container and appropriately position it's panels. 1183 */ 1184 private void preparePopupContent() { 1185 mContentContainer.removeAllViews(); 1186 1187 // Add views in the specified order so they stack up as expected. 1188 // Order: overflowPanel, mainPanel, overflowButton. 1189 if (hasOverflow()) { 1190 mContentContainer.addView(mOverflowPanel); 1191 } 1192 mContentContainer.addView(mMainPanel); 1193 if (hasOverflow()) { 1194 mContentContainer.addView(mOverflowButton); 1195 } 1196 setPanelsStatesAtRestingPosition(); 1197 setContentAreaAsTouchableSurface(); 1198 1199 // The positioning of contents in RTL is wrong when the view is first rendered. 1200 // Hide the view and post a runnable to recalculate positions and render the view. 1201 // TODO: Investigate why this happens and fix. 1202 if (isRTL()) { 1203 mContentContainer.setAlpha(0); 1204 mContentContainer.post(mPreparePopupContentRTLHelper); 1205 } 1206 } 1207 1208 /** 1209 * Clears out the panels and their container. Resets their calculated sizes. 1210 */ 1211 private void clearPanels() { 1212 mOverflowPanelSize = null; 1213 mMainPanelSize = null; 1214 mIsOverflowOpen = false; 1215 mMainPanel.removeAllViews(); 1216 ArrayAdapter<MenuItem> overflowPanelAdapter = 1217 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter(); 1218 overflowPanelAdapter.clear(); 1219 mOverflowPanel.setAdapter(overflowPanelAdapter); 1220 mContentContainer.removeAllViews(); 1221 } 1222 1223 private void positionContentYCoordinatesIfOpeningOverflowUpwards() { 1224 if (mOpenOverflowUpwards) { 1225 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight()); 1226 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight()); 1227 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight()); 1228 } 1229 } 1230 1231 private int getOverflowWidth() { 1232 int overflowWidth = 0; 1233 final int count = mOverflowPanel.getAdapter().getCount(); 1234 for (int i = 0; i < count; i++) { 1235 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i); 1236 overflowWidth = 1237 Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth); 1238 } 1239 return overflowWidth; 1240 } 1241 1242 private int calculateOverflowHeight(int maxItemSize) { 1243 // Maximum of 4 items, minimum of 2 if the overflow has to scroll. 1244 int actualSize = Math.min( 1245 MAX_OVERFLOW_SIZE, 1246 Math.min( 1247 Math.max(MIN_OVERFLOW_SIZE, maxItemSize), 1248 mOverflowPanel.getCount())); 1249 int extension = 0; 1250 if (actualSize < mOverflowPanel.getCount()) { 1251 // The overflow will require scrolling to get to all the items. 1252 // Extend the height so that part of the hidden items is displayed. 1253 extension = (int) (getLineHeight(mContext) * 0.5f); 1254 } 1255 return actualSize * getLineHeight(mContext) 1256 + mOverflowButtonSize.getHeight() 1257 + extension; 1258 } 1259 1260 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) { 1261 View button = menuItemButton; 1262 if (isIconOnlyMenuItem(menuItem)) { 1263 button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button); 1264 } 1265 button.setTag(menuItem); 1266 button.setOnClickListener(mMenuItemButtonOnClickListener); 1267 } 1268 1269 /** 1270 * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.* 1271 * animations. See comment about this in the code. 1272 */ 1273 private int getAdjustedDuration(int originalDuration) { 1274 if (mTransitionDurationScale < 150) { 1275 // For smaller transition, decrease the time. 1276 return Math.max(originalDuration - 50, 0); 1277 } else if (mTransitionDurationScale > 300) { 1278 // For bigger transition, increase the time. 1279 return originalDuration + 50; 1280 } 1281 1282 // Scale the animation duration with getDurationScale(). This allows 1283 // android.view.animation.* animations to scale just like android.animation.* animations 1284 // when animator duration scale is adjusted in "Developer Options". 1285 // For this reason, do not use this method for android.animation.* animations. 1286 return (int) (originalDuration * ValueAnimator.getDurationScale()); 1287 } 1288 1289 private void maybeComputeTransitionDurationScale() { 1290 if (mMainPanelSize != null && mOverflowPanelSize != null) { 1291 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth(); 1292 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight(); 1293 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) / 1294 mContentContainer.getContext().getResources().getDisplayMetrics().density); 1295 } 1296 } 1297 1298 private ViewGroup createMainPanel() { 1299 ViewGroup mainPanel = new LinearLayout(mContext) { 1300 @Override 1301 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1302 if (isOverflowAnimating()) { 1303 // Update widthMeasureSpec to make sure that this view is not clipped 1304 // as we offset it's coordinates with respect to it's parent. 1305 widthMeasureSpec = MeasureSpec.makeMeasureSpec( 1306 mMainPanelSize.getWidth(), 1307 MeasureSpec.EXACTLY); 1308 } 1309 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1310 } 1311 1312 @Override 1313 public boolean onInterceptTouchEvent(MotionEvent ev) { 1314 // Intercept the touch event while the overflow is animating. 1315 return isOverflowAnimating(); 1316 } 1317 }; 1318 return mainPanel; 1319 } 1320 1321 private ImageButton createOverflowButton() { 1322 final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext) 1323 .inflate(R.layout.floating_popup_overflow_button, null); 1324 overflowButton.setImageDrawable(mOverflow); 1325 overflowButton.setOnClickListener(new View.OnClickListener() { 1326 @Override 1327 public void onClick(View v) { 1328 if (mIsOverflowOpen) { 1329 overflowButton.setImageDrawable(mToOverflow); 1330 mToOverflow.start(); 1331 closeOverflow(); 1332 } else { 1333 overflowButton.setImageDrawable(mToArrow); 1334 mToArrow.start(); 1335 openOverflow(); 1336 } 1337 } 1338 }); 1339 return overflowButton; 1340 } 1341 1342 private OverflowPanel createOverflowPanel() { 1343 final OverflowPanel overflowPanel = new OverflowPanel(this); 1344 overflowPanel.setLayoutParams(new ViewGroup.LayoutParams( 1345 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 1346 overflowPanel.setDivider(null); 1347 overflowPanel.setDividerHeight(0); 1348 1349 final ArrayAdapter adapter = 1350 new ArrayAdapter<MenuItem>(mContext, 0) { 1351 @Override 1352 public int getViewTypeCount() { 1353 return mOverflowPanelViewHelper.getViewTypeCount(); 1354 } 1355 1356 @Override 1357 public int getItemViewType(int position) { 1358 return mOverflowPanelViewHelper.getItemViewType(getItem(position)); 1359 } 1360 1361 @Override 1362 public View getView(int position, View convertView, ViewGroup parent) { 1363 return mOverflowPanelViewHelper.getView( 1364 getItem(position), mOverflowPanelSize.getWidth(), convertView); 1365 } 1366 }; 1367 overflowPanel.setAdapter(adapter); 1368 1369 overflowPanel.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1370 @Override 1371 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1372 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position); 1373 if (mOnMenuItemClickListener != null) { 1374 mOnMenuItemClickListener.onMenuItemClick(menuItem); 1375 } 1376 } 1377 }); 1378 1379 return overflowPanel; 1380 } 1381 1382 private boolean isOverflowAnimating() { 1383 final boolean overflowOpening = mOpenOverflowAnimation.hasStarted() 1384 && !mOpenOverflowAnimation.hasEnded(); 1385 final boolean overflowClosing = mCloseOverflowAnimation.hasStarted() 1386 && !mCloseOverflowAnimation.hasEnded(); 1387 return overflowOpening || overflowClosing; 1388 } 1389 1390 private Animation.AnimationListener createOverflowAnimationListener() { 1391 Animation.AnimationListener listener = new Animation.AnimationListener() { 1392 @Override 1393 public void onAnimationStart(Animation animation) { 1394 // Disable the overflow button while it's animating. 1395 // It will be re-enabled when the animation stops. 1396 mOverflowButton.setEnabled(false); 1397 // Ensure both panels have visibility turned on when the overflow animation 1398 // starts. 1399 mMainPanel.setVisibility(View.VISIBLE); 1400 mOverflowPanel.setVisibility(View.VISIBLE); 1401 } 1402 1403 @Override 1404 public void onAnimationEnd(Animation animation) { 1405 // Posting this because it seems like this is called before the animation 1406 // actually ends. 1407 mContentContainer.post(new Runnable() { 1408 @Override 1409 public void run() { 1410 setPanelsStatesAtRestingPosition(); 1411 setContentAreaAsTouchableSurface(); 1412 } 1413 }); 1414 } 1415 1416 @Override 1417 public void onAnimationRepeat(Animation animation) { 1418 } 1419 }; 1420 return listener; 1421 } 1422 1423 private static Size measure(View view) { 1424 Preconditions.checkState(view.getParent() == null); 1425 view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1426 return new Size(view.getMeasuredWidth(), view.getMeasuredHeight()); 1427 } 1428 1429 private static void setSize(View view, int width, int height) { 1430 view.setMinimumWidth(width); 1431 view.setMinimumHeight(height); 1432 ViewGroup.LayoutParams params = view.getLayoutParams(); 1433 params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params; 1434 params.width = width; 1435 params.height = height; 1436 view.setLayoutParams(params); 1437 } 1438 1439 private static void setSize(View view, Size size) { 1440 setSize(view, size.getWidth(), size.getHeight()); 1441 } 1442 1443 private static void setWidth(View view, int width) { 1444 ViewGroup.LayoutParams params = view.getLayoutParams(); 1445 setSize(view, width, params.height); 1446 } 1447 1448 private static void setHeight(View view, int height) { 1449 ViewGroup.LayoutParams params = view.getLayoutParams(); 1450 setSize(view, params.width, height); 1451 } 1452 1453 private static int getLineHeight(Context context) { 1454 return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height); 1455 } 1456 1457 /** 1458 * A custom ListView for the overflow panel. 1459 */ 1460 private static final class OverflowPanel extends ListView { 1461 1462 private final FloatingToolbarPopup mPopup; 1463 1464 OverflowPanel(FloatingToolbarPopup popup) { 1465 super(Preconditions.checkNotNull(popup).mContext); 1466 this.mPopup = popup; 1467 setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3); 1468 setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); 1469 } 1470 1471 @Override 1472 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1473 // Update heightMeasureSpec to make sure that this view is not clipped 1474 // as we offset it's coordinates with respect to it's parent. 1475 int height = mPopup.mOverflowPanelSize.getHeight() 1476 - mPopup.mOverflowButtonSize.getHeight(); 1477 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 1478 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1479 } 1480 1481 @Override 1482 public boolean dispatchTouchEvent(MotionEvent ev) { 1483 if (mPopup.isOverflowAnimating()) { 1484 // Eat the touch event. 1485 return true; 1486 } 1487 return super.dispatchTouchEvent(ev); 1488 } 1489 1490 @Override 1491 protected boolean awakenScrollBars() { 1492 return super.awakenScrollBars(); 1493 } 1494 } 1495 1496 /** 1497 * A custom interpolator used for various floating toolbar animations. 1498 */ 1499 private static final class LogAccelerateInterpolator implements Interpolator { 1500 1501 private static final int BASE = 100; 1502 private static final float LOGS_SCALE = 1f / computeLog(1, BASE); 1503 1504 private static float computeLog(float t, int base) { 1505 return (float) (1 - Math.pow(base, -t)); 1506 } 1507 1508 @Override 1509 public float getInterpolation(float t) { 1510 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE; 1511 } 1512 } 1513 1514 /** 1515 * A helper for generating views for the overflow panel. 1516 */ 1517 private static final class OverflowPanelViewHelper { 1518 1519 private static final int NUM_OF_VIEW_TYPES = 2; 1520 private static final int VIEW_TYPE_STRING_TITLE = 0; 1521 private static final int VIEW_TYPE_ICON_ONLY = 1; 1522 1523 private final TextView mStringTitleViewCalculator; 1524 private final View mIconOnlyViewCalculator; 1525 1526 private final Context mContext; 1527 1528 public OverflowPanelViewHelper(Context context) { 1529 mContext = Preconditions.checkNotNull(context); 1530 mStringTitleViewCalculator = getStringTitleView(null, 0, null); 1531 mIconOnlyViewCalculator = getIconOnlyView(null, 0, null); 1532 } 1533 1534 public int getViewTypeCount() { 1535 return NUM_OF_VIEW_TYPES; 1536 } 1537 1538 public View getView(MenuItem menuItem, int minimumWidth, View convertView) { 1539 Preconditions.checkNotNull(menuItem); 1540 if (getItemViewType(menuItem) == VIEW_TYPE_ICON_ONLY) { 1541 return getIconOnlyView(menuItem, minimumWidth, convertView); 1542 } 1543 return getStringTitleView(menuItem, minimumWidth, convertView); 1544 } 1545 1546 public int getItemViewType(MenuItem menuItem) { 1547 Preconditions.checkNotNull(menuItem); 1548 if (isIconOnlyMenuItem(menuItem)) { 1549 return VIEW_TYPE_ICON_ONLY; 1550 } 1551 return VIEW_TYPE_STRING_TITLE; 1552 } 1553 1554 public int calculateWidth(MenuItem menuItem) { 1555 final View calculator; 1556 if (isIconOnlyMenuItem(menuItem)) { 1557 ((ImageView) mIconOnlyViewCalculator 1558 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1559 .setImageDrawable(menuItem.getIcon()); 1560 calculator = mIconOnlyViewCalculator; 1561 } else { 1562 mStringTitleViewCalculator.setText(menuItem.getTitle()); 1563 calculator = mStringTitleViewCalculator; 1564 } 1565 calculator.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 1566 return calculator.getMeasuredWidth(); 1567 } 1568 1569 private TextView getStringTitleView( 1570 MenuItem menuItem, int minimumWidth, View convertView) { 1571 TextView menuButton; 1572 if (convertView != null) { 1573 menuButton = (TextView) convertView; 1574 } else { 1575 menuButton = (TextView) LayoutInflater.from(mContext) 1576 .inflate(R.layout.floating_popup_overflow_list_item, null); 1577 menuButton.setLayoutParams(new ViewGroup.LayoutParams( 1578 ViewGroup.LayoutParams.MATCH_PARENT, 1579 ViewGroup.LayoutParams.WRAP_CONTENT)); 1580 } 1581 if (menuItem != null) { 1582 menuButton.setText(menuItem.getTitle()); 1583 menuButton.setContentDescription(menuItem.getTitle()); 1584 menuButton.setMinimumWidth(minimumWidth); 1585 } 1586 return menuButton; 1587 } 1588 1589 private View getIconOnlyView( 1590 MenuItem menuItem, int minimumWidth, View convertView) { 1591 View menuButton; 1592 if (convertView != null) { 1593 menuButton = convertView; 1594 } else { 1595 menuButton = LayoutInflater.from(mContext).inflate( 1596 R.layout.floating_popup_overflow_image_list_item, null); 1597 menuButton.setLayoutParams(new ViewGroup.LayoutParams( 1598 ViewGroup.LayoutParams.WRAP_CONTENT, 1599 ViewGroup.LayoutParams.WRAP_CONTENT)); 1600 } 1601 if (menuItem != null) { 1602 ((ImageView) menuButton 1603 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1604 .setImageDrawable(menuItem.getIcon()); 1605 menuButton.setMinimumWidth(minimumWidth); 1606 } 1607 return menuButton; 1608 } 1609 } 1610 } 1611 1612 /** 1613 * @return {@code true} if the menu item does not not have a string title but has an icon. 1614 * {@code false} otherwise. 1615 */ 1616 private static boolean isIconOnlyMenuItem(MenuItem menuItem) { 1617 if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) { 1618 return true; 1619 } 1620 return false; 1621 } 1622 1623 /** 1624 * Creates and returns a menu button for the specified menu item. 1625 */ 1626 private static View createMenuItemButton(Context context, MenuItem menuItem) { 1627 if (isIconOnlyMenuItem(menuItem)) { 1628 View imageMenuItemButton = LayoutInflater.from(context) 1629 .inflate(R.layout.floating_popup_menu_image_button, null); 1630 ((ImageButton) imageMenuItemButton 1631 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1632 .setImageDrawable(menuItem.getIcon()); 1633 return imageMenuItemButton; 1634 } 1635 1636 Button menuItemButton = (Button) LayoutInflater.from(context) 1637 .inflate(R.layout.floating_popup_menu_button, null); 1638 menuItemButton.setText(menuItem.getTitle()); 1639 menuItemButton.setContentDescription(menuItem.getTitle()); 1640 return menuItemButton; 1641 } 1642 1643 private static ViewGroup createContentContainer(Context context) { 1644 ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context) 1645 .inflate(R.layout.floating_popup_container, null); 1646 contentContainer.setLayoutParams(new ViewGroup.LayoutParams( 1647 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 1648 contentContainer.setTag(FLOATING_TOOLBAR_TAG); 1649 return contentContainer; 1650 } 1651 1652 private static PopupWindow createPopupWindow(ViewGroup content) { 1653 ViewGroup popupContentHolder = new LinearLayout(content.getContext()); 1654 PopupWindow popupWindow = new PopupWindow(popupContentHolder); 1655 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false) 1656 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects. 1657 popupWindow.setClippingEnabled(false); 1658 popupWindow.setWindowLayoutType( 1659 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); 1660 popupWindow.setAnimationStyle(0); 1661 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 1662 content.setLayoutParams(new ViewGroup.LayoutParams( 1663 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 1664 popupContentHolder.addView(content); 1665 return popupWindow; 1666 } 1667 1668 /** 1669 * Creates an "appear" animation for the specified view. 1670 * 1671 * @param view The view to animate 1672 */ 1673 private static AnimatorSet createEnterAnimation(View view) { 1674 AnimatorSet animation = new AnimatorSet(); 1675 animation.playTogether( 1676 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150)); 1677 return animation; 1678 } 1679 1680 /** 1681 * Creates a "disappear" animation for the specified view. 1682 * 1683 * @param view The view to animate 1684 * @param startDelay The start delay of the animation 1685 * @param listener The animation listener 1686 */ 1687 private static AnimatorSet createExitAnimation( 1688 View view, int startDelay, Animator.AnimatorListener listener) { 1689 AnimatorSet animation = new AnimatorSet(); 1690 animation.playTogether( 1691 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100)); 1692 animation.setStartDelay(startDelay); 1693 animation.addListener(listener); 1694 return animation; 1695 } 1696 1697 /** 1698 * Returns a re-themed context with controlled look and feel for views. 1699 */ 1700 private static Context applyDefaultTheme(Context originalContext) { 1701 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme}); 1702 boolean isLightTheme = a.getBoolean(0, true); 1703 int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material; 1704 a.recycle(); 1705 return new ContextThemeWrapper(originalContext, themeId); 1706 } 1707} 1708