FloatingToolbar.java revision 6d2b75c86d7007512a6ad935e5125b735106d90a
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 mDismissAnimation;
297        private final AnimatorSet mHideAnimation;
298        private final AnimationSet mOpenOverflowAnimation = new AnimationSet(true) {
299            @Override
300            public void cancel() {
301                if (hasStarted() && !hasEnded()) {
302                    super.cancel();
303                    setOverflowPanelAsContent();
304                }
305            }
306        };
307        private final AnimationSet mCloseOverflowAnimation = new AnimationSet(true) {
308            @Override
309            public void cancel() {
310                if (hasStarted() && !hasEnded()) {
311                    super.cancel();
312                    setMainPanelAsContent();
313                }
314            }
315        };
316
317        private final Runnable mOpenOverflow = new Runnable() {
318            @Override
319            public void run() {
320                openOverflow();
321            }
322        };
323        private final Runnable mCloseOverflow = new Runnable() {
324            @Override
325            public void run() {
326                closeOverflow();
327            }
328        };
329
330        private final Rect mViewPort = new Rect();
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            mDismissAnimation = createExitAnimation(
364                    mContentContainer,
365                    150,  // startDelay
366                    new AnimatorListenerAdapter() {
367                        @Override
368                        public void onAnimationEnd(Animator animation) {
369                            mPopupWindow.dismiss();
370                            mContentContainer.removeAllViews();
371                        }
372                    });
373            mHideAnimation = createExitAnimation(
374                    mContentContainer,
375                    0,  // startDelay
376                    new AnimatorListenerAdapter() {
377                        @Override
378                        public void onAnimationEnd(Animator animation) {
379                            mPopupWindow.dismiss();
380                        }
381                    });
382            mMarginHorizontal = parent.getResources()
383                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
384            mMarginVertical = parent.getResources()
385                    .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
386        }
387
388        /**
389         * Lays out buttons for the specified menu items.
390         */
391        public void layoutMenuItems(
392                List<MenuItem> menuItems,
393                MenuItem.OnMenuItemClickListener menuItemClickListener,
394                int suggestedWidth) {
395            Preconditions.checkNotNull(menuItems);
396
397            mContentContainer.removeAllViews();
398            if (mMainPanel == null) {
399                mMainPanel = new FloatingToolbarMainPanel(mParent.getContext(), mOpenOverflow);
400            }
401            List<MenuItem> overflowMenuItems =
402                    mMainPanel.layoutMenuItems(menuItems, getToolbarWidth(suggestedWidth));
403            mMainPanel.setOnMenuItemClickListener(menuItemClickListener);
404            if (!overflowMenuItems.isEmpty()) {
405                if (mOverflowPanel == null) {
406                    mOverflowPanel =
407                            new FloatingToolbarOverflowPanel(mParent.getContext(), mCloseOverflow);
408                }
409                mOverflowPanel.setMenuItems(overflowMenuItems);
410                mOverflowPanel.setOnMenuItemClickListener(menuItemClickListener);
411            }
412            updatePopupSize();
413        }
414
415        /**
416         * Shows this popup at the specified coordinates.
417         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
418         */
419        public void show(Rect contentRect) {
420            Preconditions.checkNotNull(contentRect);
421
422            if (isShowing()) {
423                return;
424            }
425
426            mHidden = false;
427            mDismissed = false;
428            cancelDismissAndHideAnimations();
429            cancelOverflowAnimations();
430
431            // Make sure a panel is set as the content.
432            if (mContentContainer.getChildCount() == 0) {
433                setMainPanelAsContent();
434                // If we're yet to show the popup, set the container visibility to zero.
435                // The "show" animation will make this visible.
436                mContentContainer.setAlpha(0);
437            }
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            refreshViewPort();
529
530            int availableHeightAboveContent =
531                    contentRect.top - mViewPort.top - 2 * mMarginVertical;
532            int availableHeightBelowContent =
533                    mViewPort.bottom - contentRect.bottom - 2 * mMarginVertical;
534            int availableHeightThroughContent =
535                    mViewPort.bottom - contentRect.top + getToolbarHeightWithVerticalMargin();
536
537            int x = contentRect.centerX() - getWidth() / 2;
538            // Update x so that the toolbar isn't rendered behind the nav bar in landscape.
539            x = Math.max(0, Math.min(x, mViewPort.right - getWidth()));
540
541            int y;
542            if (mOverflowPanel == null) {  // There is no overflow.
543                if (availableHeightAboveContent > getToolbarHeightWithVerticalMargin()) {
544                    // There is enough space at the top of the content.
545                    y = contentRect.top - getToolbarHeightWithVerticalMargin();
546                } else if (availableHeightBelowContent > getToolbarHeightWithVerticalMargin()) {
547                    // There is enough space at the bottom of the content.
548                    y = contentRect.bottom;
549                } else {
550                    // Not enough space. Prefer to position as high as possible.
551                    y = Math.max(
552                            mViewPort.top,
553                            contentRect.top - getToolbarHeightWithVerticalMargin());
554                }
555            } else {  // There is an overflow.
556                if (availableHeightAboveContent > mOverflowPanel.getMinimumHeight()) {
557                    // There is enough space at the top of the content rect for the overflow.
558                    // Position above and open upwards.
559                    updateOverflowHeight(availableHeightAboveContent);
560                    y = contentRect.top - getHeight();
561                    mOverflowDirection = OVERFLOW_DIRECTION_UP;
562                } else if (availableHeightAboveContent > getToolbarHeightWithVerticalMargin()
563                        && availableHeightThroughContent > mOverflowPanel.getMinimumHeight()) {
564                    // There is enough space at the top of the content rect for the main panel
565                    // but not the overflow.
566                    // Position above but open downwards.
567                    updateOverflowHeight(availableHeightThroughContent);
568                    y = contentRect.top - getToolbarHeightWithVerticalMargin();
569                    mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
570                } else if (availableHeightBelowContent > mOverflowPanel.getMinimumHeight()) {
571                    // There is enough space at the bottom of the content rect for the overflow.
572                    // Position below and open downwards.
573                    updateOverflowHeight(availableHeightBelowContent);
574                    y = contentRect.bottom;
575                    mOverflowDirection = OVERFLOW_DIRECTION_DOWN;
576                } else {
577                    // Not enough space.
578                    // Position at the bottom of the view port and open upwards.
579                    updateOverflowHeight(mViewPort.height());
580                    y = mViewPort.bottom - getHeight();
581                    mOverflowDirection = OVERFLOW_DIRECTION_UP;
582                }
583                mOverflowPanel.setOverflowDirection(mOverflowDirection);
584            }
585
586            mCoords.set(x, y);
587        }
588
589        private int getToolbarHeightWithVerticalMargin() {
590            return getEstimatedToolbarHeight(mParent.getContext()) + mMarginVertical * 2;
591        }
592
593        /**
594         * Performs the "show" animation on the floating popup.
595         */
596        private void runShowAnimation() {
597            createEnterAnimation(mContentContainer).start();
598        }
599
600        /**
601         * Performs the "dismiss" animation on the floating popup.
602         */
603        private void runDismissAnimation() {
604            mDismissAnimation.start();
605        }
606
607        /**
608         * Performs the "hide" animation on the floating popup.
609         */
610        private void runHideAnimation() {
611            mHideAnimation.start();
612        }
613
614        private void cancelDismissAndHideAnimations() {
615            mDismissAnimation.cancel();
616            mHideAnimation.cancel();
617        }
618
619        private void cancelOverflowAnimations() {
620            mOpenOverflowAnimation.cancel();
621            mCloseOverflowAnimation.cancel();
622        }
623
624        /**
625         * Opens the floating toolbar overflow.
626         * This method should not be called if menu items have not been laid out with
627         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
628         *
629         * @throws IllegalStateException if called when menu items have not been laid out.
630         */
631        private void openOverflow() {
632            Preconditions.checkState(mMainPanel != null);
633            Preconditions.checkState(mOverflowPanel != null);
634
635            mMainPanel.fadeOut(true);
636            Size overflowPanelSize = mOverflowPanel.measure();
637            final int targetWidth = overflowPanelSize.getWidth();
638            final int targetHeight = overflowPanelSize.getHeight();
639            final boolean morphUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
640            final int startWidth = mContentContainer.getWidth();
641            final int startHeight = mContentContainer.getHeight();
642            final float startY = mContentContainer.getY();
643            final float left = mContentContainer.getX();
644            final float right = left + mContentContainer.getWidth();
645            Animation widthAnimation = new Animation() {
646                @Override
647                protected void applyTransformation(float interpolatedTime, Transformation t) {
648                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
649                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
650                    params.width = startWidth + deltaWidth;
651                    mContentContainer.setLayoutParams(params);
652                    if (isRTL()) {
653                        mContentContainer.setX(left);
654                    } else {
655                        mContentContainer.setX(right - mContentContainer.getWidth());
656                    }
657                }
658            };
659            Animation heightAnimation = new Animation() {
660                @Override
661                protected void applyTransformation(float interpolatedTime, Transformation t) {
662                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
663                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
664                    params.height = startHeight + deltaHeight;
665                    mContentContainer.setLayoutParams(params);
666                    if (morphUpwards) {
667                        float y = startY - (mContentContainer.getHeight() - startHeight);
668                        mContentContainer.setY(y);
669                    }
670                }
671            };
672            widthAnimation.setDuration(240);
673            heightAnimation.setDuration(180);
674            heightAnimation.setStartOffset(60);
675            mOpenOverflowAnimation.getAnimations().clear();
676            mOpenOverflowAnimation.setAnimationListener(mOnOverflowOpened);
677            mOpenOverflowAnimation.addAnimation(widthAnimation);
678            mOpenOverflowAnimation.addAnimation(heightAnimation);
679            mContentContainer.startAnimation(mOpenOverflowAnimation);
680        }
681
682        /**
683         * Opens the floating toolbar overflow.
684         * This method should not be called if menu items have not been laid out with
685         * {@link #layoutMenuItems(java.util.List, MenuItem.OnMenuItemClickListener, int)}.
686         *
687         * @throws IllegalStateException if called when menu items have not been laid out.
688         */
689        private void closeOverflow() {
690            Preconditions.checkState(mMainPanel != null);
691            Preconditions.checkState(mOverflowPanel != null);
692
693            mOverflowPanel.fadeOut(true);
694            Size mainPanelSize = mMainPanel.measure();
695            final int targetWidth = mainPanelSize.getWidth();
696            final int targetHeight = mainPanelSize.getHeight();
697            final int startWidth = mContentContainer.getWidth();
698            final int startHeight = mContentContainer.getHeight();
699            final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
700            final boolean morphedUpwards = (mOverflowDirection == OVERFLOW_DIRECTION_UP);
701            final float left = mContentContainer.getX();
702            final float right = left + mContentContainer.getWidth();
703            Animation widthAnimation = new Animation() {
704                @Override
705                protected void applyTransformation(float interpolatedTime, Transformation t) {
706                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
707                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
708                    params.width = startWidth + deltaWidth;
709                    mContentContainer.setLayoutParams(params);
710                    if (isRTL()) {
711                        mContentContainer.setX(left);
712                    } else {
713                        mContentContainer.setX(right - mContentContainer.getWidth());
714                    }
715                }
716            };
717            Animation heightAnimation = new Animation() {
718                @Override
719                protected void applyTransformation(float interpolatedTime, Transformation t) {
720                    ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
721                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
722                    params.height = startHeight + deltaHeight;
723                    mContentContainer.setLayoutParams(params);
724                    if (morphedUpwards) {
725                        mContentContainer.setY(bottom - mContentContainer.getHeight());
726                    }
727                }
728            };
729            widthAnimation.setDuration(150);
730            widthAnimation.setStartOffset(150);
731            heightAnimation.setDuration(210);
732            mCloseOverflowAnimation.getAnimations().clear();
733            mCloseOverflowAnimation.setAnimationListener(mOnOverflowClosed);
734            mCloseOverflowAnimation.addAnimation(widthAnimation);
735            mCloseOverflowAnimation.addAnimation(heightAnimation);
736            mContentContainer.startAnimation(mCloseOverflowAnimation);
737        }
738
739        /**
740         * Prepares the content container for show and update calls.
741         */
742        private void preparePopupContent() {
743            // Reset visibility.
744            if (mMainPanel != null) {
745                mMainPanel.fadeIn(false);
746            }
747            if (mOverflowPanel != null) {
748                mOverflowPanel.fadeIn(false);
749            }
750
751            // Reset position.
752            if (isMainPanelContent()) {
753                positionMainPanel();
754            }
755            if (isOverflowPanelContent()) {
756                positionOverflowPanel();
757            }
758        }
759
760        private boolean isMainPanelContent() {
761            return mMainPanel != null
762                    && mContentContainer.getChildAt(0) == mMainPanel.getView();
763        }
764
765        private boolean isOverflowPanelContent() {
766            return mOverflowPanel != null
767                    && mContentContainer.getChildAt(0) == mOverflowPanel.getView();
768        }
769
770        /**
771         * Sets the current content to be the main view panel.
772         */
773        private void setMainPanelAsContent() {
774            // This should never be called if the main panel has not been initialized.
775            Preconditions.checkNotNull(mMainPanel);
776            mContentContainer.removeAllViews();
777            Size mainPanelSize = mMainPanel.measure();
778            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
779            params.width = mainPanelSize.getWidth();
780            params.height = mainPanelSize.getHeight();
781            mContentContainer.setLayoutParams(params);
782            mContentContainer.addView(mMainPanel.getView());
783            setContentAreaAsTouchableSurface();
784        }
785
786        /**
787         * Sets the current content to be the overflow view panel.
788         */
789        private void setOverflowPanelAsContent() {
790            // This should never be called if the overflow panel has not been initialized.
791            Preconditions.checkNotNull(mOverflowPanel);
792            mContentContainer.removeAllViews();
793            Size overflowPanelSize = mOverflowPanel.measure();
794            ViewGroup.LayoutParams params = mContentContainer.getLayoutParams();
795            params.width = overflowPanelSize.getWidth();
796            params.height = overflowPanelSize.getHeight();
797            mContentContainer.setLayoutParams(params);
798            mContentContainer.addView(mOverflowPanel.getView());
799            setContentAreaAsTouchableSurface();
800        }
801
802        /**
803         * Places the main view panel at the appropriate resting coordinates.
804         */
805        private void positionMainPanel() {
806            Preconditions.checkNotNull(mMainPanel);
807            mContentContainer.setX(mMarginHorizontal);
808
809            float y = mMarginVertical;
810            if  (mOverflowDirection == OVERFLOW_DIRECTION_UP) {
811                y = getHeight()
812                        - (mMainPanel.getView().getMeasuredHeight() + mMarginVertical);
813            }
814            mContentContainer.setY(y);
815            setContentAreaAsTouchableSurface();
816        }
817
818        /**
819         * Places the main view panel at the appropriate resting coordinates.
820         */
821        private void positionOverflowPanel() {
822            Preconditions.checkNotNull(mOverflowPanel);
823            float x;
824            if (isRTL()) {
825                x = mMarginHorizontal;
826            } else {
827                x = mPopupWindow.getWidth()
828                    - (mOverflowPanel.getView().getMeasuredWidth() + mMarginHorizontal);
829            }
830            mContentContainer.setX(x);
831            mContentContainer.setY(mMarginVertical);
832            setContentAreaAsTouchableSurface();
833        }
834
835        private void updateOverflowHeight(int height) {
836            if (mOverflowPanel != null) {
837                mOverflowPanel.setSuggestedHeight(height);
838
839                // Re-measure the popup and it's contents.
840                boolean mainPanelContent = isMainPanelContent();
841                boolean overflowPanelContent = isOverflowPanelContent();
842                mContentContainer.removeAllViews();  // required to update popup size.
843                updatePopupSize();
844                // Reset the appropriate content.
845                if (mainPanelContent) {
846                    setMainPanelAsContent();
847                }
848                if (overflowPanelContent) {
849                    setOverflowPanelAsContent();
850                }
851            }
852        }
853
854        private void updatePopupSize() {
855            int width = 0;
856            int height = 0;
857            if (mMainPanel != null) {
858                Size mainPanelSize = mMainPanel.measure();
859                width = mainPanelSize.getWidth();
860                height = mainPanelSize.getHeight();
861            }
862            if (mOverflowPanel != null) {
863                Size overflowPanelSize = mOverflowPanel.measure();
864                width = Math.max(width, overflowPanelSize.getWidth());
865                height = Math.max(height, overflowPanelSize.getHeight());
866            }
867            mPopupWindow.setWidth(width + mMarginHorizontal * 2);
868            mPopupWindow.setHeight(height + mMarginVertical * 2);
869        }
870
871
872        private void refreshViewPort() {
873            mParent.getWindowVisibleDisplayFrame(mViewPort);
874        }
875
876        private int getToolbarWidth(int suggestedWidth) {
877            int width = suggestedWidth;
878            refreshViewPort();
879            int maximumWidth = mViewPort.width() - 2 * mParent.getResources()
880                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
881            if (width <= 0) {
882                width = mParent.getResources()
883                        .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
884            }
885            return Math.min(width, maximumWidth);
886        }
887
888        /**
889         * Sets the touchable region of this popup to be zero. This means that all touch events on
890         * this popup will go through to the surface behind it.
891         */
892        private void setZeroTouchableSurface() {
893            mTouchableRegion.setEmpty();
894        }
895
896        /**
897         * Sets the touchable region of this popup to be the area occupied by its content.
898         */
899        private void setContentAreaAsTouchableSurface() {
900            if (!mPopupWindow.isShowing()) {
901                mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
902            }
903            int width = mContentContainer.getMeasuredWidth();
904            int height = mContentContainer.getMeasuredHeight();
905            mTouchableRegion.set(
906                    (int) mContentContainer.getX(),
907                    (int) mContentContainer.getY(),
908                    (int) mContentContainer.getX() + width,
909                    (int) mContentContainer.getY() + height);
910        }
911
912        /**
913         * Make the touchable area of this popup be the area specified by mTouchableRegion.
914         * This should be called after the popup window has been dismissed (dismiss/hide)
915         * and is probably being re-shown with a new content root view.
916         */
917        private void setTouchableSurfaceInsetsComputer() {
918            ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
919                    .getRootView()
920                    .getViewTreeObserver();
921            viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
922            viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
923        }
924
925        private boolean isRTL() {
926            return mContentContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
927        }
928    }
929
930    /**
931     * A widget that holds the primary menu items in the floating toolbar.
932     */
933    private static final class FloatingToolbarMainPanel {
934
935        private final Context mContext;
936        private final ViewGroup mContentView;
937        private final View.OnClickListener mMenuItemButtonOnClickListener =
938                new View.OnClickListener() {
939                    @Override
940                    public void onClick(View v) {
941                        if (v.getTag() instanceof MenuItem) {
942                            if (mOnMenuItemClickListener != null) {
943                                mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
944                            }
945                        }
946                    }
947                };
948        private final ViewFader viewFader;
949        private final Runnable mOpenOverflow;
950
951        private View mOpenOverflowButton;
952        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
953
954        /**
955         * Initializes a floating toolbar popup main view panel.
956         *
957         * @param context
958         * @param openOverflow  The code that opens the toolbar popup overflow.
959         */
960        public FloatingToolbarMainPanel(Context context, Runnable openOverflow) {
961            mContext = Preconditions.checkNotNull(context);
962            mContentView = new LinearLayout(context);
963            viewFader = new ViewFader(mContentView);
964            mOpenOverflow = Preconditions.checkNotNull(openOverflow);
965        }
966
967        /**
968         * Fits as many menu items in the main panel and returns a list of the menu items that
969         * were not fit in.
970         *
971         * @return The menu items that are not included in this main panel.
972         */
973        public List<MenuItem> layoutMenuItems(List<MenuItem> menuItems, int width) {
974            Preconditions.checkNotNull(menuItems);
975
976            // Reserve space for the "open overflow" button.
977            final int toolbarWidth = width - getEstimatedOpenOverflowButtonWidth(mContext);
978
979            int availableWidth = toolbarWidth;
980            final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
981
982            mContentView.removeAllViews();
983
984            boolean isFirstItem = true;
985            while (!remainingMenuItems.isEmpty()) {
986                final MenuItem menuItem = remainingMenuItems.peek();
987                View menuItemButton = createMenuItemButton(mContext, menuItem);
988
989                // Adding additional start padding for the first button to even out button spacing.
990                if (isFirstItem) {
991                    menuItemButton.setPaddingRelative(
992                            (int) (1.5 * menuItemButton.getPaddingStart()),
993                            menuItemButton.getPaddingTop(),
994                            menuItemButton.getPaddingEnd(),
995                            menuItemButton.getPaddingBottom());
996                    isFirstItem = false;
997                }
998
999                // Adding additional end padding for the last button to even out button spacing.
1000                if (remainingMenuItems.size() == 1) {
1001                    menuItemButton.setPaddingRelative(
1002                            menuItemButton.getPaddingStart(),
1003                            menuItemButton.getPaddingTop(),
1004                            (int) (1.5 * menuItemButton.getPaddingEnd()),
1005                            menuItemButton.getPaddingBottom());
1006                }
1007
1008                menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1009                int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
1010                if (menuItemButtonWidth <= availableWidth) {
1011                    setButtonTagAndClickListener(menuItemButton, menuItem);
1012                    mContentView.addView(menuItemButton);
1013                    ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1014                    params.width = menuItemButtonWidth;
1015                    menuItemButton.setLayoutParams(params);
1016                    availableWidth -= menuItemButtonWidth;
1017                    remainingMenuItems.pop();
1018                } else {
1019                    if (mOpenOverflowButton == null) {
1020                        mOpenOverflowButton = LayoutInflater.from(mContext)
1021                                .inflate(R.layout.floating_popup_open_overflow_button, null);
1022                        mOpenOverflowButton.setOnClickListener(new View.OnClickListener() {
1023                            @Override
1024                            public void onClick(View v) {
1025                                if (mOpenOverflowButton != null) {
1026                                    mOpenOverflow.run();
1027                                }
1028                            }
1029                        });
1030                    }
1031                    mContentView.addView(mOpenOverflowButton);
1032                    break;
1033                }
1034            }
1035            return remainingMenuItems;
1036        }
1037
1038        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1039            mOnMenuItemClickListener = listener;
1040        }
1041
1042        public View getView() {
1043            return mContentView;
1044        }
1045
1046        public void fadeIn(boolean animate) {
1047            viewFader.fadeIn(animate);
1048        }
1049
1050        public void fadeOut(boolean animate) {
1051            viewFader.fadeOut(animate);
1052        }
1053
1054        /**
1055         * Returns how big this panel's view should be.
1056         * This method should only be called when the view has not been attached to a parent
1057         * otherwise it will throw an illegal state.
1058         */
1059        public Size measure() throws IllegalStateException {
1060            Preconditions.checkState(mContentView.getParent() == null);
1061            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1062            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1063        }
1064
1065        private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1066            View button = menuItemButton;
1067            if (isIconOnlyMenuItem(menuItem)) {
1068                button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1069            }
1070            button.setTag(menuItem);
1071            button.setOnClickListener(mMenuItemButtonOnClickListener);
1072        }
1073    }
1074
1075
1076    /**
1077     * A widget that holds the overflow items in the floating toolbar.
1078     */
1079    private static final class FloatingToolbarOverflowPanel {
1080
1081        private final LinearLayout mContentView;
1082        private final ViewGroup mBackButtonContainer;
1083        private final View mBackButton;
1084        private final ListView mListView;
1085        private final TextView mListViewItemWidthCalculator;
1086        private final ViewFader mViewFader;
1087        private final Runnable mCloseOverflow;
1088
1089        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
1090        private int mOverflowWidth = 0;
1091        private int mSuggestedHeight;
1092
1093        /**
1094         * Initializes a floating toolbar popup overflow view panel.
1095         *
1096         * @param context
1097         * @param closeOverflow  The code that closes the toolbar popup's overflow.
1098         */
1099        public FloatingToolbarOverflowPanel(Context context, Runnable closeOverflow) {
1100            mCloseOverflow = Preconditions.checkNotNull(closeOverflow);
1101            mSuggestedHeight = getScreenHeight(context);
1102
1103            mContentView = new LinearLayout(context);
1104            mContentView.setOrientation(LinearLayout.VERTICAL);
1105            mViewFader = new ViewFader(mContentView);
1106
1107            mBackButton = LayoutInflater.from(context)
1108                    .inflate(R.layout.floating_popup_close_overflow_button, null);
1109            mBackButton.setOnClickListener(new View.OnClickListener() {
1110                @Override
1111                public void onClick(View v) {
1112                    mCloseOverflow.run();
1113                }
1114            });
1115            mBackButtonContainer = new LinearLayout(context);
1116            mBackButtonContainer.addView(mBackButton);
1117
1118            mListView = createOverflowListView();
1119            mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1120                @Override
1121                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1122                    MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(position);
1123                    if (mOnMenuItemClickListener != null) {
1124                        mOnMenuItemClickListener.onMenuItemClick(menuItem);
1125                    }
1126                }
1127            });
1128
1129            mContentView.addView(mListView);
1130            mContentView.addView(mBackButtonContainer);
1131
1132            mListViewItemWidthCalculator = createOverflowMenuItemButton(context);
1133            mListViewItemWidthCalculator.setLayoutParams(new ViewGroup.LayoutParams(
1134                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1135        }
1136
1137        /**
1138         * Sets the menu items to be displayed in the overflow.
1139         */
1140        public void setMenuItems(List<MenuItem> menuItems) {
1141            ArrayAdapter overflowListViewAdapter = (ArrayAdapter) mListView.getAdapter();
1142            overflowListViewAdapter.clear();
1143            overflowListViewAdapter.addAll(menuItems);
1144            setListViewHeight();
1145            setOverflowWidth();
1146        }
1147
1148        public void setOnMenuItemClickListener(MenuItem.OnMenuItemClickListener listener) {
1149            mOnMenuItemClickListener = listener;
1150        }
1151
1152        /**
1153         * Notifies the overflow of the current direction in which the overflow will be opened.
1154         *
1155         * @param overflowDirection  {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_UP}
1156         *   or {@link FloatingToolbarPopup#OVERFLOW_DIRECTION_DOWN}.
1157         */
1158        public void setOverflowDirection(int overflowDirection) {
1159            mContentView.removeView(mBackButtonContainer);
1160            int index = (overflowDirection == FloatingToolbarPopup.OVERFLOW_DIRECTION_UP)? 1 : 0;
1161            mContentView.addView(mBackButtonContainer, index);
1162        }
1163
1164        public void setSuggestedHeight(int height) {
1165            mSuggestedHeight = height;
1166            setListViewHeight();
1167        }
1168
1169        public int getMinimumHeight() {
1170            return mContentView.getContext().getResources().
1171                    getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height)
1172                    + getEstimatedToolbarHeight(mContentView.getContext());
1173        }
1174
1175        /**
1176         * Returns the content view of the overflow.
1177         */
1178        public View getView() {
1179            return mContentView;
1180        }
1181
1182        public void fadeIn(boolean animate) {
1183            mViewFader.fadeIn(animate);
1184        }
1185
1186        public void fadeOut(boolean animate) {
1187            mViewFader.fadeOut(animate);
1188        }
1189
1190        /**
1191         * Returns how big this panel's view should be.
1192         * This method should only be called when the view has not been attached to a parent.
1193         *
1194         * @throws IllegalStateException
1195         */
1196        public Size measure() {
1197            Preconditions.checkState(mContentView.getParent() == null);
1198            mContentView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1199            return new Size(mContentView.getMeasuredWidth(), mContentView.getMeasuredHeight());
1200        }
1201
1202        private void setListViewHeight() {
1203            int itemHeight = getEstimatedToolbarHeight(mContentView.getContext());
1204            int height = mListView.getAdapter().getCount() * itemHeight;
1205            int maxHeight = mContentView.getContext().getResources().
1206                    getDimensionPixelSize(R.dimen.floating_toolbar_maximum_overflow_height);
1207            int minHeight = mContentView.getContext().getResources().
1208                    getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height);
1209            int suggestedListViewHeight = mSuggestedHeight - (mSuggestedHeight % itemHeight)
1210                    - itemHeight;  // reserve space for the back button.
1211            ViewGroup.LayoutParams params = mListView.getLayoutParams();
1212            if (suggestedListViewHeight <= 0) {
1213                // Invalid height. Use the maximum height available.
1214                params.height = Math.min(maxHeight, height);
1215            } else if (suggestedListViewHeight < minHeight) {
1216                // Height is smaller than minimum allowed. Use minimum height.
1217                params.height = minHeight;
1218            } else {
1219                // Use the suggested height. Cap it at the maximum available height.
1220                params.height = Math.min(Math.min(suggestedListViewHeight, maxHeight), height);
1221            }
1222            mListView.setLayoutParams(params);
1223        }
1224
1225        private int setOverflowWidth() {
1226            for (int i = 0; i < mListView.getAdapter().getCount(); i++) {
1227                MenuItem menuItem = (MenuItem) mListView.getAdapter().getItem(i);
1228                Preconditions.checkNotNull(menuItem);
1229                mListViewItemWidthCalculator.setText(menuItem.getTitle());
1230                mListViewItemWidthCalculator.measure(
1231                        MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1232                mOverflowWidth = Math.max(
1233                        mListViewItemWidthCalculator.getMeasuredWidth(), mOverflowWidth);
1234            }
1235            return mOverflowWidth;
1236        }
1237
1238        private ListView createOverflowListView() {
1239            final Context context = mContentView.getContext();
1240            final ListView overflowListView = new ListView(context);
1241            overflowListView.setLayoutParams(new ViewGroup.LayoutParams(
1242                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1243            overflowListView.setDivider(null);
1244            overflowListView.setDividerHeight(0);
1245
1246            final int viewTypeCount = 2;
1247            final int stringLabelViewType = 0;
1248            final int iconOnlyViewType = 1;
1249            final ArrayAdapter overflowListViewAdapter =
1250                    new ArrayAdapter<MenuItem>(context, 0) {
1251                        @Override
1252                        public int getViewTypeCount() {
1253                            return viewTypeCount;
1254                        }
1255
1256                        @Override
1257                        public int getItemViewType(int position) {
1258                            if (isIconOnlyMenuItem(getItem(position))) {
1259                                return iconOnlyViewType;
1260                            }
1261                            return stringLabelViewType;
1262                        }
1263
1264                        @Override
1265                        public View getView(int position, View convertView, ViewGroup parent) {
1266                            if (getItemViewType(position) == iconOnlyViewType) {
1267                                return getIconOnlyView(position, convertView);
1268                            }
1269                            return getStringTitleView(position, convertView);
1270                        }
1271
1272                        private View getStringTitleView(int position, View convertView) {
1273                            TextView menuButton;
1274                            if (convertView != null) {
1275                                menuButton = (TextView) convertView;
1276                            } else {
1277                                menuButton = createOverflowMenuItemButton(context);
1278                            }
1279                            MenuItem menuItem = getItem(position);
1280                            menuButton.setText(menuItem.getTitle());
1281                            menuButton.setContentDescription(menuItem.getTitle());
1282                            menuButton.setMinimumWidth(mOverflowWidth);
1283                            return menuButton;
1284                        }
1285
1286                        private View getIconOnlyView(int position, View convertView) {
1287                            View menuButton;
1288                            if (convertView != null) {
1289                                menuButton = convertView;
1290                            } else {
1291                                menuButton = LayoutInflater.from(context).inflate(
1292                                        R.layout.floating_popup_overflow_image_list_item, null);
1293                            }
1294                            MenuItem menuItem = getItem(position);
1295                            ((ImageView) menuButton
1296                                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1297                                    .setImageDrawable(menuItem.getIcon());
1298                            menuButton.setMinimumWidth(mOverflowWidth);
1299                            return menuButton;
1300                        }
1301                    };
1302            overflowListView.setAdapter(overflowListViewAdapter);
1303            return overflowListView;
1304        }
1305    }
1306
1307
1308    /**
1309     * A helper for fading in or out a view.
1310     */
1311    private static final class ViewFader {
1312
1313        private static final int FADE_OUT_DURATION = 250;
1314        private static final int FADE_IN_DURATION = 150;
1315
1316        private final View mView;
1317        private final ObjectAnimator mFadeOutAnimation;
1318        private final ObjectAnimator mFadeInAnimation;
1319
1320        private ViewFader(View view) {
1321            mView = Preconditions.checkNotNull(view);
1322            mFadeOutAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0)
1323                    .setDuration(FADE_OUT_DURATION);
1324            mFadeInAnimation = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1)
1325                    .setDuration(FADE_IN_DURATION);
1326        }
1327
1328        public void fadeIn(boolean animate) {
1329            cancelFadeAnimations();
1330            if (animate) {
1331                mFadeInAnimation.start();
1332            } else {
1333                mView.setAlpha(1);
1334            }
1335        }
1336
1337        public void fadeOut(boolean animate) {
1338            cancelFadeAnimations();
1339            if (animate) {
1340                mFadeOutAnimation.start();
1341            } else {
1342                mView.setAlpha(0);
1343            }
1344        }
1345
1346        private void cancelFadeAnimations() {
1347            mFadeInAnimation.cancel();
1348            mFadeOutAnimation.cancel();
1349        }
1350    }
1351
1352    /**
1353     * @return {@code true} if the menu item does not not have a string title but has an icon.
1354     *   {@code false} otherwise.
1355     */
1356    private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1357        if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1358            return true;
1359        }
1360        return false;
1361    }
1362
1363    /**
1364     * Creates and returns a menu button for the specified menu item.
1365     */
1366    private static View createMenuItemButton(Context context, MenuItem menuItem) {
1367        if (isIconOnlyMenuItem(menuItem)) {
1368            View imageMenuItemButton = LayoutInflater.from(context)
1369                    .inflate(R.layout.floating_popup_menu_image_button, null);
1370            ((ImageButton) imageMenuItemButton
1371                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1372                    .setImageDrawable(menuItem.getIcon());
1373            return imageMenuItemButton;
1374        }
1375
1376        Button menuItemButton = (Button) LayoutInflater.from(context)
1377                .inflate(R.layout.floating_popup_menu_button, null);
1378        menuItemButton.setText(menuItem.getTitle());
1379        menuItemButton.setContentDescription(menuItem.getTitle());
1380        return menuItemButton;
1381    }
1382
1383    /**
1384     * Creates and returns a styled floating toolbar overflow list view item.
1385     */
1386    private static TextView createOverflowMenuItemButton(Context context) {
1387        return (TextView) LayoutInflater.from(context)
1388                .inflate(R.layout.floating_popup_overflow_list_item, null);
1389    }
1390
1391    private static ViewGroup createContentContainer(Context context) {
1392        return (ViewGroup) LayoutInflater.from(context)
1393                .inflate(R.layout.floating_popup_container, null);
1394    }
1395
1396    private static PopupWindow createPopupWindow(View content) {
1397        ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1398        PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1399        popupWindow.setWindowLayoutType(
1400                WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1401        popupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
1402        popupWindow.setAnimationStyle(0);
1403        popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1404        content.setLayoutParams(new ViewGroup.LayoutParams(
1405                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1406        popupContentHolder.addView(content);
1407        return popupWindow;
1408    }
1409
1410    /**
1411     * Creates an "appear" animation for the specified view.
1412     *
1413     * @param view  The view to animate
1414     */
1415    private static AnimatorSet createEnterAnimation(View view) {
1416        AnimatorSet animation =  new AnimatorSet();
1417        animation.playTogether(
1418                ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(200),
1419                // Make sure that view.x is always fixed throughout the duration of this animation.
1420                ObjectAnimator.ofFloat(view, View.X, view.getX(), view.getX()));
1421        animation.setStartDelay(50);
1422        return animation;
1423    }
1424
1425    /**
1426     * Creates a "disappear" animation for the specified view.
1427     *
1428     * @param view  The view to animate
1429     * @param startDelay  The start delay of the animation
1430     * @param listener  The animation listener
1431     */
1432    private static AnimatorSet createExitAnimation(
1433            View view, int startDelay, Animator.AnimatorListener listener) {
1434        AnimatorSet animation =  new AnimatorSet();
1435        animation.playTogether(
1436                ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(200));
1437        animation.setStartDelay(startDelay);
1438        animation.addListener(listener);
1439        return animation;
1440    }
1441
1442    private static int getEstimatedToolbarHeight(Context context) {
1443        return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1444    }
1445
1446    private static int getEstimatedOpenOverflowButtonWidth(Context context) {
1447        return context.getResources()
1448                .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width);
1449    }
1450
1451    private static int getAdjustedToolbarWidth(Context context, int width) {
1452        int maximumWidth = getScreenWidth(context) - 2 * context.getResources()
1453                .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1454
1455        if (width <= 0 || width > maximumWidth) {
1456            int defaultWidth = context.getResources()
1457                    .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1458            width = Math.min(defaultWidth, maximumWidth);
1459        }
1460        return width;
1461    }
1462
1463    /**
1464     * Returns the device's screen width.
1465     */
1466    private static int getScreenWidth(Context context) {
1467        return context.getResources().getDisplayMetrics().widthPixels;
1468    }
1469
1470    /**
1471     * Returns the device's screen height.
1472     */
1473    private static int getScreenHeight(Context context) {
1474        return context.getResources().getDisplayMetrics().heightPixels;
1475    }
1476}
1477