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