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 (isInRTLMode()) {
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 = isInRTLMode() ?
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 = isInRTLMode() ?
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 (isInRTLMode()) {
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 = isInRTLMode() ?
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 = isInRTLMode() ?
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 (isInRTLMode()) {
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 (isInRTLMode()) {
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 isInRTLMode() {
1091            return mContext.getApplicationInfo().hasRtlSupport()
1092                    && mContext.getResources().getConfiguration().getLayoutDirection()
1093                            == View.LAYOUT_DIRECTION_RTL;
1094        }
1095
1096        private boolean hasOverflow() {
1097            return mOverflowPanelSize != null;
1098        }
1099
1100        /**
1101         * Fits as many menu items in the main panel and returns a list of the menu items that
1102         * were not fit in.
1103         *
1104         * @return The menu items that are not included in this main panel.
1105         */
1106        public List<MenuItem> layoutMainPanelItems(
1107                List<MenuItem> menuItems, final int toolbarWidth) {
1108            Preconditions.checkNotNull(menuItems);
1109
1110            int availableWidth = toolbarWidth;
1111            final LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems);
1112
1113            mMainPanel.removeAllViews();
1114            mMainPanel.setPaddingRelative(0, 0, 0, 0);
1115
1116            boolean isFirstItem = true;
1117            while (!remainingMenuItems.isEmpty()) {
1118                final MenuItem menuItem = remainingMenuItems.peek();
1119                View menuItemButton = createMenuItemButton(mContext, menuItem);
1120
1121                // Adding additional start padding for the first button to even out button spacing.
1122                if (isFirstItem) {
1123                    menuItemButton.setPaddingRelative(
1124                            (int) (1.5 * menuItemButton.getPaddingStart()),
1125                            menuItemButton.getPaddingTop(),
1126                            menuItemButton.getPaddingEnd(),
1127                            menuItemButton.getPaddingBottom());
1128                    isFirstItem = false;
1129                }
1130
1131                // Adding additional end padding for the last button to even out button spacing.
1132                if (remainingMenuItems.size() == 1) {
1133                    menuItemButton.setPaddingRelative(
1134                            menuItemButton.getPaddingStart(),
1135                            menuItemButton.getPaddingTop(),
1136                            (int) (1.5 * menuItemButton.getPaddingEnd()),
1137                            menuItemButton.getPaddingBottom());
1138                }
1139
1140                menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1141                int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth);
1142                // Check if we can fit an item while reserving space for the overflowButton.
1143                boolean canFitWithOverflow =
1144                        menuItemButtonWidth <= availableWidth - mOverflowButtonSize.getWidth();
1145                boolean canFitNoOverflow =
1146                        remainingMenuItems.size() == 1 && menuItemButtonWidth <= availableWidth;
1147                if (canFitWithOverflow || canFitNoOverflow) {
1148                    setButtonTagAndClickListener(menuItemButton, menuItem);
1149                    mMainPanel.addView(menuItemButton);
1150                    ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
1151                    params.width = menuItemButtonWidth;
1152                    menuItemButton.setLayoutParams(params);
1153                    availableWidth -= menuItemButtonWidth;
1154                    remainingMenuItems.pop();
1155                } else {
1156                    // Reserve space for overflowButton.
1157                    mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
1158                    break;
1159                }
1160            }
1161            mMainPanelSize = measure(mMainPanel);
1162            return remainingMenuItems;
1163        }
1164
1165        private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
1166            ArrayAdapter<MenuItem> overflowPanelAdapter =
1167                    (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1168            overflowPanelAdapter.clear();
1169            final int size = menuItems.size();
1170            for (int i = 0; i < size; i++) {
1171                overflowPanelAdapter.add(menuItems.get(i));
1172            }
1173            mOverflowPanel.setAdapter(overflowPanelAdapter);
1174            if (mOpenOverflowUpwards) {
1175                mOverflowPanel.setY(0);
1176            } else {
1177                mOverflowPanel.setY(mOverflowButtonSize.getHeight());
1178            }
1179
1180            int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
1181            int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
1182            mOverflowPanelSize = new Size(width, height);
1183            setSize(mOverflowPanel, mOverflowPanelSize);
1184        }
1185
1186        /**
1187         * Resets the content container and appropriately position it's panels.
1188         */
1189        private void preparePopupContent() {
1190            mContentContainer.removeAllViews();
1191
1192            // Add views in the specified order so they stack up as expected.
1193            // Order: overflowPanel, mainPanel, overflowButton.
1194            if (hasOverflow()) {
1195                mContentContainer.addView(mOverflowPanel);
1196            }
1197            mContentContainer.addView(mMainPanel);
1198            if (hasOverflow()) {
1199                mContentContainer.addView(mOverflowButton);
1200            }
1201            setPanelsStatesAtRestingPosition();
1202            setContentAreaAsTouchableSurface();
1203
1204            // The positioning of contents in RTL is wrong when the view is first rendered.
1205            // Hide the view and post a runnable to recalculate positions and render the view.
1206            // TODO: Investigate why this happens and fix.
1207            if (isInRTLMode()) {
1208                mContentContainer.setAlpha(0);
1209                mContentContainer.post(mPreparePopupContentRTLHelper);
1210            }
1211        }
1212
1213        /**
1214         * Clears out the panels and their container. Resets their calculated sizes.
1215         */
1216        private void clearPanels() {
1217            mOverflowPanelSize = null;
1218            mMainPanelSize = null;
1219            mIsOverflowOpen = false;
1220            mMainPanel.removeAllViews();
1221            ArrayAdapter<MenuItem> overflowPanelAdapter =
1222                    (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
1223            overflowPanelAdapter.clear();
1224            mOverflowPanel.setAdapter(overflowPanelAdapter);
1225            mContentContainer.removeAllViews();
1226        }
1227
1228        private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
1229            if (mOpenOverflowUpwards) {
1230                mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
1231                mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
1232                mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
1233            }
1234        }
1235
1236        private int getOverflowWidth() {
1237            int overflowWidth = 0;
1238            final int count = mOverflowPanel.getAdapter().getCount();
1239            for (int i = 0; i < count; i++) {
1240                MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
1241                overflowWidth =
1242                        Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
1243            }
1244            return overflowWidth;
1245        }
1246
1247        private int calculateOverflowHeight(int maxItemSize) {
1248            // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
1249            int actualSize = Math.min(
1250                    MAX_OVERFLOW_SIZE,
1251                    Math.min(
1252                            Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
1253                            mOverflowPanel.getCount()));
1254            int extension = 0;
1255            if (actualSize < mOverflowPanel.getCount()) {
1256                // The overflow will require scrolling to get to all the items.
1257                // Extend the height so that part of the hidden items is displayed.
1258                extension = (int) (getLineHeight(mContext) * 0.5f);
1259            }
1260            return actualSize * getLineHeight(mContext)
1261                    + mOverflowButtonSize.getHeight()
1262                    + extension;
1263        }
1264
1265        private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
1266            View button = menuItemButton;
1267            if (isIconOnlyMenuItem(menuItem)) {
1268                button = menuItemButton.findViewById(R.id.floating_toolbar_menu_item_image_button);
1269            }
1270            button.setTag(menuItem);
1271            button.setOnClickListener(mMenuItemButtonOnClickListener);
1272        }
1273
1274        /**
1275         * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
1276         * animations. See comment about this in the code.
1277         */
1278        private int getAdjustedDuration(int originalDuration) {
1279            if (mTransitionDurationScale < 150) {
1280                // For smaller transition, decrease the time.
1281                return Math.max(originalDuration - 50, 0);
1282            } else if (mTransitionDurationScale > 300) {
1283                // For bigger transition, increase the time.
1284                return originalDuration + 50;
1285            }
1286
1287            // Scale the animation duration with getDurationScale(). This allows
1288            // android.view.animation.* animations to scale just like android.animation.* animations
1289            // when  animator duration scale is adjusted in "Developer Options".
1290            // For this reason, do not use this method for android.animation.* animations.
1291            return (int) (originalDuration * ValueAnimator.getDurationScale());
1292        }
1293
1294        private void maybeComputeTransitionDurationScale() {
1295            if (mMainPanelSize != null && mOverflowPanelSize != null) {
1296                int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
1297                int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
1298                mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
1299                        mContentContainer.getContext().getResources().getDisplayMetrics().density);
1300            }
1301        }
1302
1303        private ViewGroup createMainPanel() {
1304            ViewGroup mainPanel = new LinearLayout(mContext) {
1305                @Override
1306                protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1307                    if (isOverflowAnimating()) {
1308                        // Update widthMeasureSpec to make sure that this view is not clipped
1309                        // as we offset it's coordinates with respect to it's parent.
1310                        widthMeasureSpec = MeasureSpec.makeMeasureSpec(
1311                                mMainPanelSize.getWidth(),
1312                                MeasureSpec.EXACTLY);
1313                    }
1314                    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1315                }
1316
1317                @Override
1318                public boolean onInterceptTouchEvent(MotionEvent ev) {
1319                    // Intercept the touch event while the overflow is animating.
1320                    return isOverflowAnimating();
1321                }
1322            };
1323            return mainPanel;
1324        }
1325
1326        private ImageButton createOverflowButton() {
1327            final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
1328                    .inflate(R.layout.floating_popup_overflow_button, null);
1329            overflowButton.setImageDrawable(mOverflow);
1330            overflowButton.setOnClickListener(new View.OnClickListener() {
1331                @Override
1332                public void onClick(View v) {
1333                    if (mIsOverflowOpen) {
1334                        overflowButton.setImageDrawable(mToOverflow);
1335                        mToOverflow.start();
1336                        closeOverflow();
1337                    } else {
1338                        overflowButton.setImageDrawable(mToArrow);
1339                        mToArrow.start();
1340                        openOverflow();
1341                    }
1342                }
1343            });
1344            return overflowButton;
1345        }
1346
1347        private OverflowPanel createOverflowPanel() {
1348            final OverflowPanel overflowPanel = new OverflowPanel(this);
1349            overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
1350                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1351            overflowPanel.setDivider(null);
1352            overflowPanel.setDividerHeight(0);
1353
1354            final ArrayAdapter adapter =
1355                    new ArrayAdapter<MenuItem>(mContext, 0) {
1356                        @Override
1357                        public int getViewTypeCount() {
1358                            return mOverflowPanelViewHelper.getViewTypeCount();
1359                        }
1360
1361                        @Override
1362                        public int getItemViewType(int position) {
1363                            return mOverflowPanelViewHelper.getItemViewType(getItem(position));
1364                        }
1365
1366                        @Override
1367                        public View getView(int position, View convertView, ViewGroup parent) {
1368                            return mOverflowPanelViewHelper.getView(
1369                                    getItem(position), mOverflowPanelSize.getWidth(), convertView);
1370                        }
1371                    };
1372            overflowPanel.setAdapter(adapter);
1373
1374            overflowPanel.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1375                @Override
1376                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1377                    MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
1378                    if (mOnMenuItemClickListener != null) {
1379                        mOnMenuItemClickListener.onMenuItemClick(menuItem);
1380                    }
1381                }
1382            });
1383
1384            return overflowPanel;
1385        }
1386
1387        private boolean isOverflowAnimating() {
1388            final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
1389                    && !mOpenOverflowAnimation.hasEnded();
1390            final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
1391                    && !mCloseOverflowAnimation.hasEnded();
1392            return overflowOpening || overflowClosing;
1393        }
1394
1395        private Animation.AnimationListener createOverflowAnimationListener() {
1396            Animation.AnimationListener listener = new Animation.AnimationListener() {
1397                @Override
1398                public void onAnimationStart(Animation animation) {
1399                    // Disable the overflow button while it's animating.
1400                    // It will be re-enabled when the animation stops.
1401                    mOverflowButton.setEnabled(false);
1402                    // Ensure both panels have visibility turned on when the overflow animation
1403                    // starts.
1404                    mMainPanel.setVisibility(View.VISIBLE);
1405                    mOverflowPanel.setVisibility(View.VISIBLE);
1406                }
1407
1408                @Override
1409                public void onAnimationEnd(Animation animation) {
1410                    // Posting this because it seems like this is called before the animation
1411                    // actually ends.
1412                    mContentContainer.post(new Runnable() {
1413                        @Override
1414                        public void run() {
1415                            setPanelsStatesAtRestingPosition();
1416                            setContentAreaAsTouchableSurface();
1417                        }
1418                    });
1419                }
1420
1421                @Override
1422                public void onAnimationRepeat(Animation animation) {
1423                }
1424            };
1425            return listener;
1426        }
1427
1428        private static Size measure(View view) {
1429            Preconditions.checkState(view.getParent() == null);
1430            view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
1431            return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
1432        }
1433
1434        private static void setSize(View view, int width, int height) {
1435            view.setMinimumWidth(width);
1436            view.setMinimumHeight(height);
1437            ViewGroup.LayoutParams params = view.getLayoutParams();
1438            params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
1439            params.width = width;
1440            params.height = height;
1441            view.setLayoutParams(params);
1442        }
1443
1444        private static void setSize(View view, Size size) {
1445            setSize(view, size.getWidth(), size.getHeight());
1446        }
1447
1448        private static void setWidth(View view, int width) {
1449            ViewGroup.LayoutParams params = view.getLayoutParams();
1450            setSize(view, width, params.height);
1451        }
1452
1453        private static void setHeight(View view, int height) {
1454            ViewGroup.LayoutParams params = view.getLayoutParams();
1455            setSize(view, params.width, height);
1456        }
1457
1458        private static int getLineHeight(Context context) {
1459            return context.getResources().getDimensionPixelSize(R.dimen.floating_toolbar_height);
1460        }
1461
1462        /**
1463         * A custom ListView for the overflow panel.
1464         */
1465        private static final class OverflowPanel extends ListView {
1466
1467            private final FloatingToolbarPopup mPopup;
1468
1469            OverflowPanel(FloatingToolbarPopup popup) {
1470                super(Preconditions.checkNotNull(popup).mContext);
1471                this.mPopup = popup;
1472                setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
1473                setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
1474            }
1475
1476            @Override
1477            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1478                // Update heightMeasureSpec to make sure that this view is not clipped
1479                // as we offset it's coordinates with respect to it's parent.
1480                int height = mPopup.mOverflowPanelSize.getHeight()
1481                        - mPopup.mOverflowButtonSize.getHeight();
1482                heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1483                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1484            }
1485
1486            @Override
1487            public boolean dispatchTouchEvent(MotionEvent ev) {
1488                if (mPopup.isOverflowAnimating()) {
1489                    // Eat the touch event.
1490                    return true;
1491                }
1492                return super.dispatchTouchEvent(ev);
1493            }
1494
1495            @Override
1496            protected boolean awakenScrollBars() {
1497                return super.awakenScrollBars();
1498            }
1499        }
1500
1501        /**
1502         * A custom interpolator used for various floating toolbar animations.
1503         */
1504        private static final class LogAccelerateInterpolator implements Interpolator {
1505
1506            private static final int BASE = 100;
1507            private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
1508
1509            private static float computeLog(float t, int base) {
1510                return (float) (1 - Math.pow(base, -t));
1511            }
1512
1513            @Override
1514            public float getInterpolation(float t) {
1515                return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
1516            }
1517        }
1518
1519        /**
1520         * A helper for generating views for the overflow panel.
1521         */
1522        private static final class OverflowPanelViewHelper {
1523
1524            private static final int NUM_OF_VIEW_TYPES = 2;
1525            private static final int VIEW_TYPE_STRING_TITLE = 0;
1526            private static final int VIEW_TYPE_ICON_ONLY = 1;
1527
1528            private final TextView mStringTitleViewCalculator;
1529            private final View mIconOnlyViewCalculator;
1530
1531            private final Context mContext;
1532
1533            public OverflowPanelViewHelper(Context context) {
1534                mContext = Preconditions.checkNotNull(context);
1535                mStringTitleViewCalculator = getStringTitleView(null, 0, null);
1536                mIconOnlyViewCalculator = getIconOnlyView(null, 0, null);
1537            }
1538
1539            public int getViewTypeCount() {
1540                return NUM_OF_VIEW_TYPES;
1541            }
1542
1543            public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
1544                Preconditions.checkNotNull(menuItem);
1545                if (getItemViewType(menuItem) == VIEW_TYPE_ICON_ONLY) {
1546                    return getIconOnlyView(menuItem, minimumWidth, convertView);
1547                }
1548                return getStringTitleView(menuItem, minimumWidth, convertView);
1549            }
1550
1551            public int getItemViewType(MenuItem menuItem) {
1552                Preconditions.checkNotNull(menuItem);
1553                if (isIconOnlyMenuItem(menuItem)) {
1554                    return VIEW_TYPE_ICON_ONLY;
1555                }
1556                return VIEW_TYPE_STRING_TITLE;
1557            }
1558
1559            public int calculateWidth(MenuItem menuItem) {
1560                final View calculator;
1561                if (isIconOnlyMenuItem(menuItem)) {
1562                    ((ImageView) mIconOnlyViewCalculator
1563                            .findViewById(R.id.floating_toolbar_menu_item_image_button))
1564                            .setImageDrawable(menuItem.getIcon());
1565                    calculator = mIconOnlyViewCalculator;
1566                } else {
1567                    mStringTitleViewCalculator.setText(menuItem.getTitle());
1568                    calculator = mStringTitleViewCalculator;
1569                }
1570                calculator.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
1571                return calculator.getMeasuredWidth();
1572            }
1573
1574            private TextView getStringTitleView(
1575                    MenuItem menuItem, int minimumWidth, View convertView) {
1576                TextView menuButton;
1577                if (convertView != null) {
1578                    menuButton = (TextView) convertView;
1579                } else {
1580                    menuButton = (TextView) LayoutInflater.from(mContext)
1581                            .inflate(R.layout.floating_popup_overflow_list_item, null);
1582                    menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1583                            ViewGroup.LayoutParams.MATCH_PARENT,
1584                            ViewGroup.LayoutParams.WRAP_CONTENT));
1585                }
1586                if (menuItem != null) {
1587                    menuButton.setText(menuItem.getTitle());
1588                    menuButton.setContentDescription(menuItem.getTitle());
1589                    menuButton.setMinimumWidth(minimumWidth);
1590                }
1591                return menuButton;
1592            }
1593
1594            private View getIconOnlyView(
1595                    MenuItem menuItem, int minimumWidth, View convertView) {
1596                View menuButton;
1597                if (convertView != null) {
1598                    menuButton = convertView;
1599                } else {
1600                    menuButton = LayoutInflater.from(mContext).inflate(
1601                            R.layout.floating_popup_overflow_image_list_item, null);
1602                    menuButton.setLayoutParams(new ViewGroup.LayoutParams(
1603                            ViewGroup.LayoutParams.WRAP_CONTENT,
1604                            ViewGroup.LayoutParams.WRAP_CONTENT));
1605                }
1606                if (menuItem != null) {
1607                    ((ImageView) menuButton
1608                            .findViewById(R.id.floating_toolbar_menu_item_image_button))
1609                            .setImageDrawable(menuItem.getIcon());
1610                    menuButton.setMinimumWidth(minimumWidth);
1611                }
1612                return menuButton;
1613            }
1614        }
1615    }
1616
1617    /**
1618     * @return {@code true} if the menu item does not not have a string title but has an icon.
1619     *   {@code false} otherwise.
1620     */
1621    private static boolean isIconOnlyMenuItem(MenuItem menuItem) {
1622        if (TextUtils.isEmpty(menuItem.getTitle()) && menuItem.getIcon() != null) {
1623            return true;
1624        }
1625        return false;
1626    }
1627
1628    /**
1629     * Creates and returns a menu button for the specified menu item.
1630     */
1631    private static View createMenuItemButton(Context context, MenuItem menuItem) {
1632        if (isIconOnlyMenuItem(menuItem)) {
1633            View imageMenuItemButton = LayoutInflater.from(context)
1634                    .inflate(R.layout.floating_popup_menu_image_button, null);
1635            ((ImageButton) imageMenuItemButton
1636                    .findViewById(R.id.floating_toolbar_menu_item_image_button))
1637                    .setImageDrawable(menuItem.getIcon());
1638            return imageMenuItemButton;
1639        }
1640
1641        Button menuItemButton = (Button) LayoutInflater.from(context)
1642                .inflate(R.layout.floating_popup_menu_button, null);
1643        menuItemButton.setText(menuItem.getTitle());
1644        menuItemButton.setContentDescription(menuItem.getTitle());
1645        return menuItemButton;
1646    }
1647
1648    private static ViewGroup createContentContainer(Context context) {
1649        ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
1650                .inflate(R.layout.floating_popup_container, null);
1651        contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
1652                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1653        contentContainer.setTag(FLOATING_TOOLBAR_TAG);
1654        return contentContainer;
1655    }
1656
1657    private static PopupWindow createPopupWindow(ViewGroup content) {
1658        ViewGroup popupContentHolder = new LinearLayout(content.getContext());
1659        PopupWindow popupWindow = new PopupWindow(popupContentHolder);
1660        // TODO: Use .setLayoutInScreenEnabled(true) instead of .setClippingEnabled(false)
1661        // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
1662        popupWindow.setClippingEnabled(false);
1663        popupWindow.setWindowLayoutType(
1664                WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
1665        popupWindow.setAnimationStyle(0);
1666        popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1667        content.setLayoutParams(new ViewGroup.LayoutParams(
1668                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
1669        popupContentHolder.addView(content);
1670        return popupWindow;
1671    }
1672
1673    /**
1674     * Creates an "appear" animation for the specified view.
1675     *
1676     * @param view  The view to animate
1677     */
1678    private static AnimatorSet createEnterAnimation(View view) {
1679        AnimatorSet animation = new AnimatorSet();
1680        animation.playTogether(
1681                ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
1682        return animation;
1683    }
1684
1685    /**
1686     * Creates a "disappear" animation for the specified view.
1687     *
1688     * @param view  The view to animate
1689     * @param startDelay  The start delay of the animation
1690     * @param listener  The animation listener
1691     */
1692    private static AnimatorSet createExitAnimation(
1693            View view, int startDelay, Animator.AnimatorListener listener) {
1694        AnimatorSet animation =  new AnimatorSet();
1695        animation.playTogether(
1696                ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
1697        animation.setStartDelay(startDelay);
1698        animation.addListener(listener);
1699        return animation;
1700    }
1701
1702    /**
1703     * Returns a re-themed context with controlled look and feel for views.
1704     */
1705    private static Context applyDefaultTheme(Context originalContext) {
1706        TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
1707        boolean isLightTheme = a.getBoolean(0, true);
1708        int themeId = isLightTheme ? R.style.Theme_Material_Light : R.style.Theme_Material;
1709        a.recycle();
1710        return new ContextThemeWrapper(originalContext, themeId);
1711    }
1712}
1713