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