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 17 package com.android.internal.widget; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.ValueAnimator; 24 import android.content.Context; 25 import android.content.res.TypedArray; 26 import android.graphics.Color; 27 import android.graphics.Point; 28 import android.graphics.Rect; 29 import android.graphics.Region; 30 import android.graphics.drawable.AnimatedVectorDrawable; 31 import android.graphics.drawable.ColorDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.text.TextUtils; 34 import android.util.Size; 35 import android.view.ContextThemeWrapper; 36 import android.view.Gravity; 37 import android.view.LayoutInflater; 38 import android.view.Menu; 39 import android.view.MenuItem; 40 import android.view.MotionEvent; 41 import android.view.View; 42 import android.view.View.MeasureSpec; 43 import android.view.View.OnLayoutChangeListener; 44 import android.view.ViewConfiguration; 45 import android.view.ViewGroup; 46 import android.view.ViewTreeObserver; 47 import android.view.Window; 48 import android.view.WindowManager; 49 import android.view.animation.Animation; 50 import android.view.animation.AnimationSet; 51 import android.view.animation.Transformation; 52 import android.view.animation.AnimationUtils; 53 import android.view.animation.Interpolator; 54 import android.widget.AdapterView; 55 import android.widget.ArrayAdapter; 56 import android.widget.Button; 57 import android.widget.ImageButton; 58 import android.widget.ImageView; 59 import android.widget.LinearLayout; 60 import android.widget.ListView; 61 import android.widget.PopupWindow; 62 import android.widget.TextView; 63 64 import java.util.ArrayList; 65 import java.util.LinkedList; 66 import java.util.List; 67 68 import com.android.internal.R; 69 import 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 */ 77 public 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 (isInRTLMode()) { 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 = isInRTLMode() ? 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 = isInRTLMode() ? 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 (isInRTLMode()) { 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 = isInRTLMode() ? 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 = isInRTLMode() ? 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 mOverflowButton.setContentDescription(mContext.getString( 903 R.string.floating_toolbar_close_overflow_description)); 904 905 // Update x-coordinates depending on RTL state. 906 if (isInRTLMode()) { 907 mContentContainer.setX(mMarginHorizontal); // align left 908 mMainPanel.setX(0); // align left 909 mOverflowButton.setX( // align right 910 containerSize.getWidth() - mOverflowButtonSize.getWidth()); 911 mOverflowPanel.setX(0); // align left 912 } else { 913 mContentContainer.setX( // align right 914 mPopupWindow.getWidth() - 915 containerSize.getWidth() - mMarginHorizontal); 916 mMainPanel.setX(-mContentContainer.getX()); // align right 917 mOverflowButton.setX(0); // align left 918 mOverflowPanel.setX(0); // align left 919 } 920 921 // Update y-coordinates depending on overflow's open direction. 922 if (mOpenOverflowUpwards) { 923 mContentContainer.setY(mMarginVertical); // align top 924 mMainPanel.setY( // align bottom 925 containerSize.getHeight() - mContentContainer.getHeight()); 926 mOverflowButton.setY( // align bottom 927 containerSize.getHeight() - mOverflowButtonSize.getHeight()); 928 mOverflowPanel.setY(0); // align top 929 } else { 930 // opens downwards. 931 mContentContainer.setY(mMarginVertical); // align top 932 mMainPanel.setY(0); // align top 933 mOverflowButton.setY(0); // align top 934 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom 935 } 936 } else { 937 // Overflow not open. Set closed state. 938 final Size containerSize = mMainPanelSize; 939 setSize(mContentContainer, containerSize); 940 mMainPanel.setAlpha(1); 941 mMainPanel.setVisibility(View.VISIBLE); 942 mOverflowPanel.setAlpha(0); 943 mOverflowPanel.setVisibility(View.INVISIBLE); 944 mOverflowButton.setImageDrawable(mOverflow); 945 mOverflowButton.setContentDescription(mContext.getString( 946 R.string.floating_toolbar_open_overflow_description)); 947 948 if (hasOverflow()) { 949 // Update x-coordinates depending on RTL state. 950 if (isInRTLMode()) { 951 mContentContainer.setX(mMarginHorizontal); // align left 952 mMainPanel.setX(0); // align left 953 mOverflowButton.setX(0); // align left 954 mOverflowPanel.setX(0); // align left 955 } else { 956 mContentContainer.setX( // align right 957 mPopupWindow.getWidth() - 958 containerSize.getWidth() - mMarginHorizontal); 959 mMainPanel.setX(0); // align left 960 mOverflowButton.setX( // align right 961 containerSize.getWidth() - mOverflowButtonSize.getWidth()); 962 mOverflowPanel.setX( // align right 963 containerSize.getWidth() - mOverflowPanelSize.getWidth()); 964 } 965 966 // Update y-coordinates depending on overflow's open direction. 967 if (mOpenOverflowUpwards) { 968 mContentContainer.setY( // align bottom 969 mMarginVertical + 970 mOverflowPanelSize.getHeight() - containerSize.getHeight()); 971 mMainPanel.setY(0); // align top 972 mOverflowButton.setY(0); // align top 973 mOverflowPanel.setY( // align bottom 974 containerSize.getHeight() - mOverflowPanelSize.getHeight()); 975 } else { 976 // opens downwards. 977 mContentContainer.setY(mMarginVertical); // align top 978 mMainPanel.setY(0); // align top 979 mOverflowButton.setY(0); // align top 980 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom 981 } 982 } else { 983 // No overflow. 984 mContentContainer.setX(mMarginHorizontal); // align left 985 mContentContainer.setY(mMarginVertical); // align top 986 mMainPanel.setX(0); // align left 987 mMainPanel.setY(0); // align top 988 } 989 } 990 } 991 992 private void updateOverflowHeight(int suggestedHeight) { 993 if (hasOverflow()) { 994 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) / 995 getLineHeight(mContext); 996 final int newHeight = calculateOverflowHeight(maxItemSize); 997 if (mOverflowPanelSize.getHeight() != newHeight) { 998 mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight); 999 } 1000 setSize(mOverflowPanel, mOverflowPanelSize); 1001 if (mIsOverflowOpen) { 1002 setSize(mContentContainer, mOverflowPanelSize); 1003 if (mOpenOverflowUpwards) { 1004 final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight; 1005 mContentContainer.setY(mContentContainer.getY() + deltaHeight); 1006 mOverflowButton.setY(mOverflowButton.getY() - deltaHeight); 1007 } 1008 } else { 1009 setSize(mContentContainer, mMainPanelSize); 1010 } 1011 updatePopupSize(); 1012 } 1013 } 1014 1015 private void updatePopupSize() { 1016 int width = 0; 1017 int height = 0; 1018 if (mMainPanelSize != null) { 1019 width = Math.max(width, mMainPanelSize.getWidth()); 1020 height = Math.max(height, mMainPanelSize.getHeight()); 1021 } 1022 if (mOverflowPanelSize != null) { 1023 width = Math.max(width, mOverflowPanelSize.getWidth()); 1024 height = Math.max(height, mOverflowPanelSize.getHeight()); 1025 } 1026 mPopupWindow.setWidth(width + mMarginHorizontal * 2); 1027 mPopupWindow.setHeight(height + mMarginVertical * 2); 1028 maybeComputeTransitionDurationScale(); 1029 } 1030 1031 private void refreshViewPort() { 1032 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen); 1033 } 1034 1035 private int getAdjustedToolbarWidth(int suggestedWidth) { 1036 int width = suggestedWidth; 1037 refreshViewPort(); 1038 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources() 1039 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin); 1040 if (width <= 0) { 1041 width = mParent.getResources() 1042 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width); 1043 } 1044 return Math.min(width, maximumWidth); 1045 } 1046 1047 /** 1048 * Sets the touchable region of this popup to be zero. This means that all touch events on 1049 * this popup will go through to the surface behind it. 1050 */ 1051 private void setZeroTouchableSurface() { 1052 mTouchableRegion.setEmpty(); 1053 } 1054 1055 /** 1056 * Sets the touchable region of this popup to be the area occupied by its content. 1057 */ 1058 private void setContentAreaAsTouchableSurface() { 1059 Preconditions.checkNotNull(mMainPanelSize); 1060 final int width; 1061 final int height; 1062 if (mIsOverflowOpen) { 1063 Preconditions.checkNotNull(mOverflowPanelSize); 1064 width = mOverflowPanelSize.getWidth(); 1065 height = mOverflowPanelSize.getHeight(); 1066 } else { 1067 width = mMainPanelSize.getWidth(); 1068 height = mMainPanelSize.getHeight(); 1069 } 1070 mTouchableRegion.set( 1071 (int) mContentContainer.getX(), 1072 (int) mContentContainer.getY(), 1073 (int) mContentContainer.getX() + width, 1074 (int) mContentContainer.getY() + height); 1075 } 1076 1077 /** 1078 * Make the touchable area of this popup be the area specified by mTouchableRegion. 1079 * This should be called after the popup window has been dismissed (dismiss/hide) 1080 * and is probably being re-shown with a new content root view. 1081 */ 1082 private void setTouchableSurfaceInsetsComputer() { 1083 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView() 1084 .getRootView() 1085 .getViewTreeObserver(); 1086 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer); 1087 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer); 1088 } 1089 1090 private boolean isInRTLMode() { 1091 return mContext.getApplicationInfo().hasRtlSupport() 1092 && mContext.getResources().getConfiguration().getLayoutDirection() 1093 == View.LAYOUT_DIRECTION_RTL; 1094 } 1095 1096 private boolean hasOverflow() { 1097 return mOverflowPanelSize != null; 1098 } 1099 1100 /** 1101 * Fits as many menu items in the main panel and returns a list of the menu items that 1102 * were not fit in. 1103 * 1104 * @return The menu items that are not included in this main panel. 1105 */ 1106 public List<MenuItem> layoutMainPanelItems( 1107 List<MenuItem> menuItems, final int toolbarWidth) { 1108 Preconditions.checkNotNull(menuItems); 1109 1110 int availableWidth = toolbarWidth; 1111 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems); 1112 1113 mMainPanel.removeAllViews(); 1114 mMainPanel.setPaddingRelative(0, 0, 0, 0); 1115 1116 boolean isFirstItem = true; 1117 while (!remainingMenuItems.isEmpty()) { 1118 final MenuItem menuItem = remainingMenuItems.peek(); 1119 View menuItemButton = createMenuItemButton(mContext, menuItem); 1120 1121 // Adding additional start padding for the first button to even out button spacing. 1122 if (isFirstItem) { 1123 menuItemButton.setPaddingRelative( 1124 (int) (1.5 * menuItemButton.getPaddingStart()), 1125 menuItemButton.getPaddingTop(), 1126 menuItemButton.getPaddingEnd(), 1127 menuItemButton.getPaddingBottom()); 1128 isFirstItem = false; 1129 } 1130 1131 // Adding additional end padding for the last button to even out button spacing. 1132 if (remainingMenuItems.size() == 1) { 1133 menuItemButton.setPaddingRelative( 1134 menuItemButton.getPaddingStart(), 1135 menuItemButton.getPaddingTop(), 1136 (int) (1.5 * menuItemButton.getPaddingEnd()), 1137 menuItemButton.getPaddingBottom()); 1138 } 1139 1140 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1141 int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); 1142 // Check if we can fit an item while reserving space for the overflowButton. 1143 boolean canFitWithOverflow = 1144 menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth(); 1145 boolean canFitNoOverflow = 1146 remainingMenuItems.size() == 1 && menuItemButtonWidth <= availableWidth; 1147 if (canFitWithOverflow || canFitNoOverflow) { 1148 setButtonTagAndClickListener(menuItemButton, menuItem); 1149 mMainPanel.addView(menuItemButton); 1150 ViewGroup.LayoutParams params = menuItemButton.getLayoutParams(); 1151 params.width = menuItemButtonWidth; 1152 menuItemButton.setLayoutParams(params); 1153 availableWidth -= menuItemButtonWidth; 1154 remainingMenuItems.pop(); 1155 } else { 1156 // Reserve space for overflowButton. 1157 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0); 1158 break; 1159 } 1160 } 1161 mMainPanelSize = measure(mMainPanel); 1162 return remainingMenuItems; 1163 } 1164 1165 private void layoutOverflowPanelItems(List<MenuItem> menuItems) { 1166 ArrayAdapter<MenuItem> overflowPanelAdapter = 1167 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter(); 1168 overflowPanelAdapter.clear(); 1169 final int size = menuItems.size(); 1170 for (int i = 0; i < size; i++) { 1171 overflowPanelAdapter.add(menuItems.get(i)); 1172 } 1173 mOverflowPanel.setAdapter(overflowPanelAdapter); 1174 if (mOpenOverflowUpwards) { 1175 mOverflowPanel.setY(0); 1176 } else { 1177 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); 1178 } 1179 1180 int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth()); 1181 int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE); 1182 mOverflowPanelSize = new Size(width, height); 1183 setSize(mOverflowPanel, mOverflowPanelSize); 1184 } 1185 1186 /** 1187 * Resets the content container and appropriately position it's panels. 1188 */ 1189 private void preparePopupContent() { 1190 mContentContainer.removeAllViews(); 1191 1192 // Add views in the specified order so they stack up as expected. 1193 // Order: overflowPanel, mainPanel, overflowButton. 1194 if (hasOverflow()) { 1195 mContentContainer.addView(mOverflowPanel); 1196 } 1197 mContentContainer.addView(mMainPanel); 1198 if (hasOverflow()) { 1199 mContentContainer.addView(mOverflowButton); 1200 } 1201 setPanelsStatesAtRestingPosition(); 1202 setContentAreaAsTouchableSurface(); 1203 1204 // The positioning of contents in RTL is wrong when the view is first rendered. 1205 // Hide the view and post a runnable to recalculate positions and render the view. 1206 // TODO: Investigate why this happens and fix. 1207 if (isInRTLMode()) { 1208 mContentContainer.setAlpha(0); 1209 mContentContainer.post(mPreparePopupContentRTLHelper); 1210 } 1211 } 1212 1213 /** 1214 * Clears out the panels and their container. Resets their calculated sizes. 1215 */ 1216 private void clearPanels() { 1217 mOverflowPanelSize = null; 1218 mMainPanelSize = null; 1219 mIsOverflowOpen = false; 1220 mMainPanel.removeAllViews(); 1221 ArrayAdapter<MenuItem> overflowPanelAdapter = 1222 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter(); 1223 overflowPanelAdapter.clear(); 1224 mOverflowPanel.setAdapter(overflowPanelAdapter); 1225 mContentContainer.removeAllViews(); 1226 } 1227 1228 private void positionContentYCoordinatesIfOpeningOverflowUpwards() { 1229 if (mOpenOverflowUpwards) { 1230 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight()); 1231 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight()); 1232 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight()); 1233 } 1234 } 1235 1236 private int getOverflowWidth() { 1237 int overflowWidth = 0; 1238 final int count = mOverflowPanel.getAdapter().getCount(); 1239 for (int i = 0; i < count; i++) { 1240 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i); 1241 overflowWidth = 1242 Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth); 1243 } 1244 return overflowWidth; 1245 } 1246 1247 private int calculateOverflowHeight(int maxItemSize) { 1248 // Maximum of 4 items, minimum of 2 if the overflow has to scroll. 1249 int actualSize = Math.min( 1250 MAX_OVERFLOW_SIZE, 1251 Math.min( 1252 Math.max(MIN_OVERFLOW_SIZE, maxItemSize), 1253 mOverflowPanel.getCount())); 1254 int extension = 0; 1255 if (actualSize < mOverflowPanel.getCount()) { 1256 // The overflow will require scrolling to get to all the items. 1257 // Extend the height so that part of the hidden items is displayed. 1258 extension = (int) (getLineHeight(mContext) * 0.5f); 1259 } 1260 return actualSize * getLineHeight(mContext) 1261 + mOverflowButtonSize.getHeight() 1262 + extension; 1263 } 1264 1265 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) { 1266 View button = menuItemButton; 1267 if (isIconOnlyMenuItem(menuItem)) { 1268 button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button); 1269 } 1270 button.setTag(menuItem); 1271 button.setOnClickListener(mMenuItemButtonOnClickListener); 1272 } 1273 1274 /** 1275 * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.* 1276 * animations. See comment about this in the code. 1277 */ 1278 private int getAdjustedDuration(int originalDuration) { 1279 if (mTransitionDurationScale < 150) { 1280 // For smaller transition, decrease the time. 1281 return Math.max(originalDuration - 50, 0); 1282 } else if (mTransitionDurationScale > 300) { 1283 // For bigger transition, increase the time. 1284 return originalDuration + 50; 1285 } 1286 1287 // Scale the animation duration with getDurationScale(). This allows 1288 // android.view.animation.* animations to scale just like android.animation.* animations 1289 // when animator duration scale is adjusted in "Developer Options". 1290 // For this reason, do not use this method for android.animation.* animations. 1291 return (int) (originalDuration * ValueAnimator.getDurationScale()); 1292 } 1293 1294 private void maybeComputeTransitionDurationScale() { 1295 if (mMainPanelSize != null && mOverflowPanelSize != null) { 1296 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth(); 1297 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight(); 1298 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) / 1299 mContentContainer.getContext().getResources().getDisplayMetrics().density); 1300 } 1301 } 1302 1303 private ViewGroup createMainPanel() { 1304 ViewGroup mainPanel = new LinearLayout(mContext) { 1305 @Override 1306 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1307 if (isOverflowAnimating()) { 1308 // Update widthMeasureSpec to make sure that this view is not clipped 1309 // as we offset it's coordinates with respect to it's parent. 1310 widthMeasureSpec = MeasureSpec.makeMeasureSpec( 1311 mMainPanelSize.getWidth(), 1312 MeasureSpec.EXACTLY); 1313 } 1314 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1315 } 1316 1317 @Override 1318 public boolean onInterceptTouchEvent(MotionEvent ev) { 1319 // Intercept the touch event while the overflow is animating. 1320 return isOverflowAnimating(); 1321 } 1322 }; 1323 return mainPanel; 1324 } 1325 1326 private ImageButton createOverflowButton() { 1327 final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext) 1328 .inflate(R.layout.floating_popup_overflow_button, null); 1329 overflowButton.setImageDrawable(mOverflow); 1330 overflowButton.setOnClickListener(new View.OnClickListener() { 1331 @Override 1332 public void onClick(View v) { 1333 if (mIsOverflowOpen) { 1334 overflowButton.setImageDrawable(mToOverflow); 1335 mToOverflow.start(); 1336 closeOverflow(); 1337 } else { 1338 overflowButton.setImageDrawable(mToArrow); 1339 mToArrow.start(); 1340 openOverflow(); 1341 } 1342 } 1343 }); 1344 return overflowButton; 1345 } 1346 1347 private OverflowPanel createOverflowPanel() { 1348 final OverflowPanel overflowPanel = new OverflowPanel(this); 1349 overflowPanel.setLayoutParams(new ViewGroup.LayoutParams( 1350 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 1351 overflowPanel.setDivider(null); 1352 overflowPanel.setDividerHeight(0); 1353 1354 final ArrayAdapter adapter = 1355 new ArrayAdapter<MenuItem>(mContext, 0) { 1356 @Override 1357 public int getViewTypeCount() { 1358 return mOverflowPanelViewHelper.getViewTypeCount(); 1359 } 1360 1361 @Override 1362 public int getItemViewType(int position) { 1363 return mOverflowPanelViewHelper.getItemViewType(getItem(position)); 1364 } 1365 1366 @Override 1367 public View getView(int position, View convertView, ViewGroup parent) { 1368 return mOverflowPanelViewHelper.getView( 1369 getItem(position), mOverflowPanelSize.getWidth(), convertView); 1370 } 1371 }; 1372 overflowPanel.setAdapter(adapter); 1373 1374 overflowPanel.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1375 @Override 1376 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1377 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position); 1378 if (mOnMenuItemClickListener != null) { 1379 mOnMenuItemClickListener.onMenuItemClick(menuItem); 1380 } 1381 } 1382 }); 1383 1384 return overflowPanel; 1385 } 1386 1387 private boolean isOverflowAnimating() { 1388 final boolean overflowOpening = mOpenOverflowAnimation.hasStarted() 1389 && !mOpenOverflowAnimation.hasEnded(); 1390 final boolean overflowClosing = mCloseOverflowAnimation.hasStarted() 1391 && !mCloseOverflowAnimation.hasEnded(); 1392 return overflowOpening || overflowClosing; 1393 } 1394 1395 private Animation.AnimationListener createOverflowAnimationListener() { 1396 Animation.AnimationListener listener = new Animation.AnimationListener() { 1397 @Override 1398 public void onAnimationStart(Animation animation) { 1399 // Disable the overflow button while it's animating. 1400 // It will be re-enabled when the animation stops. 1401 mOverflowButton.setEnabled(false); 1402 // Ensure both panels have visibility turned on when the overflow animation 1403 // starts. 1404 mMainPanel.setVisibility(View.VISIBLE); 1405 mOverflowPanel.setVisibility(View.VISIBLE); 1406 } 1407 1408 @Override 1409 public void onAnimationEnd(Animation animation) { 1410 // Posting this because it seems like this is called before the animation 1411 // actually ends. 1412 mContentContainer.post(new Runnable() { 1413 @Override 1414 public void run() { 1415 setPanelsStatesAtRestingPosition(); 1416 setContentAreaAsTouchableSurface(); 1417 } 1418 }); 1419 } 1420 1421 @Override 1422 public void onAnimationRepeat(Animation animation) { 1423 } 1424 }; 1425 return listener; 1426 } 1427 1428 private static Size measure(View view) { 1429 Preconditions.checkState(view.getParent() == null); 1430 view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 1431 return new Size(view.getMeasuredWidth(), view.getMeasuredHeight()); 1432 } 1433 1434 private static void setSize(View view, int width, int height) { 1435 view.setMinimumWidth(width); 1436 view.setMinimumHeight(height); 1437 ViewGroup.LayoutParams params = view.getLayoutParams(); 1438 params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params; 1439 params.width = width; 1440 params.height = height; 1441 view.setLayoutParams(params); 1442 } 1443 1444 private static void setSize(View view, Size size) { 1445 setSize(view, size.getWidth(), size.getHeight()); 1446 } 1447 1448 private static void setWidth(View view, int width) { 1449 ViewGroup.LayoutParams params = view.getLayoutParams(); 1450 setSize(view, width, params.height); 1451 } 1452 1453 private static void setHeight(View view, int height) { 1454 ViewGroup.LayoutParams params = view.getLayoutParams(); 1455 setSize(view, params.width, height); 1456 } 1457 1458 private static int getLineHeight(Context context) { 1459 return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height); 1460 } 1461 1462 /** 1463 * A custom ListView for the overflow panel. 1464 */ 1465 private static final class OverflowPanel extends ListView { 1466 1467 private final FloatingToolbarPopup mPopup; 1468 1469 OverflowPanel(FloatingToolbarPopup popup) { 1470 super(Preconditions.checkNotNull(popup).mContext); 1471 this.mPopup = popup; 1472 setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3); 1473 setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); 1474 } 1475 1476 @Override 1477 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1478 // Update heightMeasureSpec to make sure that this view is not clipped 1479 // as we offset it's coordinates with respect to it's parent. 1480 int height = mPopup.mOverflowPanelSize.getHeight() 1481 - mPopup.mOverflowButtonSize.getHeight(); 1482 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 1483 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1484 } 1485 1486 @Override 1487 public boolean dispatchTouchEvent(MotionEvent ev) { 1488 if (mPopup.isOverflowAnimating()) { 1489 // Eat the touch event. 1490 return true; 1491 } 1492 return super.dispatchTouchEvent(ev); 1493 } 1494 1495 @Override 1496 protected boolean awakenScrollBars() { 1497 return super.awakenScrollBars(); 1498 } 1499 } 1500 1501 /** 1502 * A custom interpolator used for various floating toolbar animations. 1503 */ 1504 private static final class LogAccelerateInterpolator implements Interpolator { 1505 1506 private static final int BASE = 100; 1507 private static final float LOGS_SCALE = 1f / computeLog(1, BASE); 1508 1509 private static float computeLog(float t, int base) { 1510 return (float) (1 - Math.pow(base, -t)); 1511 } 1512 1513 @Override 1514 public float getInterpolation(float t) { 1515 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE; 1516 } 1517 } 1518 1519 /** 1520 * A helper for generating views for the overflow panel. 1521 */ 1522 private static final class OverflowPanelViewHelper { 1523 1524 private static final int NUM_OF_VIEW_TYPES = 2; 1525 private static final int VIEW_TYPE_STRING_TITLE = 0; 1526 private static final int VIEW_TYPE_ICON_ONLY = 1; 1527 1528 private final TextView mStringTitleViewCalculator; 1529 private final View mIconOnlyViewCalculator; 1530 1531 private final Context mContext; 1532 1533 public OverflowPanelViewHelper(Context context) { 1534 mContext = Preconditions.checkNotNull(context); 1535 mStringTitleViewCalculator = getStringTitleView(null, 0, null); 1536 mIconOnlyViewCalculator = getIconOnlyView(null, 0, null); 1537 } 1538 1539 public int getViewTypeCount() { 1540 return NUM_OF_VIEW_TYPES; 1541 } 1542 1543 public View getView(MenuItem menuItem, int minimumWidth, View convertView) { 1544 Preconditions.checkNotNull(menuItem); 1545 if (getItemViewType(menuItem) == VIEW_TYPE_ICON_ONLY) { 1546 return getIconOnlyView(menuItem, minimumWidth, convertView); 1547 } 1548 return getStringTitleView(menuItem, minimumWidth, convertView); 1549 } 1550 1551 public int getItemViewType(MenuItem menuItem) { 1552 Preconditions.checkNotNull(menuItem); 1553 if (isIconOnlyMenuItem(menuItem)) { 1554 return VIEW_TYPE_ICON_ONLY; 1555 } 1556 return VIEW_TYPE_STRING_TITLE; 1557 } 1558 1559 public int calculateWidth(MenuItem menuItem) { 1560 final View calculator; 1561 if (isIconOnlyMenuItem(menuItem)) { 1562 ((ImageView) mIconOnlyViewCalculator 1563 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1564 .setImageDrawable(menuItem.getIcon()); 1565 calculator = mIconOnlyViewCalculator; 1566 } else { 1567 mStringTitleViewCalculator.setText(menuItem.getTitle()); 1568 calculator = mStringTitleViewCalculator; 1569 } 1570 calculator.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 1571 return calculator.getMeasuredWidth(); 1572 } 1573 1574 private TextView getStringTitleView( 1575 MenuItem menuItem, int minimumWidth, View convertView) { 1576 TextView menuButton; 1577 if (convertView != null) { 1578 menuButton = (TextView) convertView; 1579 } else { 1580 menuButton = (TextView) LayoutInflater.from(mContext) 1581 .inflate(R.layout.floating_popup_overflow_list_item, null); 1582 menuButton.setLayoutParams(new ViewGroup.LayoutParams( 1583 ViewGroup.LayoutParams.MATCH_PARENT, 1584 ViewGroup.LayoutParams.WRAP_CONTENT)); 1585 } 1586 if (menuItem != null) { 1587 menuButton.setText(menuItem.getTitle()); 1588 menuButton.setContentDescription(menuItem.getTitle()); 1589 menuButton.setMinimumWidth(minimumWidth); 1590 } 1591 return menuButton; 1592 } 1593 1594 private View getIconOnlyView( 1595 MenuItem menuItem, int minimumWidth, View convertView) { 1596 View menuButton; 1597 if (convertView != null) { 1598 menuButton = convertView; 1599 } else { 1600 menuButton = LayoutInflater.from(mContext).inflate( 1601 R.layout.floating_popup_overflow_image_list_item, null); 1602 menuButton.setLayoutParams(new ViewGroup.LayoutParams( 1603 ViewGroup.LayoutParams.WRAP_CONTENT, 1604 ViewGroup.LayoutParams.WRAP_CONTENT)); 1605 } 1606 if (menuItem != null) { 1607 ((ImageView) menuButton 1608 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1609 .setImageDrawable(menuItem.getIcon()); 1610 menuButton.setMinimumWidth(minimumWidth); 1611 } 1612 return menuButton; 1613 } 1614 } 1615 } 1616 1617 /** 1618 * @return {@code true} if the menu item does not not have a string title but has an icon. 1619 * {@code false} otherwise. 1620 */ 1621 private static boolean isIconOnlyMenuItem(MenuItem menuItem) { 1622 if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) { 1623 return true; 1624 } 1625 return false; 1626 } 1627 1628 /** 1629 * Creates and returns a menu button for the specified menu item. 1630 */ 1631 private static View createMenuItemButton(Context context, MenuItem menuItem) { 1632 if (isIconOnlyMenuItem(menuItem)) { 1633 View imageMenuItemButton = LayoutInflater.from(context) 1634 .inflate(R.layout.floating_popup_menu_image_button, null); 1635 ((ImageButton) imageMenuItemButton 1636 .findViewById(R.id.floating_toolbar_menu_item_image_button)) 1637 .setImageDrawable(menuItem.getIcon()); 1638 return imageMenuItemButton; 1639 } 1640 1641 Button menuItemButton = (Button) LayoutInflater.from(context) 1642 .inflate(R.layout.floating_popup_menu_button, null); 1643 menuItemButton.setText(menuItem.getTitle()); 1644 menuItemButton.setContentDescription(menuItem.getTitle()); 1645 return menuItemButton; 1646 } 1647 1648 private static ViewGroup createContentContainer(Context context) { 1649 ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context) 1650 .inflate(R.layout.floating_popup_container, null); 1651 contentContainer.setLayoutParams(new ViewGroup.LayoutParams( 1652 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 1653 contentContainer.setTag(FLOATING_TOOLBAR_TAG); 1654 return contentContainer; 1655 } 1656 1657 private static PopupWindow createPopupWindow(ViewGroup content) { 1658 ViewGroup popupContentHolder = new LinearLayout(content.getContext()); 1659 PopupWindow popupWindow = new PopupWindow(popupContentHolder); 1660 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false) 1661 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects. 1662 popupWindow.setClippingEnabled(false); 1663 popupWindow.setWindowLayoutType( 1664 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); 1665 popupWindow.setAnimationStyle(0); 1666 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 1667 content.setLayoutParams(new ViewGroup.LayoutParams( 1668 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 1669 popupContentHolder.addView(content); 1670 return popupWindow; 1671 } 1672 1673 /** 1674 * Creates an "appear" animation for the specified view. 1675 * 1676 * @param view The view to animate 1677 */ 1678 private static AnimatorSet createEnterAnimation(View view) { 1679 AnimatorSet animation = new AnimatorSet(); 1680 animation.playTogether( 1681 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150)); 1682 return animation; 1683 } 1684 1685 /** 1686 * Creates a "disappear" animation for the specified view. 1687 * 1688 * @param view The view to animate 1689 * @param startDelay The start delay of the animation 1690 * @param listener The animation listener 1691 */ 1692 private static AnimatorSet createExitAnimation( 1693 View view, int startDelay, Animator.AnimatorListener listener) { 1694 AnimatorSet animation = new AnimatorSet(); 1695 animation.playTogether( 1696 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100)); 1697 animation.setStartDelay(startDelay); 1698 animation.addListener(listener); 1699 return animation; 1700 } 1701 1702 /** 1703 * Returns a re-themed context with controlled look and feel for views. 1704 */ 1705 private static Context applyDefaultTheme(Context originalContext) { 1706 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme}); 1707 boolean isLightTheme = a.getBoolean(0, true); 1708 int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material; 1709 a.recycle(); 1710 return new ContextThemeWrapper(originalContext, themeId); 1711 } 1712 } 1713