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