OSDN Git Service

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