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