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.animation.ValueAnimator;
24import android.content.Context;
25import android.content.res.TypedArray;
26import android.graphics.Color;
27import android.graphics.Point;
28import android.graphics.Rect;
29import android.graphics.Region;
30import android.graphics.drawable.AnimatedVectorDrawable;
31import android.graphics.drawable.ColorDrawable;
32import android.graphics.drawable.Drawable;
33import android.text.TextUtils;
34import android.util.Size;
35import android.view.ContextThemeWrapper;
36import android.view.Gravity;
37import android.view.LayoutInflater;
38import android.view.Menu;
39import android.view.MenuItem;
40import android.view.MotionEvent;
41import android.view.View;
42import android.view.View.MeasureSpec;
43import android.view.View.OnLayoutChangeListener;
44import android.view.ViewConfiguration;
45import android.view.ViewGroup;
46import android.view.ViewTreeObserver;
47import android.view.Window;
48import android.view.WindowManager;
49import android.view.animation.Animation;
50import android.view.animation.AnimationSet;
51import android.view.animation.Transformation;
52import android.view.animation.AnimationUtils;
53import android.view.animation.Interpolator;
54import android.widget.AdapterView;
55import android.widget.ArrayAdapter;
56import android.widget.Button;
57import android.widget.ImageButton;
58import android.widget.ImageView;
59import android.widget.LinearLayout;
60import android.widget.ListView;
61import android.widget.PopupWindow;
62import android.widget.TextView;
63
64import java.util.ArrayList;
65import java.util.LinkedList;
66import java.util.List;
67
68import com.android.internal.R;
69import com.android.internal.util.Preconditions;
70
71/**
72 * A floating toolbar for showing contextual menu items.
73 * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
74 * the remaining menu items in a vertical overflow view when the overflow button is clicked.
75 * The horizontal toolbar morphs into the vertical overflow view.
76 */
77public final class FloatingToolbar {
78
79    // This class is responsible for the public API of the floating toolbar.
80    // It delegates rendering operations to the FloatingToolbarPopup.
81
82    public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
83
84    private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
85            new MenuItem.OnMenuItemClickListener() {
86                @Override
87                public boolean onMenuItemClick(MenuItem item) {
88                    return false;
89                }
90            };
91
92    private final Context mContext;
93    private final Window mWindow;
94    private final FloatingToolbarPopup mPopup;
95
96    private final Rect mContentRect = new Rect();
97    private final Rect mPreviousContentRect = new Rect();
98
99    private Menu mMenu;
100    private List<Object> mShowingMenuItems = new ArrayList<Object>();
101    private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
102
103    private int mSuggestedWidth;
104    private boolean mWidthChanged = true;
105
106    private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
107
108        private final Rect mNewRect = new Rect();
109        private final Rect mOldRect = new Rect();
110
111        @Override
112        public void onLayoutChange(
113                View view,
114                int newLeft, int newRight, int newTop, int newBottom,
115                int oldLeft, int oldRight, int oldTop, int oldBottom) {
116            mNewRect.set(newLeft, newRight, newTop, newBottom);
117            mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
118            if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
119                mWidthChanged = true;
120                updateLayout();
121            }
122        }
123    };
124
125    /**
126     * Initializes a floating toolbar.
127     */
128    public FloatingToolbar(Context context, Window window) {
129        mContext = applyDefaultTheme(Preconditions.checkNotNull(context));
130        mWindow = Preconditions.checkNotNull(window);
131        mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
132    }
133
134    /**
135     * Sets the menu to be shown in this floating toolbar.
136     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
137     * toolbar.
138     */
139    public FloatingToolbar setMenu(Menu menu) {
140        mMenu = Preconditions.checkNotNull(menu);
141        return this;
142    }
143
144    /**
145     * Sets the custom listener for invocation of menu items in this floating toolbar.
146     */
147    public FloatingToolbar setOnMenuItemClickListener(
148            MenuItem.OnMenuItemClickListener menuItemClickListener) {
149        if (menuItemClickListener != null) {
150            mMenuItemClickListener = menuItemClickListener;
151        } else {
152            mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
153        }
154        return this;
155    }
156
157    /**
158     * Sets the content rectangle. This is the area of the interesting content that this toolbar
159     * should avoid obstructing.
160     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
161     * toolbar.
162     */
163    public FloatingToolbar setContentRect(Rect rect) {
164        mContentRect.set(Preconditions.checkNotNull(rect));
165        return this;
166    }
167
168    /**
169     * Sets the suggested width of this floating toolbar.
170     * The actual width will be about this size but there are no guarantees that it will be exactly
171     * the suggested width.
172     * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
173     * toolbar.
174     */
175    public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
176        // Check if there's been a substantial width spec change.
177        int difference = Math.abs(suggestedWidth - mSuggestedWidth);
178        mWidthChanged = difference > (mSuggestedWidth * 0.2);
179
180        mSuggestedWidth = suggestedWidth;
181        return this;
182    }
183
184    /**
185     * Shows this floating toolbar.
186     */
187    public FloatingToolbar show() {
188        registerOrientationHandler();
189        doShow();
190        return this;
191    }
192
193    /**
194     * Updates this floating toolbar to reflect recent position and view updates.
195     * NOTE: This method is a no-op if the toolbar isn't showing.
196     */
197    public FloatingToolbar updateLayout() {
198        if (mPopup.isShowing()) {
199            doShow();
200        }
201        return this;
202    }
203
204    /**
205     * Dismisses this floating toolbar.
206     */
207    public void dismiss() {
208        unregisterOrientationHandler();
209        mPopup.dismiss();
210    }
211
212    /**
213     * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
214     * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
215     */
216    public void hide() {
217        mPopup.hide();
218    }
219
220    /**
221     * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
222     */
223    public boolean isShowing() {
224        return mPopup.isShowing();
225    }
226
227    /**
228     * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
229     */
230    public boolean isHidden() {
231        return mPopup.isHidden();
232    }
233
234    private void doShow() {
235        List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
236        if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
237            mPopup.dismiss();
238            mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
239            mShowingMenuItems = getShowingMenuItemsReferences(menuItems);
240        }
241        if (!mPopup.isShowing()) {
242            mPopup.show(mContentRect);
243        } else if (!mPreviousContentRect.equals(mContentRect)) {
244            mPopup.updateCoordinates(mContentRect);
245        }
246        mWidthChanged = false;
247        mPreviousContentRect.set(mContentRect);
248    }
249
250    /**
251     * Returns true if this floating toolbar is currently showing the specified menu items.
252     */
253    private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
254        return mShowingMenuItems.equals(getShowingMenuItemsReferences(menuItems));
255    }
256
257    /**
258     * Returns the visible and enabled menu items in the specified menu.
259     * This method is recursive.
260     */
261    private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
262        List<MenuItem> menuItems = new ArrayList<MenuItem>();
263        for (int i = 0; (menu != null) && (i < menu.size()); i++) {
264            MenuItem menuItem = menu.getItem(i);
265            if (menuItem.isVisible() && menuItem.isEnabled()) {
266                Menu subMenu = menuItem.getSubMenu();
267                if (subMenu != null) {
268                    menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
269                } else {
270                    menuItems.add(menuItem);
271                }
272            }
273        }
274        return menuItems;
275    }
276
277    private List<Object> getShowingMenuItemsReferences(List<MenuItem> menuItems) {
278        List<Object> references = new ArrayList<Object>();
279        for (MenuItem menuItem : menuItems) {
280            if (isIconOnlyMenuItem(menuItem)) {
281                references.add(menuItem.getIcon());
282            } else {
283                references.add(menuItem.getTitle());
284            }
285        }
286        return references;
287    }
288
289    private void registerOrientationHandler() {
290        unregisterOrientationHandler();
291        mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
292    }
293
294    private void unregisterOrientationHandler() {
295        mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
296    }
297
298
299    /**
300     * A popup window used by the floating toolbar.
301     *
302     * This class is responsible for the rendering/animation of the floating toolbar.
303     * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
304     * to transition between panels.
305     */
306    private static final class FloatingToolbarPopup {
307
308        /* Minimum and maximum number of items allowed in the overflow. */
309        private static final int MIN_OVERFLOW_SIZE = 2;
310        private static final int MAX_OVERFLOW_SIZE = 4;
311
312        private final Context mContext;
313        private final View mParent;  // Parent for the popup window.
314        private final PopupWindow mPopupWindow;
315
316        /* Margins between the popup window and it's content. */
317        private final int mMarginHorizontal;
318        private final int mMarginVertical;
319
320        /* View components */
321        private final ViewGroup mContentContainer;  // holds all contents.
322        private final ViewGroup mMainPanel;  // holds menu items that are initially displayed.
323        private final OverflowPanel mOverflowPanel;  // holds menu items hidden in the overflow.
324        private final ImageButton mOverflowButton;  // opens/closes the overflow.
325        /* overflow button drawables. */
326        private final Drawable mArrow;
327        private final Drawable mOverflow;
328        private final AnimatedVectorDrawable mToArrow;
329        private final AnimatedVectorDrawable mToOverflow;
330
331        private final OverflowPanelViewHelper mOverflowPanelViewHelper;
332
333        /* Animation interpolators. */
334        private final Interpolator mLogAccelerateInterpolator;
335        private final Interpolator mFastOutSlowInInterpolator;
336        private final Interpolator mLinearOutSlowInInterpolator;
337        private final Interpolator mFastOutLinearInInterpolator;
338
339        /* Animations. */
340        private final AnimatorSet mShowAnimation;
341        private final AnimatorSet mDismissAnimation;
342        private final AnimatorSet mHideAnimation;
343        private final AnimationSet mOpenOverflowAnimation;
344        private final AnimationSet mCloseOverflowAnimation;
345        private final Animation.AnimationListener mOverflowAnimationListener;
346
347        private final Rect mViewPortOnScreen = new Rect();  // portion of screen we can draw in.
348        private final Point mCoordsOnWindow = new Point();  // popup window coordinates.
349        /* Temporary data holders. Reset values before using. */
350        private final int[] mTmpCoords = new int[2];
351        private final Rect mTmpRect = new Rect();
352
353        private final Region mTouchableRegion = new Region();
354        private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
355                new ViewTreeObserver.OnComputeInternalInsetsListener() {
356                    public void onComputeInternalInsets(
357                            ViewTreeObserver.InternalInsetsInfo info) {
358                        info.contentInsets.setEmpty();
359                        info.visibleInsets.setEmpty();
360                        info.touchableRegion.set(mTouchableRegion);
361                        info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo
362                                .TOUCHABLE_INSETS_REGION);
363                    }
364                };
365
366        /**
367         * @see OverflowPanelViewHelper#preparePopupContent().
368         */
369        private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
370            @Override
371            public void run() {
372                setPanelsStatesAtRestingPosition();
373                setContentAreaAsTouchableSurface();
374                mContentContainer.setAlpha(1);
375            }
376        };
377
378        private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
379        private boolean mHidden; // tracks whether this popup is hidden or hiding.
380
381        /* Calculated sizes for panels and overflow button. */
382        private final Size mOverflowButtonSize;
383        private Size mOverflowPanelSize;  // Should be null when there is no overflow.
384        private Size mMainPanelSize;
385
386        /* Item click listeners */
387        private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
388        private final View.OnClickListener mMenuItemButtonOnClickListener =
389                new View.OnClickListener() {
390                    @Override
391                    public void onClick(View v) {
392                        if (v.getTag() instanceof MenuItem) {
393                            if (mOnMenuItemClickListener != null) {
394                                mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
395                            }
396                        }
397                    }
398                };
399
400        private boolean mOpenOverflowUpwards;  // Whether the overflow opens upwards or downwards.
401        private boolean mIsOverflowOpen;
402
403        private int mTransitionDurationScale;  // Used to scale the toolbar transition duration.
404
405        /**
406         * Initializes a new floating toolbar popup.
407         *
408         * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
409         *      from.
410         */
411        public FloatingToolbarPopup(Context context, View parent) {
412            mParent = Preconditions.checkNotNull(parent);
413            mContext = Preconditions.checkNotNull(context);
414            mContentContainer = createContentContainer(context);
415            mPopupWindow = createPopupWindow(mContentContainer);
416            mMarginHorizontal = parent.getResources()
417                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
418            mMarginVertical = parent.getResources()
419                    .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
420
421            // Interpolators
422            mLogAccelerateInterpolator = new LogAccelerateInterpolator();
423            mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
424                    mContext, android.R.interpolator.fast_out_slow_in);
425            mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
426                    mContext, android.R.interpolator.linear_out_slow_in);
427            mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
428                    mContext, android.R.interpolator.fast_out_linear_in);
429
430            // Drawables. Needed for views.
431            mArrow = mContext.getResources()
432                    .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
433            mArrow.setAutoMirrored(true);
434            mOverflow = mContext.getResources()
435                    .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
436            mOverflow.setAutoMirrored(true);
437            mToArrow = (AnimatedVectorDrawable) mContext.getResources()
438                    .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
439            mToArrow.setAutoMirrored(true);
440            mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
441                    .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
442            mToOverflow.setAutoMirrored(true);
443
444            // Views
445            mOverflowButton = createOverflowButton();
446            mOverflowButtonSize = measure(mOverflowButton);
447            mMainPanel = createMainPanel();
448            mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext);
449            mOverflowPanel = createOverflowPanel();
450
451            // Animation. Need views.
452            mOverflowAnimationListener = createOverflowAnimationListener();
453            mOpenOverflowAnimation = new AnimationSet(true);
454            mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
455            mCloseOverflowAnimation = new AnimationSet(true);
456            mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
457            mShowAnimation = createEnterAnimation(mContentContainer);
458            mDismissAnimation = createExitAnimation(
459                    mContentContainer,
460                    150,  // startDelay
461                    new AnimatorListenerAdapter() {
462                        @Override
463                        public void onAnimationEnd(Animator animation) {
464                            mPopupWindow.dismiss();
465                            mContentContainer.removeAllViews();
466                        }
467                    });
468            mHideAnimation = createExitAnimation(
469                    mContentContainer,
470                    0,  // startDelay
471                    new AnimatorListenerAdapter() {
472                        @Override
473                        public void onAnimationEnd(Animator animation) {
474                            mPopupWindow.dismiss();
475                        }
476                    });
477        }
478
479        /**
480         * Lays out buttons for the specified menu items.
481         * Requires a subsequent call to {@link #show()} to show the items.
482         */
483        public void layoutMenuItems(
484                List<MenuItem> menuItems,
485                MenuItem.OnMenuItemClickListener menuItemClickListener,
486                int suggestedWidth) {
487            mOnMenuItemClickListener = menuItemClickListener;
488            cancelOverflowAnimations();
489            clearPanels();
490            menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
491            if (!menuItems.isEmpty()) {
492                // Add remaining items to the overflow.
493                layoutOverflowPanelItems(menuItems);
494            }
495            updatePopupSize();
496        }
497
498        /**
499         * Shows this popup at the specified coordinates.
500         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
501         */
502        public void show(Rect contentRectOnScreen) {
503            Preconditions.checkNotNull(contentRectOnScreen);
504
505            if (isShowing()) {
506                return;
507            }
508
509            mHidden = false;
510            mDismissed = false;
511            cancelDismissAndHideAnimations();
512            cancelOverflowAnimations();
513
514            refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
515            preparePopupContent();
516            // We need to specify the position in window coordinates.
517            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
518            // specify the popup position in screen coordinates.
519            mPopupWindow.showAtLocation(
520                    mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
521            setTouchableSurfaceInsetsComputer();
522            runShowAnimation();
523        }
524
525        /**
526         * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
527         */
528        public void dismiss() {
529            if (mDismissed) {
530                return;
531            }
532
533            mHidden = false;
534            mDismissed = true;
535            mHideAnimation.cancel();
536
537            runDismissAnimation();
538            setZeroTouchableSurface();
539        }
540
541        /**
542         * Hides this popup. This is a no-op if this popup is not showing.
543         * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
544         */
545        public void hide() {
546            if (!isShowing()) {
547                return;
548            }
549
550            mHidden = true;
551            runHideAnimation();
552            setZeroTouchableSurface();
553        }
554
555        /**
556         * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
557         */
558        public boolean isShowing() {
559            return !mDismissed && !mHidden;
560        }
561
562        /**
563         * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
564         */
565        public boolean isHidden() {
566            return mHidden;
567        }
568
569        /**
570         * Updates the coordinates of this popup.
571         * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
572         * This is a no-op if this popup is not showing.
573         */
574        public void updateCoordinates(Rect contentRectOnScreen) {
575            Preconditions.checkNotNull(contentRectOnScreen);
576
577            if (!isShowing() || !mPopupWindow.isShowing()) {
578                return;
579            }
580
581            cancelOverflowAnimations();
582            refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
583            preparePopupContent();
584            // We need to specify the position in window coordinates.
585            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can
586            // specify the popup position in screen coordinates.
587            mPopupWindow.update(
588                    mCoordsOnWindow.x, mCoordsOnWindow.y,
589                    mPopupWindow.getWidth(), mPopupWindow.getHeight());
590        }
591
592        private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
593            refreshViewPort();
594
595            // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
596            // landscape.
597            final int x = Math.min(
598                    contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
599                    mViewPortOnScreen.right - mPopupWindow.getWidth());
600
601            final int y;
602
603            final int availableHeightAboveContent =
604                    contentRectOnScreen.top - mViewPortOnScreen.top;
605            final int availableHeightBelowContent =
606                    mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
607
608            final int margin = 2 * mMarginVertical;
609            final int toolbarHeightWithVerticalMargin = getLineHeight(mContext) + margin;
610
611            if (!hasOverflow()) {
612                if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
613                    // There is enough space at the top of the content.
614                    y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
615                } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
616                    // There is enough space at the bottom of the content.
617                    y = contentRectOnScreen.bottom;
618                } else if (availableHeightBelowContent >= getLineHeight(mContext)) {
619                    // Just enough space to fit the toolbar with no vertical margins.
620                    y = contentRectOnScreen.bottom - mMarginVertical;
621                } else {
622                    // Not enough space. Prefer to position as high as possible.
623                    y = Math.max(
624                            mViewPortOnScreen.top,
625                            contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
626                }
627            } else {
628                // Has an overflow.
629                final int minimumOverflowHeightWithMargin =
630                        calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
631                final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
632                        contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
633                final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
634                        mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
635
636                if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
637                    // There is enough space at the top of the content rect for the overflow.
638                    // Position above and open upwards.
639                    updateOverflowHeight(availableHeightAboveContent - margin);
640                    y = contentRectOnScreen.top - mPopupWindow.getHeight();
641                    mOpenOverflowUpwards = true;
642                } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
643                        && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
644                    // There is enough space at the top of the content rect for the main panel
645                    // but not the overflow.
646                    // Position above but open downwards.
647                    updateOverflowHeight(availableHeightThroughContentDown - margin);
648                    y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
649                    mOpenOverflowUpwards = false;
650                } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
651                    // There is enough space at the bottom of the content rect for the overflow.
652                    // Position below and open downwards.
653                    updateOverflowHeight(availableHeightBelowContent - margin);
654                    y = contentRectOnScreen.bottom;
655                    mOpenOverflowUpwards = false;
656                } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
657                        && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
658                    // There is enough space at the bottom of the content rect for the main panel
659                    // but not the overflow.
660                    // Position below but open upwards.
661                    updateOverflowHeight(availableHeightThroughContentUp - margin);
662                    y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
663                            mPopupWindow.getHeight();
664                    mOpenOverflowUpwards = true;
665                } else {
666                    // Not enough space.
667                    // Position at the top of the view port and open downwards.
668                    updateOverflowHeight(mViewPortOnScreen.height() - margin);
669                    y = mViewPortOnScreen.top;
670                    mOpenOverflowUpwards = false;
671                }
672            }
673
674            // We later specify the location of PopupWindow relative to the attached window.
675            // The idea here is that 1) we can get the location of a View in both window coordinates
676            // and screen coordiantes, where the offset between them should be equal to the window
677            // origin, and 2) we can use an arbitrary for this calculation while calculating the
678            // location of the rootview is supposed to be least expensive.
679            // TODO: Consider to use PopupWindow.setLayoutInScreenEnabled(true) so that we can avoid
680            // the following calculation.
681            mParent.getRootView().getLocationOnScreen(mTmpCoords);
682            int rootViewLeftOnScreen = mTmpCoords[0];
683            int rootViewTopOnScreen = mTmpCoords[1];
684            mParent.getRootView().getLocationInWindow(mTmpCoords);
685            int rootViewLeftOnWindow = mTmpCoords[0];
686            int rootViewTopOnWindow = mTmpCoords[1];
687            int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
688            int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
689            mCoordsOnWindow.set(
690                    Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
691        }
692
693        /**
694         * Performs the "show" animation on the floating popup.
695         */
696        private void runShowAnimation() {
697            mShowAnimation.start();
698        }
699
700        /**
701         * Performs the "dismiss" animation on the floating popup.
702         */
703        private void runDismissAnimation() {
704            mDismissAnimation.start();
705        }
706
707        /**
708         * Performs the "hide" animation on the floating popup.
709         */
710        private void runHideAnimation() {
711            mHideAnimation.start();
712        }
713
714        private void cancelDismissAndHideAnimations() {
715            mDismissAnimation.cancel();
716            mHideAnimation.cancel();
717        }
718
719        private void cancelOverflowAnimations() {
720            mContentContainer.clearAnimation();
721            mMainPanel.animate().cancel();
722            mOverflowPanel.animate().cancel();
723            mToArrow.stop();
724            mToOverflow.stop();
725        }
726
727        private void openOverflow() {
728            final int targetWidth = mOverflowPanelSize.getWidth();
729            final int targetHeight = mOverflowPanelSize.getHeight();
730            final int startWidth = mContentContainer.getWidth();
731            final int startHeight = mContentContainer.getHeight();
732            final float startY = mContentContainer.getY();
733            final float left = mContentContainer.getX();
734            final float right = left + mContentContainer.getWidth();
735            Animation widthAnimation = new Animation() {
736                @Override
737                protected void applyTransformation(float interpolatedTime, Transformation t) {
738                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
739                    setWidth(mContentContainer, startWidth + deltaWidth);
740                    if (isRTL()) {
741                        mContentContainer.setX(left);
742
743                        // Lock the panels in place.
744                        mMainPanel.setX(0);
745                        mOverflowPanel.setX(0);
746                    } else {
747                        mContentContainer.setX(right - mContentContainer.getWidth());
748
749                        // Offset the panels' positions so they look like they're locked in place
750                        // on the screen.
751                        mMainPanel.setX(mContentContainer.getWidth() - startWidth);
752                        mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
753                    }
754                }
755            };
756            Animation heightAnimation = new Animation() {
757                @Override
758                protected void applyTransformation(float interpolatedTime, Transformation t) {
759                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
760                    setHeight(mContentContainer, startHeight + deltaHeight);
761                    if (mOpenOverflowUpwards) {
762                        mContentContainer.setY(
763                                startY - (mContentContainer.getHeight() - startHeight));
764                        positionContentYCoordinatesIfOpeningOverflowUpwards();
765                    }
766                }
767            };
768            final float overflowButtonStartX = mOverflowButton.getX();
769            final float overflowButtonTargetX = isRTL() ?
770                    overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
771                    overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
772            Animation overflowButtonAnimation = new Animation() {
773                @Override
774                protected void applyTransformation(float interpolatedTime, Transformation t) {
775                    float overflowButtonX = overflowButtonStartX
776                            + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
777                    float deltaContainerWidth = isRTL() ?
778                            0 :
779                            mContentContainer.getWidth() - startWidth;
780                    float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
781                    mOverflowButton.setX(actualOverflowButtonX);
782                }
783            };
784            widthAnimation.setInterpolator(mLogAccelerateInterpolator);
785            widthAnimation.setDuration(getAdjustedDuration(250));
786            heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
787            heightAnimation.setDuration(getAdjustedDuration(250));
788            overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
789            overflowButtonAnimation.setDuration(getAdjustedDuration(250));
790            mOpenOverflowAnimation.getAnimations().clear();
791            mOpenOverflowAnimation.getAnimations().clear();
792            mOpenOverflowAnimation.addAnimation(widthAnimation);
793            mOpenOverflowAnimation.addAnimation(heightAnimation);
794            mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
795            mContentContainer.startAnimation(mOpenOverflowAnimation);
796            mIsOverflowOpen = true;
797            mMainPanel.animate()
798                    .alpha(0).withLayer()
799                    .setInterpolator(mLinearOutSlowInInterpolator)
800                    .setDuration(250)
801                    .start();
802            mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
803        }
804
805        private void closeOverflow() {
806            final int targetWidth = mMainPanelSize.getWidth();
807            final int startWidth = mContentContainer.getWidth();
808            final float left = mContentContainer.getX();
809            final float right = left + mContentContainer.getWidth();
810            Animation widthAnimation = new Animation() {
811                @Override
812                protected void applyTransformation(float interpolatedTime, Transformation t) {
813                    int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
814                    setWidth(mContentContainer, startWidth + deltaWidth);
815                    if (isRTL()) {
816                        mContentContainer.setX(left);
817
818                        // Lock the panels in place.
819                        mMainPanel.setX(0);
820                        mOverflowPanel.setX(0);
821                    } else {
822                        mContentContainer.setX(right - mContentContainer.getWidth());
823
824                        // Offset the panels' positions so they look like they're locked in place
825                        // on the screen.
826                        mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
827                        mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
828                    }
829                }
830            };
831            final int targetHeight = mMainPanelSize.getHeight();
832            final int startHeight = mContentContainer.getHeight();
833            final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
834            Animation heightAnimation = new Animation() {
835                @Override
836                protected void applyTransformation(float interpolatedTime, Transformation t) {
837                    int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
838                    setHeight(mContentContainer, startHeight + deltaHeight);
839                    if (mOpenOverflowUpwards) {
840                        mContentContainer.setY(bottom - mContentContainer.getHeight());
841                        positionContentYCoordinatesIfOpeningOverflowUpwards();
842                    }
843                }
844            };
845            final float overflowButtonStartX = mOverflowButton.getX();
846            final float overflowButtonTargetX = isRTL() ?
847                    overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
848                    overflowButtonStartX + startWidth - mOverflowButton.getWidth();
849            Animation overflowButtonAnimation = new Animation() {
850                @Override
851                protected void applyTransformation(float interpolatedTime, Transformation t) {
852                    float overflowButtonX = overflowButtonStartX
853                            + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
854                    float deltaContainerWidth = isRTL() ?
855                            0 :
856                            mContentContainer.getWidth() - startWidth;
857                    float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
858                    mOverflowButton.setX(actualOverflowButtonX);
859                }
860            };
861            widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
862            widthAnimation.setDuration(getAdjustedDuration(250));
863            heightAnimation.setInterpolator(mLogAccelerateInterpolator);
864            heightAnimation.setDuration(getAdjustedDuration(250));
865            overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
866            overflowButtonAnimation.setDuration(getAdjustedDuration(250));
867            mCloseOverflowAnimation.getAnimations().clear();
868            mCloseOverflowAnimation.addAnimation(widthAnimation);
869            mCloseOverflowAnimation.addAnimation(heightAnimation);
870            mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
871            mContentContainer.startAnimation(mCloseOverflowAnimation);
872            mIsOverflowOpen = false;
873            mMainPanel.animate()
874                    .alpha(1).withLayer()
875                    .setInterpolator(mFastOutLinearInInterpolator)
876                    .setDuration(100)
877                    .start();
878            mOverflowPanel.animate()
879                    .alpha(0).withLayer()
880                    .setInterpolator(mLinearOutSlowInInterpolator)
881                    .setDuration(150)
882                    .start();
883        }
884
885        /**
886         * Defines the position of the floating toolbar popup panels when transition animation has
887         * stopped.
888         */
889        private void setPanelsStatesAtRestingPosition() {
890            mOverflowButton.setEnabled(true);
891            mOverflowPanel.awakenScrollBars();
892
893            if (mIsOverflowOpen) {
894                // Set open state.
895                final Size containerSize = mOverflowPanelSize;
896                setSize(mContentContainer, containerSize);
897                mMainPanel.setAlpha(0);
898                mMainPanel.setVisibility(View.INVISIBLE);
899                mOverflowPanel.setAlpha(1);
900                mOverflowPanel.setVisibility(View.VISIBLE);
901                mOverflowButton.setImageDrawable(mArrow);
902                mOverflowButton.setContentDescription(mContext.getString(
903                        R.string.floating_toolbar_close_overflow_description));
904
905                // Update x-coordinates depending on RTL state.
906                if (isRTL()) {
907                    mContentContainer.setX(mMarginHorizontal);  // align left
908                    mMainPanel.setX(0);  // align left
909                    mOverflowButton.setX(  // align right
910                            containerSize.getWidth() - mOverflowButtonSize.getWidth());
911                    mOverflowPanel.setX(0);  // align left
912                } else {
913                    mContentContainer.setX(  // align right
914                            mPopupWindow.getWidth() -
915                                    containerSize.getWidth() - mMarginHorizontal);
916                    mMainPanel.setX(-mContentContainer.getX());  // align right
917                    mOverflowButton.setX(0);  // align left
918                    mOverflowPanel.setX(0);  // align left
919                }
920
921                // Update y-coordinates depending on overflow's open direction.
922                if (mOpenOverflowUpwards) {
923                    mContentContainer.setY(mMarginVertical);  // align top
924                    mMainPanel.setY(  // align bottom
925                            containerSize.getHeight() - mContentContainer.getHeight());
926                    mOverflowButton.setY(  // align bottom
927                            containerSize.getHeight() - mOverflowButtonSize.getHeight());
928                    mOverflowPanel.setY(0);  // align top
929                } else {
930                    // opens downwards.
931                    mContentContainer.setY(mMarginVertical);  // align top
932                    mMainPanel.setY(0);  // align top
933                    mOverflowButton.setY(0);  // align top
934                    mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
935                }
936            } else {
937                // Overflow not open. Set closed state.
938                final Size containerSize = mMainPanelSize;
939                setSize(mContentContainer, containerSize);
940                mMainPanel.setAlpha(1);
941                mMainPanel.setVisibility(View.VISIBLE);
942                mOverflowPanel.setAlpha(0);
943                mOverflowPanel.setVisibility(View.INVISIBLE);
944                mOverflowButton.setImageDrawable(mOverflow);
945                mOverflowButton.setContentDescription(mContext.getString(
946                        R.string.floating_toolbar_open_overflow_description));
947
948                if (hasOverflow()) {
949                    // Update x-coordinates depending on RTL state.
950                    if (isRTL()) {
951                        mContentContainer.setX(mMarginHorizontal);  // align left
952                        mMainPanel.setX(0);  // align left
953                        mOverflowButton.setX(0);  // align left
954                        mOverflowPanel.setX(0);  // align left
955                    } else {
956                        mContentContainer.setX(  // align right
957                                mPopupWindow.getWidth() -
958                                        containerSize.getWidth() - mMarginHorizontal);
959                        mMainPanel.setX(0);  // align left
960                        mOverflowButton.setX(  // align right
961                                containerSize.getWidth() - mOverflowButtonSize.getWidth());
962                        mOverflowPanel.setX(  // align right
963                                containerSize.getWidth() - mOverflowPanelSize.getWidth());
964                    }
965
966                    // Update y-coordinates depending on overflow's open direction.
967                    if (mOpenOverflowUpwards) {
968                        mContentContainer.setY(  // align bottom
969                                mMarginVertical +
970                                        mOverflowPanelSize.getHeight() - containerSize.getHeight());
971                        mMainPanel.setY(0);  // align top
972                        mOverflowButton.setY(0);  // align top
973                        mOverflowPanel.setY(  // align bottom
974                                containerSize.getHeight() - mOverflowPanelSize.getHeight());
975                    } else {
976                        // opens downwards.
977                        mContentContainer.setY(mMarginVertical);  // align top
978                        mMainPanel.setY(0);  // align top
979                        mOverflowButton.setY(0);  // align top
980                        mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
981                    }
982                } else {
983                    // No overflow.
984                    mContentContainer.setX(mMarginHorizontal);  // align left
985                    mContentContainer.setY(mMarginVertical);  // align top
986                    mMainPanel.setX(0);  // align left
987                    mMainPanel.setY(0);  // align top
988                }
989            }
990        }
991
992        private void updateOverflowHeight(int suggestedHeight) {
993            if (hasOverflow()) {
994                final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
995                        getLineHeight(mContext);
996                final int newHeight = calculateOverflowHeight(maxItemSize);
997                if (mOverflowPanelSize.getHeight() != newHeight) {
998                    mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
999                }
1000                setSize(mOverflowPanel, mOverflowPanelSize);
1001                if (mIsOverflowOpen) {
1002                    setSize(mContentContainer, mOverflowPanelSize);
1003                    if (mOpenOverflowUpwards) {
1004                        final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
1005                        mContentContainer.setY(mContentContainer.getY() + deltaHeight);
1006                        mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
1007                    }
1008                } else {
1009                    setSize(mContentContainer, mMainPanelSize);
1010                }
1011                updatePopupSize();
1012            }
1013        }
1014
1015        private void updatePopupSize() {
1016            int width = 0;
1017            int height = 0;
1018            if (mMainPanelSize != null) {
1019                width = Math.max(width, mMainPanelSize.getWidth());
1020                height = Math.max(height, mMainPanelSize.getHeight());
1021            }
1022            if (mOverflowPanelSize != null) {
1023                width = Math.max(width, mOverflowPanelSize.getWidth());
1024                height = Math.max(height, mOverflowPanelSize.getHeight());
1025            }
1026            mPopupWindow.setWidth(width + mMarginHorizontal * 2);
1027            mPopupWindow.setHeight(height + mMarginVertical * 2);
1028            maybeComputeTransitionDurationScale();
1029        }
1030
1031        private void refreshViewPort() {
1032            mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
1033        }
1034
1035        private int getAdjustedToolbarWidth(int suggestedWidth) {
1036            int width = suggestedWidth;
1037            refreshViewPort();
1038            int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
1039                    .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
1040            if (width <= 0) {
1041                width = mParent.getResources()
1042                        .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
1043            }
1044            return Math.min(width, maximumWidth);
1045        }
1046
1047        /**
1048         * Sets the touchable region of this popup to be zero. This means that all touch events on
1049         * this popup will go through to the surface behind it.
1050         */
1051        private void setZeroTouchableSurface() {
1052            mTouchableRegion.setEmpty();
1053        }
1054
1055        /**
1056         * Sets the touchable region of this popup to be the area occupied by its content.
1057         */
1058        private void setContentAreaAsTouchableSurface() {
1059            Preconditions.checkNotNull(mMainPanelSize);
1060            final int width;
1061            final int height;
1062            if (mIsOverflowOpen) {
1063                Preconditions.checkNotNull(mOverflowPanelSize);
1064                width = mOverflowPanelSize.getWidth();
1065                height = mOverflowPanelSize.getHeight();
1066            } else {
1067                width = mMainPanelSize.getWidth();
1068                height = mMainPanelSize.getHeight();
1069            }
1070            mTouchableRegion.set(
1071                    (int) mContentContainer.getX(),
1072                    (int) mContentContainer.getY(),
1073                    (int) mContentContainer.getX() + width,
1074                    (int) mContentContainer.getY() + height);
1075        }
1076
1077        /**
1078         * Make the touchable area of this popup be the area specified by mTouchableRegion.
1079         * This should be called after the popup window has been dismissed (dismiss/hide)
1080         * and is probably being re-shown with a new content root view.
1081         */
1082        private void setTouchableSurfaceInsetsComputer() {
1083            ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
1084                    .getRootView()
1085                    .getViewTreeObserver();
1086            viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
1087            viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
1088        }
1089
1090        private boolean isRTL() {
1091            return mContext.getResources().getConfiguration().getLayoutDirection()
1092                    == View.LAYOUT_DIRECTION_RTL;
1093        }
1094
1095        private boolean hasOverflow() {
1096            return mOverflowPanelSize != null;
1097        }
1098
1099        /**
1100         * Fits as many menu items in the main panel and returns a list of the menu items that
1101         * were not fit in.
1102         *
1103         * @return The menu items that are not included in this main panel.
1104         */
1105        public List<MenuItem> layoutMainPanelItems(
1106                List<MenuItem> menuItems, final int toolbarWidth) {
1107            Preconditions.checkNotNull(menuItems);
1108
1109            int availableWidth = toolbarWidth;
1110            final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
1111
1112            mMainPanel.removeAllViews();
1113            mMainPanel.setPaddingRelative(0, 0, 0, 0);
1114
1115            boolean isFirstItem = true;
1116            while (!remainingMenuItems.isEmpty()) {
1117                final MenuItem menuItem = remainingMenuItems.peek();
1118                View menuItemButton = createMenuItemButton(mContext, menuItem);
1119
1120                // Adding additional start padding for the first button to even out button spacing.
1121                if (isFirstItem) {
1122                    menuItemButton.setPaddingRelative(
1123                            (int) (1.5 * menuItemButton.getPaddingStart()),
1124                            menuItemButton.getPaddingTop(),
1125                            menuItemButton.getPaddingEnd(),
1126                            menuItemButton.getPaddingBottom());
1127                    isFirstItem = false;
1128                }
1129
1130                // Adding additional end padding for the last button to even out button spacing.
1131                if (remainingMenuItems.size() == 1) {
1132                    menuItemButton.setPaddingRelative(
1133                            menuItemButton.getPaddingStart(),
1134                            menuItemButton.getPaddingTop(),
1135                            (int) (1.5 * menuItemButton.getPaddingEnd()),
1136                            menuItemButton.getPaddingBottom());
1137                }
1138
1139                menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1140                int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
1141                // Check if we can fit an item while reserving space for the overflowButton.
1142                boolean canFitWithOverflow =
1143                        menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth();
1144                boolean canFitNoOverflow =
1145                        remainingMenuItems.size() == 1 && menuItemButtonWidth <= availableWidth;
1146                if (canFitWithOverflow || canFitNoOverflow) {
1147                    setButtonTagAndClickListener(menuItemButton, menuItem);
1148                    mMainPanel.addView(menuItemButton);
1149                    ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1150                    params.width = menuItemButtonWidth;
1151                    menuItemButton.setLayoutParams(params);
1152                    availableWidth -= menuItemButtonWidth;
1153                    remainingMenuItems.pop();
1154                } else {
1155                    // Reserve space for overflowButton.
1156                    mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1157                    break;
1158                }
1159            }
1160            mMainPanelSize = measure(mMainPanel);
1161            return remainingMenuItems;
1162        }
1163
1164        private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
1165            ArrayAdapter<MenuItem> overflowPanelAdapter =
1166                    (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1167            overflowPanelAdapter.clear();
1168            final int size = menuItems.size();
1169            for (int i = 0; i < size; i++) {
1170                overflowPanelAdapter.add(menuItems.get(i));
1171            }
1172            mOverflowPanel.setAdapter(overflowPanelAdapter);
1173            if (mOpenOverflowUpwards) {
1174                mOverflowPanel.setY(0);
1175            } else {
1176                mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1177            }
1178
1179            int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
1180            int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
1181            mOverflowPanelSize = new Size(width, height);
1182            setSize(mOverflowPanel, mOverflowPanelSize);
1183        }
1184
1185        /**
1186         * Resets the content container and appropriately position it's panels.
1187         */
1188        private void preparePopupContent() {
1189            mContentContainer.removeAllViews();
1190
1191            // Add views in the specified order so they stack up as expected.
1192            // Order: overflowPanel, mainPanel, overflowButton.
1193            if (hasOverflow()) {
1194                mContentContainer.addView(mOverflowPanel);
1195            }
1196            mContentContainer.addView(mMainPanel);
1197            if (hasOverflow()) {
1198                mContentContainer.addView(mOverflowButton);
1199            }
1200            setPanelsStatesAtRestingPosition();
1201            setContentAreaAsTouchableSurface();
1202
1203            // The positioning of contents in RTL is wrong when the view is first rendered.
1204            // Hide the view and post a runnable to recalculate positions and render the view.
1205            // TODO: Investigate why this happens and fix.
1206            if (isRTL()) {
1207                mContentContainer.setAlpha(0);
1208                mContentContainer.post(mPreparePopupContentRTLHelper);
1209            }
1210        }
1211
1212        /**
1213         * Clears out the panels and their container. Resets their calculated sizes.
1214         */
1215        private void clearPanels() {
1216            mOverflowPanelSize = null;
1217            mMainPanelSize = null;
1218            mIsOverflowOpen = false;
1219            mMainPanel.removeAllViews();
1220            ArrayAdapter<MenuItem> overflowPanelAdapter =
1221                    (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1222            overflowPanelAdapter.clear();
1223            mOverflowPanel.setAdapter(overflowPanelAdapter);
1224            mContentContainer.removeAllViews();
1225        }
1226
1227        private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
1228            if (mOpenOverflowUpwards) {
1229                mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
1230                mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
1231                mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
1232            }
1233        }
1234
1235        private int getOverflowWidth() {
1236            int overflowWidth = 0;
1237            final int count = mOverflowPanel.getAdapter().getCount();
1238            for (int i = 0; i < count; i++) {
1239                MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
1240                overflowWidth =
1241                        Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1242            }
1243            return overflowWidth;
1244        }
1245
1246        private int calculateOverflowHeight(int maxItemSize) {
1247            // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
1248            int actualSize = Math.min(
1249                    MAX_OVERFLOW_SIZE,
1250                    Math.min(
1251                            Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1252                            mOverflowPanel.getCount()));
1253            int extension = 0;
1254            if (actualSize < mOverflowPanel.getCount()) {
1255                // The overflow will require scrolling to get to all the items.
1256                // Extend the height so that part of the hidden items is displayed.
1257                extension = (int) (getLineHeight(mContext) * 0.5f);
1258            }
1259            return actualSize * getLineHeight(mContext)
1260                    + mOverflowButtonSize.getHeight()
1261                    + extension;
1262        }
1263
1264        private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1265            View button = menuItemButton;
1266            if (isIconOnlyMenuItem(menuItem)) {
1267                button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1268            }
1269            button.setTag(menuItem);
1270            button.setOnClickListener(mMenuItemButtonOnClickListener);
1271        }
1272
1273        /**
1274         * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1275         * animations. See comment about this in the code.
1276         */
1277        private int getAdjustedDuration(int originalDuration) {
1278            if (mTransitionDurationScale < 150) {
1279                // For smaller transition, decrease the time.
1280                return Math.max(originalDuration - 50, 0);
1281            } else if (mTransitionDurationScale > 300) {
1282                // For bigger transition, increase the time.
1283                return originalDuration + 50;
1284            }
1285
1286            // Scale the animation duration with getDurationScale(). This allows
1287            // android.view.animation.* animations to scale just like android.animation.* animations
1288            // when  animator duration scale is adjusted in "Developer Options".
1289            // For this reason, do not use this method for android.animation.* animations.
1290            return (int) (originalDuration * ValueAnimator.getDurationScale());
1291        }
1292
1293        private void maybeComputeTransitionDurationScale() {
1294            if (mMainPanelSize != null && mOverflowPanelSize != null) {
1295                int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
1296                int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
1297                mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
1298                        mContentContainer.getContext().getResources().getDisplayMetrics().density);
1299            }
1300        }
1301
1302        private ViewGroup createMainPanel() {
1303            ViewGroup mainPanel = new LinearLayout(mContext) {
1304                @Override
1305                protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1306                    if (isOverflowAnimating()) {
1307                        // Update widthMeasureSpec to make sure that this view is not clipped
1308                        // as we offset it's coordinates with respect to it's parent.
1309                        widthMeasureSpec = MeasureSpec.makeMeasureSpec(
1310                                mMainPanelSize.getWidth(),
1311                                MeasureSpec.EXACTLY);
1312                    }
1313                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1314                }
1315
1316                @Override
1317                public boolean onInterceptTouchEvent(MotionEvent ev) {
1318                    // Intercept the touch event while the overflow is animating.
1319                    return isOverflowAnimating();
1320                }
1321            };
1322            return mainPanel;
1323        }
1324
1325        private ImageButton createOverflowButton() {
1326            final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
1327                    .inflate(R.layout.floating_popup_overflow_button, null);
1328            overflowButton.setImageDrawable(mOverflow);
1329            overflowButton.setOnClickListener(new View.OnClickListener() {
1330                @Override
1331                public void onClick(View v) {
1332                    if (mIsOverflowOpen) {
1333                        overflowButton.setImageDrawable(mToOverflow);
1334                        mToOverflow.start();
1335                        closeOverflow();
1336                    } else {
1337                        overflowButton.setImageDrawable(mToArrow);
1338                        mToArrow.start();
1339                        openOverflow();
1340                    }
1341                }
1342            });
1343            return overflowButton;
1344        }
1345
1346        private OverflowPanel createOverflowPanel() {
1347            final OverflowPanel overflowPanel = new OverflowPanel(this);
1348            overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
1349                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1350            overflowPanel.setDivider(null);
1351            overflowPanel.setDividerHeight(0);
1352
1353            final ArrayAdapter adapter =
1354                    new ArrayAdapter<MenuItem>(mContext, 0) {
1355                        @Override
1356                        public int getViewTypeCount() {
1357                            return mOverflowPanelViewHelper.getViewTypeCount();
1358                        }
1359
1360                        @Override
1361                        public int getItemViewType(int position) {
1362                            return mOverflowPanelViewHelper.getItemViewType(getItem(position));
1363                        }
1364
1365                        @Override
1366                        public View getView(int position, View convertView, ViewGroup parent) {
1367                            return mOverflowPanelViewHelper.getView(
1368                                    getItem(position), mOverflowPanelSize.getWidth(), convertView);
1369                        }
1370                    };
1371            overflowPanel.setAdapter(adapter);
1372
1373            overflowPanel.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1374                @Override
1375                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1376                    MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
1377                    if (mOnMenuItemClickListener != null) {
1378                        mOnMenuItemClickListener.onMenuItemClick(menuItem);
1379                    }
1380                }
1381            });
1382
1383            return overflowPanel;
1384        }
1385
1386        private boolean isOverflowAnimating() {
1387            final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
1388                    && !mOpenOverflowAnimation.hasEnded();
1389            final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
1390                    && !mCloseOverflowAnimation.hasEnded();
1391            return overflowOpening || overflowClosing;
1392        }
1393
1394        private Animation.AnimationListener createOverflowAnimationListener() {
1395            Animation.AnimationListener listener = new Animation.AnimationListener() {
1396                @Override
1397                public void onAnimationStart(Animation animation) {
1398                    // Disable the overflow button while it's animating.
1399                    // It will be re-enabled when the animation stops.
1400                    mOverflowButton.setEnabled(false);
1401                    // Ensure both panels have visibility turned on when the overflow animation
1402                    // starts.
1403                    mMainPanel.setVisibility(View.VISIBLE);
1404                    mOverflowPanel.setVisibility(View.VISIBLE);
1405                }
1406
1407                @Override
1408                public void onAnimationEnd(Animation animation) {
1409                    // Posting this because it seems like this is called before the animation
1410                    // actually ends.
1411                    mContentContainer.post(new Runnable() {
1412                        @Override
1413                        public void run() {
1414                            setPanelsStatesAtRestingPosition();
1415                            setContentAreaAsTouchableSurface();
1416                        }
1417                    });
1418                }
1419
1420                @Override
1421                public void onAnimationRepeat(Animation animation) {
1422                }
1423            };
1424            return listener;
1425        }
1426
1427        private static Size measure(View view) {
1428            Preconditions.checkState(view.getParent() == null);
1429            view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1430            return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
1431        }
1432
1433        private static void setSize(View view, int width, int height) {
1434            view.setMinimumWidth(width);
1435            view.setMinimumHeight(height);
1436            ViewGroup.LayoutParams params = view.getLayoutParams();
1437            params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
1438            params.width = width;
1439            params.height = height;
1440            view.setLayoutParams(params);
1441        }
1442
1443        private static void setSize(View view, Size size) {
1444            setSize(view, size.getWidth(), size.getHeight());
1445        }
1446
1447        private static void setWidth(View view, int width) {
1448            ViewGroup.LayoutParams params = view.getLayoutParams();
1449            setSize(view, width, params.height);
1450        }
1451
1452        private static void setHeight(View view, int height) {
1453            ViewGroup.LayoutParams params = view.getLayoutParams();
1454            setSize(view, params.width, height);
1455        }
1456
1457        private static int getLineHeight(Context context) {
1458            return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1459        }
1460
1461        /**
1462         * A custom ListView for the overflow panel.
1463         */
1464        private static final class OverflowPanel extends ListView {
1465
1466            private final FloatingToolbarPopup mPopup;
1467
1468            OverflowPanel(FloatingToolbarPopup popup) {
1469                super(Preconditions.checkNotNull(popup).mContext);
1470                this.mPopup = popup;
1471                setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
1472                setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
1473            }
1474
1475            @Override
1476            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1477                // Update heightMeasureSpec to make sure that this view is not clipped
1478                // as we offset it's coordinates with respect to it's parent.
1479                int height = mPopup.mOverflowPanelSize.getHeight()
1480                        - mPopup.mOverflowButtonSize.getHeight();
1481                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1482                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1483            }
1484
1485            @Override
1486            public boolean dispatchTouchEvent(MotionEvent ev) {
1487                if (mPopup.isOverflowAnimating()) {
1488                    // Eat the touch event.
1489                    return true;
1490                }
1491                return super.dispatchTouchEvent(ev);
1492            }
1493
1494            @Override
1495            protected boolean awakenScrollBars() {
1496                return super.awakenScrollBars();
1497            }
1498        }
1499
1500        /**
1501         * A custom interpolator used for various floating toolbar animations.
1502         */
1503        private static final class LogAccelerateInterpolator implements Interpolator {
1504
1505            private static final int BASE = 100;
1506            private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
1507
1508            private static float computeLog(float t, int base) {
1509                return (float) (1 - Math.pow(base, -t));
1510            }
1511
1512            @Override
1513            public float getInterpolation(float t) {
1514                return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1515            }
1516        }
1517
1518        /**
1519         * A helper for generating views for the overflow panel.
1520         */
1521        private static final class OverflowPanelViewHelper {
1522
1523            private static final int NUM_OF_VIEW_TYPES = 2;
1524            private static final int VIEW_TYPE_STRING_TITLE = 0;
1525            private static final int VIEW_TYPE_ICON_ONLY = 1;
1526
1527            private final TextView mStringTitleViewCalculator;
1528            private final View mIconOnlyViewCalculator;
1529
1530            private final Context mContext;
1531
1532            public OverflowPanelViewHelper(Context context) {
1533                mContext = Preconditions.checkNotNull(context);
1534                mStringTitleViewCalculator = getStringTitleView(null, 0, null);
1535                mIconOnlyViewCalculator = getIconOnlyView(null, 0, null);
1536            }
1537
1538            public int getViewTypeCount() {
1539                return NUM_OF_VIEW_TYPES;
1540            }
1541
1542            public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
1543                Preconditions.checkNotNull(menuItem);
1544                if (getItemViewType(menuItem) == VIEW_TYPE_ICON_ONLY) {
1545                    return getIconOnlyView(menuItem, minimumWidth, convertView);
1546                }
1547                return getStringTitleView(menuItem, minimumWidth, convertView);
1548            }
1549
1550            public int getItemViewType(MenuItem menuItem) {
1551                Preconditions.checkNotNull(menuItem);
1552                if (isIconOnlyMenuItem(menuItem)) {
1553                    return VIEW_TYPE_ICON_ONLY;
1554                }
1555                return VIEW_TYPE_STRING_TITLE;
1556            }
1557
1558            public int calculateWidth(MenuItem menuItem) {
1559                final View calculator;
1560                if (isIconOnlyMenuItem(menuItem)) {
1561                    ((ImageView) mIconOnlyViewCalculator
1562                            .findViewById(R.id.floating_toolbar_menu_item_image_button))
1563                            .setImageDrawable(menuItem.getIcon());
1564                    calculator = mIconOnlyViewCalculator;
1565                } else {
1566                    mStringTitleViewCalculator.setText(menuItem.getTitle());
1567                    calculator = mStringTitleViewCalculator;
1568                }
1569                calculator.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1570                return calculator.getMeasuredWidth();
1571            }
1572
1573            private TextView getStringTitleView(
1574                    MenuItem menuItem, int minimumWidth, View convertView) {
1575                TextView menuButton;
1576                if (convertView != null) {
1577                    menuButton = (TextView) convertView;
1578                } else {
1579                    menuButton = (TextView) LayoutInflater.from(mContext)
1580                            .inflate(R.layout.floating_popup_overflow_list_item, null);
1581                    menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1582                            ViewGroup.LayoutParams.MATCH_PARENT,
1583                            ViewGroup.LayoutParams.WRAP_CONTENT));
1584                }
1585                if (menuItem != null) {
1586                    menuButton.setText(menuItem.getTitle());
1587                    menuButton.setContentDescription(menuItem.getTitle());
1588                    menuButton.setMinimumWidth(minimumWidth);
1589                }
1590                return menuButton;
1591            }
1592
1593            private View getIconOnlyView(
1594                    MenuItem menuItem, int minimumWidth, View convertView) {
1595                View menuButton;
1596                if (convertView != null) {
1597                    menuButton = convertView;
1598                } else {
1599                    menuButton = LayoutInflater.from(mContext).inflate(
1600                            R.layout.floating_popup_overflow_image_list_item, null);
1601                    menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1602                            ViewGroup.LayoutParams.WRAP_CONTENT,
1603                            ViewGroup.LayoutParams.WRAP_CONTENT));
1604                }
1605                if (menuItem != null) {
1606                    ((ImageView) menuButton
1607                            .findViewById(R.id.floating_toolbar_menu_item_image_button))
1608                            .setImageDrawable(menuItem.getIcon());
1609                    menuButton.setMinimumWidth(minimumWidth);
1610                }
1611                return menuButton;
1612            }
1613        }
1614    }
1615
1616    /**
1617     * @return {@code true} if the menu item does not not have a string title but has an icon.
1618     *   {@code false} otherwise.
1619     */
1620    private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1621        if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1622            return true;
1623        }
1624        return false;
1625    }
1626
1627    /**
1628     * Creates and returns a menu button for the specified menu item.
1629     */
1630    private static View createMenuItemButton(Context context, MenuItem menuItem) {
1631        if (isIconOnlyMenuItem(menuItem)) {
1632            View imageMenuItemButton = LayoutInflater.from(context)
1633                    .inflate(R.layout.floating_popup_menu_image_button, null);
1634            ((ImageButton) imageMenuItemButton
1635                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1636                    .setImageDrawable(menuItem.getIcon());
1637            return imageMenuItemButton;
1638        }
1639
1640        Button menuItemButton = (Button) LayoutInflater.from(context)
1641                .inflate(R.layout.floating_popup_menu_button, null);
1642        menuItemButton.setText(menuItem.getTitle());
1643        menuItemButton.setContentDescription(menuItem.getTitle());
1644        return menuItemButton;
1645    }
1646
1647    private static ViewGroup createContentContainer(Context context) {
1648        ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
1649                .inflate(R.layout.floating_popup_container, null);
1650        contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
1651                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1652        contentContainer.setTag(FLOATING_TOOLBAR_TAG);
1653        return contentContainer;
1654    }
1655
1656    private static PopupWindow createPopupWindow(ViewGroup content) {
1657        ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1658        PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1659        // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1660        // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1661        popupWindow.setClippingEnabled(false);
1662        popupWindow.setWindowLayoutType(
1663                WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1664        popupWindow.setAnimationStyle(0);
1665        popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1666        content.setLayoutParams(new ViewGroup.LayoutParams(
1667                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1668        popupContentHolder.addView(content);
1669        return popupWindow;
1670    }
1671
1672    /**
1673     * Creates an "appear" animation for the specified view.
1674     *
1675     * @param view  The view to animate
1676     */
1677    private static AnimatorSet createEnterAnimation(View view) {
1678        AnimatorSet animation = new AnimatorSet();
1679        animation.playTogether(
1680                ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
1681        return animation;
1682    }
1683
1684    /**
1685     * Creates a "disappear" animation for the specified view.
1686     *
1687     * @param view  The view to animate
1688     * @param startDelay  The start delay of the animation
1689     * @param listener  The animation listener
1690     */
1691    private static AnimatorSet createExitAnimation(
1692            View view, int startDelay, Animator.AnimatorListener listener) {
1693        AnimatorSet animation =  new AnimatorSet();
1694        animation.playTogether(
1695                ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
1696        animation.setStartDelay(startDelay);
1697        animation.addListener(listener);
1698        return animation;
1699    }
1700
1701    /**
1702     * Returns a re-themed context with controlled look and feel for views.
1703     */
1704    private static Context applyDefaultTheme(Context originalContext) {
1705        TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1706        boolean isLightTheme = a.getBoolean(0, true);
1707        int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material;
1708        a.recycle();
1709        return new ContextThemeWrapper(originalContext, themeId);
1710    }
1711}
1712