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