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.ViewGroup;
45 import android.view.ViewTreeObserver;
46 import android.view.Window;
47 import android.view.WindowManager;
48 import android.view.animation.Animation;
49 import android.view.animation.AnimationSet;
50 import android.view.animation.Transformation;
51 import android.view.animation.AnimationUtils;
52 import android.view.animation.Interpolator;
53 import android.widget.AdapterView;
54 import android.widget.ArrayAdapter;
55 import android.widget.Button;
56 import android.widget.ImageButton;
57 import android.widget.ImageView;
58 import android.widget.LinearLayout;
59 import android.widget.ListView;
60 import android.widget.PopupWindow;
61 import android.widget.TextView;
63 import java.util.ArrayList;
64 import java.util.LinkedList;
65 import java.util.List;
67 import com.android.internal.R;
68 import com.android.internal.util.Preconditions;
71 * A floating toolbar for showing contextual menu items.
72 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
73 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
74 * The horizontal toolbar morphs into the vertical overflow view.
76 public final class FloatingToolbar {
78 // This class is responsible for the public API of the floating toolbar.
79 // It delegates rendering operations to the FloatingToolbarPopup.
81 public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
83 private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
84 new MenuItem.OnMenuItemClickListener() {
86 public boolean onMenuItemClick(MenuItem item) {
91 private final Context mContext;
92 private final Window mWindow;
93 private final FloatingToolbarPopup mPopup;
95 private final Rect mContentRect = new Rect();
96 private final Rect mPreviousContentRect = new Rect();
99 private List<Object> mShowingMenuItems = new ArrayList<Object>();
100 private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
102 private int mSuggestedWidth;
103 private boolean mWidthChanged = true;
105 private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
107 private final Rect mNewRect = new Rect();
108 private final Rect mOldRect = new Rect();
111 public void onLayoutChange(
113 int newLeft, int newRight, int newTop, int newBottom,
114 int oldLeft, int oldRight, int oldTop, int oldBottom) {
115 mNewRect.set(newLeft, newRight, newTop, newBottom);
116 mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
117 if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
118 mWidthChanged = true;
125 * Initializes a floating toolbar.
127 public FloatingToolbar(Context context, Window window) {
128 mContext = applyDefaultTheme(Preconditions.checkNotNull(context));
129 mWindow = Preconditions.checkNotNull(window);
130 mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
134 * Sets the menu to be shown in this floating toolbar.
135 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
138 public FloatingToolbar setMenu(Menu menu) {
139 mMenu = Preconditions.checkNotNull(menu);
144 * Sets the custom listener for invocation of menu items in this floating toolbar.
146 public FloatingToolbar setOnMenuItemClickListener(
147 MenuItem.OnMenuItemClickListener menuItemClickListener) {
148 if (menuItemClickListener != null) {
149 mMenuItemClickListener = menuItemClickListener;
151 mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
157 * Sets the content rectangle. This is the area of the interesting content that this toolbar
158 * should avoid obstructing.
159 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
162 public FloatingToolbar setContentRect(Rect rect) {
163 mContentRect.set(Preconditions.checkNotNull(rect));
168 * Sets the suggested width of this floating toolbar.
169 * The actual width will be about this size but there are no guarantees that it will be exactly
170 * the suggested width.
171 * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
174 public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
175 // Check if there's been a substantial width spec change.
176 int difference = Math.abs(suggestedWidth - mSuggestedWidth);
177 mWidthChanged = difference > (mSuggestedWidth * 0.2);
179 mSuggestedWidth = suggestedWidth;
184 * Shows this floating toolbar.
186 public FloatingToolbar show() {
187 registerOrientationHandler();
193 * Updates this floating toolbar to reflect recent position and view updates.
194 * NOTE: This method is a no-op if the toolbar isn't showing.
196 public FloatingToolbar updateLayout() {
197 if (mPopup.isShowing()) {
204 * Dismisses this floating toolbar.
206 public void dismiss() {
207 unregisterOrientationHandler();
212 * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
213 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
220 * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
222 public boolean isShowing() {
223 return mPopup.isShowing();
227 * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
229 public boolean isHidden() {
230 return mPopup.isHidden();
233 private void doShow() {
234 List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
235 if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
237 mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
238 mShowingMenuItems = getShowingMenuItemsReferences(menuItems);
240 if (!mPopup.isShowing()) {
241 mPopup.show(mContentRect);
242 } else if (!mPreviousContentRect.equals(mContentRect)) {
243 mPopup.updateCoordinates(mContentRect);
245 mWidthChanged = false;
246 mPreviousContentRect.set(mContentRect);
250 * Returns true if this floating toolbar is currently showing the specified menu items.
252 private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
253 return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems));
257 * Returns the visible and enabled menu items in the specified menu.
258 * This method is recursive.
260 private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
261 List<MenuItem> menuItems = new ArrayList<MenuItem>();
262 for (int i = 0; (menu != null) && (i < menu.size()); i++) {
263 MenuItem menuItem = menu.getItem(i);
264 if (menuItem.isVisible() && menuItem.isEnabled()) {
265 Menu subMenu = menuItem.getSubMenu();
266 if (subMenu != null) {
267 menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
269 menuItems.add(menuItem);
276 private List<Object> getShowingMenuItemsReferences(List<MenuItem> menuItems) {
277 List<Object> references = new ArrayList<Object>();
278 for (MenuItem menuItem : menuItems) {
279 if (isIconOnlyMenuItem(menuItem)) {
280 references.add(menuItem.getIcon());
282 references.add(menuItem.getTitle());
288 private void registerOrientationHandler() {
289 unregisterOrientationHandler();
290 mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
293 private void unregisterOrientationHandler() {
294 mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
299 * A popup window used by the floating toolbar.
301 * This class is responsible for the rendering/animation of the floating toolbar.
302 * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
303 * to transition between panels.
305 private static final class FloatingToolbarPopup {
307 /* Minimum and maximum number of items allowed in the overflow. */
308 private static final int MIN_OVERFLOW_SIZE = 2;
309 private static final int MAX_OVERFLOW_SIZE = 4;
311 /* The duration of the overflow button vector animation duration. */
312 private static final int OVERFLOW_BUTTON_ANIMATION_DELAY = 400;
314 private final Context mContext;
315 private final View mParent; // Parent for the popup window.
316 private final PopupWindow mPopupWindow;
318 /* Margins between the popup window and it's content. */
319 private final int mMarginHorizontal;
320 private final int mMarginVertical;
322 /* View components */
323 private final ViewGroup mContentContainer; // holds all contents.
324 private final ViewGroup mMainPanel; // holds menu items that are initially displayed.
325 private final OverflowPanel mOverflowPanel; // holds menu items hidden in the overflow.
326 private final ImageButton mOverflowButton; // opens/closes the overflow.
327 /* overflow button drawables. */
328 private final Drawable mArrow;
329 private final Drawable mOverflow;
330 private final AnimatedVectorDrawable mToArrow;
331 private final AnimatedVectorDrawable mToOverflow;
333 private final OverflowPanelViewHelper mOverflowPanelViewHelper;
335 /* Animation interpolators. */
336 private final Interpolator mLogAccelerateInterpolator;
337 private final Interpolator mFastOutSlowInInterpolator;
338 private final Interpolator mLinearOutSlowInInterpolator;
339 private final Interpolator mFastOutLinearInInterpolator;
342 private final AnimatorSet mShowAnimation;
343 private final AnimatorSet mDismissAnimation;
344 private final AnimatorSet mHideAnimation;
345 private final AnimationSet mOpenOverflowAnimation;
346 private final AnimationSet mCloseOverflowAnimation;
347 private final Animation.AnimationListener mOverflowAnimationListener;
349 private final Rect mViewPortOnScreen = new Rect(); // portion of screen we can draw in.
350 private final Point mCoordsOnWindow = new Point(); // popup window coordinates.
351 /* Temporary data holders. Reset values before using. */
352 private final int[] mTmpCoords = new int[2];
353 private final Rect mTmpRect = new Rect();
355 private final Region mTouchableRegion = new Region();
356 private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
357 new ViewTreeObserver.OnComputeInternalInsetsListener() {
358 public void onComputeInternalInsets(
359 ViewTreeObserver.InternalInsetsInfo info) {
360 info.contentInsets.setEmpty();
361 info.visibleInsets.setEmpty();
362 info.touchableRegion.set(mTouchableRegion);
363 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
364 .TOUCHABLE_INSETS_REGION);
369 * @see OverflowPanelViewHelper#preparePopupContent().
371 private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
374 setPanelsStatesAtRestingPosition();
375 setContentAreaAsTouchableSurface();
376 mContentContainer.setAlpha(1);
380 /* Runnable to reset the overflow button's drawable after an overflow transition. */
381 private final Runnable mResetOverflowButtonDrawable = new Runnable() {
384 if (mIsOverflowOpen) {
385 mOverflowButton.setImageDrawable(mArrow);
387 mOverflowButton.setImageDrawable(mOverflow);
392 private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
393 private boolean mHidden; // tracks whether this popup is hidden or hiding.
395 /* Calculated sizes for panels and overflow button. */
396 private final Size mOverflowButtonSize;
397 private Size mOverflowPanelSize; // Should be null when there is no overflow.
398 private Size mMainPanelSize;
400 /* Item click listeners */
401 private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
402 private final View.OnClickListener mMenuItemButtonOnClickListener =
403 new View.OnClickListener() {
405 public void onClick(View v) {
406 if (v.getTag() instanceof MenuItem) {
407 if (mOnMenuItemClickListener != null) {
408 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
414 private boolean mOpenOverflowUpwards; // Whether the overflow opens upwards or downwards.
415 private boolean mIsOverflowOpen;
417 private int mTransitionDurationScale; // Used to scale the toolbar transition duration.
420 * Initializes a new floating toolbar popup.
422 * @param parent A parent view to get the {@link android.view.View#getWindowToken()} token
425 public FloatingToolbarPopup(Context context, View parent) {
426 mParent = Preconditions.checkNotNull(parent);
427 mContext = Preconditions.checkNotNull(context);
428 mContentContainer = createContentContainer(context);
429 mPopupWindow = createPopupWindow(mContentContainer);
430 mMarginHorizontal = parent.getResources()
431 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
432 mMarginVertical = parent.getResources()
433 .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
436 mLogAccelerateInterpolator = new LogAccelerateInterpolator();
437 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
438 mContext, android.R.interpolator.fast_out_slow_in);
439 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
440 mContext, android.R.interpolator.linear_out_slow_in);
441 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
442 mContext, android.R.interpolator.fast_out_linear_in);
444 // Drawables. Needed for views.
445 mArrow = mContext.getResources()
446 .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
447 mArrow.setAutoMirrored(true);
448 mOverflow = mContext.getResources()
449 .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
450 mOverflow.setAutoMirrored(true);
451 mToArrow = (AnimatedVectorDrawable) mContext.getResources()
452 .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
453 mToArrow.setAutoMirrored(true);
454 mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
455 .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
456 mToOverflow.setAutoMirrored(true);
459 mOverflowButton = createOverflowButton();
460 mOverflowButtonSize = measure(mOverflowButton);
461 mMainPanel = createMainPanel();
462 mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext);
463 mOverflowPanel = createOverflowPanel();
465 // Animation. Need views.
466 mOverflowAnimationListener = createOverflowAnimationListener();
467 mOpenOverflowAnimation = new AnimationSet(true);
468 mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
469 mCloseOverflowAnimation = new AnimationSet(true);
470 mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
471 mShowAnimation = createEnterAnimation(mContentContainer);
472 mDismissAnimation = createExitAnimation(
475 new AnimatorListenerAdapter() {
477 public void onAnimationEnd(Animator animation) {
478 mPopupWindow.dismiss();
479 mContentContainer.removeAllViews();
482 mHideAnimation = createExitAnimation(
485 new AnimatorListenerAdapter() {
487 public void onAnimationEnd(Animator animation) {
488 mPopupWindow.dismiss();
494 * Lays out buttons for the specified menu items.
495 * Requires a subsequent call to {@link #show()} to show the items.
497 public void layoutMenuItems(
498 List<MenuItem> menuItems,
499 MenuItem.OnMenuItemClickListener menuItemClickListener,
500 int suggestedWidth) {
501 mOnMenuItemClickListener = menuItemClickListener;
502 cancelOverflowAnimations();
504 menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
505 if (!menuItems.isEmpty()) {
506 // Add remaining items to the overflow.
507 layoutOverflowPanelItems(menuItems);
513 * Shows this popup at the specified coordinates.
514 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
516 public void show(Rect contentRectOnScreen) {
517 Preconditions.checkNotNull(contentRectOnScreen);
525 cancelDismissAndHideAnimations();
526 cancelOverflowAnimations();
528 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
529 preparePopupContent();
530 // We need to specify the position in window coordinates.
531 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
532 // specify the popup position in screen coordinates.
533 mPopupWindow.showAtLocation(
534 mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
535 setTouchableSurfaceInsetsComputer();
540 * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
542 public void dismiss() {
549 mHideAnimation.cancel();
551 runDismissAnimation();
552 setZeroTouchableSurface();
556 * Hides this popup. This is a no-op if this popup is not showing.
557 * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
566 setZeroTouchableSurface();
570 * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
572 public boolean isShowing() {
573 return !mDismissed && !mHidden;
577 * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
579 public boolean isHidden() {
584 * Updates the coordinates of this popup.
585 * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
586 * This is a no-op if this popup is not showing.
588 public void updateCoordinates(Rect contentRectOnScreen) {
589 Preconditions.checkNotNull(contentRectOnScreen);
591 if (!isShowing() || !mPopupWindow.isShowing()) {
595 cancelOverflowAnimations();
596 refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
597 preparePopupContent();
598 // We need to specify the position in window coordinates.
599 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
600 // specify the popup position in screen coordinates.
602 mCoordsOnWindow.x, mCoordsOnWindow.y,
603 mPopupWindow.getWidth(), mPopupWindow.getHeight());
606 private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
609 int x = contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2;
610 // Update x so that the toolbar isn't rendered behind the nav bar in landscape.
611 x = Math.max(0, Math.min(x, mViewPortOnScreen.right - mPopupWindow.getWidth()));
615 final int availableHeightAboveContent =
616 contentRectOnScreen.top - mViewPortOnScreen.top;
617 final int availableHeightBelowContent =
618 mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
620 final int margin = 2 * mMarginVertical;
621 final int toolbarHeightWithVerticalMargin = getLineHeight(mContext) + margin;
623 if (!hasOverflow()) {
624 if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
625 // There is enough space at the top of the content.
626 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
627 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
628 // There is enough space at the bottom of the content.
629 y = contentRectOnScreen.bottom;
630 } else if (availableHeightBelowContent >= getLineHeight(mContext)) {
631 // Just enough space to fit the toolbar with no vertical margins.
632 y = contentRectOnScreen.bottom - mMarginVertical;
634 // Not enough space. Prefer to position as high as possible.
636 mViewPortOnScreen.top,
637 contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
641 final int minimumOverflowHeightWithMargin =
642 calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
643 final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
644 contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
645 final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
646 mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
648 if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
649 // There is enough space at the top of the content rect for the overflow.
650 // Position above and open upwards.
651 updateOverflowHeight(availableHeightAboveContent - margin);
652 y = contentRectOnScreen.top - mPopupWindow.getHeight();
653 mOpenOverflowUpwards = true;
654 } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
655 && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
656 // There is enough space at the top of the content rect for the main panel
657 // but not the overflow.
658 // Position above but open downwards.
659 updateOverflowHeight(availableHeightThroughContentDown - margin);
660 y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
661 mOpenOverflowUpwards = false;
662 } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
663 // There is enough space at the bottom of the content rect for the overflow.
664 // Position below and open downwards.
665 updateOverflowHeight(availableHeightBelowContent - margin);
666 y = contentRectOnScreen.bottom;
667 mOpenOverflowUpwards = false;
668 } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
669 && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
670 // There is enough space at the bottom of the content rect for the main panel
671 // but not the overflow.
672 // Position below but open upwards.
673 updateOverflowHeight(availableHeightThroughContentUp - margin);
674 y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
675 mPopupWindow.getHeight();
676 mOpenOverflowUpwards = true;
679 // Position at the top of the view port and open downwards.
680 updateOverflowHeight(mViewPortOnScreen.height() - margin);
681 y = mViewPortOnScreen.top;
682 mOpenOverflowUpwards = false;
686 // We later specify the location of PopupWindow relative to the attached window.
687 // The idea here is that 1) we can get the location of a View in both window coordinates
688 // and screen coordiantes, where the offset between them should be equal to the window
689 // origin, and 2) we can use an arbitrary for this calculation while calculating the
690 // location of the rootview is supposed to be least expensive.
691 // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid
692 // the following calculation.
693 mParent.getRootView().getLocationOnScreen(mTmpCoords);
694 int rootViewLeftOnScreen = mTmpCoords[0];
695 int rootViewTopOnScreen = mTmpCoords[1];
696 mParent.getRootView().getLocationInWindow(mTmpCoords);
697 int rootViewLeftOnWindow = mTmpCoords[0];
698 int rootViewTopOnWindow = mTmpCoords[1];
699 int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
700 int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
701 mCoordsOnWindow.set(x - windowLeftOnScreen, y - windowTopOnScreen);
705 * Performs the "show" animation on the floating popup.
707 private void runShowAnimation() {
708 mShowAnimation.start();
712 * Performs the "dismiss" animation on the floating popup.
714 private void runDismissAnimation() {
715 mDismissAnimation.start();
719 * Performs the "hide" animation on the floating popup.
721 private void runHideAnimation() {
722 mHideAnimation.start();
725 private void cancelDismissAndHideAnimations() {
726 mDismissAnimation.cancel();
727 mHideAnimation.cancel();
730 private void cancelOverflowAnimations() {
731 mContentContainer.clearAnimation();
732 mMainPanel.animate().cancel();
733 mOverflowPanel.animate().cancel();
738 private void openOverflow() {
739 final int targetWidth = mOverflowPanelSize.getWidth();
740 final int targetHeight = mOverflowPanelSize.getHeight();
741 final int startWidth = mContentContainer.getWidth();
742 final int startHeight = mContentContainer.getHeight();
743 final float startY = mContentContainer.getY();
744 final float left = mContentContainer.getX();
745 final float right = left + mContentContainer.getWidth();
746 Animation widthAnimation = new Animation() {
748 protected void applyTransformation(float interpolatedTime, Transformation t) {
749 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
750 setWidth(mContentContainer, startWidth + deltaWidth);
752 mContentContainer.setX(left);
754 // Lock the panels in place.
756 mOverflowPanel.setX(0);
758 mContentContainer.setX(right - mContentContainer.getWidth());
760 // Offset the panels' positions so they look like they're locked in place
762 mMainPanel.setX(mContentContainer.getWidth() - startWidth);
763 mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
767 Animation heightAnimation = new Animation() {
769 protected void applyTransformation(float interpolatedTime, Transformation t) {
770 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
771 setHeight(mContentContainer, startHeight + deltaHeight);
772 if (mOpenOverflowUpwards) {
773 mContentContainer.setY(
774 startY - (mContentContainer.getHeight() - startHeight));
775 positionContentYCoordinatesIfOpeningOverflowUpwards();
779 final float overflowButtonStartX = mOverflowButton.getX();
780 final float overflowButtonTargetX = isRTL() ?
781 overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
782 overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
783 Animation overflowButtonAnimation = new Animation() {
785 protected void applyTransformation(float interpolatedTime, Transformation t) {
786 float overflowButtonX = overflowButtonStartX
787 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
788 float deltaContainerWidth = isRTL() ?
790 mContentContainer.getWidth() - startWidth;
791 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
792 mOverflowButton.setX(actualOverflowButtonX);
795 widthAnimation.setInterpolator(mLogAccelerateInterpolator);
796 widthAnimation.setDuration(getAdjustedDuration(250));
797 heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
798 heightAnimation.setDuration(getAdjustedDuration(250));
799 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
800 overflowButtonAnimation.setDuration(getAdjustedDuration(250));
801 mOpenOverflowAnimation.getAnimations().clear();
802 mOpenOverflowAnimation.getAnimations().clear();
803 mOpenOverflowAnimation.addAnimation(widthAnimation);
804 mOpenOverflowAnimation.addAnimation(heightAnimation);
805 mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
806 mContentContainer.startAnimation(mOpenOverflowAnimation);
807 mIsOverflowOpen = true;
809 .alpha(0).withLayer()
810 .setInterpolator(mLinearOutSlowInInterpolator)
813 mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
816 private void closeOverflow() {
817 final int targetWidth = mMainPanelSize.getWidth();
818 final int startWidth = mContentContainer.getWidth();
819 final float left = mContentContainer.getX();
820 final float right = left + mContentContainer.getWidth();
821 Animation widthAnimation = new Animation() {
823 protected void applyTransformation(float interpolatedTime, Transformation t) {
824 int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
825 setWidth(mContentContainer, startWidth + deltaWidth);
827 mContentContainer.setX(left);
829 // Lock the panels in place.
831 mOverflowPanel.setX(0);
833 mContentContainer.setX(right - mContentContainer.getWidth());
835 // Offset the panels' positions so they look like they're locked in place
837 mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
838 mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
842 final int targetHeight = mMainPanelSize.getHeight();
843 final int startHeight = mContentContainer.getHeight();
844 final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
845 Animation heightAnimation = new Animation() {
847 protected void applyTransformation(float interpolatedTime, Transformation t) {
848 int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
849 setHeight(mContentContainer, startHeight + deltaHeight);
850 if (mOpenOverflowUpwards) {
851 mContentContainer.setY(bottom - mContentContainer.getHeight());
852 positionContentYCoordinatesIfOpeningOverflowUpwards();
856 final float overflowButtonStartX = mOverflowButton.getX();
857 final float overflowButtonTargetX = isRTL() ?
858 overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
859 overflowButtonStartX + startWidth - mOverflowButton.getWidth();
860 Animation overflowButtonAnimation = new Animation() {
862 protected void applyTransformation(float interpolatedTime, Transformation t) {
863 float overflowButtonX = overflowButtonStartX
864 + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
865 float deltaContainerWidth = isRTL() ?
867 mContentContainer.getWidth() - startWidth;
868 float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
869 mOverflowButton.setX(actualOverflowButtonX);
872 widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
873 widthAnimation.setDuration(getAdjustedDuration(250));
874 heightAnimation.setInterpolator(mLogAccelerateInterpolator);
875 heightAnimation.setDuration(getAdjustedDuration(250));
876 overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
877 overflowButtonAnimation.setDuration(getAdjustedDuration(250));
878 mCloseOverflowAnimation.getAnimations().clear();
879 mCloseOverflowAnimation.addAnimation(widthAnimation);
880 mCloseOverflowAnimation.addAnimation(heightAnimation);
881 mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
882 mContentContainer.startAnimation(mCloseOverflowAnimation);
883 mIsOverflowOpen = false;
885 .alpha(1).withLayer()
886 .setInterpolator(mFastOutLinearInInterpolator)
889 mOverflowPanel.animate()
890 .alpha(0).withLayer()
891 .setInterpolator(mLinearOutSlowInInterpolator)
896 private void setPanelsStatesAtRestingPosition() {
897 mOverflowButton.setEnabled(true);
898 mOverflowPanel.awakenScrollBars();
900 if (mIsOverflowOpen) {
902 final Size containerSize = mOverflowPanelSize;
903 setSize(mContentContainer, containerSize);
904 mMainPanel.setAlpha(0);
905 mOverflowPanel.setAlpha(1);
906 mOverflowButton.setImageDrawable(mArrow);
908 // Update x-coordinates depending on RTL state.
910 mContentContainer.setX(mMarginHorizontal); // align left
911 mMainPanel.setX(0); // align left
912 mOverflowButton.setX( // align right
913 containerSize.getWidth() - mOverflowButtonSize.getWidth());
914 mOverflowPanel.setX(0); // align left
916 mContentContainer.setX( // align right
918 mMainPanelSize.getWidth() - containerSize.getWidth());
919 mMainPanel.setX(-mContentContainer.getX()); // align right
920 mOverflowButton.setX(0); // align left
921 mOverflowPanel.setX(0); // align left
924 // Update y-coordinates depending on overflow's open direction.
925 if (mOpenOverflowUpwards) {
926 mContentContainer.setY(mMarginVertical); // align top
927 mMainPanel.setY( // align bottom
928 containerSize.getHeight() - mContentContainer.getHeight());
929 mOverflowButton.setY( // align bottom
930 containerSize.getHeight() - mOverflowButtonSize.getHeight());
931 mOverflowPanel.setY(0); // align top
934 mContentContainer.setY(mMarginVertical); // align top
935 mMainPanel.setY(0); // align top
936 mOverflowButton.setY(0); // align top
937 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
940 // Overflow not open. Set closed state.
941 final Size containerSize = mMainPanelSize;
942 setSize(mContentContainer, containerSize);
943 mMainPanel.setAlpha(1);
944 mOverflowPanel.setAlpha(0);
945 mOverflowButton.setImageDrawable(mOverflow);
948 // Update x-coordinates depending on RTL state.
950 mContentContainer.setX(mMarginHorizontal); // align left
951 mMainPanel.setX(0); // align left
952 mOverflowButton.setX(0); // align left
953 mOverflowPanel.setX(0); // align left
955 mContentContainer.setX(mMarginHorizontal); // align left
956 mMainPanel.setX(0); // align left
957 mOverflowButton.setX( // align right
958 containerSize.getWidth() - mOverflowButtonSize.getWidth());
959 mOverflowPanel.setX( // align right
960 containerSize.getWidth() - mOverflowPanelSize.getWidth());
963 // Update y-coordinates depending on overflow's open direction.
964 if (mOpenOverflowUpwards) {
965 mContentContainer.setY( // align bottom
967 mOverflowPanelSize.getHeight() - containerSize.getHeight());
968 mMainPanel.setY(0); // align top
969 mOverflowButton.setY(0); // align top
970 mOverflowPanel.setY( // align bottom
971 containerSize.getHeight() - mOverflowPanelSize.getHeight());
974 mContentContainer.setY(mMarginVertical); // align top
975 mMainPanel.setY(0); // align top
976 mOverflowButton.setY(0); // align top
977 mOverflowPanel.setY(mOverflowButtonSize.getHeight()); // align bottom
981 mContentContainer.setX(mMarginHorizontal); // align left
982 mContentContainer.setY(mMarginVertical); // align top
983 mMainPanel.setX(0); // align left
984 mMainPanel.setY(0); // align top
989 private void updateOverflowHeight(int suggestedHeight) {
991 final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
992 getLineHeight(mContext);
993 final int newHeight = calculateOverflowHeight(maxItemSize);
994 if (mOverflowPanelSize.getHeight() != newHeight) {
995 mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
997 setSize(mOverflowPanel, mOverflowPanelSize);
998 if (mIsOverflowOpen) {
999 setSize(mContentContainer, mOverflowPanelSize);
1000 if (mOpenOverflowUpwards) {
1001 final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
1002 mContentContainer.setY(mContentContainer.getY() + deltaHeight);
1003 mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
1006 setSize(mContentContainer, mMainPanelSize);
1012 private void updatePopupSize() {
1015 if (mMainPanelSize != null) {
1016 width = Math.max(width, mMainPanelSize.getWidth());
1017 height = Math.max(height, mMainPanelSize.getHeight());
1019 if (mOverflowPanelSize != null) {
1020 width = Math.max(width, mOverflowPanelSize.getWidth());
1021 height = Math.max(height, mOverflowPanelSize.getHeight());
1023 mPopupWindow.setWidth(width + mMarginHorizontal * 2);
1024 mPopupWindow.setHeight(height + mMarginVertical * 2);
1025 maybeComputeTransitionDurationScale();
1028 private void refreshViewPort() {
1029 mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
1032 private int getAdjustedToolbarWidth(int suggestedWidth) {
1033 int width = suggestedWidth;
1035 int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
1036 .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1038 width = mParent.getResources()
1039 .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1041 return Math.min(width, maximumWidth);
1045 * Sets the touchable region of this popup to be zero. This means that all touch events on
1046 * this popup will go through to the surface behind it.
1048 private void setZeroTouchableSurface() {
1049 mTouchableRegion.setEmpty();
1053 * Sets the touchable region of this popup to be the area occupied by its content.
1055 private void setContentAreaAsTouchableSurface() {
1056 Preconditions.checkNotNull(mMainPanelSize);
1059 if (mIsOverflowOpen) {
1060 Preconditions.checkNotNull(mOverflowPanelSize);
1061 width = mOverflowPanelSize.getWidth();
1062 height = mOverflowPanelSize.getHeight();
1064 width = mMainPanelSize.getWidth();
1065 height = mMainPanelSize.getHeight();
1067 mTouchableRegion.set(
1068 (int) mContentContainer.getX(),
1069 (int) mContentContainer.getY(),
1070 (int) mContentContainer.getX() + width,
1071 (int) mContentContainer.getY() + height);
1075 * Make the touchable area of this popup be the area specified by mTouchableRegion.
1076 * This should be called after the popup window has been dismissed (dismiss/hide)
1077 * and is probably being re-shown with a new content root view.
1079 private void setTouchableSurfaceInsetsComputer() {
1080 ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
1082 .getViewTreeObserver();
1083 viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
1084 viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
1087 private boolean isRTL() {
1088 return mContext.getResources().getConfiguration().getLayoutDirection()
1089 == View.LAYOUT_DIRECTION_RTL;
1092 private boolean hasOverflow() {
1093 return mOverflowPanelSize != null;
1097 * Fits as many menu items in the main panel and returns a list of the menu items that
1100 * @return The menu items that are not included in this main panel.
1102 public List<MenuItem> layoutMainPanelItems(
1103 List<MenuItem> menuItems, final int toolbarWidth) {
1104 Preconditions.checkNotNull(menuItems);
1106 int availableWidth = toolbarWidth;
1107 final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
1109 mMainPanel.removeAllViews();
1110 mMainPanel.setPaddingRelative(0, 0, 0, 0);
1112 boolean isFirstItem = true;
1113 while (!remainingMenuItems.isEmpty()) {
1114 final MenuItem menuItem = remainingMenuItems.peek();
1115 View menuItemButton = createMenuItemButton(mContext, menuItem);
1117 // Adding additional start padding for the first button to even out button spacing.
1119 menuItemButton.setPaddingRelative(
1120 (int) (1.5 * menuItemButton.getPaddingStart()),
1121 menuItemButton.getPaddingTop(),
1122 menuItemButton.getPaddingEnd(),
1123 menuItemButton.getPaddingBottom());
1124 isFirstItem = false;
1127 // Adding additional end padding for the last button to even out button spacing.
1128 if (remainingMenuItems.size() == 1) {
1129 menuItemButton.setPaddingRelative(
1130 menuItemButton.getPaddingStart(),
1131 menuItemButton.getPaddingTop(),
1132 (int) (1.5 * menuItemButton.getPaddingEnd()),
1133 menuItemButton.getPaddingBottom());
1136 menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1137 int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
1138 // Check if we can fit an item while reserving space for the overflowButton.
1139 boolean canFitWithOverflow =
1140 menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth();
1141 boolean canFitNoOverflow =
1142 remainingMenuItems.size() == 1 && menuItemButtonWidth <= availableWidth;
1143 if (canFitWithOverflow || canFitNoOverflow) {
1144 setButtonTagAndClickListener(menuItemButton, menuItem);
1145 mMainPanel.addView(menuItemButton);
1146 ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1147 params.width = menuItemButtonWidth;
1148 menuItemButton.setLayoutParams(params);
1149 availableWidth -= menuItemButtonWidth;
1150 remainingMenuItems.pop();
1152 // Reserve space for overflowButton.
1153 mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1157 mMainPanelSize = measure(mMainPanel);
1158 return remainingMenuItems;
1161 private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
1162 ArrayAdapter<MenuItem> overflowPanelAdapter =
1163 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1164 overflowPanelAdapter.clear();
1165 final int size = menuItems.size();
1166 for (int i = 0; i < size; i++) {
1167 overflowPanelAdapter.add(menuItems.get(i));
1169 mOverflowPanel.setAdapter(overflowPanelAdapter);
1170 if (mOpenOverflowUpwards) {
1171 mOverflowPanel.setY(0);
1173 mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1176 int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
1177 int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
1178 mOverflowPanelSize = new Size(width, height);
1179 setSize(mOverflowPanel, mOverflowPanelSize);
1183 * Resets the content container and appropriately position it's panels.
1185 private void preparePopupContent() {
1186 mContentContainer.removeAllViews();
1188 // Add views in the specified order so they stack up as expected.
1189 // Order: overflowPanel, mainPanel, overflowButton.
1190 if (hasOverflow()) {
1191 mContentContainer.addView(mOverflowPanel);
1193 mContentContainer.addView(mMainPanel);
1194 if (hasOverflow()) {
1195 mContentContainer.addView(mOverflowButton);
1197 setPanelsStatesAtRestingPosition();
1198 setContentAreaAsTouchableSurface();
1200 // The positioning of contents in RTL is wrong when the view is first rendered.
1201 // Hide the view and post a runnable to recalculate positions and render the view.
1202 // TODO: Investigate why this happens and fix.
1204 mContentContainer.setAlpha(0);
1205 mContentContainer.post(mPreparePopupContentRTLHelper);
1210 * Clears out the panels and their container. Resets their calculated sizes.
1212 private void clearPanels() {
1213 mOverflowPanelSize = null;
1214 mMainPanelSize = null;
1215 mIsOverflowOpen = false;
1216 mMainPanel.removeAllViews();
1217 ArrayAdapter<MenuItem> overflowPanelAdapter =
1218 (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1219 overflowPanelAdapter.clear();
1220 mOverflowPanel.setAdapter(overflowPanelAdapter);
1221 mContentContainer.removeAllViews();
1224 private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
1225 if (mOpenOverflowUpwards) {
1226 mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
1227 mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
1228 mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
1232 private int getOverflowWidth() {
1233 int overflowWidth = 0;
1234 final int count = mOverflowPanel.getAdapter().getCount();
1235 for (int i = 0; i < count; i++) {
1236 MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
1238 Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1240 return overflowWidth;
1243 private int calculateOverflowHeight(int maxItemSize) {
1244 // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
1245 int actualSize = Math.min(
1248 Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1249 mOverflowPanel.getCount()));
1250 return actualSize * getLineHeight(mContext) + mOverflowButtonSize.getHeight();
1253 private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1254 View button = menuItemButton;
1255 if (isIconOnlyMenuItem(menuItem)) {
1256 button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1258 button.setTag(menuItem);
1259 button.setOnClickListener(mMenuItemButtonOnClickListener);
1263 * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1264 * animations. See comment about this in the code.
1266 private int getAdjustedDuration(int originalDuration) {
1267 if (mTransitionDurationScale < 150) {
1268 // For smaller transition, decrease the time.
1269 return Math.max(originalDuration - 50, 0);
1270 } else if (mTransitionDurationScale > 300) {
1271 // For bigger transition, increase the time.
1272 return originalDuration + 50;
1275 // Scale the animation duration with getDurationScale(). This allows
1276 // android.view.animation.* animations to scale just like android.animation.* animations
1277 // when animator duration scale is adjusted in "Developer Options".
1278 // For this reason, do not use this method for android.animation.* animations.
1279 return (int) (originalDuration * ValueAnimator.getDurationScale());
1282 private void maybeComputeTransitionDurationScale() {
1283 if (mMainPanelSize != null && mOverflowPanelSize != null) {
1284 int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
1285 int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
1286 mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
1287 mContentContainer.getContext().getResources().getDisplayMetrics().density);
1291 private ViewGroup createMainPanel() {
1292 ViewGroup mainPanel = new LinearLayout(mContext) {
1294 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1295 if (isOverflowAnimating()) {
1296 // Update widthMeasureSpec to make sure that this view is not clipped
1297 // as we offset it's coordinates with respect to it's parent.
1298 widthMeasureSpec = MeasureSpec.makeMeasureSpec(
1299 mMainPanelSize.getWidth(),
1300 MeasureSpec.EXACTLY);
1302 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1306 public boolean onInterceptTouchEvent(MotionEvent ev) {
1307 // Intercept the touch event while the overflow is animating.
1308 return isOverflowAnimating();
1314 private ImageButton createOverflowButton() {
1315 final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
1316 .inflate(R.layout.floating_popup_overflow_button, null);
1317 overflowButton.setImageDrawable(mOverflow);
1318 overflowButton.setOnClickListener(new View.OnClickListener() {
1320 public void onClick(View v) {
1321 if (mIsOverflowOpen) {
1322 overflowButton.setImageDrawable(mToOverflow);
1323 mToOverflow.start();
1326 overflowButton.setImageDrawable(mToArrow);
1330 overflowButton.postDelayed(
1331 mResetOverflowButtonDrawable, OVERFLOW_BUTTON_ANIMATION_DELAY);
1334 return overflowButton;
1337 private OverflowPanel createOverflowPanel() {
1338 final OverflowPanel overflowPanel = new OverflowPanel(this);
1339 overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
1340 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1341 overflowPanel.setDivider(null);
1342 overflowPanel.setDividerHeight(0);
1344 final ArrayAdapter adapter =
1345 new ArrayAdapter<MenuItem>(mContext, 0) {
1347 public int getViewTypeCount() {
1348 return mOverflowPanelViewHelper.getViewTypeCount();
1352 public int getItemViewType(int position) {
1353 return mOverflowPanelViewHelper.getItemViewType(getItem(position));
1357 public View getView(int position, View convertView, ViewGroup parent) {
1358 return mOverflowPanelViewHelper.getView(
1359 getItem(position), mOverflowPanelSize.getWidth(), convertView);
1362 overflowPanel.setAdapter(adapter);
1364 overflowPanel.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1366 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1367 MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
1368 if (mOnMenuItemClickListener != null) {
1369 mOnMenuItemClickListener.onMenuItemClick(menuItem);
1374 return overflowPanel;
1377 private boolean isOverflowAnimating() {
1378 final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
1379 && !mOpenOverflowAnimation.hasEnded();
1380 final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
1381 && !mCloseOverflowAnimation.hasEnded();
1382 return overflowOpening || overflowClosing;
1385 private Animation.AnimationListener createOverflowAnimationListener() {
1386 Animation.AnimationListener listener = new Animation.AnimationListener() {
1388 public void onAnimationStart(Animation animation) {
1389 // Disable the overflow button while it's animating.
1390 // It will be re-enabled when the animation stops.
1391 mOverflowButton.setEnabled(false);
1395 public void onAnimationEnd(Animation animation) {
1396 // Posting this because it seems like this is called before the animation
1398 mContentContainer.post(new Runnable() {
1401 setPanelsStatesAtRestingPosition();
1402 setContentAreaAsTouchableSurface();
1408 public void onAnimationRepeat(Animation animation) {
1414 private static Size measure(View view) {
1415 Preconditions.checkState(view.getParent() == null);
1416 view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1417 return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
1420 private static void setSize(View view, int width, int height) {
1421 view.setMinimumWidth(width);
1422 view.setMinimumHeight(height);
1423 ViewGroup.LayoutParams params = view.getLayoutParams();
1424 params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
1425 params.width = width;
1426 params.height = height;
1427 view.setLayoutParams(params);
1430 private static void setSize(View view, Size size) {
1431 setSize(view, size.getWidth(), size.getHeight());
1434 private static void setWidth(View view, int width) {
1435 ViewGroup.LayoutParams params = view.getLayoutParams();
1436 setSize(view, width, params.height);
1439 private static void setHeight(View view, int height) {
1440 ViewGroup.LayoutParams params = view.getLayoutParams();
1441 setSize(view, params.width, height);
1444 private static int getLineHeight(Context context) {
1445 return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1449 * A custom ListView for the overflow panel.
1451 private static final class OverflowPanel extends ListView {
1453 private final FloatingToolbarPopup mPopup;
1455 OverflowPanel(FloatingToolbarPopup popup) {
1456 super(Preconditions.checkNotNull(popup).mContext);
1457 this.mPopup = popup;
1461 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1462 // Update heightMeasureSpec to make sure that this view is not clipped
1463 // as we offset it's coordinates with respect to it's parent.
1464 int height = mPopup.mOverflowPanelSize.getHeight()
1465 - mPopup.mOverflowButtonSize.getHeight();
1466 heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1467 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1471 public boolean dispatchTouchEvent(MotionEvent ev) {
1472 if (mPopup.isOverflowAnimating()) {
1473 // Eat the touch event.
1476 return super.dispatchTouchEvent(ev);
1480 protected boolean awakenScrollBars() {
1481 return super.awakenScrollBars();
1486 * A custom interpolator used for various floating toolbar animations.
1488 private static final class LogAccelerateInterpolator implements Interpolator {
1490 private static final int BASE = 100;
1491 private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
1493 private static float computeLog(float t, int base) {
1494 return (float) (1 - Math.pow(base, -t));
1498 public float getInterpolation(float t) {
1499 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1504 * A helper for generating views for the overflow panel.
1506 private static final class OverflowPanelViewHelper {
1508 private static final int NUM_OF_VIEW_TYPES = 2;
1509 private static final int VIEW_TYPE_STRING_TITLE = 0;
1510 private static final int VIEW_TYPE_ICON_ONLY = 1;
1512 private final TextView mStringTitleViewCalculator;
1513 private final View mIconOnlyViewCalculator;
1515 private final Context mContext;
1517 public OverflowPanelViewHelper(Context context) {
1518 mContext = Preconditions.checkNotNull(context);
1519 mStringTitleViewCalculator = getStringTitleView(null, 0, null);
1520 mIconOnlyViewCalculator = getIconOnlyView(null, 0, null);
1523 public int getViewTypeCount() {
1524 return NUM_OF_VIEW_TYPES;
1527 public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
1528 Preconditions.checkNotNull(menuItem);
1529 if (getItemViewType(menuItem) == VIEW_TYPE_ICON_ONLY) {
1530 return getIconOnlyView(menuItem, minimumWidth, convertView);
1532 return getStringTitleView(menuItem, minimumWidth, convertView);
1535 public int getItemViewType(MenuItem menuItem) {
1536 Preconditions.checkNotNull(menuItem);
1537 if (isIconOnlyMenuItem(menuItem)) {
1538 return VIEW_TYPE_ICON_ONLY;
1540 return VIEW_TYPE_STRING_TITLE;
1543 public int calculateWidth(MenuItem menuItem) {
1544 final View calculator;
1545 if (isIconOnlyMenuItem(menuItem)) {
1546 ((ImageView) mIconOnlyViewCalculator
1547 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1548 .setImageDrawable(menuItem.getIcon());
1549 calculator = mIconOnlyViewCalculator;
1551 mStringTitleViewCalculator.setText(menuItem.getTitle());
1552 calculator = mStringTitleViewCalculator;
1554 calculator.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1555 return calculator.getMeasuredWidth();
1558 private TextView getStringTitleView(
1559 MenuItem menuItem, int minimumWidth, View convertView) {
1560 TextView menuButton;
1561 if (convertView != null) {
1562 menuButton = (TextView) convertView;
1564 menuButton = (TextView) LayoutInflater.from(mContext)
1565 .inflate(R.layout.floating_popup_overflow_list_item, null);
1566 menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1567 ViewGroup.LayoutParams.MATCH_PARENT,
1568 ViewGroup.LayoutParams.WRAP_CONTENT));
1570 if (menuItem != null) {
1571 menuButton.setText(menuItem.getTitle());
1572 menuButton.setContentDescription(menuItem.getTitle());
1573 menuButton.setMinimumWidth(minimumWidth);
1578 private View getIconOnlyView(
1579 MenuItem menuItem, int minimumWidth, View convertView) {
1581 if (convertView != null) {
1582 menuButton = convertView;
1584 menuButton = LayoutInflater.from(mContext).inflate(
1585 R.layout.floating_popup_overflow_image_list_item, null);
1586 menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1587 ViewGroup.LayoutParams.WRAP_CONTENT,
1588 ViewGroup.LayoutParams.WRAP_CONTENT));
1590 if (menuItem != null) {
1591 ((ImageView) menuButton
1592 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1593 .setImageDrawable(menuItem.getIcon());
1594 menuButton.setMinimumWidth(minimumWidth);
1602 * @return {@code true} if the menu item does not not have a string title but has an icon.
1603 * {@code false} otherwise.
1605 private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1606 if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1613 * Creates and returns a menu button for the specified menu item.
1615 private static View createMenuItemButton(Context context, MenuItem menuItem) {
1616 if (isIconOnlyMenuItem(menuItem)) {
1617 View imageMenuItemButton = LayoutInflater.from(context)
1618 .inflate(R.layout.floating_popup_menu_image_button, null);
1619 ((ImageButton) imageMenuItemButton
1620 .findViewById(R.id.floating_toolbar_menu_item_image_button))
1621 .setImageDrawable(menuItem.getIcon());
1622 return imageMenuItemButton;
1625 Button menuItemButton = (Button) LayoutInflater.from(context)
1626 .inflate(R.layout.floating_popup_menu_button, null);
1627 menuItemButton.setText(menuItem.getTitle());
1628 menuItemButton.setContentDescription(menuItem.getTitle());
1629 return menuItemButton;
1632 private static ViewGroup createContentContainer(Context context) {
1633 ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
1634 .inflate(R.layout.floating_popup_container, null);
1635 contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
1636 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1637 contentContainer.setTag(FLOATING_TOOLBAR_TAG);
1638 return contentContainer;
1641 private static PopupWindow createPopupWindow(ViewGroup content) {
1642 ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1643 PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1644 // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1645 // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1646 popupWindow.setClippingEnabled(false);
1647 popupWindow.setWindowLayoutType(
1648 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1649 popupWindow.setAnimationStyle(0);
1650 popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1651 content.setLayoutParams(new ViewGroup.LayoutParams(
1652 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1653 popupContentHolder.addView(content);
1658 * Creates an "appear" animation for the specified view.
1660 * @param view The view to animate
1662 private static AnimatorSet createEnterAnimation(View view) {
1663 AnimatorSet animation = new AnimatorSet();
1664 animation.playTogether(
1665 ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
1670 * Creates a "disappear" animation for the specified view.
1672 * @param view The view to animate
1673 * @param startDelay The start delay of the animation
1674 * @param listener The animation listener
1676 private static AnimatorSet createExitAnimation(
1677 View view, int startDelay, Animator.AnimatorListener listener) {
1678 AnimatorSet animation = new AnimatorSet();
1679 animation.playTogether(
1680 ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
1681 animation.setStartDelay(startDelay);
1682 animation.addListener(listener);
1687 * Returns a re-themed context with controlled look and feel for views.
1689 private static Context applyDefaultTheme(Context originalContext) {
1690 TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1691 boolean isLightTheme = a.getBoolean(0, true);
1692 int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material;
1694 return new ContextThemeWrapper(originalContext, themeId);