OSDN Git Service

resolve merge conflicts of 51e4acb29d to nyc-dev
[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.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;
62
63 import java.util.ArrayList;
64 import java.util.LinkedList;
65 import java.util.List;
66
67 import com.android.internal.R;
68 import com.android.internal.util.Preconditions;
69
70 /**
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.
75  */
76 public final class FloatingToolbar {
77
78     // This class is responsible for the public API of the floating toolbar.
79     // It delegates rendering operations to the FloatingToolbarPopup.
80
81     public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
82
83     private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
84             new MenuItem.OnMenuItemClickListener() {
85                 @Override
86                 public boolean onMenuItemClick(MenuItem item) {
87                     return false;
88                 }
89             };
90
91     private final Context mContext;
92     private final Window mWindow;
93     private final FloatingToolbarPopup mPopup;
94
95     private final Rect mContentRect = new Rect();
96     private final Rect mPreviousContentRect = new Rect();
97
98     private Menu mMenu;
99     private List<Object> mShowingMenuItems = new ArrayList<Object>();
100     private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
101
102     private int mSuggestedWidth;
103     private boolean mWidthChanged = true;
104
105     private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
106
107         private final Rect mNewRect = new Rect();
108         private final Rect mOldRect = new Rect();
109
110         @Override
111         public void onLayoutChange(
112                 View view,
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;
119                 updateLayout();
120             }
121         }
122     };
123
124     /**
125      * Initializes a floating toolbar.
126      */
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());
131     }
132
133     /**
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
136      * toolbar.
137      */
138     public FloatingToolbar setMenu(Menu menu) {
139         mMenu = Preconditions.checkNotNull(menu);
140         return this;
141     }
142
143     /**
144      * Sets the custom listener for invocation of menu items in this floating toolbar.
145      */
146     public FloatingToolbar setOnMenuItemClickListener(
147             MenuItem.OnMenuItemClickListener menuItemClickListener) {
148         if (menuItemClickListener != null) {
149             mMenuItemClickListener = menuItemClickListener;
150         } else {
151             mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
152         }
153         return this;
154     }
155
156     /**
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
160      * toolbar.
161      */
162     public FloatingToolbar setContentRect(Rect rect) {
163         mContentRect.set(Preconditions.checkNotNull(rect));
164         return this;
165     }
166
167     /**
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
172      * toolbar.
173      */
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);
178
179         mSuggestedWidth = suggestedWidth;
180         return this;
181     }
182
183     /**
184      * Shows this floating toolbar.
185      */
186     public FloatingToolbar show() {
187         registerOrientationHandler();
188         doShow();
189         return this;
190     }
191
192     /**
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.
195      */
196     public FloatingToolbar updateLayout() {
197         if (mPopup.isShowing()) {
198             doShow();
199         }
200         return this;
201     }
202
203     /**
204      * Dismisses this floating toolbar.
205      */
206     public void dismiss() {
207         unregisterOrientationHandler();
208         mPopup.dismiss();
209     }
210
211     /**
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.
214      */
215     public void hide() {
216         mPopup.hide();
217     }
218
219     /**
220      * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
221      */
222     public boolean isShowing() {
223         return mPopup.isShowing();
224     }
225
226     /**
227      * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
228      */
229     public boolean isHidden() {
230         return mPopup.isHidden();
231     }
232
233     private void doShow() {
234         List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
235         if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
236             mPopup.dismiss();
237             mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
238             mShowingMenuItems = getShowingMenuItemsReferences(menuItems);
239         }
240         if (!mPopup.isShowing()) {
241             mPopup.show(mContentRect);
242         } else if (!mPreviousContentRect.equals(mContentRect)) {
243             mPopup.updateCoordinates(mContentRect);
244         }
245         mWidthChanged = false;
246         mPreviousContentRect.set(mContentRect);
247     }
248
249     /**
250      * Returns true if this floating toolbar is currently showing the specified menu items.
251      */
252     private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
253         return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems));
254     }
255
256     /**
257      * Returns the visible and enabled menu items in the specified menu.
258      * This method is recursive.
259      */
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));
268                 } else {
269                     menuItems.add(menuItem);
270                 }
271             }
272         }
273         return menuItems;
274     }
275
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());
281             } else {
282                 references.add(menuItem.getTitle());
283             }
284         }
285         return references;
286     }
287
288     private void registerOrientationHandler() {
289         unregisterOrientationHandler();
290         mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
291     }
292
293     private void unregisterOrientationHandler() {
294         mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
295     }
296
297
298     /**
299      * A popup window used by the floating toolbar.
300      *
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.
304      */
305     private static final class FloatingToolbarPopup {
306
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;
310
311         /* The duration of the overflow button vector animation duration. */
312         private static final int OVERFLOW_BUTTON_ANIMATION_DELAY = 400;
313
314         private final Context mContext;
315         private final View mParent;  // Parent for the popup window.
316         private final PopupWindow mPopupWindow;
317
318         /* Margins between the popup window and it's content. */
319         private final int mMarginHorizontal;
320         private final int mMarginVertical;
321
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;
332
333         private final OverflowPanelViewHelper mOverflowPanelViewHelper;
334
335         /* Animation interpolators. */
336         private final Interpolator mLogAccelerateInterpolator;
337         private final Interpolator mFastOutSlowInInterpolator;
338         private final Interpolator mLinearOutSlowInInterpolator;
339         private final Interpolator mFastOutLinearInInterpolator;
340
341         /* Animations. */
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;
348
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();
354
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);
365                     }
366                 };
367
368         /**
369          * @see OverflowPanelViewHelper#preparePopupContent().
370          */
371         private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
372             @Override
373             public void run() {
374                 setPanelsStatesAtRestingPosition();
375                 setContentAreaAsTouchableSurface();
376                 mContentContainer.setAlpha(1);
377             }
378         };
379
380         /* Runnable to reset the overflow button's drawable after an overflow transition. */
381         private final Runnable mResetOverflowButtonDrawable = new Runnable() {
382             @Override
383             public void run() {
384                 if (mIsOverflowOpen) {
385                     mOverflowButton.setImageDrawable(mArrow);
386                 } else {
387                     mOverflowButton.setImageDrawable(mOverflow);
388                 }
389             }
390         };
391
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.
394
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;
399
400         /* Item click listeners */
401         private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
402         private final View.OnClickListener mMenuItemButtonOnClickListener =
403                 new View.OnClickListener() {
404                     @Override
405                     public void onClick(View v) {
406                         if (v.getTag() instanceof MenuItem) {
407                             if (mOnMenuItemClickListener != null) {
408                                 mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
409                             }
410                         }
411                     }
412                 };
413
414         private boolean mOpenOverflowUpwards;  // Whether the overflow opens upwards or downwards.
415         private boolean mIsOverflowOpen;
416
417         private int mTransitionDurationScale;  // Used to scale the toolbar transition duration.
418
419         /**
420          * Initializes a new floating toolbar popup.
421          *
422          * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
423          *      from.
424          */
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);
434
435             // Interpolators
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);
443
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);
457
458             // Views
459             mOverflowButton = createOverflowButton();
460             mOverflowButtonSize = measure(mOverflowButton);
461             mMainPanel = createMainPanel();
462             mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext);
463             mOverflowPanel = createOverflowPanel();
464
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(
473                     mContentContainer,
474                     150,  // startDelay
475                     new AnimatorListenerAdapter() {
476                         @Override
477                         public void onAnimationEnd(Animator animation) {
478                             mPopupWindow.dismiss();
479                             mContentContainer.removeAllViews();
480                         }
481                     });
482             mHideAnimation = createExitAnimation(
483                     mContentContainer,
484                     0,  // startDelay
485                     new AnimatorListenerAdapter() {
486                         @Override
487                         public void onAnimationEnd(Animator animation) {
488                             mPopupWindow.dismiss();
489                         }
490                     });
491         }
492
493         /**
494          * Lays out buttons for the specified menu items.
495          * Requires a subsequent call to {@link #show()} to show the items.
496          */
497         public void layoutMenuItems(
498                 List<MenuItem> menuItems,
499                 MenuItem.OnMenuItemClickListener menuItemClickListener,
500                 int suggestedWidth) {
501             mOnMenuItemClickListener = menuItemClickListener;
502             cancelOverflowAnimations();
503             clearPanels();
504             menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
505             if (!menuItems.isEmpty()) {
506                 // Add remaining items to the overflow.
507                 layoutOverflowPanelItems(menuItems);
508             }
509             updatePopupSize();
510         }
511
512         /**
513          * Shows this popup at the specified coordinates.
514          * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
515          */
516         public void show(Rect contentRectOnScreen) {
517             Preconditions.checkNotNull(contentRectOnScreen);
518
519             if (isShowing()) {
520                 return;
521             }
522
523             mHidden = false;
524             mDismissed = false;
525             cancelDismissAndHideAnimations();
526             cancelOverflowAnimations();
527
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();
536             runShowAnimation();
537         }
538
539         /**
540          * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
541          */
542         public void dismiss() {
543             if (mDismissed) {
544                 return;
545             }
546
547             mHidden = false;
548             mDismissed = true;
549             mHideAnimation.cancel();
550
551             runDismissAnimation();
552             setZeroTouchableSurface();
553         }
554
555         /**
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.
558          */
559         public void hide() {
560             if (!isShowing()) {
561                 return;
562             }
563
564             mHidden = true;
565             runHideAnimation();
566             setZeroTouchableSurface();
567         }
568
569         /**
570          * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
571          */
572         public boolean isShowing() {
573             return !mDismissed && !mHidden;
574         }
575
576         /**
577          * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
578          */
579         public boolean isHidden() {
580             return mHidden;
581         }
582
583         /**
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.
587          */
588         public void updateCoordinates(Rect contentRectOnScreen) {
589             Preconditions.checkNotNull(contentRectOnScreen);
590
591             if (!isShowing() || !mPopupWindow.isShowing()) {
592                 return;
593             }
594
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.
601             mPopupWindow.update(
602                     mCoordsOnWindow.x, mCoordsOnWindow.y,
603                     mPopupWindow.getWidth(), mPopupWindow.getHeight());
604         }
605
606         private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
607             refreshViewPort();
608
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()));
612
613             final int y;
614
615             final int availableHeightAboveContent =
616                     contentRectOnScreen.top - mViewPortOnScreen.top;
617             final int availableHeightBelowContent =
618                     mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
619
620             final int margin = 2 * mMarginVertical;
621             final int toolbarHeightWithVerticalMargin = getLineHeight(mContext) + margin;
622
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;
633                 } else {
634                     // Not enough space. Prefer to position as high as possible.
635                     y = Math.max(
636                             mViewPortOnScreen.top,
637                             contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
638                 }
639             } else {
640                 // Has an overflow.
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;
647
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;
677                 } else {
678                     // Not enough space.
679                     // Position at the top of the view port and open downwards.
680                     updateOverflowHeight(mViewPortOnScreen.height() - margin);
681                     y = mViewPortOnScreen.top;
682                     mOpenOverflowUpwards = false;
683                 }
684             }
685
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);
702         }
703
704         /**
705          * Performs the "show" animation on the floating popup.
706          */
707         private void runShowAnimation() {
708             mShowAnimation.start();
709         }
710
711         /**
712          * Performs the "dismiss" animation on the floating popup.
713          */
714         private void runDismissAnimation() {
715             mDismissAnimation.start();
716         }
717
718         /**
719          * Performs the "hide" animation on the floating popup.
720          */
721         private void runHideAnimation() {
722             mHideAnimation.start();
723         }
724
725         private void cancelDismissAndHideAnimations() {
726             mDismissAnimation.cancel();
727             mHideAnimation.cancel();
728         }
729
730         private void cancelOverflowAnimations() {
731             mContentContainer.clearAnimation();
732             mMainPanel.animate().cancel();
733             mOverflowPanel.animate().cancel();
734             mToArrow.stop();
735             mToOverflow.stop();
736         }
737
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() {
747                 @Override
748                 protected void applyTransformation(float interpolatedTime, Transformation t) {
749                     int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
750                     setWidth(mContentContainer, startWidth + deltaWidth);
751                     if (isRTL()) {
752                         mContentContainer.setX(left);
753
754                         // Lock the panels in place.
755                         mMainPanel.setX(0);
756                         mOverflowPanel.setX(0);
757                     } else {
758                         mContentContainer.setX(right - mContentContainer.getWidth());
759
760                         // Offset the panels' positions so they look like they're locked in place
761                         // on the screen.
762                         mMainPanel.setX(mContentContainer.getWidth() - startWidth);
763                         mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
764                     }
765                 }
766             };
767             Animation heightAnimation = new Animation() {
768                 @Override
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();
776                     }
777                 }
778             };
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() {
784                 @Override
785                 protected void applyTransformation(float interpolatedTime, Transformation t) {
786                     float overflowButtonX = overflowButtonStartX
787                             + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
788                     float deltaContainerWidth = isRTL() ?
789                             0 :
790                             mContentContainer.getWidth() - startWidth;
791                     float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
792                     mOverflowButton.setX(actualOverflowButtonX);
793                 }
794             };
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;
808             mMainPanel.animate()
809                     .alpha(0).withLayer()
810                     .setInterpolator(mLinearOutSlowInInterpolator)
811                     .setDuration(250)
812                     .start();
813             mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
814         }
815
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() {
822                 @Override
823                 protected void applyTransformation(float interpolatedTime, Transformation t) {
824                     int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
825                     setWidth(mContentContainer, startWidth + deltaWidth);
826                     if (isRTL()) {
827                         mContentContainer.setX(left);
828
829                         // Lock the panels in place.
830                         mMainPanel.setX(0);
831                         mOverflowPanel.setX(0);
832                     } else {
833                         mContentContainer.setX(right - mContentContainer.getWidth());
834
835                         // Offset the panels' positions so they look like they're locked in place
836                         // on the screen.
837                         mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
838                         mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
839                     }
840                 }
841             };
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() {
846                 @Override
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();
853                     }
854                 }
855             };
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() {
861                 @Override
862                 protected void applyTransformation(float interpolatedTime, Transformation t) {
863                     float overflowButtonX = overflowButtonStartX
864                             + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
865                     float deltaContainerWidth = isRTL() ?
866                             0 :
867                             mContentContainer.getWidth() - startWidth;
868                     float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
869                     mOverflowButton.setX(actualOverflowButtonX);
870                 }
871             };
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;
884             mMainPanel.animate()
885                     .alpha(1).withLayer()
886                     .setInterpolator(mFastOutLinearInInterpolator)
887                     .setDuration(100)
888                     .start();
889             mOverflowPanel.animate()
890                     .alpha(0).withLayer()
891                     .setInterpolator(mLinearOutSlowInInterpolator)
892                     .setDuration(150)
893                     .start();
894         }
895
896         private void setPanelsStatesAtRestingPosition() {
897             mOverflowButton.setEnabled(true);
898             mOverflowPanel.awakenScrollBars();
899
900             if (mIsOverflowOpen) {
901                 // Set open state.
902                 final Size containerSize = mOverflowPanelSize;
903                 setSize(mContentContainer, containerSize);
904                 mMainPanel.setAlpha(0);
905                 mOverflowPanel.setAlpha(1);
906                 mOverflowButton.setImageDrawable(mArrow);
907
908                 // Update x-coordinates depending on RTL state.
909                 if (isRTL()) {
910                     mContentContainer.setX(mMarginHorizontal);  // align left
911                     mMainPanel.setX(0);  // align left
912                     mOverflowButton.setX(  // align right
913                             containerSize.getWidth() - mOverflowButtonSize.getWidth());
914                     mOverflowPanel.setX(0);  // align left
915                 } else {
916                     mContentContainer.setX(  // align right
917                             mMarginHorizontal +
918                                     mMainPanelSize.getWidth() - containerSize.getWidth());
919                     mMainPanel.setX(-mContentContainer.getX());  // align right
920                     mOverflowButton.setX(0);  // align left
921                     mOverflowPanel.setX(0);  // align left
922                 }
923
924                 // Update y-coordinates depending on overflow's open direction.
925                 if (mOpenOverflowUpwards) {
926                     mContentContainer.setY(mMarginVertical);  // align top
927                     mMainPanel.setY(  // align bottom
928                             containerSize.getHeight() - mContentContainer.getHeight());
929                     mOverflowButton.setY(  // align bottom
930                             containerSize.getHeight() - mOverflowButtonSize.getHeight());
931                     mOverflowPanel.setY(0);  // align top
932                 } else {
933                     // opens downwards.
934                     mContentContainer.setY(mMarginVertical);  // align top
935                     mMainPanel.setY(0);  // align top
936                     mOverflowButton.setY(0);  // align top
937                     mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
938                 }
939             } else {
940                 // 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);
946
947                 if (hasOverflow()) {
948                     // Update x-coordinates depending on RTL state.
949                     if (isRTL()) {
950                         mContentContainer.setX(mMarginHorizontal);  // align left
951                         mMainPanel.setX(0);  // align left
952                         mOverflowButton.setX(0);  // align left
953                         mOverflowPanel.setX(0);  // align left
954                     } else {
955                         mContentContainer.setX(mMarginHorizontal);  // align left
956                         mMainPanel.setX(0);  // align left
957                         mOverflowButton.setX(  // align right
958                                 containerSize.getWidth() - mOverflowButtonSize.getWidth());
959                         mOverflowPanel.setX(  // align right
960                                 containerSize.getWidth() - mOverflowPanelSize.getWidth());
961                     }
962
963                     // Update y-coordinates depending on overflow's open direction.
964                     if (mOpenOverflowUpwards) {
965                         mContentContainer.setY(  // align bottom
966                                 mMarginVertical +
967                                         mOverflowPanelSize.getHeight() - containerSize.getHeight());
968                         mMainPanel.setY(0);  // align top
969                         mOverflowButton.setY(0);  // align top
970                         mOverflowPanel.setY(  // align bottom
971                                 containerSize.getHeight() - mOverflowPanelSize.getHeight());
972                     } else {
973                         // opens downwards.
974                         mContentContainer.setY(mMarginVertical);  // align top
975                         mMainPanel.setY(0);  // align top
976                         mOverflowButton.setY(0);  // align top
977                         mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
978                     }
979                 } else {
980                     // No overflow.
981                     mContentContainer.setX(mMarginHorizontal);  // align left
982                     mContentContainer.setY(mMarginVertical);  // align top
983                     mMainPanel.setX(0);  // align left
984                     mMainPanel.setY(0);  // align top
985                 }
986             }
987         }
988
989         private void updateOverflowHeight(int suggestedHeight) {
990             if (hasOverflow()) {
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);
996                 }
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);
1004                     }
1005                 } else {
1006                     setSize(mContentContainer, mMainPanelSize);
1007                 }
1008                 updatePopupSize();
1009             }
1010         }
1011
1012         private void updatePopupSize() {
1013             int width = 0;
1014             int height = 0;
1015             if (mMainPanelSize != null) {
1016                 width = Math.max(width, mMainPanelSize.getWidth());
1017                 height = Math.max(height, mMainPanelSize.getHeight());
1018             }
1019             if (mOverflowPanelSize != null) {
1020                 width = Math.max(width, mOverflowPanelSize.getWidth());
1021                 height = Math.max(height, mOverflowPanelSize.getHeight());
1022             }
1023             mPopupWindow.setWidth(width + mMarginHorizontal * 2);
1024             mPopupWindow.setHeight(height + mMarginVertical * 2);
1025             maybeComputeTransitionDurationScale();
1026         }
1027
1028         private void refreshViewPort() {
1029             mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
1030         }
1031
1032         private int getAdjustedToolbarWidth(int suggestedWidth) {
1033             int width = suggestedWidth;
1034             refreshViewPort();
1035             int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
1036                     .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1037             if (width <= 0) {
1038                 width = mParent.getResources()
1039                         .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1040             }
1041             return Math.min(width, maximumWidth);
1042         }
1043
1044         /**
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.
1047          */
1048         private void setZeroTouchableSurface() {
1049             mTouchableRegion.setEmpty();
1050         }
1051
1052         /**
1053          * Sets the touchable region of this popup to be the area occupied by its content.
1054          */
1055         private void setContentAreaAsTouchableSurface() {
1056             Preconditions.checkNotNull(mMainPanelSize);
1057             final int width;
1058             final int height;
1059             if (mIsOverflowOpen) {
1060                 Preconditions.checkNotNull(mOverflowPanelSize);
1061                 width = mOverflowPanelSize.getWidth();
1062                 height = mOverflowPanelSize.getHeight();
1063             } else {
1064                 width = mMainPanelSize.getWidth();
1065                 height = mMainPanelSize.getHeight();
1066             }
1067             mTouchableRegion.set(
1068                     (int) mContentContainer.getX(),
1069                     (int) mContentContainer.getY(),
1070                     (int) mContentContainer.getX() + width,
1071                     (int) mContentContainer.getY() + height);
1072         }
1073
1074         /**
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.
1078          */
1079         private void setTouchableSurfaceInsetsComputer() {
1080             ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
1081                     .getRootView()
1082                     .getViewTreeObserver();
1083             viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
1084             viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
1085         }
1086
1087         private boolean isRTL() {
1088             return mContext.getResources().getConfiguration().getLayoutDirection()
1089                     == View.LAYOUT_DIRECTION_RTL;
1090         }
1091
1092         private boolean hasOverflow() {
1093             return mOverflowPanelSize != null;
1094         }
1095
1096         /**
1097          * Fits as many menu items in the main panel and returns a list of the menu items that
1098          * were not fit in.
1099          *
1100          * @return The menu items that are not included in this main panel.
1101          */
1102         public List<MenuItem> layoutMainPanelItems(
1103                 List<MenuItem> menuItems, final int toolbarWidth) {
1104             Preconditions.checkNotNull(menuItems);
1105
1106             int availableWidth = toolbarWidth;
1107             final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
1108
1109             mMainPanel.removeAllViews();
1110             mMainPanel.setPaddingRelative(0, 0, 0, 0);
1111
1112             boolean isFirstItem = true;
1113             while (!remainingMenuItems.isEmpty()) {
1114                 final MenuItem menuItem = remainingMenuItems.peek();
1115                 View menuItemButton = createMenuItemButton(mContext, menuItem);
1116
1117                 // Adding additional start padding for the first button to even out button spacing.
1118                 if (isFirstItem) {
1119                     menuItemButton.setPaddingRelative(
1120                             (int) (1.5 * menuItemButton.getPaddingStart()),
1121                             menuItemButton.getPaddingTop(),
1122                             menuItemButton.getPaddingEnd(),
1123                             menuItemButton.getPaddingBottom());
1124                     isFirstItem = false;
1125                 }
1126
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());
1134                 }
1135
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();
1151                 } else {
1152                     // Reserve space for overflowButton.
1153                     mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1154                     break;
1155                 }
1156             }
1157             mMainPanelSize = measure(mMainPanel);
1158             return remainingMenuItems;
1159         }
1160
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));
1168             }
1169             mOverflowPanel.setAdapter(overflowPanelAdapter);
1170             if (mOpenOverflowUpwards) {
1171                 mOverflowPanel.setY(0);
1172             } else {
1173                 mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1174             }
1175
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);
1180         }
1181
1182         /**
1183          * Resets the content container and appropriately position it's panels.
1184          */
1185         private void preparePopupContent() {
1186             mContentContainer.removeAllViews();
1187
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);
1192             }
1193             mContentContainer.addView(mMainPanel);
1194             if (hasOverflow()) {
1195                 mContentContainer.addView(mOverflowButton);
1196             }
1197             setPanelsStatesAtRestingPosition();
1198             setContentAreaAsTouchableSurface();
1199
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.
1203             if (isRTL()) {
1204                 mContentContainer.setAlpha(0);
1205                 mContentContainer.post(mPreparePopupContentRTLHelper);
1206             }
1207         }
1208
1209         /**
1210          * Clears out the panels and their container. Resets their calculated sizes.
1211          */
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();
1222         }
1223
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());
1229             }
1230         }
1231
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);
1237                 overflowWidth =
1238                         Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1239             }
1240             return overflowWidth;
1241         }
1242
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(
1246                     MAX_OVERFLOW_SIZE,
1247                     Math.min(
1248                             Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1249                             mOverflowPanel.getCount()));
1250             return actualSize * getLineHeight(mContext) + mOverflowButtonSize.getHeight();
1251         }
1252
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);
1257             }
1258             button.setTag(menuItem);
1259             button.setOnClickListener(mMenuItemButtonOnClickListener);
1260         }
1261
1262         /**
1263          * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1264          * animations. See comment about this in the code.
1265          */
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;
1273             }
1274
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());
1280         }
1281
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);
1288             }
1289         }
1290
1291         private ViewGroup createMainPanel() {
1292             ViewGroup mainPanel = new LinearLayout(mContext) {
1293                 @Override
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);
1301                     }
1302                     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1303                 }
1304
1305                 @Override
1306                 public boolean onInterceptTouchEvent(MotionEvent ev) {
1307                     // Intercept the touch event while the overflow is animating.
1308                     return isOverflowAnimating();
1309                 }
1310             };
1311             return mainPanel;
1312         }
1313
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() {
1319                 @Override
1320                 public void onClick(View v) {
1321                     if (mIsOverflowOpen) {
1322                         overflowButton.setImageDrawable(mToOverflow);
1323                         mToOverflow.start();
1324                         closeOverflow();
1325                     } else {
1326                         overflowButton.setImageDrawable(mToArrow);
1327                         mToArrow.start();
1328                         openOverflow();
1329                     }
1330                     overflowButton.postDelayed(
1331                             mResetOverflowButtonDrawable, OVERFLOW_BUTTON_ANIMATION_DELAY);
1332                 }
1333             });
1334             return overflowButton;
1335         }
1336
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);
1343
1344             final ArrayAdapter adapter =
1345                     new ArrayAdapter<MenuItem>(mContext, 0) {
1346                         @Override
1347                         public int getViewTypeCount() {
1348                             return mOverflowPanelViewHelper.getViewTypeCount();
1349                         }
1350
1351                         @Override
1352                         public int getItemViewType(int position) {
1353                             return mOverflowPanelViewHelper.getItemViewType(getItem(position));
1354                         }
1355
1356                         @Override
1357                         public View getView(int position, View convertView, ViewGroup parent) {
1358                             return mOverflowPanelViewHelper.getView(
1359                                     getItem(position), mOverflowPanelSize.getWidth(), convertView);
1360                         }
1361                     };
1362             overflowPanel.setAdapter(adapter);
1363
1364             overflowPanel.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1365                 @Override
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);
1370                     }
1371                 }
1372             });
1373
1374             return overflowPanel;
1375         }
1376
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;
1383         }
1384
1385         private Animation.AnimationListener createOverflowAnimationListener() {
1386             Animation.AnimationListener listener = new Animation.AnimationListener() {
1387                 @Override
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);
1392                 }
1393
1394                 @Override
1395                 public void onAnimationEnd(Animation animation) {
1396                     // Posting this because it seems like this is called before the animation
1397                     // actually ends.
1398                     mContentContainer.post(new Runnable() {
1399                         @Override
1400                         public void run() {
1401                             setPanelsStatesAtRestingPosition();
1402                             setContentAreaAsTouchableSurface();
1403                         }
1404                     });
1405                 }
1406
1407                 @Override
1408                 public void onAnimationRepeat(Animation animation) {
1409                 }
1410             };
1411             return listener;
1412         }
1413
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());
1418         }
1419
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);
1428         }
1429
1430         private static void setSize(View view, Size size) {
1431             setSize(view, size.getWidth(), size.getHeight());
1432         }
1433
1434         private static void setWidth(View view, int width) {
1435             ViewGroup.LayoutParams params = view.getLayoutParams();
1436             setSize(view, width, params.height);
1437         }
1438
1439         private static void setHeight(View view, int height) {
1440             ViewGroup.LayoutParams params = view.getLayoutParams();
1441             setSize(view, params.width, height);
1442         }
1443
1444         private static int getLineHeight(Context context) {
1445             return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1446         }
1447
1448         /**
1449          * A custom ListView for the overflow panel.
1450          */
1451         private static final class OverflowPanel extends ListView {
1452
1453             private final FloatingToolbarPopup mPopup;
1454
1455             OverflowPanel(FloatingToolbarPopup popup) {
1456                 super(Preconditions.checkNotNull(popup).mContext);
1457                 this.mPopup = popup;
1458             }
1459
1460             @Override
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);
1468             }
1469
1470             @Override
1471             public boolean dispatchTouchEvent(MotionEvent ev) {
1472                 if (mPopup.isOverflowAnimating()) {
1473                     // Eat the touch event.
1474                     return true;
1475                 }
1476                 return super.dispatchTouchEvent(ev);
1477             }
1478
1479             @Override
1480             protected boolean awakenScrollBars() {
1481                 return super.awakenScrollBars();
1482             }
1483         }
1484
1485         /**
1486          * A custom interpolator used for various floating toolbar animations.
1487          */
1488         private static final class LogAccelerateInterpolator implements Interpolator {
1489
1490             private static final int BASE = 100;
1491             private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
1492
1493             private static float computeLog(float t, int base) {
1494                 return (float) (1 - Math.pow(base, -t));
1495             }
1496
1497             @Override
1498             public float getInterpolation(float t) {
1499                 return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1500             }
1501         }
1502
1503         /**
1504          * A helper for generating views for the overflow panel.
1505          */
1506         private static final class OverflowPanelViewHelper {
1507
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;
1511
1512             private final TextView mStringTitleViewCalculator;
1513             private final View mIconOnlyViewCalculator;
1514
1515             private final Context mContext;
1516
1517             public OverflowPanelViewHelper(Context context) {
1518                 mContext = Preconditions.checkNotNull(context);
1519                 mStringTitleViewCalculator = getStringTitleView(null, 0, null);
1520                 mIconOnlyViewCalculator = getIconOnlyView(null, 0, null);
1521             }
1522
1523             public int getViewTypeCount() {
1524                 return NUM_OF_VIEW_TYPES;
1525             }
1526
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);
1531                 }
1532                 return getStringTitleView(menuItem, minimumWidth, convertView);
1533             }
1534
1535             public int getItemViewType(MenuItem menuItem) {
1536                 Preconditions.checkNotNull(menuItem);
1537                 if (isIconOnlyMenuItem(menuItem)) {
1538                     return VIEW_TYPE_ICON_ONLY;
1539                 }
1540                 return VIEW_TYPE_STRING_TITLE;
1541             }
1542
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;
1550                 } else {
1551                     mStringTitleViewCalculator.setText(menuItem.getTitle());
1552                     calculator = mStringTitleViewCalculator;
1553                 }
1554                 calculator.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1555                 return calculator.getMeasuredWidth();
1556             }
1557
1558             private TextView getStringTitleView(
1559                     MenuItem menuItem, int minimumWidth, View convertView) {
1560                 TextView menuButton;
1561                 if (convertView != null) {
1562                     menuButton = (TextView) convertView;
1563                 } else {
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));
1569                 }
1570                 if (menuItem != null) {
1571                     menuButton.setText(menuItem.getTitle());
1572                     menuButton.setContentDescription(menuItem.getTitle());
1573                     menuButton.setMinimumWidth(minimumWidth);
1574                 }
1575                 return menuButton;
1576             }
1577
1578             private View getIconOnlyView(
1579                     MenuItem menuItem, int minimumWidth, View convertView) {
1580                 View menuButton;
1581                 if (convertView != null) {
1582                     menuButton = convertView;
1583                 } else {
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));
1589                 }
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);
1595                 }
1596                 return menuButton;
1597             }
1598         }
1599     }
1600
1601     /**
1602      * @return {@code true} if the menu item does not not have a string title but has an icon.
1603      *   {@code false} otherwise.
1604      */
1605     private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1606         if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1607             return true;
1608         }
1609         return false;
1610     }
1611
1612     /**
1613      * Creates and returns a menu button for the specified menu item.
1614      */
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;
1623         }
1624
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;
1630     }
1631
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;
1639     }
1640
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);
1654         return popupWindow;
1655     }
1656
1657     /**
1658      * Creates an "appear" animation for the specified view.
1659      *
1660      * @param view  The view to animate
1661      */
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));
1666         return animation;
1667     }
1668
1669     /**
1670      * Creates a "disappear" animation for the specified view.
1671      *
1672      * @param view  The view to animate
1673      * @param startDelay  The start delay of the animation
1674      * @param listener  The animation listener
1675      */
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);
1683         return animation;
1684     }
1685
1686     /**
1687      * Returns a re-themed context with controlled look and feel for views.
1688      */
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;
1693         a.recycle();
1694         return new ContextThemeWrapper(originalContext, themeId);
1695     }
1696 }