FloatingToolbar.java revision 6c5ac8e9e6b4b06c460cd2231cdeb93b373efe5d
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<CharSequence> mShowingTitles = new ArrayList<CharSequence>();
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            mShowingTitles = getMenuItemTitles(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 mShowingTitles.equals(getMenuItemTitles(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<CharSequence> getMenuItemTitles(List<MenuItem> menuItems) {
237        List<CharSequence> titles = new ArrayList<CharSequence>();
238        for (MenuItem menuItem : menuItems) {
239            titles.add(menuItem.getTitle());
240        }
241        return titles;
242    }
243
244
245    /**
246     * A popup window used by the floating toolbar.
247     *
248     * This class is responsible for the rendering/animation of the floating toolbar.
249     * It can hold one of 2 panels (i.e. main panel and overflow panel) at a time.
250     * It delegates specific panel functionality to the appropriate panel.
251     */
252    private static final class FloatingToolbarPopup {
253
254        public static final int OVERFLOW_DIRECTION_UP = 0;
255        public static final int OVERFLOW_DIRECTION_DOWN = 1;
256
257        private final View mParent;
258        private final PopupWindow mPopupWindow;
259        private final ViewGroup mContentContainer;
260        private final int mMarginHorizontal;
261        private final int mMarginVertical;
262
263        private final Animation.AnimationListener mOnOverflowOpened =
264                new Animation.AnimationListener() {
265                    @Override
266                    public void onAnimationStart(Animation animation) {}
267
268                    @Override
269                    public void onAnimationEnd(Animation animation) {
270                        setOverflowPanelAsContent();
271                        mOverflowPanel.fadeIn(true);
272                    }
273
274                    @Override
275                    public void onAnimationRepeat(Animation animation) {}
276                };
277        private final Animation.AnimationListener mOnOverflowClosed =
278                new Animation.AnimationListener() {
279                    @Override
280                    public void onAnimationStart(Animation animation) {}
281
282                    @Override
283                    public void onAnimationEnd(Animation animation) {
284                        setMainPanelAsContent();
285                        mMainPanel.fadeIn(true);
286                    }
287
288                    @Override
289                    public void onAnimationRepeat(Animation animation) {
290                    }
291                };
292        private final AnimatorSet mShowAnimation;
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 Point mCoords = new Point();
328
329        private final Region mTouchableRegion = new Region();
330        private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
331                new ViewTreeObserver.OnComputeInternalInsetsListener() {
332                    public void onComputeInternalInsets(
333                            ViewTreeObserver.InternalInsetsInfo info) {
334                        info.contentInsets.setEmpty();
335                        info.visibleInsets.setEmpty();
336                        info.touchableRegion.set(mTouchableRegion);
337                        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
338                                .TOUCHABLE_INSETS_REGION);
339                    }
340                };
341
342        private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
343        private boolean mHidden; // tracks whether this popup is hidden or hiding.
344
345        private FloatingToolbarOverflowPanel mOverflowPanel;
346        private FloatingToolbarMainPanel mMainPanel;
347        private int mOverflowDirection;
348
349        /**
350         * Initializes a new floating toolbar popup.
351         *
352         * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
353         *      from.
354         */
355        public FloatingToolbarPopup(View parent) {
356            mMarginHorizontal = parent.getResources()
357                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
358            mMarginVertical = parent.getResources()
359                    .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
360            mParent = Preconditions.checkNotNull(parent);
361            mContentContainer = createContentContainer(parent.getContext());
362            mPopupWindow = createPopupWindow(mContentContainer);
363            mShowAnimation = createGrowFadeInFromBottom(mContentContainer, mMarginHorizontal);
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        }
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, 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            updateOverflowHeight(contentRect.top - (mMarginVertical * 2));
436            refreshCoordinatesAndOverflowDirection(contentRect);
437            preparePopupContent();
438            mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, mCoords.x, mCoords.y);
439            setTouchableSurfaceInsetsComputer();
440            runShowAnimation();
441        }
442
443        /**
444         * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
445         */
446        public void dismiss() {
447            if (mDismissed) {
448                return;
449            }
450
451            mHidden = false;
452            mDismissed = true;
453            mHideAnimation.cancel();
454            runDismissAnimation();
455            setZeroTouchableSurface();
456        }
457
458        /**
459         * Hides this popup. This is a no-op if this popup is not showing.
460         * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
461         */
462        public void hide() {
463            if (!isShowing()) {
464                return;
465            }
466
467            mHidden = true;
468            runHideAnimation();
469            setZeroTouchableSurface();
470        }
471
472        /**
473         * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
474         */
475        public boolean isShowing() {
476            return !mDismissed && !mHidden;
477        }
478
479        /**
480         * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
481         */
482        public boolean isHidden() {
483            return mHidden;
484        }
485
486        /**
487         * Updates the coordinates of this popup.
488         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
489         * This is a no-op if this popup is not showing.
490         */
491        public void updateCoordinates(Rect contentRect) {
492            Preconditions.checkNotNull(contentRect);
493
494            if (!isShowing() || !mPopupWindow.isShowing()) {
495                return;
496            }
497
498            cancelOverflowAnimations();
499            refreshCoordinatesAndOverflowDirection(contentRect);
500            preparePopupContent();
501            mPopupWindow.update(mCoords.x, mCoords.y, getWidth(), getHeight());
502        }
503
504        /**
505         * Returns the width of this popup.
506         */
507        public int getWidth() {
508            return mPopupWindow.getWidth();
509        }
510
511        /**
512         * Returns the height of this popup.
513         */
514        public int getHeight() {
515            return mPopupWindow.getHeight();
516        }
517
518        /**
519         * Returns the context this popup is running in.
520         */
521        public Context getContext() {
522            return mContentContainer.getContext();
523        }
524
525        private void refreshCoordinatesAndOverflowDirection(Rect contentRect) {
526            int x = contentRect.centerX() - getWidth() / 2;
527            int y;
528            if (contentRect.top > getHeight()) {
529                y = contentRect.top - getHeight();
530                mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_UP;
531            } else if (contentRect.top > getToolbarHeightWithVerticalMargin()) {
532                y = contentRect.top - getToolbarHeightWithVerticalMargin();
533                mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
534            } else {
535                y = contentRect.bottom;
536                mOverflowDirection = FloatingToolbarPopup.OVERFLOW_DIRECTION_DOWN;
537            }
538            mCoords.set(x, y);
539            if (mOverflowPanel != null) {
540                mOverflowPanel.setOverflowDirection(mOverflowDirection);
541            }
542        }
543
544        private int getToolbarHeightWithVerticalMargin() {
545            return getEstimatedToolbarHeight(mParent.getContext()) + mMarginVertical * 2;
546        }
547
548        /**
549         * Performs the "show" animation on the floating popup.
550         */
551        private void runShowAnimation() {
552            mShowAnimation.start();
553        }
554
555        /**
556         * Performs the "dismiss" animation on the floating popup.
557         */
558        private void runDismissAnimation() {
559            mDismissAnimation.start();
560        }
561
562        /**
563         * Performs the "hide" animation on the floating popup.
564         */
565        private void runHideAnimation() {
566            mHideAnimation.start();
567        }
568
569        private void cancelDismissAndHideAnimations() {
570            mDismissAnimation.cancel();
571            mHideAnimation.cancel();
572        }
573
574        private void cancelOverflowAnimations() {
575            mOpenOverflowAnimation.cancel();
576            mCloseOverflowAnimation.cancel();
577        }
578
579        /**
580         * Opens the floating toolbar overflow.
581         * This method should not be called if menu items have not been laid out with
582         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
583         *
584         * @throws IllegalStateException if called when menu items have not been laid out.
585         */
586        private void openOverflow() {
587            Preconditions.checkState(mMainPanel != null);
588            Preconditions.checkState(mOverflowPanel != null);
589
590            mMainPanel.fadeOut(true);
591            Size overflowPanelSize = mOverflowPanel.measure();
592            final int targetWidth = overflowPanelSize.getWidth();
593            final int targetHeight = overflowPanelSize.getHeight();
594            final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
595            final int startWidth = mContentContainer.getWidth();
596            final int startHeight = mContentContainer.getHeight();
597            final float startY = mContentContainer.getY();
598            final float left = mContentContainer.getX();
599            final float right = left + mContentContainer.getWidth();
600            final boolean rtl = mContentContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
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                    if (rtl) {
609                        mContentContainer.setX(left);
610                    } else {
611                        mContentContainer.setX(right - mContentContainer.getWidth());
612                    }
613                }
614            };
615            Animation heightAnimation = new Animation() {
616                @Override
617                protected void applyTransformation(float interpolatedTime, Transformation t) {
618                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
619                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
620                    params.height = startHeight + deltaHeight;
621                    mContentContainer.setLayoutParams(params);
622                    if (morphUpwards) {
623                        float y = startY - (mContentContainer.getHeight() - startHeight);
624                        mContentContainer.setY(y);
625                    }
626                }
627            };
628            widthAnimation.setDuration(240);
629            heightAnimation.setDuration(180);
630            heightAnimation.setStartOffset(60);
631            mOpenOverflowAnimation.getAnimations().clear();
632            mOpenOverflowAnimation.setAnimationListener(mOnOverflowOpened);
633            mOpenOverflowAnimation.addAnimation(widthAnimation);
634            mOpenOverflowAnimation.addAnimation(heightAnimation);
635            mContentContainer.startAnimation(mOpenOverflowAnimation);
636        }
637
638        /**
639         * Opens the floating toolbar overflow.
640         * This method should not be called if menu items have not been laid out with
641         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
642         *
643         * @throws IllegalStateException if called when menu items have not been laid out.
644         */
645        private void closeOverflow() {
646            Preconditions.checkState(mMainPanel != null);
647            Preconditions.checkState(mOverflowPanel != null);
648
649            mOverflowPanel.fadeOut(true);
650            Size mainPanelSize = mMainPanel.measure();
651            final int targetWidth = mainPanelSize.getWidth();
652            final int targetHeight = mainPanelSize.getHeight();
653            final int startWidth = mContentContainer.getWidth();
654            final int startHeight = mContentContainer.getHeight();
655            final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
656            final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
657            final float left = mContentContainer.getX();
658            final float right = left + mContentContainer.getWidth();
659            final boolean rtl = mContentContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
660            Animation widthAnimation = new Animation() {
661                @Override
662                protected void applyTransformation(float interpolatedTime, Transformation t) {
663                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
664                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
665                    params.width = startWidth + deltaWidth;
666                    mContentContainer.setLayoutParams(params);
667                    if (rtl) {
668                        mContentContainer.setX(left);
669                    } else {
670                        mContentContainer.setX(right - mContentContainer.getWidth());
671                    }
672                }
673            };
674            Animation heightAnimation = new Animation() {
675                @Override
676                protected void applyTransformation(float interpolatedTime, Transformation t) {
677                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
678                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
679                    params.height = startHeight + deltaHeight;
680                    mContentContainer.setLayoutParams(params);
681                    if (morphedUpwards) {
682                        mContentContainer.setY(bottom - mContentContainer.getHeight());
683                    }
684                }
685            };
686            widthAnimation.setDuration(150);
687            widthAnimation.setStartOffset(150);
688            heightAnimation.setDuration(210);
689            mCloseOverflowAnimation.getAnimations().clear();
690            mCloseOverflowAnimation.setAnimationListener(mOnOverflowClosed);
691            mCloseOverflowAnimation.addAnimation(widthAnimation);
692            mCloseOverflowAnimation.addAnimation(heightAnimation);
693            mContentContainer.startAnimation(mCloseOverflowAnimation);
694        }
695
696        /**
697         * Prepares the content container for show and update calls.
698         */
699        private void preparePopupContent() {
700            // Reset visibility.
701            if (mMainPanel != null) {
702                mMainPanel.fadeIn(false);
703            }
704            if (mOverflowPanel != null) {
705                mOverflowPanel.fadeIn(false);
706            }
707
708            // Reset position.
709            if (isMainPanelContent()) {
710                positionMainPanel();
711            }
712            if (isOverflowPanelContent()) {
713                positionOverflowPanel();
714            }
715        }
716
717        private boolean isMainPanelContent() {
718            return mMainPanel != null
719                    && mContentContainer.getChildAt(0) == mMainPanel.getView();
720        }
721
722        private boolean isOverflowPanelContent() {
723            return mOverflowPanel != null
724                    && mContentContainer.getChildAt(0) == mOverflowPanel.getView();
725        }
726
727        /**
728         * Sets the current content to be the main view panel.
729         */
730        private void setMainPanelAsContent() {
731            // This should never be called if the main panel has not been initialized.
732            Preconditions.checkNotNull(mMainPanel);
733            mContentContainer.removeAllViews();
734            Size mainPanelSize = mMainPanel.measure();
735            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
736            params.width = mainPanelSize.getWidth();
737            params.height = mainPanelSize.getHeight();
738            mContentContainer.setLayoutParams(params);
739            mContentContainer.addView(mMainPanel.getView());
740            setContentAreaAsTouchableSurface();
741        }
742
743        /**
744         * Sets the current content to be the overflow view panel.
745         */
746        private void setOverflowPanelAsContent() {
747            // This should never be called if the overflow panel has not been initialized.
748            Preconditions.checkNotNull(mOverflowPanel);
749            mContentContainer.removeAllViews();
750            Size overflowPanelSize = mOverflowPanel.measure();
751            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
752            params.width = overflowPanelSize.getWidth();
753            params.height = overflowPanelSize.getHeight();
754            mContentContainer.setLayoutParams(params);
755            mContentContainer.addView(mOverflowPanel.getView());
756            setContentAreaAsTouchableSurface();
757        }
758
759        /**
760         * Places the main view panel at the appropriate resting coordinates.
761         */
762        private void positionMainPanel() {
763            Preconditions.checkNotNull(mMainPanel);
764            mContentContainer.setX(mMarginHorizontal);
765
766            float y = mMarginVertical;
767            if  (mOverflowDirection == OVERFLOW_DIRECTION_UP) {
768                y = getHeight()
769                        - (mMainPanel.getView().getMeasuredHeight() + mMarginVertical);
770            }
771            mContentContainer.setY(y);
772            setContentAreaAsTouchableSurface();
773        }
774
775        /**
776         * Places the main view panel at the appropriate resting coordinates.
777         */
778        private void positionOverflowPanel() {
779            Preconditions.checkNotNull(mOverflowPanel);
780            float x = mPopupWindow.getWidth()
781                    - (mOverflowPanel.getView().getMeasuredWidth() + mMarginHorizontal);
782            mContentContainer.setX(x);
783            mContentContainer.setY(mMarginVertical);
784            setContentAreaAsTouchableSurface();
785        }
786
787        private void updateOverflowHeight(int height) {
788            if (mOverflowPanel != null) {
789                mOverflowPanel.setSuggestedHeight(height);
790
791                // Re-measure the popup and it's contents.
792                boolean mainPanelContent = isMainPanelContent();
793                boolean overflowPanelContent = isOverflowPanelContent();
794                mContentContainer.removeAllViews();  // required to update popup size.
795                updatePopupSize();
796                // Reset the appropriate content.
797                if (mainPanelContent) {
798                    setMainPanelAsContent();
799                }
800                if (overflowPanelContent) {
801                    setOverflowPanelAsContent();
802                }
803            }
804        }
805
806        private void updatePopupSize() {
807            int width = 0;
808            int height = 0;
809            if (mMainPanel != null) {
810                Size mainPanelSize = mMainPanel.measure();
811                width = mainPanelSize.getWidth();
812                height = mainPanelSize.getHeight();
813            }
814            if (mOverflowPanel != null) {
815                Size overflowPanelSize = mOverflowPanel.measure();
816                width = Math.max(width, overflowPanelSize.getWidth());
817                height = Math.max(height, overflowPanelSize.getHeight());
818            }
819            mPopupWindow.setWidth(width + mMarginHorizontal * 2);
820            mPopupWindow.setHeight(height + mMarginVertical * 2);
821        }
822
823        /**
824         * Sets the touchable region of this popup to be zero. This means that all touch events on
825         * this popup will go through to the surface behind it.
826         */
827        private void setZeroTouchableSurface() {
828            mTouchableRegion.setEmpty();
829        }
830
831        /**
832         * Sets the touchable region of this popup to be the area occupied by its content.
833         */
834        private void setContentAreaAsTouchableSurface() {
835            if (!mPopupWindow.isShowing()) {
836                mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
837            }
838            int width = mContentContainer.getMeasuredWidth();
839            int height = mContentContainer.getMeasuredHeight();
840            mTouchableRegion.set(
841                    (int) mContentContainer.getX(),
842                    (int) mContentContainer.getY(),
843                    (int) mContentContainer.getX() + width,
844                    (int) mContentContainer.getY() + height);
845        }
846
847        /**
848         * Make the touchable area of this popup be the area specified by mTouchableRegion.
849         * This should be called after the popup window has been dismissed (dismiss/hide)
850         * and is probably being re-shown with a new content root view.
851         */
852        private void setTouchableSurfaceInsetsComputer() {
853            ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
854                    .getRootView()
855                    .getViewTreeObserver();
856            viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
857            viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
858        }
859    }
860
861    /**
862     * A widget that holds the primary menu items in the floating toolbar.
863     */
864    private static final class FloatingToolbarMainPanel {
865
866        private final Context mContext;
867        private final ViewGroup mContentView;
868        private final View.OnClickListener mMenuItemButtonOnClickListener =
869                new View.OnClickListener() {
870                    @Override
871                    public void onClick(View v) {
872                        if (v.getTag() instanceof MenuItem) {
873                            if (mOnMenuItemClickListener != null) {
874                                mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
875                            }
876                        }
877                    }
878                };
879        private final ViewFader viewFader;
880        private final Runnable mOpenOverflow;
881
882        private View mOpenOverflowButton;
883        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
884
885        /**
886         * Initializes a floating toolbar popup main view panel.
887         *
888         * @param context
889         * @param openOverflow  The code that opens the toolbar popup overflow.
890         */
891        public FloatingToolbarMainPanel(Context context, Runnable openOverflow) {
892            mContext = Preconditions.checkNotNull(context);
893            mContentView = new LinearLayout(context);
894            viewFader = new ViewFader(mContentView);
895            mOpenOverflow = Preconditions.checkNotNull(openOverflow);
896        }
897
898        /**
899         * Fits as many menu items in the main panel and returns a list of the menu items that
900         * were not fit in.
901         *
902         * @return The menu items that are not included in this main panel.
903         */
904        public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int suggestedWidth) {
905            Preconditions.checkNotNull(menuItems);
906
907            final int toolbarWidth = getAdjustedToolbarWidth(mContext, suggestedWidth)
908                    // Reserve space for the "open overflow" button.
909                    - getEstimatedOpenOverflowButtonWidth(mContext);
910
911            int availableWidth = toolbarWidth;
912            final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
913
914            mContentView.removeAllViews();
915
916            boolean isFirstItem = true;
917            while (!remainingMenuItems.isEmpty()) {
918                final MenuItem menuItem = remainingMenuItems.peek();
919                View menuItemButton = createMenuItemButton(mContext, menuItem);
920
921                // Adding additional start padding for the first button to even out button spacing.
922                if (isFirstItem) {
923                    menuItemButton.setPaddingRelative(
924                            (int) (1.5 * menuItemButton.getPaddingStart()),
925                            menuItemButton.getPaddingTop(),
926                            menuItemButton.getPaddingEnd(),
927                            menuItemButton.getPaddingBottom());
928                    isFirstItem = false;
929                }
930
931                // Adding additional end padding for the last button to even out button spacing.
932                if (remainingMenuItems.size() == 1) {
933                    menuItemButton.setPaddingRelative(
934                            menuItemButton.getPaddingStart(),
935                            menuItemButton.getPaddingTop(),
936                            (int) (1.5 * menuItemButton.getPaddingEnd()),
937                            menuItemButton.getPaddingBottom());
938                }
939
940                menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
941                int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
942                if (menuItemButtonWidth <= availableWidth) {
943                    setButtonTagAndClickListener(menuItemButton, menuItem);
944                    mContentView.addView(menuItemButton);
945                    ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
946                    params.width = menuItemButtonWidth;
947                    menuItemButton.setLayoutParams(params);
948                    availableWidth -= menuItemButtonWidth;
949                    remainingMenuItems.pop();
950                } else {
951                    if (mOpenOverflowButton == null) {
952                        mOpenOverflowButton = LayoutInflater.from(mContext)
953                                .inflate(R.layout.floating_popup_open_overflow_button, null);
954                        mOpenOverflowButton.setOnClickListener(new View.OnClickListener() {
955                            @Override
956                            public void onClick(View v) {
957                                if (mOpenOverflowButton != null) {
958                                    mOpenOverflow.run();
959                                }
960                            }
961                        });
962                    }
963                    mContentView.addView(mOpenOverflowButton);
964                    break;
965                }
966            }
967            return remainingMenuItems;
968        }
969
970        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
971            mOnMenuItemClickListener = listener;
972        }
973
974        public View getView() {
975            return mContentView;
976        }
977
978        public void fadeIn(boolean animate) {
979            viewFader.fadeIn(animate);
980        }
981
982        public void fadeOut(boolean animate) {
983            viewFader.fadeOut(animate);
984        }
985
986        /**
987         * Returns how big this panel's view should be.
988         * This method should only be called when the view has not been attached to a parent
989         * otherwise it will throw an illegal state.
990         */
991        public Size measure() throws IllegalStateException {
992            Preconditions.checkState(mContentView.getParent() == null);
993            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
994            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
995        }
996
997        private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
998            View button = menuItemButton;
999            if (isIconOnlyMenuItem(menuItem)) {
1000                button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1001            }
1002            button.setTag(menuItem);
1003            button.setOnClickListener(mMenuItemButtonOnClickListener);
1004        }
1005    }
1006
1007
1008    /**
1009     * A widget that holds the overflow items in the floating toolbar.
1010     */
1011    private static final class FloatingToolbarOverflowPanel {
1012
1013        private final LinearLayout mContentView;
1014        private final ViewGroup mBackButtonContainer;
1015        private final View mBackButton;
1016        private final ListView mListView;
1017        private final TextView mListViewItemWidthCalculator;
1018        private final ViewFader mViewFader;
1019        private final Runnable mCloseOverflow;
1020
1021        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
1022        private int mOverflowWidth = 0;
1023        private int mSuggestedHeight;
1024
1025        /**
1026         * Initializes a floating toolbar popup overflow view panel.
1027         *
1028         * @param context
1029         * @param closeOverflow  The code that closes the toolbar popup's overflow.
1030         */
1031        public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) {
1032            mCloseOverflow = Preconditions.checkNotNull(closeOverflow);
1033            mSuggestedHeight = getScreenHeight(context);
1034
1035            mContentView = new LinearLayout(context);
1036            mContentView.setOrientation(LinearLayout.VERTICAL);
1037            mViewFader = new ViewFader(mContentView);
1038
1039            mBackButton = LayoutInflater.from(context)
1040                    .inflate(R.layout.floating_popup_close_overflow_button, null);
1041            mBackButton.setOnClickListener(new View.OnClickListener() {
1042                @Override
1043                public void onClick(View v) {
1044                    mCloseOverflow.run();
1045                }
1046            });
1047            mBackButtonContainer = new LinearLayout(context);
1048            mBackButtonContainer.addView(mBackButton);
1049
1050            mListView = createOverflowListView();
1051            mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1052                @Override
1053                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1054                    MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position);
1055                    if (mOnMenuItemClickListener != null) {
1056                        mOnMenuItemClickListener.onMenuItemClick(menuItem);
1057                    }
1058                }
1059            });
1060
1061            mContentView.addView(mListView);
1062            mContentView.addView(mBackButtonContainer);
1063
1064            mListViewItemWidthCalculator = createOverflowMenuItemButton(context);
1065            mListViewItemWidthCalculator.setLayoutParams(new ViewGroup.LayoutParams(
1066                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1067        }
1068
1069        /**
1070         * Sets the menu items to be displayed in the overflow.
1071         */
1072        public void setMenuItems(List<MenuItem> menuItems) {
1073            ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter();
1074            overflowListViewAdapter.clear();
1075            overflowListViewAdapter.addAll(menuItems);
1076            setListViewHeight();
1077            setOverflowWidth();
1078        }
1079
1080        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1081            mOnMenuItemClickListener = listener;
1082        }
1083
1084        /**
1085         * Notifies the overflow of the current direction in which the overflow will be opened.
1086         *
1087         * @param overflowDirection  {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP}
1088         *   or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}.
1089         */
1090        public void setOverflowDirection(int overflowDirection) {
1091            mContentView.removeView(mBackButtonContainer);
1092            int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0;
1093            mContentView.addView(mBackButtonContainer, index);
1094        }
1095
1096        public void setSuggestedHeight(int height) {
1097            mSuggestedHeight = height;
1098            setListViewHeight();
1099        }
1100
1101        /**
1102         * Returns the content view of the overflow.
1103         */
1104        public View getView() {
1105            return mContentView;
1106        }
1107
1108        public void fadeIn(boolean animate) {
1109            mViewFader.fadeIn(animate);
1110        }
1111
1112        public void fadeOut(boolean animate) {
1113            mViewFader.fadeOut(animate);
1114        }
1115
1116        /**
1117         * Returns how big this panel's view should be.
1118         * This method should only be called when the view has not been attached to a parent.
1119         *
1120         * @throws IllegalStateException
1121         */
1122        public Size measure() {
1123            Preconditions.checkState(mContentView.getParent() == null);
1124            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1125            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1126        }
1127
1128        private void setListViewHeight() {
1129            int itemHeight = getEstimatedToolbarHeight(mContentView.getContext());
1130            int height = mListView.getAdapter().getCount() * itemHeight;
1131            int maxHeight = mContentView.getContext().getResources().
1132                    getDimensionPixelSize(R.dimen.floating_toolbar_maximum_overflow_height);
1133            int minHeight = mContentView.getContext().getResources().
1134                    getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
1135            int availableHeight = mSuggestedHeight - (mSuggestedHeight % itemHeight)
1136                    - itemHeight;  // reserve space for the back button.
1137            ViewGroup.LayoutParams params = mListView.getLayoutParams();
1138            if (availableHeight >= minHeight) {
1139                params.height = Math.min(Math.min(availableHeight, maxHeight), height);
1140            } else {
1141                params.height = Math.min(maxHeight, height);
1142            }
1143            mListView.setLayoutParams(params);
1144        }
1145
1146        private int setOverflowWidth() {
1147            for (int i = 0; i < mListView.getAdapter().getCount(); i++) {
1148                MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(i);
1149                Preconditions.checkNotNull(menuItem);
1150                mListViewItemWidthCalculator.setText(menuItem.getTitle());
1151                mListViewItemWidthCalculator.measure(
1152                        MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1153                mOverflowWidth = Math.max(
1154                        mListViewItemWidthCalculator.getMeasuredWidth(), mOverflowWidth);
1155            }
1156            return mOverflowWidth;
1157        }
1158
1159        private ListView createOverflowListView() {
1160            final Context context = mContentView.getContext();
1161            final ListView overflowListView = new ListView(context);
1162            overflowListView.setLayoutParams(new ViewGroup.LayoutParams(
1163                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1164            overflowListView.setDivider(null);
1165            overflowListView.setDividerHeight(0);
1166
1167            final int viewTypeCount = 2;
1168            final int stringLabelViewType = 0;
1169            final int iconOnlyViewType = 1;
1170            final ArrayAdapter overflowListViewAdapter =
1171                    new ArrayAdapter<MenuItem>(context, 0) {
1172                        @Override
1173                        public int getViewTypeCount() {
1174                            return viewTypeCount;
1175                        }
1176
1177                        @Override
1178                        public int getItemViewType(int position) {
1179                            if (isIconOnlyMenuItem(getItem(position))) {
1180                                return iconOnlyViewType;
1181                            }
1182                            return stringLabelViewType;
1183                        }
1184
1185                        @Override
1186                        public View getView(int position, View convertView, ViewGroup parent) {
1187                            if (getItemViewType(position) == iconOnlyViewType) {
1188                                return getIconOnlyView(position, convertView);
1189                            }
1190                            return getStringTitleView(position, convertView);
1191                        }
1192
1193                        private View getStringTitleView(int position, View convertView) {
1194                            TextView menuButton;
1195                            if (convertView != null) {
1196                                menuButton = (TextView) convertView;
1197                            } else {
1198                                menuButton = createOverflowMenuItemButton(context);
1199                            }
1200                            MenuItem menuItem = getItem(position);
1201                            menuButton.setText(menuItem.getTitle());
1202                            menuButton.setContentDescription(menuItem.getTitle());
1203                            menuButton.setMinimumWidth(mOverflowWidth);
1204                            return menuButton;
1205                        }
1206
1207                        private View getIconOnlyView(int position, View convertView) {
1208                            View menuButton;
1209                            if (convertView != null) {
1210                                menuButton = convertView;
1211                            } else {
1212                                menuButton = LayoutInflater.from(context).inflate(
1213                                        R.layout.floating_popup_overflow_image_list_item, null);
1214                            }
1215                            MenuItem menuItem = getItem(position);
1216                            ((ImageView) menuButton
1217                                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1218                                    .setImageDrawable(menuItem.getIcon());
1219                            menuButton.setMinimumWidth(mOverflowWidth);
1220                            return menuButton;
1221                        }
1222                    };
1223            overflowListView.setAdapter(overflowListViewAdapter);
1224            return overflowListView;
1225        }
1226    }
1227
1228
1229    /**
1230     * A helper for fading in or out a view.
1231     */
1232    private static final class ViewFader {
1233
1234        private static final int FADE_OUT_DURATION = 250;
1235        private static final int FADE_IN_DURATION = 150;
1236
1237        private final View mView;
1238        private final ObjectAnimator mFadeOutAnimation;
1239        private final ObjectAnimator mFadeInAnimation;
1240
1241        private ViewFader(View view) {
1242            mView = Preconditions.checkNotNull(view);
1243            mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0)
1244                    .setDuration(FADE_OUT_DURATION);
1245            mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1)
1246                    .setDuration(FADE_IN_DURATION);
1247        }
1248
1249        public void fadeIn(boolean animate) {
1250            cancelFadeAnimations();
1251            if (animate) {
1252                mFadeInAnimation.start();
1253            } else {
1254                mView.setAlpha(1);
1255            }
1256        }
1257
1258        public void fadeOut(boolean animate) {
1259            cancelFadeAnimations();
1260            if (animate) {
1261                mFadeOutAnimation.start();
1262            } else {
1263                mView.setAlpha(0);
1264            }
1265        }
1266
1267        private void cancelFadeAnimations() {
1268            mFadeInAnimation.cancel();
1269            mFadeOutAnimation.cancel();
1270        }
1271    }
1272
1273    /**
1274     * @return {@code true} if the menu item does not not have a string title but has an icon.
1275     *   {@code false} otherwise.
1276     */
1277    private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1278        if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1279            return true;
1280        }
1281        return false;
1282    }
1283
1284    /**
1285     * Creates and returns a menu button for the specified menu item.
1286     */
1287    private static View createMenuItemButton(Context context, MenuItem menuItem) {
1288        if (isIconOnlyMenuItem(menuItem)) {
1289            View imageMenuItemButton = LayoutInflater.from(context)
1290                    .inflate(R.layout.floating_popup_menu_image_button, null);
1291            ((ImageButton) imageMenuItemButton
1292                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1293                    .setImageDrawable(menuItem.getIcon());
1294            return imageMenuItemButton;
1295        }
1296
1297        Button menuItemButton = (Button) LayoutInflater.from(context)
1298                .inflate(R.layout.floating_popup_menu_button, null);
1299        menuItemButton.setText(menuItem.getTitle());
1300        menuItemButton.setContentDescription(menuItem.getTitle());
1301        return menuItemButton;
1302    }
1303
1304    /**
1305     * Creates and returns a styled floating toolbar overflow list view item.
1306     */
1307    private static TextView createOverflowMenuItemButton(Context context) {
1308        return (TextView) LayoutInflater.from(context)
1309                .inflate(R.layout.floating_popup_overflow_list_item, null);
1310    }
1311
1312    private static ViewGroup createContentContainer(Context context) {
1313        return (ViewGroup) LayoutInflater.from(context)
1314                .inflate(R.layout.floating_popup_container, null);
1315    }
1316
1317    private static PopupWindow createPopupWindow(View content) {
1318        ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1319        PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1320        popupWindow.setWindowLayoutType(
1321                WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1322        popupWindow.setAnimationStyle(0);
1323        popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1324        content.setLayoutParams(new ViewGroup.LayoutParams(
1325                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1326        popupContentHolder.addView(content);
1327        return popupWindow;
1328    }
1329
1330    /**
1331     * Creates a "grow and fade in from the bottom" animation for the specified view.
1332     *
1333     * @param view  The view to animate
1334     */
1335    private static AnimatorSet createGrowFadeInFromBottom(View view, int x) {
1336        AnimatorSet growFadeInFromBottomAnimation =  new AnimatorSet();
1337        growFadeInFromBottomAnimation.playTogether(
1338                ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125),
1339                ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125),
1340                ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75),
1341                // Make sure that view.x is always fixed throughout the duration of this animation.
1342                ObjectAnimator.ofFloat(view, View.X, x, x));
1343        growFadeInFromBottomAnimation.setStartDelay(50);
1344        return growFadeInFromBottomAnimation;
1345    }
1346
1347    /**
1348     * Creates a "shrink and fade out from bottom" animation for the specified view.
1349     *
1350     * @param view  The view to animate
1351     * @param startDelay  The start delay of the animation
1352     * @param listener  The animation listener
1353     */
1354    private static AnimatorSet createShrinkFadeOutFromBottomAnimation(
1355            View view, int startDelay, Animator.AnimatorListener listener) {
1356        AnimatorSet shrinkFadeOutFromBottomAnimation =  new AnimatorSet();
1357        shrinkFadeOutFromBottomAnimation.playTogether(
1358                ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125),
1359                ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75));
1360        shrinkFadeOutFromBottomAnimation.setStartDelay(startDelay);
1361        shrinkFadeOutFromBottomAnimation.addListener(listener);
1362        return shrinkFadeOutFromBottomAnimation;
1363    }
1364
1365    private static int getEstimatedToolbarHeight(Context context) {
1366        return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1367    }
1368
1369    private static int getEstimatedOpenOverflowButtonWidth(Context context) {
1370        return context.getResources()
1371                .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
1372    }
1373
1374    private static int getAdjustedToolbarWidth(Context context, int width) {
1375        int maximumWidth = getScreenWidth(context) - 2 * context.getResources()
1376                .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1377
1378        if (width <= 0 || width > maximumWidth) {
1379            int defaultWidth = context.getResources()
1380                    .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1381            width = Math.min(defaultWidth, maximumWidth);
1382        }
1383        return width;
1384    }
1385
1386    /**
1387     * Returns the device's screen width.
1388     */
1389    private static int getScreenWidth(Context context) {
1390        return context.getResources().getDisplayMetrics().widthPixels;
1391    }
1392
1393    /**
1394     * Returns the device's screen height.
1395     */
1396    private static int getScreenHeight(Context context) {
1397        return context.getResources().getDisplayMetrics().heightPixels;
1398    }
1399}
1400