FloatingToolbar.java revision 9b9d2c572fe26ddbdd0aed8b9d5899b0f9b5c08c
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
17package com.android.internal.widget;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.content.ComponentCallbacks;
24import android.content.Context;
25import android.content.res.Configuration;
26import android.content.res.TypedArray;
27import android.graphics.Color;
28import android.graphics.Point;
29import android.graphics.Rect;
30import android.graphics.Region;
31import android.graphics.drawable.ColorDrawable;
32import android.text.TextUtils;
33import android.util.Size;
34import android.view.ContextThemeWrapper;
35import android.view.Gravity;
36import android.view.LayoutInflater;
37import android.view.Menu;
38import android.view.MenuItem;
39import android.view.View;
40import android.view.View.MeasureSpec;
41import android.view.ViewGroup;
42import android.view.ViewTreeObserver;
43import android.view.Window;
44import android.view.WindowManager;
45import android.view.animation.Animation;
46import android.view.animation.AnimationSet;
47import android.view.animation.Transformation;
48import android.widget.AdapterView;
49import android.widget.ArrayAdapter;
50import android.widget.Button;
51import android.widget.ImageButton;
52import android.widget.ImageView;
53import android.widget.LinearLayout;
54import android.widget.ListView;
55import android.widget.PopupWindow;
56import android.widget.TextView;
57
58import java.util.ArrayList;
59import java.util.LinkedList;
60import java.util.List;
61
62import com.android.internal.R;
63import com.android.internal.util.Preconditions;
64
65/**
66 * A floating toolbar for showing contextual menu items.
67 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
68 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
69 * The horizontal toolbar morphs into the vertical overflow view.
70 */
71public final class FloatingToolbar {
72
73    // This class is responsible for the public API of the floating toolbar.
74    // It delegates rendering operations to the FloatingToolbarPopup.
75
76    private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
77            new MenuItem.OnMenuItemClickListener() {
78                @Override
79                public boolean onMenuItemClick(MenuItem item) {
80                    return false;
81                }
82            };
83
84    private final Context mContext;
85    private final FloatingToolbarPopup mPopup;
86
87    private final Rect mContentRect = new Rect();
88    private final Rect mPreviousContentRect = new Rect();
89
90    private Menu mMenu;
91    private List<Object> mShowingMenuItems = new ArrayList<Object>();
92    private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
93
94    private int mSuggestedWidth;
95    private boolean mWidthChanged = true;
96
97    private final ComponentCallbacks mOrientationChangeHandler = new ComponentCallbacks() {
98        @Override
99        public void onConfigurationChanged(Configuration newConfig) {
100            if (mPopup.isShowing() && mPopup.viewPortHasChanged()) {
101                mWidthChanged = true;
102                updateLayout();
103            }
104        }
105
106        @Override
107        public void onLowMemory() {}
108    };
109
110    /**
111     * Initializes a floating toolbar.
112     */
113    public FloatingToolbar(Context context, Window window) {
114        Preconditions.checkNotNull(context);
115        Preconditions.checkNotNull(window);
116        mContext = applyDefaultTheme(context);
117        mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
118    }
119
120    /**
121     * Sets the menu to be shown in this floating toolbar.
122     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
123     * toolbar.
124     */
125    public FloatingToolbar setMenu(Menu menu) {
126        mMenu = Preconditions.checkNotNull(menu);
127        return this;
128    }
129
130    /**
131     * Sets the custom listener for invocation of menu items in this floating toolbar.
132     */
133    public FloatingToolbar setOnMenuItemClickListener(
134            MenuItem.OnMenuItemClickListener menuItemClickListener) {
135        if (menuItemClickListener != null) {
136            mMenuItemClickListener = menuItemClickListener;
137        } else {
138            mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
139        }
140        return this;
141    }
142
143    /**
144     * Sets the content rectangle. This is the area of the interesting content that this toolbar
145     * should avoid obstructing.
146     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
147     * toolbar.
148     */
149    public FloatingToolbar setContentRect(Rect rect) {
150        mContentRect.set(Preconditions.checkNotNull(rect));
151        return this;
152    }
153
154    /**
155     * Sets the suggested width of this floating toolbar.
156     * The actual width will be about this size but there are no guarantees that it will be exactly
157     * the suggested width.
158     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
159     * toolbar.
160     */
161    public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
162        // Check if there's been a substantial width spec change.
163        int difference = Math.abs(suggestedWidth - mSuggestedWidth);
164        mWidthChanged = difference > (mSuggestedWidth * 0.2);
165
166        mSuggestedWidth = suggestedWidth;
167        return this;
168    }
169
170    /**
171     * Shows this floating toolbar.
172     */
173    public FloatingToolbar show() {
174        mContext.unregisterComponentCallbacks(mOrientationChangeHandler);
175        mContext.registerComponentCallbacks(mOrientationChangeHandler);
176        List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
177        if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
178            mPopup.dismiss();
179            mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
180            mShowingMenuItems = getShowingMenuItemsReferences(menuItems);
181        }
182        if (!mPopup.isShowing()) {
183            mPopup.show(mContentRect);
184        } else if (!mPreviousContentRect.equals(mContentRect)) {
185            mPopup.updateCoordinates(mContentRect);
186        }
187        mWidthChanged = false;
188        mPreviousContentRect.set(mContentRect);
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            // show() performs all the logic we need here.
199            show();
200        }
201        return this;
202    }
203
204    /**
205     * Dismisses this floating toolbar.
206     */
207    public void dismiss() {
208        mContext.unregisterComponentCallbacks(mOrientationChangeHandler);
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    /**
235     * Returns true if this floating toolbar is currently showing the specified menu items.
236     */
237    private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
238        return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems));
239    }
240
241    /**
242     * Returns the visible and enabled menu items in the specified menu.
243     * This method is recursive.
244     */
245    private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
246        List<MenuItem> menuItems = new ArrayList<MenuItem>();
247        for (int i = 0; (menu != null) && (i < menu.size()); i++) {
248            MenuItem menuItem = menu.getItem(i);
249            if (menuItem.isVisible() && menuItem.isEnabled()) {
250                Menu subMenu = menuItem.getSubMenu();
251                if (subMenu != null) {
252                    menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
253                } else {
254                    menuItems.add(menuItem);
255                }
256            }
257        }
258        return menuItems;
259    }
260
261    private List<Object> getShowingMenuItemsReferences(List<MenuItem> menuItems) {
262        List<Object> references = new ArrayList<Object>();
263        for (MenuItem menuItem : menuItems) {
264            if (isIconOnlyMenuItem(menuItem)) {
265                references.add(menuItem.getIcon());
266            } else {
267                references.add(menuItem.getTitle());
268            }
269        }
270        return references;
271    }
272
273
274    /**
275     * A popup window used by the floating toolbar.
276     *
277     * This class is responsible for the rendering/animation of the floating toolbar.
278     * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time.
279     * It delegates specific panel functionality to the appropriate panel.
280     */
281    private static final class FloatingToolbarPopup {
282
283        public static final int OVERFLOW_DIRECTION_UP = 0;
284        public static final int OVERFLOW_DIRECTION_DOWN = 1;
285
286        private final Context mContext;
287        private final View mParent;
288        private final int[] mParentPositionOnScreen = new int[2];
289        private final PopupWindow mPopupWindow;
290        private final ViewGroup mContentContainer;
291        private final int mMarginHorizontal;
292        private final int mMarginVertical;
293
294        private final Animation.AnimationListener mOnOverflowOpened =
295                new Animation.AnimationListener() {
296                    @Override
297                    public void onAnimationStart(Animation animation) {}
298
299                    @Override
300                    public void onAnimationEnd(Animation animation) {
301                        setOverflowPanelAsContent();
302                        mOverflowPanel.fadeIn(true);
303                    }
304
305                    @Override
306                    public void onAnimationRepeat(Animation animation) {}
307                };
308        private final Animation.AnimationListener mOnOverflowClosed =
309                new Animation.AnimationListener() {
310                    @Override
311                    public void onAnimationStart(Animation animation) {}
312
313                    @Override
314                    public void onAnimationEnd(Animation animation) {
315                        setMainPanelAsContent();
316                        mMainPanel.fadeIn(true);
317                    }
318
319                    @Override
320                    public void onAnimationRepeat(Animation animation) {
321                    }
322                };
323        private final AnimatorSet mDismissAnimation;
324        private final AnimatorSet mHideAnimation;
325        private final AnimationSet mOpenOverflowAnimation = new AnimationSet(true);
326        private final AnimationSet mCloseOverflowAnimation = new AnimationSet(true);
327
328        private final Runnable mOpenOverflow = new Runnable() {
329            @Override
330            public void run() {
331                openOverflow();
332            }
333        };
334        private final Runnable mCloseOverflow = new Runnable() {
335            @Override
336            public void run() {
337                closeOverflow();
338            }
339        };
340
341        private final Rect mViewPortOnScreen = new Rect();
342        private final Point mCoordsOnScreen = new Point();
343        private final Rect mTmpRect = new Rect();
344
345        private final Region mTouchableRegion = new Region();
346        private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
347                new ViewTreeObserver.OnComputeInternalInsetsListener() {
348                    public void onComputeInternalInsets(
349                            ViewTreeObserver.InternalInsetsInfo info) {
350                        info.contentInsets.setEmpty();
351                        info.visibleInsets.setEmpty();
352                        info.touchableRegion.set(mTouchableRegion);
353                        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
354                                .TOUCHABLE_INSETS_REGION);
355                    }
356                };
357
358        private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
359        private boolean mHidden; // tracks whether this popup is hidden or hiding.
360
361        private FloatingToolbarOverflowPanel mOverflowPanel;
362        private FloatingToolbarMainPanel mMainPanel;
363        private int mOverflowDirection;
364
365        /**
366         * Initializes a new floating toolbar popup.
367         *
368         * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
369         *      from.
370         */
371        public FloatingToolbarPopup(Context context, View parent) {
372            mParent = Preconditions.checkNotNull(parent);
373            mContext = Preconditions.checkNotNull(context);
374            mContentContainer = createContentContainer(context);
375            mPopupWindow = createPopupWindow(mContentContainer);
376            mDismissAnimation = createExitAnimation(
377                    mContentContainer,
378                    150,  // startDelay
379                    new AnimatorListenerAdapter() {
380                        @Override
381                        public void onAnimationEnd(Animator animation) {
382                            mPopupWindow.dismiss();
383                            mContentContainer.removeAllViews();
384                        }
385                    });
386            mHideAnimation = createExitAnimation(
387                    mContentContainer,
388                    0,  // startDelay
389                    new AnimatorListenerAdapter() {
390                        @Override
391                        public void onAnimationEnd(Animator animation) {
392                            mPopupWindow.dismiss();
393                        }
394                    });
395            mMarginHorizontal = parent.getResources()
396                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
397            mMarginVertical = parent.getResources()
398                    .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
399        }
400
401        /**
402         * Lays out buttons for the specified menu items.
403         */
404        public void layoutMenuItems(
405                List<MenuItem> menuItems,
406                MenuItem.OnMenuItemClickListener menuItemClickListener,
407                int suggestedWidth) {
408            Preconditions.checkNotNull(menuItems);
409
410            mContentContainer.removeAllViews();
411            if (mMainPanel == null) {
412                mMainPanel = new FloatingToolbarMainPanel(mContext, mOpenOverflow);
413            }
414            List<MenuItem> overflowMenuItems =
415                    mMainPanel.layoutMenuItems(menuItems, getToolbarWidth(suggestedWidth));
416            mMainPanel.setOnMenuItemClickListener(menuItemClickListener);
417            if (!overflowMenuItems.isEmpty()) {
418                if (mOverflowPanel == null) {
419                    mOverflowPanel =
420                            new FloatingToolbarOverflowPanel(mContext, mCloseOverflow);
421                }
422                mOverflowPanel.setMenuItems(overflowMenuItems);
423                mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener);
424            }
425            updatePopupSize();
426        }
427
428        /**
429         * Shows this popup at the specified coordinates.
430         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
431         */
432        public void show(Rect contentRectOnScreen) {
433            Preconditions.checkNotNull(contentRectOnScreen);
434
435            if (isShowing()) {
436                return;
437            }
438
439            mHidden = false;
440            mDismissed = false;
441            cancelDismissAndHideAnimations();
442            cancelOverflowAnimations();
443
444            // Make sure a panel is set as the content.
445            if (mContentContainer.getChildCount() == 0) {
446                setMainPanelAsContent();
447                // If we're yet to show the popup, set the container visibility to zero.
448                // The "show" animation will make this visible.
449                mContentContainer.setAlpha(0);
450            }
451            refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
452            preparePopupContent();
453            // PopupWindow#showAtLocation() receives the location relative to the attached window
454            // hence the following code is correct when and only when mParent is aligned to the
455            // top-left of the attached window.
456            // TODO: Fix the following logic so that mParent can be placed at anywhere.
457            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
458            // specify the popup poision in screen coordinates.
459            mParent.getLocationOnScreen(mParentPositionOnScreen);
460            final int relativeX = mCoordsOnScreen.x - mParentPositionOnScreen[0];
461            final int relativeY = mCoordsOnScreen.y - mParentPositionOnScreen[1];
462            mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, relativeX, relativeY);
463            setTouchableSurfaceInsetsComputer();
464            runShowAnimation();
465        }
466
467        /**
468         * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
469         */
470        public void dismiss() {
471            if (mDismissed) {
472                return;
473            }
474
475            mHidden = false;
476            mDismissed = true;
477            mHideAnimation.cancel();
478            runDismissAnimation();
479            setZeroTouchableSurface();
480        }
481
482        /**
483         * Hides this popup. This is a no-op if this popup is not showing.
484         * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
485         */
486        public void hide() {
487            if (!isShowing()) {
488                return;
489            }
490
491            mHidden = true;
492            runHideAnimation();
493            setZeroTouchableSurface();
494        }
495
496        /**
497         * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
498         */
499        public boolean isShowing() {
500            return !mDismissed && !mHidden;
501        }
502
503        /**
504         * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
505         */
506        public boolean isHidden() {
507            return mHidden;
508        }
509
510        /**
511         * Updates the coordinates of this popup.
512         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
513         * This is a no-op if this popup is not showing.
514         */
515        public void updateCoordinates(Rect contentRectOnScreen) {
516            Preconditions.checkNotNull(contentRectOnScreen);
517
518            if (!isShowing() || !mPopupWindow.isShowing()) {
519                return;
520            }
521
522            cancelOverflowAnimations();
523            refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
524            preparePopupContent();
525            // PopupWindow#update() receives the location relative to the attached window hence
526            // the following code is correct when and only when mParent is aligned to the top-left
527            // of the attached window.
528            // TODO: Fix the following logic so that mParent can be placed at anywhere.
529            // We need to specify the offset relative to mParent.
530            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
531            // specify the popup poision in screen coordinates.
532            mParent.getLocationOnScreen(mParentPositionOnScreen);
533            final int relativeX = mCoordsOnScreen.x - mParentPositionOnScreen[0];
534            final int relativeY = mCoordsOnScreen.y - mParentPositionOnScreen[1];
535            mPopupWindow.update(relativeX, relativeY, getWidth(), getHeight());
536        }
537
538        /**
539         * Returns the width of this popup.
540         */
541        public int getWidth() {
542            return mPopupWindow.getWidth();
543        }
544
545        /**
546         * Returns the height of this popup.
547         */
548        public int getHeight() {
549            return mPopupWindow.getHeight();
550        }
551
552        /**
553         * Returns the context this popup is running in.
554         */
555        public Context getContext() {
556            return mContext;
557        }
558
559        private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
560            refreshViewPort();
561
562            int x = contentRectOnScreen.centerX() - getWidth() / 2;
563            // Update x so that the toolbar isn't rendered behind the nav bar in landscape.
564            x = Math.max(0, Math.min(x, mViewPortOnScreen.right - getWidth()));
565
566            int y;
567
568            int availableHeightAboveContent = contentRectOnScreen.top - mViewPortOnScreen.top;
569            int availableHeightBelowContent = mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
570
571            if (mOverflowPanel == null) {  // There is no overflow.
572                if (availableHeightAboveContent >= getToolbarHeightWithVerticalMargin()) {
573                    // There is enough space at the top of the content.
574                    y = contentRectOnScreen.top - getToolbarHeightWithVerticalMargin();
575                } else if (availableHeightBelowContent >= getToolbarHeightWithVerticalMargin()) {
576                    // There is enough space at the bottom of the content.
577                    y = contentRectOnScreen.bottom;
578                } else if (availableHeightBelowContent >= getEstimatedToolbarHeight(mContext)) {
579                    // Just enough space to fit the toolbar with no vertical margins.
580                    y = contentRectOnScreen.bottom - mMarginVertical;
581                } else {
582                    // Not enough space. Prefer to position as high as possible.
583                    y = Math.max(
584                            mViewPortOnScreen.top,
585                            contentRectOnScreen.top - getToolbarHeightWithVerticalMargin());
586                }
587            } else {  // There is an overflow.
588                int margin = 2 * mMarginVertical;
589                int minimumOverflowHeightWithMargin = mOverflowPanel.getMinimumHeight() + margin;
590                int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
591                        contentRectOnScreen.top + getToolbarHeightWithVerticalMargin();
592                int availableHeightThroughContentUp = contentRectOnScreen.bottom -
593                        mViewPortOnScreen.top + getToolbarHeightWithVerticalMargin();
594
595                if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
596                    // There is enough space at the top of the content rect for the overflow.
597                    // Position above and open upwards.
598                    updateOverflowHeight(availableHeightAboveContent - margin);
599                    y = contentRectOnScreen.top - getHeight();
600                    mOverflowDirection = OVERFLOW_DIRECTION_UP;
601                } else if (availableHeightAboveContent >= getToolbarHeightWithVerticalMargin()
602                        && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
603                    // There is enough space at the top of the content rect for the main panel
604                    // but not the overflow.
605                    // Position above but open downwards.
606                    updateOverflowHeight(availableHeightThroughContentDown - margin);
607                    y = contentRectOnScreen.top - getToolbarHeightWithVerticalMargin();
608                    mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
609                } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
610                    // There is enough space at the bottom of the content rect for the overflow.
611                    // Position below and open downwards.
612                    updateOverflowHeight(availableHeightBelowContent - margin);
613                    y = contentRectOnScreen.bottom;
614                    mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
615                } else if (availableHeightBelowContent >= getToolbarHeightWithVerticalMargin()
616                        && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
617                    // There is enough space at the bottom of the content rect for the main panel
618                    // but not the overflow.
619                    // Position below but open upwards.
620                    updateOverflowHeight(availableHeightThroughContentUp - margin);
621                    y = contentRectOnScreen.bottom + getToolbarHeightWithVerticalMargin() -
622                            getHeight();
623                    mOverflowDirection = OVERFLOW_DIRECTION_UP;
624                } else {
625                    // Not enough space.
626                    // Position at the top of the view port and open downwards.
627                    updateOverflowHeight(mViewPortOnScreen.height() - margin);
628                    y = mViewPortOnScreen.top;
629                    mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
630                }
631                mOverflowPanel.setOverflowDirection(mOverflowDirection);
632            }
633
634            mCoordsOnScreen.set(x, y);
635        }
636
637        private int getToolbarHeightWithVerticalMargin() {
638            return getEstimatedToolbarHeight(mContext) + mMarginVertical * 2;
639        }
640
641        /**
642         * Performs the "show" animation on the floating popup.
643         */
644        private void runShowAnimation() {
645            createEnterAnimation(mContentContainer).start();
646        }
647
648        /**
649         * Performs the "dismiss" animation on the floating popup.
650         */
651        private void runDismissAnimation() {
652            mDismissAnimation.start();
653        }
654
655        /**
656         * Performs the "hide" animation on the floating popup.
657         */
658        private void runHideAnimation() {
659            mHideAnimation.start();
660        }
661
662        private void cancelDismissAndHideAnimations() {
663            mDismissAnimation.cancel();
664            mHideAnimation.cancel();
665        }
666
667        private void cancelOverflowAnimations() {
668            if (mOpenOverflowAnimation.hasStarted()
669                    && !mOpenOverflowAnimation.hasEnded()) {
670                // Remove the animation listener, stop the animation,
671                // then trigger the lister explicitly so it is not posted
672                // to the message queue.
673                mOpenOverflowAnimation.setAnimationListener(null);
674                mContentContainer.clearAnimation();
675                mOnOverflowOpened.onAnimationEnd(null);
676            }
677            if (mCloseOverflowAnimation.hasStarted()
678                    && !mCloseOverflowAnimation.hasEnded()) {
679                // Remove the animation listener, stop the animation,
680                // then trigger the lister explicitly so it is not posted
681                // to the message queue.
682                mCloseOverflowAnimation.setAnimationListener(null);
683                mContentContainer.clearAnimation();
684                mOnOverflowClosed.onAnimationEnd(null);
685            }
686        }
687
688        /**
689         * Opens the floating toolbar overflow.
690         * This method should not be called if menu items have not been laid out with
691         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
692         *
693         * @throws IllegalStateException if called when menu items have not been laid out.
694         */
695        private void openOverflow() {
696            Preconditions.checkState(mMainPanel != null);
697            Preconditions.checkState(mOverflowPanel != null);
698
699            mMainPanel.fadeOut(true);
700            Size overflowPanelSize = mOverflowPanel.measure();
701            final int targetWidth = overflowPanelSize.getWidth();
702            final int targetHeight = overflowPanelSize.getHeight();
703            final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
704            final int startWidth = mContentContainer.getWidth();
705            final int startHeight = mContentContainer.getHeight();
706            final float startY = mContentContainer.getY();
707            final float left = mContentContainer.getX();
708            final float right = left + mContentContainer.getWidth();
709            Animation widthAnimation = new Animation() {
710                @Override
711                protected void applyTransformation(float interpolatedTime, Transformation t) {
712                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
713                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
714                    params.width = startWidth + deltaWidth;
715                    mContentContainer.setLayoutParams(params);
716                    if (isRTL()) {
717                        mContentContainer.setX(left);
718                    } else {
719                        mContentContainer.setX(right - mContentContainer.getWidth());
720                    }
721                }
722            };
723            Animation heightAnimation = new Animation() {
724                @Override
725                protected void applyTransformation(float interpolatedTime, Transformation t) {
726                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
727                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
728                    params.height = startHeight + deltaHeight;
729                    mContentContainer.setLayoutParams(params);
730                    if (morphUpwards) {
731                        float y = startY - (mContentContainer.getHeight() - startHeight);
732                        mContentContainer.setY(y);
733                    }
734                }
735            };
736            widthAnimation.setDuration(240);
737            heightAnimation.setDuration(180);
738            heightAnimation.setStartOffset(60);
739            mOpenOverflowAnimation.getAnimations().clear();
740            mOpenOverflowAnimation.setAnimationListener(mOnOverflowOpened);
741            mOpenOverflowAnimation.addAnimation(widthAnimation);
742            mOpenOverflowAnimation.addAnimation(heightAnimation);
743            mContentContainer.startAnimation(mOpenOverflowAnimation);
744        }
745
746        /**
747         * Opens the floating toolbar overflow.
748         * This method should not be called if menu items have not been laid out with
749         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
750         *
751         * @throws IllegalStateException if called when menu items have not been laid out.
752         */
753        private void closeOverflow() {
754            Preconditions.checkState(mMainPanel != null);
755            Preconditions.checkState(mOverflowPanel != null);
756
757            mOverflowPanel.fadeOut(true);
758            Size mainPanelSize = mMainPanel.measure();
759            final int targetWidth = mainPanelSize.getWidth();
760            final int targetHeight = mainPanelSize.getHeight();
761            final int startWidth = mContentContainer.getWidth();
762            final int startHeight = mContentContainer.getHeight();
763            final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
764            final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
765            final float left = mContentContainer.getX();
766            final float right = left + mContentContainer.getWidth();
767            Animation widthAnimation = new Animation() {
768                @Override
769                protected void applyTransformation(float interpolatedTime, Transformation t) {
770                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
771                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
772                    params.width = startWidth + deltaWidth;
773                    mContentContainer.setLayoutParams(params);
774                    if (isRTL()) {
775                        mContentContainer.setX(left);
776                    } else {
777                        mContentContainer.setX(right - mContentContainer.getWidth());
778                    }
779                }
780            };
781            Animation heightAnimation = new Animation() {
782                @Override
783                protected void applyTransformation(float interpolatedTime, Transformation t) {
784                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
785                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
786                    params.height = startHeight + deltaHeight;
787                    mContentContainer.setLayoutParams(params);
788                    if (morphedUpwards) {
789                        mContentContainer.setY(bottom - mContentContainer.getHeight());
790                    }
791                }
792            };
793            widthAnimation.setDuration(150);
794            widthAnimation.setStartOffset(150);
795            heightAnimation.setDuration(210);
796            mCloseOverflowAnimation.getAnimations().clear();
797            mCloseOverflowAnimation.setAnimationListener(mOnOverflowClosed);
798            mCloseOverflowAnimation.addAnimation(widthAnimation);
799            mCloseOverflowAnimation.addAnimation(heightAnimation);
800            mContentContainer.startAnimation(mCloseOverflowAnimation);
801        }
802
803        /**
804         * Prepares the content container for show and update calls.
805         */
806        private void preparePopupContent() {
807            // Reset visibility.
808            if (mMainPanel != null) {
809                mMainPanel.fadeIn(false);
810            }
811            if (mOverflowPanel != null) {
812                mOverflowPanel.fadeIn(false);
813            }
814
815            // Reset position.
816            if (isMainPanelContent()) {
817                positionMainPanel();
818            }
819            if (isOverflowPanelContent()) {
820                positionOverflowPanel();
821            }
822        }
823
824        private boolean isMainPanelContent() {
825            return mMainPanel != null
826                    && mContentContainer.getChildAt(0) == mMainPanel.getView();
827        }
828
829        private boolean isOverflowPanelContent() {
830            return mOverflowPanel != null
831                    && mContentContainer.getChildAt(0) == mOverflowPanel.getView();
832        }
833
834        /**
835         * Sets the current content to be the main view panel.
836         */
837        private void setMainPanelAsContent() {
838            // This should never be called if the main panel has not been initialized.
839            Preconditions.checkNotNull(mMainPanel);
840            mContentContainer.removeAllViews();
841            Size mainPanelSize = mMainPanel.measure();
842            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
843            params.width = mainPanelSize.getWidth();
844            params.height = mainPanelSize.getHeight();
845            mContentContainer.setLayoutParams(params);
846            mContentContainer.addView(mMainPanel.getView());
847            setContentAreaAsTouchableSurface();
848        }
849
850        /**
851         * Sets the current content to be the overflow view panel.
852         */
853        private void setOverflowPanelAsContent() {
854            // This should never be called if the overflow panel has not been initialized.
855            Preconditions.checkNotNull(mOverflowPanel);
856            mContentContainer.removeAllViews();
857            Size overflowPanelSize = mOverflowPanel.measure();
858            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
859            params.width = overflowPanelSize.getWidth();
860            params.height = overflowPanelSize.getHeight();
861            mContentContainer.setLayoutParams(params);
862            mContentContainer.addView(mOverflowPanel.getView());
863            setContentAreaAsTouchableSurface();
864        }
865
866        /**
867         * Places the main view panel at the appropriate resting coordinates.
868         */
869        private void positionMainPanel() {
870            Preconditions.checkNotNull(mMainPanel);
871            mContentContainer.setX(mMarginHorizontal);
872
873            float y = mMarginVertical;
874            if  (mOverflowDirection == OVERFLOW_DIRECTION_UP) {
875                y = getHeight()
876                        - (mMainPanel.getView().getMeasuredHeight() + mMarginVertical);
877            }
878            mContentContainer.setY(y);
879            setContentAreaAsTouchableSurface();
880        }
881
882        /**
883         * Places the main view panel at the appropriate resting coordinates.
884         */
885        private void positionOverflowPanel() {
886            Preconditions.checkNotNull(mOverflowPanel);
887            float x;
888            if (isRTL()) {
889                x = mMarginHorizontal;
890            } else {
891                x = mPopupWindow.getWidth()
892                    - (mOverflowPanel.getView().getMeasuredWidth() + mMarginHorizontal);
893            }
894            mContentContainer.setX(x);
895            mContentContainer.setY(mMarginVertical);
896            setContentAreaAsTouchableSurface();
897        }
898
899        private void updateOverflowHeight(int height) {
900            if (mOverflowPanel != null) {
901                mOverflowPanel.setSuggestedHeight(height);
902
903                // Re-measure the popup and it's contents.
904                boolean mainPanelContent = isMainPanelContent();
905                boolean overflowPanelContent = isOverflowPanelContent();
906                mContentContainer.removeAllViews();  // required to update popup size.
907                updatePopupSize();
908                // Reset the appropriate content.
909                if (mainPanelContent) {
910                    setMainPanelAsContent();
911                }
912                if (overflowPanelContent) {
913                    setOverflowPanelAsContent();
914                }
915            }
916        }
917
918        private void updatePopupSize() {
919            int width = 0;
920            int height = 0;
921            if (mMainPanel != null) {
922                Size mainPanelSize = mMainPanel.measure();
923                width = mainPanelSize.getWidth();
924                height = mainPanelSize.getHeight();
925            }
926            if (mOverflowPanel != null) {
927                Size overflowPanelSize = mOverflowPanel.measure();
928                width = Math.max(width, overflowPanelSize.getWidth());
929                height = Math.max(height, overflowPanelSize.getHeight());
930            }
931            mPopupWindow.setWidth(width + mMarginHorizontal * 2);
932            mPopupWindow.setHeight(height + mMarginVertical * 2);
933        }
934
935
936        private void refreshViewPort() {
937            mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
938        }
939
940        private boolean viewPortHasChanged() {
941            mParent.getWindowVisibleDisplayFrame(mTmpRect);
942            return !mTmpRect.equals(mViewPortOnScreen);
943        }
944
945        private int getToolbarWidth(int suggestedWidth) {
946            int width = suggestedWidth;
947            refreshViewPort();
948            int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
949                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
950            if (width <= 0) {
951                width = mParent.getResources()
952                        .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
953            }
954            return Math.min(width, maximumWidth);
955        }
956
957        /**
958         * Sets the touchable region of this popup to be zero. This means that all touch events on
959         * this popup will go through to the surface behind it.
960         */
961        private void setZeroTouchableSurface() {
962            mTouchableRegion.setEmpty();
963        }
964
965        /**
966         * Sets the touchable region of this popup to be the area occupied by its content.
967         */
968        private void setContentAreaAsTouchableSurface() {
969            if (!mPopupWindow.isShowing()) {
970                mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
971            }
972            int width = mContentContainer.getMeasuredWidth();
973            int height = mContentContainer.getMeasuredHeight();
974            mTouchableRegion.set(
975                    (int) mContentContainer.getX(),
976                    (int) mContentContainer.getY(),
977                    (int) mContentContainer.getX() + width,
978                    (int) mContentContainer.getY() + height);
979        }
980
981        /**
982         * Make the touchable area of this popup be the area specified by mTouchableRegion.
983         * This should be called after the popup window has been dismissed (dismiss/hide)
984         * and is probably being re-shown with a new content root view.
985         */
986        private void setTouchableSurfaceInsetsComputer() {
987            ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
988                    .getRootView()
989                    .getViewTreeObserver();
990            viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
991            viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
992        }
993
994        private boolean isRTL() {
995            return mContentContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
996        }
997    }
998
999    /**
1000     * A widget that holds the primary menu items in the floating toolbar.
1001     */
1002    private static final class FloatingToolbarMainPanel {
1003
1004        private final Context mContext;
1005        private final ViewGroup mContentView;
1006        private final View.OnClickListener mMenuItemButtonOnClickListener =
1007                new View.OnClickListener() {
1008                    @Override
1009                    public void onClick(View v) {
1010                        if (v.getTag() instanceof MenuItem) {
1011                            if (mOnMenuItemClickListener != null) {
1012                                mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
1013                            }
1014                        }
1015                    }
1016                };
1017        private final ViewFader viewFader;
1018        private final Runnable mOpenOverflow;
1019
1020        private View mOpenOverflowButton;
1021        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
1022
1023        /**
1024         * Initializes a floating toolbar popup main view panel.
1025         *
1026         * @param context
1027         * @param openOverflow  The code that opens the toolbar popup overflow.
1028         */
1029        public FloatingToolbarMainPanel(Context context, Runnable openOverflow) {
1030            mContext = Preconditions.checkNotNull(context);
1031            mContentView = new LinearLayout(context);
1032            viewFader = new ViewFader(mContentView);
1033            mOpenOverflow = Preconditions.checkNotNull(openOverflow);
1034        }
1035
1036        /**
1037         * Fits as many menu items in the main panel and returns a list of the menu items that
1038         * were not fit in.
1039         *
1040         * @return The menu items that are not included in this main panel.
1041         */
1042        public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int width) {
1043            Preconditions.checkNotNull(menuItems);
1044
1045            // Reserve space for the "open overflow" button.
1046            final int toolbarWidth = width - getEstimatedOpenOverflowButtonWidth(mContext);
1047
1048            int availableWidth = toolbarWidth;
1049            final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
1050
1051            mContentView.removeAllViews();
1052
1053            boolean isFirstItem = true;
1054            while (!remainingMenuItems.isEmpty()) {
1055                final MenuItem menuItem = remainingMenuItems.peek();
1056                View menuItemButton = createMenuItemButton(mContext, menuItem);
1057
1058                // Adding additional start padding for the first button to even out button spacing.
1059                if (isFirstItem) {
1060                    menuItemButton.setPaddingRelative(
1061                            (int) (1.5 * menuItemButton.getPaddingStart()),
1062                            menuItemButton.getPaddingTop(),
1063                            menuItemButton.getPaddingEnd(),
1064                            menuItemButton.getPaddingBottom());
1065                    isFirstItem = false;
1066                }
1067
1068                // Adding additional end padding for the last button to even out button spacing.
1069                if (remainingMenuItems.size() == 1) {
1070                    menuItemButton.setPaddingRelative(
1071                            menuItemButton.getPaddingStart(),
1072                            menuItemButton.getPaddingTop(),
1073                            (int) (1.5 * menuItemButton.getPaddingEnd()),
1074                            menuItemButton.getPaddingBottom());
1075                }
1076
1077                menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1078                int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
1079                if (menuItemButtonWidth <= availableWidth) {
1080                    setButtonTagAndClickListener(menuItemButton, menuItem);
1081                    mContentView.addView(menuItemButton);
1082                    ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1083                    params.width = menuItemButtonWidth;
1084                    menuItemButton.setLayoutParams(params);
1085                    availableWidth -= menuItemButtonWidth;
1086                    remainingMenuItems.pop();
1087                } else {
1088                    if (mOpenOverflowButton == null) {
1089                        mOpenOverflowButton = LayoutInflater.from(mContext)
1090                                .inflate(R.layout.floating_popup_open_overflow_button, null);
1091                        mOpenOverflowButton.setOnClickListener(new View.OnClickListener() {
1092                            @Override
1093                            public void onClick(View v) {
1094                                if (mOpenOverflowButton != null) {
1095                                    mOpenOverflow.run();
1096                                }
1097                            }
1098                        });
1099                    }
1100                    mContentView.addView(mOpenOverflowButton);
1101                    break;
1102                }
1103            }
1104            return remainingMenuItems;
1105        }
1106
1107        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1108            mOnMenuItemClickListener = listener;
1109        }
1110
1111        public View getView() {
1112            return mContentView;
1113        }
1114
1115        public void fadeIn(boolean animate) {
1116            viewFader.fadeIn(animate);
1117        }
1118
1119        public void fadeOut(boolean animate) {
1120            viewFader.fadeOut(animate);
1121        }
1122
1123        /**
1124         * Returns how big this panel's view should be.
1125         * This method should only be called when the view has not been attached to a parent
1126         * otherwise it will throw an illegal state.
1127         */
1128        public Size measure() throws IllegalStateException {
1129            Preconditions.checkState(mContentView.getParent() == null);
1130            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1131            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1132        }
1133
1134        private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1135            View button = menuItemButton;
1136            if (isIconOnlyMenuItem(menuItem)) {
1137                button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1138            }
1139            button.setTag(menuItem);
1140            button.setOnClickListener(mMenuItemButtonOnClickListener);
1141        }
1142    }
1143
1144
1145    /**
1146     * A widget that holds the overflow items in the floating toolbar.
1147     */
1148    private static final class FloatingToolbarOverflowPanel {
1149
1150        private final LinearLayout mContentView;
1151        private final ViewGroup mBackButtonContainer;
1152        private final View mBackButton;
1153        private final ListView mListView;
1154        private final TextView mListViewItemWidthCalculator;
1155        private final ViewFader mViewFader;
1156        private final Runnable mCloseOverflow;
1157
1158        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
1159        private int mOverflowWidth;
1160        private int mSuggestedHeight;
1161
1162        /**
1163         * Initializes a floating toolbar popup overflow view panel.
1164         *
1165         * @param context
1166         * @param closeOverflow  The code that closes the toolbar popup's overflow.
1167         */
1168        public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) {
1169            mCloseOverflow = Preconditions.checkNotNull(closeOverflow);
1170
1171            mContentView = new LinearLayout(context);
1172            mContentView.setOrientation(LinearLayout.VERTICAL);
1173            mViewFader = new ViewFader(mContentView);
1174
1175            mBackButton = LayoutInflater.from(context)
1176                    .inflate(R.layout.floating_popup_close_overflow_button, null);
1177            mBackButton.setOnClickListener(new View.OnClickListener() {
1178                @Override
1179                public void onClick(View v) {
1180                    mCloseOverflow.run();
1181                }
1182            });
1183            mBackButtonContainer = new LinearLayout(context);
1184            mBackButtonContainer.addView(mBackButton);
1185
1186            mListView = createOverflowListView();
1187            mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1188                @Override
1189                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1190                    MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position);
1191                    if (mOnMenuItemClickListener != null) {
1192                        mOnMenuItemClickListener.onMenuItemClick(menuItem);
1193                    }
1194                }
1195            });
1196
1197            mContentView.addView(mListView);
1198            mContentView.addView(mBackButtonContainer);
1199
1200            mListViewItemWidthCalculator = createOverflowMenuItemButton(context);
1201            mListViewItemWidthCalculator.setLayoutParams(new ViewGroup.LayoutParams(
1202                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1203        }
1204
1205        /**
1206         * Sets the menu items to be displayed in the overflow.
1207         */
1208        public void setMenuItems(List<MenuItem> menuItems) {
1209            ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter();
1210            overflowListViewAdapter.clear();
1211            overflowListViewAdapter.addAll(menuItems);
1212            setListViewHeight();
1213            setOverflowWidth();
1214        }
1215
1216        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1217            mOnMenuItemClickListener = listener;
1218        }
1219
1220        /**
1221         * Notifies the overflow of the current direction in which the overflow will be opened.
1222         *
1223         * @param overflowDirection  {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP}
1224         *   or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}.
1225         */
1226        public void setOverflowDirection(int overflowDirection) {
1227            mContentView.removeView(mBackButtonContainer);
1228            int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0;
1229            mContentView.addView(mBackButtonContainer, index);
1230        }
1231
1232        public void setSuggestedHeight(int height) {
1233            mSuggestedHeight = height;
1234            setListViewHeight();
1235        }
1236
1237        public int getMinimumHeight() {
1238            return mContentView.getContext().getResources().
1239                    getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height)
1240                    + getEstimatedToolbarHeight(mContentView.getContext());
1241        }
1242
1243        /**
1244         * Returns the content view of the overflow.
1245         */
1246        public View getView() {
1247            return mContentView;
1248        }
1249
1250        public void fadeIn(boolean animate) {
1251            mViewFader.fadeIn(animate);
1252        }
1253
1254        public void fadeOut(boolean animate) {
1255            mViewFader.fadeOut(animate);
1256        }
1257
1258        /**
1259         * Returns how big this panel's view should be.
1260         * This method should only be called when the view has not been attached to a parent.
1261         *
1262         * @throws IllegalStateException
1263         */
1264        public Size measure() {
1265            Preconditions.checkState(mContentView.getParent() == null);
1266            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1267            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1268        }
1269
1270        private void setListViewHeight() {
1271            int itemHeight = getEstimatedToolbarHeight(mContentView.getContext());
1272            int height = mListView.getAdapter().getCount() * itemHeight;
1273            int maxHeight = mContentView.getContext().getResources().
1274                    getDimensionPixelSize(R.dimen.floating_toolbar_maximum_overflow_height);
1275            int minHeight = mContentView.getContext().getResources().
1276                    getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
1277            int suggestedListViewHeight = mSuggestedHeight - (mSuggestedHeight % itemHeight)
1278                    - itemHeight;  // reserve space for the back button.
1279            ViewGroup.LayoutParams params = mListView.getLayoutParams();
1280            if (suggestedListViewHeight <= 0) {
1281                // Invalid height. Use the maximum height available.
1282                params.height = Math.min(maxHeight, height);
1283            } else if (suggestedListViewHeight < minHeight) {
1284                // Height is smaller than minimum allowed. Use minimum height.
1285                params.height = minHeight;
1286            } else {
1287                // Use the suggested height. Cap it at the maximum available height.
1288                params.height = Math.min(Math.min(suggestedListViewHeight, maxHeight), height);
1289            }
1290            mListView.setLayoutParams(params);
1291        }
1292
1293        private void setOverflowWidth() {
1294            mOverflowWidth = 0;
1295            for (int i = 0; i < mListView.getAdapter().getCount(); i++) {
1296                MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(i);
1297                Preconditions.checkNotNull(menuItem);
1298                mListViewItemWidthCalculator.setText(menuItem.getTitle());
1299                mListViewItemWidthCalculator.measure(
1300                        MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1301                mOverflowWidth = Math.max(
1302                        mListViewItemWidthCalculator.getMeasuredWidth(), mOverflowWidth);
1303            }
1304        }
1305
1306        private ListView createOverflowListView() {
1307            final Context context = mContentView.getContext();
1308            final ListView overflowListView = new ListView(context);
1309            overflowListView.setLayoutParams(new ViewGroup.LayoutParams(
1310                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1311            overflowListView.setDivider(null);
1312            overflowListView.setDividerHeight(0);
1313
1314            final int viewTypeCount = 2;
1315            final int stringLabelViewType = 0;
1316            final int iconOnlyViewType = 1;
1317            final ArrayAdapter overflowListViewAdapter =
1318                    new ArrayAdapter<MenuItem>(context, 0) {
1319                        @Override
1320                        public int getViewTypeCount() {
1321                            return viewTypeCount;
1322                        }
1323
1324                        @Override
1325                        public int getItemViewType(int position) {
1326                            if (isIconOnlyMenuItem(getItem(position))) {
1327                                return iconOnlyViewType;
1328                            }
1329                            return stringLabelViewType;
1330                        }
1331
1332                        @Override
1333                        public View getView(int position, View convertView, ViewGroup parent) {
1334                            if (getItemViewType(position) == iconOnlyViewType) {
1335                                return getIconOnlyView(position, convertView);
1336                            }
1337                            return getStringTitleView(position, convertView);
1338                        }
1339
1340                        private View getStringTitleView(int position, View convertView) {
1341                            TextView menuButton;
1342                            if (convertView != null) {
1343                                menuButton = (TextView) convertView;
1344                            } else {
1345                                menuButton = createOverflowMenuItemButton(context);
1346                            }
1347                            MenuItem menuItem = getItem(position);
1348                            menuButton.setText(menuItem.getTitle());
1349                            menuButton.setContentDescription(menuItem.getTitle());
1350                            menuButton.setMinimumWidth(mOverflowWidth);
1351                            return menuButton;
1352                        }
1353
1354                        private View getIconOnlyView(int position, View convertView) {
1355                            View menuButton;
1356                            if (convertView != null) {
1357                                menuButton = convertView;
1358                            } else {
1359                                menuButton = LayoutInflater.from(context).inflate(
1360                                        R.layout.floating_popup_overflow_image_list_item, null);
1361                            }
1362                            MenuItem menuItem = getItem(position);
1363                            ((ImageView) menuButton
1364                                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1365                                    .setImageDrawable(menuItem.getIcon());
1366                            menuButton.setMinimumWidth(mOverflowWidth);
1367                            return menuButton;
1368                        }
1369                    };
1370            overflowListView.setAdapter(overflowListViewAdapter);
1371            return overflowListView;
1372        }
1373    }
1374
1375
1376    /**
1377     * A helper for fading in or out a view.
1378     */
1379    private static final class ViewFader {
1380
1381        private static final int FADE_OUT_DURATION = 250;
1382        private static final int FADE_IN_DURATION = 150;
1383
1384        private final View mView;
1385        private final ObjectAnimator mFadeOutAnimation;
1386        private final ObjectAnimator mFadeInAnimation;
1387
1388        private ViewFader(View view) {
1389            mView = Preconditions.checkNotNull(view);
1390            mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0)
1391                    .setDuration(FADE_OUT_DURATION);
1392            mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1)
1393                    .setDuration(FADE_IN_DURATION);
1394        }
1395
1396        public void fadeIn(boolean animate) {
1397            cancelFadeAnimations();
1398            if (animate) {
1399                mFadeInAnimation.start();
1400            } else {
1401                mView.setAlpha(1);
1402            }
1403        }
1404
1405        public void fadeOut(boolean animate) {
1406            cancelFadeAnimations();
1407            if (animate) {
1408                mFadeOutAnimation.start();
1409            } else {
1410                mView.setAlpha(0);
1411            }
1412        }
1413
1414        private void cancelFadeAnimations() {
1415            mFadeInAnimation.cancel();
1416            mFadeOutAnimation.cancel();
1417        }
1418    }
1419
1420    /**
1421     * @return {@code true} if the menu item does not not have a string title but has an icon.
1422     *   {@code false} otherwise.
1423     */
1424    private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1425        if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1426            return true;
1427        }
1428        return false;
1429    }
1430
1431    /**
1432     * Creates and returns a menu button for the specified menu item.
1433     */
1434    private static View createMenuItemButton(Context context, MenuItem menuItem) {
1435        if (isIconOnlyMenuItem(menuItem)) {
1436            View imageMenuItemButton = LayoutInflater.from(context)
1437                    .inflate(R.layout.floating_popup_menu_image_button, null);
1438            ((ImageButton) imageMenuItemButton
1439                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1440                    .setImageDrawable(menuItem.getIcon());
1441            return imageMenuItemButton;
1442        }
1443
1444        Button menuItemButton = (Button) LayoutInflater.from(context)
1445                .inflate(R.layout.floating_popup_menu_button, null);
1446        menuItemButton.setText(menuItem.getTitle());
1447        menuItemButton.setContentDescription(menuItem.getTitle());
1448        return menuItemButton;
1449    }
1450
1451    /**
1452     * Creates and returns a styled floating toolbar overflow list view item.
1453     */
1454    private static TextView createOverflowMenuItemButton(Context context) {
1455        return (TextView) LayoutInflater.from(context)
1456                .inflate(R.layout.floating_popup_overflow_list_item, null);
1457    }
1458
1459    private static ViewGroup createContentContainer(Context context) {
1460        return (ViewGroup) LayoutInflater.from(context)
1461                .inflate(R.layout.floating_popup_container, null);
1462    }
1463
1464    private static PopupWindow createPopupWindow(View content) {
1465        ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1466        PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1467        // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1468        // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1469        popupWindow.setClippingEnabled(false);
1470        popupWindow.setWindowLayoutType(
1471                WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1472        popupWindow.setAnimationStyle(0);
1473        popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1474        content.setLayoutParams(new ViewGroup.LayoutParams(
1475                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1476        popupContentHolder.addView(content);
1477        return popupWindow;
1478    }
1479
1480    /**
1481     * Creates an "appear" animation for the specified view.
1482     *
1483     * @param view  The view to animate
1484     */
1485    private static AnimatorSet createEnterAnimation(View view) {
1486        AnimatorSet animation =  new AnimatorSet();
1487        animation.playTogether(
1488                ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(200),
1489                // Make sure that view.x is always fixed throughout the duration of this animation.
1490                ObjectAnimator.ofFloat(view, View.X, view.getX(), view.getX()));
1491        animation.setStartDelay(50);
1492        return animation;
1493    }
1494
1495    /**
1496     * Creates a "disappear" animation for the specified view.
1497     *
1498     * @param view  The view to animate
1499     * @param startDelay  The start delay of the animation
1500     * @param listener  The animation listener
1501     */
1502    private static AnimatorSet createExitAnimation(
1503            View view, int startDelay, Animator.AnimatorListener listener) {
1504        AnimatorSet animation =  new AnimatorSet();
1505        animation.playTogether(
1506                ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(200));
1507        animation.setStartDelay(startDelay);
1508        animation.addListener(listener);
1509        return animation;
1510    }
1511
1512    /**
1513     * Returns a re-themed context with controlled look and feel for views.
1514     */
1515    private static Context applyDefaultTheme(Context originalContext) {
1516        TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1517        boolean isLightTheme = a.getBoolean(0, true);
1518        int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material;
1519        a.recycle();
1520        return new ContextThemeWrapper(originalContext, themeId);
1521    }
1522
1523    private static int getEstimatedToolbarHeight(Context context) {
1524        return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1525    }
1526
1527    private static int getEstimatedOpenOverflowButtonWidth(Context context) {
1528        return context.getResources()
1529                .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
1530    }
1531}
1532