2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.internal.widget;
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;
64 import java.util.ArrayList;
65 import java.util.LinkedList;
66 import java.util.List;
68 import com.android.internal.R;
69 import com.android.internal.util.Preconditions;
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.
77 public final class FloatingToolbar {
79 // This class is responsible for the public API of the floating toolbar.
80 // It delegates rendering operations to the FloatingToolbarPopup.
82 public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
84 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
85 new MenuItem.OnMenuItemClickListener() {
87 public boolean onMenuItemClick(MenuItem item) {
92 private final Context mContext;
93 private final Window mWindow;
94 private final FloatingToolbarPopup mPopup;
96 private final Rect mContentRect = new Rect();
97 private final Rect mPreviousContentRect = new Rect();
100 private List<Object> mShowingMenuItems = new ArrayList<Object>();
101 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
103 private int mSuggestedWidth;
104 private boolean mWidthChanged = true;
106 private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
108 private final Rect mNewRect = new Rect();
109 private final Rect mOldRect = new Rect();
112 public void onLayoutChange(
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;
126 * Initializes a floating toolbar.
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());
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
139 public FloatingToolbar setMenu(Menu menu) {
140 mMenu = Preconditions.checkNotNull(menu);
145 * Sets the custom listener for invocation of menu items in this floating toolbar.
147 public FloatingToolbar setOnMenuItemClickListener(
148 MenuItem.OnMenuItemClickListener menuItemClickListener) {
149 if (menuItemClickListener != null) {
150 mMenuItemClickListener = menuItemClickListener;
152 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
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
163 public FloatingToolbar setContentRect(Rect rect) {
164 mContentRect.set(Preconditions.checkNotNull(rect));
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
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);
180 mSuggestedWidth = suggestedWidth;
185 * Shows this floating toolbar.
187 public FloatingToolbar show() {
188 registerOrientationHandler();
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.
197 public FloatingToolbar updateLayout() {
198 if (mPopup.isShowing()) {
205 * Dismisses this floating toolbar.
207 public void dismiss() {
208 unregisterOrientationHandler();
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.
221 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
223 public boolean isShowing() {
224 return mPopup.isShowing();
228 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
230 public boolean isHidden() {
231 return mPopup.isHidden();
234 private void doShow() {
235 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
236 if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
238 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
239 mShowingMenuItems = getShowingMenuItemsReferences(menuItems);
241 if (!mPopup.isShowing()) {
242 mPopup.show(mContentRect);
243 } else if (!mPreviousContentRect.equals(mContentRect)) {
244 mPopup.updateCoordinates(mContentRect);
246 mWidthChanged = false;
247 mPreviousContentRect.set(mContentRect);
251 * Returns true if this floating toolbar is currently showing the specified menu items.
253 private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
254 return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems));
258 * Returns the visible and enabled menu items in the specified menu.
259 * This method is recursive.
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));
270 menuItems.add(menuItem);
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());
283 references.add(menuItem.getTitle());
289 private void registerOrientationHandler() {
290 unregisterOrientationHandler();
291 mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
294 private void unregisterOrientationHandler() {
295 mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
300 * A popup window used by the floating toolbar.
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.
306 private static final class FloatingToolbarPopup {
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;
312 private final Context mContext;
313 private final View mParent; // Parent for the popup window.
314 private final PopupWindow mPopupWindow;
316 /* Margins between the popup window and it's content. */
317 private final int mMarginHorizontal;
318 private final int mMarginVertical;
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;
331 private final OverflowPanelViewHelper mOverflowPanelViewHelper;
333 /* Animation interpolators. */
334 private final Interpolator mLogAccelerateInterpolator;
335 private final Interpolator mFastOutSlowInInterpolator;
336 private final Interpolator mLinearOutSlowInInterpolator;
337 private final Interpolator mFastOutLinearInInterpolator;
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;
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();
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);
367 * @see OverflowPanelViewHelper#preparePopupContent().
369 private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
372 setPanelsStatesAtRestingPosition();
373 setContentAreaAsTouchableSurface();
374 mContentContainer.setAlpha(1);
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.
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;
386 /* Item click listeners */
387 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
388 private final View.OnClickListener mMenuItemButtonOnClickListener =
389 new View.OnClickListener() {
391 public void onClick(View v) {
392 if (v.getTag() instanceof MenuItem) {
393 if (mOnMenuItemClickListener != null) {
394 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
400 private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards.
401 private boolean mIsOverflowOpen;
403 private int mTransitionDurationScale; // Used to scale the toolbar transition duration.
406 * Initializes a new floating toolbar popup.
408 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
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);
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);
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);
445 mOverflowButton = createOverflowButton();
446 mOverflowButtonSize = measure(mOverflowButton);
447 mMainPanel = createMainPanel();
448 mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext);
449 mOverflowPanel = createOverflowPanel();
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(
461 new AnimatorListenerAdapter() {
463 public void onAnimationEnd(Animator animation) {
464 mPopupWindow.dismiss();
465 mContentContainer.removeAllViews();
468 mHideAnimation = createExitAnimation(
471 new AnimatorListenerAdapter() {
473 public void onAnimationEnd(Animator animation) {
474 mPopupWindow.dismiss();
480 * Lays out buttons for the specified menu items.
481 * Requires a subsequent call to {@link #show()} to show the items.
483 public void layoutMenuItems(
484 List<MenuItem> menuItems,
485 MenuItem.OnMenuItemClickListener menuItemClickListener,
486 int suggestedWidth) {
487 mOnMenuItemClickListener = menuItemClickListener;
488 cancelOverflowAnimations();
490 menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
491 if (!menuItems.isEmpty()) {
492 // Add remaining items to the overflow.
493 layoutOverflowPanelItems(menuItems);
499 * Shows this popup at the specified coordinates.
500 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
502 public void show(Rect contentRectOnScreen) {
503 Preconditions.checkNotNull(contentRectOnScreen);
511 cancelDismissAndHideAnimations();
512 cancelOverflowAnimations();
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();
526 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
528 public void dismiss() {
535 mHideAnimation.cancel();
537 runDismissAnimation();
538 setZeroTouchableSurface();
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.
552 setZeroTouchableSurface();
556 * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
558 public boolean isShowing() {
559 return !mDismissed && !mHidden;
563 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
565 public boolean isHidden() {
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.
574 public void updateCoordinates(Rect contentRectOnScreen) {
575 Preconditions.checkNotNull(contentRectOnScreen);
577 if (!isShowing() || !mPopupWindow.isShowing()) {
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.
588 mCoordsOnWindow.x, mCoordsOnWindow.y,
589 mPopupWindow.getWidth(), mPopupWindow.getHeight());
592 private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
595 // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
597 final int x = Math.min(
598 contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
599 mViewPortOnScreen.right - mPopupWindow.getWidth());
603 final int availableHeightAboveContent =
604 contentRectOnScreen.top - mViewPortOnScreen.top;
605 final int availableHeightBelowContent =
606 mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
608 final int margin = 2 * mMarginVertical;
609 final int toolbarHeightWithVerticalMargin = getLineHeight(mContext) + margin;
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;
622 // Not enough space. Prefer to position as high as possible.
624 mViewPortOnScreen.top,
625 contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
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;
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;
667 // Position at the top of the view port and open downwards.
668 updateOverflowHeight(mViewPortOnScreen.height() - margin);
669 y = mViewPortOnScreen.top;
670 mOpenOverflowUpwards = false;
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;
690 Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
694 * Performs the "show" animation on the floating popup.
696 private void runShowAnimation() {
697 mShowAnimation.start();
701 * Performs the "dismiss" animation on the floating popup.
703 private void runDismissAnimation() {
704 mDismissAnimation.start();
708 * Performs the "hide" animation on the floating popup.
710 private void runHideAnimation() {
711 mHideAnimation.start();
714 private void cancelDismissAndHideAnimations() {
715 mDismissAnimation.cancel();
716 mHideAnimation.cancel();
719 private void cancelOverflowAnimations() {
720 mContentContainer.clearAnimation();
721 mMainPanel.animate().cancel();
722 mOverflowPanel.animate().cancel();
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() {
737 protected void applyTransformation(float interpolatedTime, Transformation t) {
738 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
739 setWidth(mContentContainer, startWidth + deltaWidth);
741 mContentContainer.setX(left);
743 // Lock the panels in place.
745 mOverflowPanel.setX(0);
747 mContentContainer.setX(right - mContentContainer.getWidth());
749 // Offset the panels' positions so they look like they're locked in place
751 mMainPanel.setX(mContentContainer.getWidth() - startWidth);
752 mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
756 Animation heightAnimation = new Animation() {
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();
768 final float overflowButtonStartX = mOverflowButton.getX();
769 final float overflowButtonTargetX = isRTL() ?
770 overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
771 overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
772 Animation overflowButtonAnimation = new Animation() {
774 protected void applyTransformation(float interpolatedTime, Transformation t) {
775 float overflowButtonX = overflowButtonStartX
776 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
777 float deltaContainerWidth = isRTL() ?
779 mContentContainer.getWidth() - startWidth;
780 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
781 mOverflowButton.setX(actualOverflowButtonX);
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;
798 .alpha(0).withLayer()
799 .setInterpolator(mLinearOutSlowInInterpolator)
802 mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
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() {
812 protected void applyTransformation(float interpolatedTime, Transformation t) {
813 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
814 setWidth(mContentContainer, startWidth + deltaWidth);
816 mContentContainer.setX(left);
818 // Lock the panels in place.
820 mOverflowPanel.setX(0);
822 mContentContainer.setX(right - mContentContainer.getWidth());
824 // Offset the panels' positions so they look like they're locked in place
826 mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
827 mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
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() {
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();
845 final float overflowButtonStartX = mOverflowButton.getX();
846 final float overflowButtonTargetX = isRTL() ?
847 overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
848 overflowButtonStartX + startWidth - mOverflowButton.getWidth();
849 Animation overflowButtonAnimation = new Animation() {
851 protected void applyTransformation(float interpolatedTime, Transformation t) {
852 float overflowButtonX = overflowButtonStartX
853 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
854 float deltaContainerWidth = isRTL() ?
856 mContentContainer.getWidth() - startWidth;
857 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
858 mOverflowButton.setX(actualOverflowButtonX);
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;
874 .alpha(1).withLayer()
875 .setInterpolator(mFastOutLinearInInterpolator)
878 mOverflowPanel.animate()
879 .alpha(0).withLayer()
880 .setInterpolator(mLinearOutSlowInInterpolator)
886 * Defines the position of the floating toolbar popup panels when transition animation has
889 private void setPanelsStatesAtRestingPosition() {
890 mOverflowButton.setEnabled(true);
891 mOverflowPanel.awakenScrollBars();
893 if (mIsOverflowOpen) {
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);
903 // Update x-coordinates depending on RTL state.
905 mContentContainer.setX(mMarginHorizontal); // align left
906 mMainPanel.setX(0); // align left
907 mOverflowButton.setX( // align right
908 containerSize.getWidth() - mOverflowButtonSize.getWidth());
909 mOverflowPanel.setX(0); // align left
911 mContentContainer.setX( // align right
912 mPopupWindow.getWidth() -
913 containerSize.getWidth() - mMarginHorizontal);
914 mMainPanel.setX(-mContentContainer.getX()); // align right
915 mOverflowButton.setX(0); // align left
916 mOverflowPanel.setX(0); // align left
919 // Update y-coordinates depending on overflow's open direction.
920 if (mOpenOverflowUpwards) {
921 mContentContainer.setY(mMarginVertical); // align top
922 mMainPanel.setY( // align bottom
923 containerSize.getHeight() - mContentContainer.getHeight());
924 mOverflowButton.setY( // align bottom
925 containerSize.getHeight() - mOverflowButtonSize.getHeight());
926 mOverflowPanel.setY(0); // align top
929 mContentContainer.setY(mMarginVertical); // align top
930 mMainPanel.setY(0); // align top
931 mOverflowButton.setY(0); // align top
932 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
935 // Overflow not open. Set closed state.
936 final Size containerSize = mMainPanelSize;
937 setSize(mContentContainer, containerSize);
938 mMainPanel.setAlpha(1);
939 mMainPanel.setVisibility(View.VISIBLE);
940 mOverflowPanel.setAlpha(0);
941 mOverflowPanel.setVisibility(View.INVISIBLE);
942 mOverflowButton.setImageDrawable(mOverflow);
945 // Update x-coordinates depending on RTL state.
947 mContentContainer.setX(mMarginHorizontal); // align left
948 mMainPanel.setX(0); // align left
949 mOverflowButton.setX(0); // align left
950 mOverflowPanel.setX(0); // align left
952 mContentContainer.setX( // align right
953 mPopupWindow.getWidth() -
954 containerSize.getWidth() - mMarginHorizontal);
955 mMainPanel.setX(0); // align left
956 mOverflowButton.setX( // align right
957 containerSize.getWidth() - mOverflowButtonSize.getWidth());
958 mOverflowPanel.setX( // align right
959 containerSize.getWidth() - mOverflowPanelSize.getWidth());
962 // Update y-coordinates depending on overflow's open direction.
963 if (mOpenOverflowUpwards) {
964 mContentContainer.setY( // align bottom
966 mOverflowPanelSize.getHeight() - containerSize.getHeight());
967 mMainPanel.setY(0); // align top
968 mOverflowButton.setY(0); // align top
969 mOverflowPanel.setY( // align bottom
970 containerSize.getHeight() - mOverflowPanelSize.getHeight());
973 mContentContainer.setY(mMarginVertical); // align top
974 mMainPanel.setY(0); // align top
975 mOverflowButton.setY(0); // align top
976 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
980 mContentContainer.setX(mMarginHorizontal); // align left
981 mContentContainer.setY(mMarginVertical); // align top
982 mMainPanel.setX(0); // align left
983 mMainPanel.setY(0); // align top
988 private void updateOverflowHeight(int suggestedHeight) {
990 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
991 getLineHeight(mContext);
992 final int newHeight = calculateOverflowHeight(maxItemSize);
993 if (mOverflowPanelSize.getHeight() != newHeight) {
994 mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
996 setSize(mOverflowPanel, mOverflowPanelSize);
997 if (mIsOverflowOpen) {
998 setSize(mContentContainer, mOverflowPanelSize);
999 if (mOpenOverflowUpwards) {
1000 final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
1001 mContentContainer.setY(mContentContainer.getY() + deltaHeight);
1002 mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
1005 setSize(mContentContainer, mMainPanelSize);
1011 private void updatePopupSize() {
1014 if (mMainPanelSize != null) {
1015 width = Math.max(width, mMainPanelSize.getWidth());
1016 height = Math.max(height, mMainPanelSize.getHeight());
1018 if (mOverflowPanelSize != null) {
1019 width = Math.max(width, mOverflowPanelSize.getWidth());
1020 height = Math.max(height, mOverflowPanelSize.getHeight());
1022 mPopupWindow.setWidth(width + mMarginHorizontal * 2);
1023 mPopupWindow.setHeight(height + mMarginVertical * 2);
1024 maybeComputeTransitionDurationScale();
1027 private void refreshViewPort() {
1028 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
1031 private int getAdjustedToolbarWidth(int suggestedWidth) {
1032 int width = suggestedWidth;
1034 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
1035 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1037 width = mParent.getResources()
1038 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1040 return Math.min(width, maximumWidth);
1044 * Sets the touchable region of this popup to be zero. This means that all touch events on
1045 * this popup will go through to the surface behind it.
1047 private void setZeroTouchableSurface() {
1048 mTouchableRegion.setEmpty();
1052 * Sets the touchable region of this popup to be the area occupied by its content.
1054 private void setContentAreaAsTouchableSurface() {
1055 Preconditions.checkNotNull(mMainPanelSize);
1058 if (mIsOverflowOpen) {
1059 Preconditions.checkNotNull(mOverflowPanelSize);
1060 width = mOverflowPanelSize.getWidth();
1061 height = mOverflowPanelSize.getHeight();
1063 width = mMainPanelSize.getWidth();
1064 height = mMainPanelSize.getHeight();
1066 mTouchableRegion.set(
1067 (int) mContentContainer.getX(),
1068 (int) mContentContainer.getY(),
1069 (int) mContentContainer.getX() + width,
1070 (int) mContentContainer.getY() + height);
1074 * Make the touchable area of this popup be the area specified by mTouchableRegion.
1075 * This should be called after the popup window has been dismissed (dismiss/hide)
1076 * and is probably being re-shown with a new content root view.
1078 private void setTouchableSurfaceInsetsComputer() {
1079 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
1081 .getViewTreeObserver();
1082 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
1083 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
1086 private boolean isRTL() {
1087 return mContext.getResources().getConfiguration().getLayoutDirection()
1088 == View.LAYOUT_DIRECTION_RTL;
1091 private boolean hasOverflow() {
1092 return mOverflowPanelSize != null;
1096 * Fits as many menu items in the main panel and returns a list of the menu items that
1099 * @return The menu items that are not included in this main panel.
1101 public List<MenuItem> layoutMainPanelItems(
1102 List<MenuItem> menuItems, final int toolbarWidth) {
1103 Preconditions.checkNotNull(menuItems);
1105 int availableWidth = toolbarWidth;
1106 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
1108 mMainPanel.removeAllViews();
1109 mMainPanel.setPaddingRelative(0, 0, 0, 0);
1111 boolean isFirstItem = true;
1112 while (!remainingMenuItems.isEmpty()) {
1113 final MenuItem menuItem = remainingMenuItems.peek();
1114 View menuItemButton = createMenuItemButton(mContext, menuItem);
1116 // Adding additional start padding for the first button to even out button spacing.
1118 menuItemButton.setPaddingRelative(
1119 (int) (1.5 * menuItemButton.getPaddingStart()),
1120 menuItemButton.getPaddingTop(),
1121 menuItemButton.getPaddingEnd(),
1122 menuItemButton.getPaddingBottom());
1123 isFirstItem = false;
1126 // Adding additional end padding for the last button to even out button spacing.
1127 if (remainingMenuItems.size() == 1) {
1128 menuItemButton.setPaddingRelative(
1129 menuItemButton.getPaddingStart(),
1130 menuItemButton.getPaddingTop(),
1131 (int) (1.5 * menuItemButton.getPaddingEnd()),
1132 menuItemButton.getPaddingBottom());
1135 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1136 int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
1137 // Check if we can fit an item while reserving space for the overflowButton.
1138 boolean canFitWithOverflow =
1139 menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth();
1140 boolean canFitNoOverflow =
1141 remainingMenuItems.size() == 1 && menuItemButtonWidth <= availableWidth;
1142 if (canFitWithOverflow || canFitNoOverflow) {
1143 setButtonTagAndClickListener(menuItemButton, menuItem);
1144 mMainPanel.addView(menuItemButton);
1145 ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1146 params.width = menuItemButtonWidth;
1147 menuItemButton.setLayoutParams(params);
1148 availableWidth -= menuItemButtonWidth;
1149 remainingMenuItems.pop();
1151 // Reserve space for overflowButton.
1152 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1156 mMainPanelSize = measure(mMainPanel);
1157 return remainingMenuItems;
1160 private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
1161 ArrayAdapter<MenuItem> overflowPanelAdapter =
1162 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1163 overflowPanelAdapter.clear();
1164 final int size = menuItems.size();
1165 for (int i = 0; i < size; i++) {
1166 overflowPanelAdapter.add(menuItems.get(i));
1168 mOverflowPanel.setAdapter(overflowPanelAdapter);
1169 if (mOpenOverflowUpwards) {
1170 mOverflowPanel.setY(0);
1172 mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1175 int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
1176 int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
1177 mOverflowPanelSize = new Size(width, height);
1178 setSize(mOverflowPanel, mOverflowPanelSize);
1182 * Resets the content container and appropriately position it's panels.
1184 private void preparePopupContent() {
1185 mContentContainer.removeAllViews();
1187 // Add views in the specified order so they stack up as expected.
1188 // Order: overflowPanel, mainPanel, overflowButton.
1189 if (hasOverflow()) {
1190 mContentContainer.addView(mOverflowPanel);
1192 mContentContainer.addView(mMainPanel);
1193 if (hasOverflow()) {
1194 mContentContainer.addView(mOverflowButton);
1196 setPanelsStatesAtRestingPosition();
1197 setContentAreaAsTouchableSurface();
1199 // The positioning of contents in RTL is wrong when the view is first rendered.
1200 // Hide the view and post a runnable to recalculate positions and render the view.
1201 // TODO: Investigate why this happens and fix.
1203 mContentContainer.setAlpha(0);
1204 mContentContainer.post(mPreparePopupContentRTLHelper);
1209 * Clears out the panels and their container. Resets their calculated sizes.
1211 private void clearPanels() {
1212 mOverflowPanelSize = null;
1213 mMainPanelSize = null;
1214 mIsOverflowOpen = false;
1215 mMainPanel.removeAllViews();
1216 ArrayAdapter<MenuItem> overflowPanelAdapter =
1217 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1218 overflowPanelAdapter.clear();
1219 mOverflowPanel.setAdapter(overflowPanelAdapter);
1220 mContentContainer.removeAllViews();
1223 private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
1224 if (mOpenOverflowUpwards) {
1225 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
1226 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
1227 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
1231 private int getOverflowWidth() {
1232 int overflowWidth = 0;
1233 final int count = mOverflowPanel.getAdapter().getCount();
1234 for (int i = 0; i < count; i++) {
1235 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
1237 Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1239 return overflowWidth;
1242 private int calculateOverflowHeight(int maxItemSize) {
1243 // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
1244 int actualSize = Math.min(
1247 Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1248 mOverflowPanel.getCount()));
1250 if (actualSize < mOverflowPanel.getCount()) {
1251 // The overflow will require scrolling to get to all the items.
1252 // Extend the height so that part of the hidden items is displayed.
1253 extension = (int) (getLineHeight(mContext) * 0.5f);
1255 return actualSize * getLineHeight(mContext)
1256 + mOverflowButtonSize.getHeight()
1260 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1261 View button = menuItemButton;
1262 if (isIconOnlyMenuItem(menuItem)) {
1263 button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1265 button.setTag(menuItem);
1266 button.setOnClickListener(mMenuItemButtonOnClickListener);
1270 * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1271 * animations. See comment about this in the code.
1273 private int getAdjustedDuration(int originalDuration) {
1274 if (mTransitionDurationScale < 150) {
1275 // For smaller transition, decrease the time.
1276 return Math.max(originalDuration - 50, 0);
1277 } else if (mTransitionDurationScale > 300) {
1278 // For bigger transition, increase the time.
1279 return originalDuration + 50;
1282 // Scale the animation duration with getDurationScale(). This allows
1283 // android.view.animation.* animations to scale just like android.animation.* animations
1284 // when animator duration scale is adjusted in "Developer Options".
1285 // For this reason, do not use this method for android.animation.* animations.
1286 return (int) (originalDuration * ValueAnimator.getDurationScale());
1289 private void maybeComputeTransitionDurationScale() {
1290 if (mMainPanelSize != null && mOverflowPanelSize != null) {
1291 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
1292 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
1293 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
1294 mContentContainer.getContext().getResources().getDisplayMetrics().density);
1298 private ViewGroup createMainPanel() {
1299 ViewGroup mainPanel = new LinearLayout(mContext) {
1301 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1302 if (isOverflowAnimating()) {
1303 // Update widthMeasureSpec to make sure that this view is not clipped
1304 // as we offset it's coordinates with respect to it's parent.
1305 widthMeasureSpec = MeasureSpec.makeMeasureSpec(
1306 mMainPanelSize.getWidth(),
1307 MeasureSpec.EXACTLY);
1309 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1313 public boolean onInterceptTouchEvent(MotionEvent ev) {
1314 // Intercept the touch event while the overflow is animating.
1315 return isOverflowAnimating();
1321 private ImageButton createOverflowButton() {
1322 final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
1323 .inflate(R.layout.floating_popup_overflow_button, null);
1324 overflowButton.setImageDrawable(mOverflow);
1325 overflowButton.setOnClickListener(new View.OnClickListener() {
1327 public void onClick(View v) {
1328 if (mIsOverflowOpen) {
1329 overflowButton.setImageDrawable(mToOverflow);
1330 mToOverflow.start();
1333 overflowButton.setImageDrawable(mToArrow);
1339 return overflowButton;
1342 private OverflowPanel createOverflowPanel() {
1343 final OverflowPanel overflowPanel = new OverflowPanel(this);
1344 overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
1345 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1346 overflowPanel.setDivider(null);
1347 overflowPanel.setDividerHeight(0);
1349 final ArrayAdapter adapter =
1350 new ArrayAdapter<MenuItem>(mContext, 0) {
1352 public int getViewTypeCount() {
1353 return mOverflowPanelViewHelper.getViewTypeCount();
1357 public int getItemViewType(int position) {
1358 return mOverflowPanelViewHelper.getItemViewType(getItem(position));
1362 public View getView(int position, View convertView, ViewGroup parent) {
1363 return mOverflowPanelViewHelper.getView(
1364 getItem(position), mOverflowPanelSize.getWidth(), convertView);
1367 overflowPanel.setAdapter(adapter);
1369 overflowPanel.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1371 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1372 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
1373 if (mOnMenuItemClickListener != null) {
1374 mOnMenuItemClickListener.onMenuItemClick(menuItem);
1379 return overflowPanel;
1382 private boolean isOverflowAnimating() {
1383 final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
1384 && !mOpenOverflowAnimation.hasEnded();
1385 final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
1386 && !mCloseOverflowAnimation.hasEnded();
1387 return overflowOpening || overflowClosing;
1390 private Animation.AnimationListener createOverflowAnimationListener() {
1391 Animation.AnimationListener listener = new Animation.AnimationListener() {
1393 public void onAnimationStart(Animation animation) {
1394 // Disable the overflow button while it's animating.
1395 // It will be re-enabled when the animation stops.
1396 mOverflowButton.setEnabled(false);
1397 // Ensure both panels have visibility turned on when the overflow animation
1399 mMainPanel.setVisibility(View.VISIBLE);
1400 mOverflowPanel.setVisibility(View.VISIBLE);
1404 public void onAnimationEnd(Animation animation) {
1405 // Posting this because it seems like this is called before the animation
1407 mContentContainer.post(new Runnable() {
1410 setPanelsStatesAtRestingPosition();
1411 setContentAreaAsTouchableSurface();
1417 public void onAnimationRepeat(Animation animation) {
1423 private static Size measure(View view) {
1424 Preconditions.checkState(view.getParent() == null);
1425 view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1426 return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
1429 private static void setSize(View view, int width, int height) {
1430 view.setMinimumWidth(width);
1431 view.setMinimumHeight(height);
1432 ViewGroup.LayoutParams params = view.getLayoutParams();
1433 params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
1434 params.width = width;
1435 params.height = height;
1436 view.setLayoutParams(params);
1439 private static void setSize(View view, Size size) {
1440 setSize(view, size.getWidth(), size.getHeight());
1443 private static void setWidth(View view, int width) {
1444 ViewGroup.LayoutParams params = view.getLayoutParams();
1445 setSize(view, width, params.height);
1448 private static void setHeight(View view, int height) {
1449 ViewGroup.LayoutParams params = view.getLayoutParams();
1450 setSize(view, params.width, height);
1453 private static int getLineHeight(Context context) {
1454 return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1458 * A custom ListView for the overflow panel.
1460 private static final class OverflowPanel extends ListView {
1462 private final FloatingToolbarPopup mPopup;
1464 OverflowPanel(FloatingToolbarPopup popup) {
1465 super(Preconditions.checkNotNull(popup).mContext);
1466 this.mPopup = popup;
1467 setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
1468 setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
1472 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1473 // Update heightMeasureSpec to make sure that this view is not clipped
1474 // as we offset it's coordinates with respect to it's parent.
1475 int height = mPopup.mOverflowPanelSize.getHeight()
1476 - mPopup.mOverflowButtonSize.getHeight();
1477 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1478 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1482 public boolean dispatchTouchEvent(MotionEvent ev) {
1483 if (mPopup.isOverflowAnimating()) {
1484 // Eat the touch event.
1487 return super.dispatchTouchEvent(ev);
1491 protected boolean awakenScrollBars() {
1492 return super.awakenScrollBars();
1497 * A custom interpolator used for various floating toolbar animations.
1499 private static final class LogAccelerateInterpolator implements Interpolator {
1501 private static final int BASE = 100;
1502 private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
1504 private static float computeLog(float t, int base) {
1505 return (float) (1 - Math.pow(base, -t));
1509 public float getInterpolation(float t) {
1510 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1515 * A helper for generating views for the overflow panel.
1517 private static final class OverflowPanelViewHelper {
1519 private static final int NUM_OF_VIEW_TYPES = 2;
1520 private static final int VIEW_TYPE_STRING_TITLE = 0;
1521 private static final int VIEW_TYPE_ICON_ONLY = 1;
1523 private final TextView mStringTitleViewCalculator;
1524 private final View mIconOnlyViewCalculator;
1526 private final Context mContext;
1528 public OverflowPanelViewHelper(Context context) {
1529 mContext = Preconditions.checkNotNull(context);
1530 mStringTitleViewCalculator = getStringTitleView(null, 0, null);
1531 mIconOnlyViewCalculator = getIconOnlyView(null, 0, null);
1534 public int getViewTypeCount() {
1535 return NUM_OF_VIEW_TYPES;
1538 public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
1539 Preconditions.checkNotNull(menuItem);
1540 if (getItemViewType(menuItem) == VIEW_TYPE_ICON_ONLY) {
1541 return getIconOnlyView(menuItem, minimumWidth, convertView);
1543 return getStringTitleView(menuItem, minimumWidth, convertView);
1546 public int getItemViewType(MenuItem menuItem) {
1547 Preconditions.checkNotNull(menuItem);
1548 if (isIconOnlyMenuItem(menuItem)) {
1549 return VIEW_TYPE_ICON_ONLY;
1551 return VIEW_TYPE_STRING_TITLE;
1554 public int calculateWidth(MenuItem menuItem) {
1555 final View calculator;
1556 if (isIconOnlyMenuItem(menuItem)) {
1557 ((ImageView) mIconOnlyViewCalculator
1558 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1559 .setImageDrawable(menuItem.getIcon());
1560 calculator = mIconOnlyViewCalculator;
1562 mStringTitleViewCalculator.setText(menuItem.getTitle());
1563 calculator = mStringTitleViewCalculator;
1565 calculator.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1566 return calculator.getMeasuredWidth();
1569 private TextView getStringTitleView(
1570 MenuItem menuItem, int minimumWidth, View convertView) {
1571 TextView menuButton;
1572 if (convertView != null) {
1573 menuButton = (TextView) convertView;
1575 menuButton = (TextView) LayoutInflater.from(mContext)
1576 .inflate(R.layout.floating_popup_overflow_list_item, null);
1577 menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1578 ViewGroup.LayoutParams.MATCH_PARENT,
1579 ViewGroup.LayoutParams.WRAP_CONTENT));
1581 if (menuItem != null) {
1582 menuButton.setText(menuItem.getTitle());
1583 menuButton.setContentDescription(menuItem.getTitle());
1584 menuButton.setMinimumWidth(minimumWidth);
1589 private View getIconOnlyView(
1590 MenuItem menuItem, int minimumWidth, View convertView) {
1592 if (convertView != null) {
1593 menuButton = convertView;
1595 menuButton = LayoutInflater.from(mContext).inflate(
1596 R.layout.floating_popup_overflow_image_list_item, null);
1597 menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1598 ViewGroup.LayoutParams.WRAP_CONTENT,
1599 ViewGroup.LayoutParams.WRAP_CONTENT));
1601 if (menuItem != null) {
1602 ((ImageView) menuButton
1603 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1604 .setImageDrawable(menuItem.getIcon());
1605 menuButton.setMinimumWidth(minimumWidth);
1613 * @return {@code true} if the menu item does not not have a string title but has an icon.
1614 * {@code false} otherwise.
1616 private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1617 if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1624 * Creates and returns a menu button for the specified menu item.
1626 private static View createMenuItemButton(Context context, MenuItem menuItem) {
1627 if (isIconOnlyMenuItem(menuItem)) {
1628 View imageMenuItemButton = LayoutInflater.from(context)
1629 .inflate(R.layout.floating_popup_menu_image_button, null);
1630 ((ImageButton) imageMenuItemButton
1631 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1632 .setImageDrawable(menuItem.getIcon());
1633 return imageMenuItemButton;
1636 Button menuItemButton = (Button) LayoutInflater.from(context)
1637 .inflate(R.layout.floating_popup_menu_button, null);
1638 menuItemButton.setText(menuItem.getTitle());
1639 menuItemButton.setContentDescription(menuItem.getTitle());
1640 return menuItemButton;
1643 private static ViewGroup createContentContainer(Context context) {
1644 ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
1645 .inflate(R.layout.floating_popup_container, null);
1646 contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
1647 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1648 contentContainer.setTag(FLOATING_TOOLBAR_TAG);
1649 return contentContainer;
1652 private static PopupWindow createPopupWindow(ViewGroup content) {
1653 ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1654 PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1655 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1656 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1657 popupWindow.setClippingEnabled(false);
1658 popupWindow.setWindowLayoutType(
1659 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1660 popupWindow.setAnimationStyle(0);
1661 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1662 content.setLayoutParams(new ViewGroup.LayoutParams(
1663 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1664 popupContentHolder.addView(content);
1669 * Creates an "appear" animation for the specified view.
1671 * @param view The view to animate
1673 private static AnimatorSet createEnterAnimation(View view) {
1674 AnimatorSet animation = new AnimatorSet();
1675 animation.playTogether(
1676 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
1681 * Creates a "disappear" animation for the specified view.
1683 * @param view The view to animate
1684 * @param startDelay The start delay of the animation
1685 * @param listener The animation listener
1687 private static AnimatorSet createExitAnimation(
1688 View view, int startDelay, Animator.AnimatorListener listener) {
1689 AnimatorSet animation = new AnimatorSet();
1690 animation.playTogether(
1691 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
1692 animation.setStartDelay(startDelay);
1693 animation.addListener(listener);
1698 * Returns a re-themed context with controlled look and feel for views.
1700 private static Context applyDefaultTheme(Context originalContext) {
1701 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1702 boolean isLightTheme = a.getBoolean(0, true);
1703 int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material;
1705 return new ContextThemeWrapper(originalContext, themeId);