content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.internal.widget;
  
  import android.animation.Animator;
  import android.animation.AnimatorListenerAdapter;
  import android.animation.AnimatorSet;
  import android.animation.ObjectAnimator;
  import android.animation.ValueAnimator;
  import android.annotation.Nullable;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Color;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.graphics.Region;
  import android.graphics.drawable.AnimatedVectorDrawable;
  import android.graphics.drawable.ColorDrawable;
  import android.graphics.drawable.Drawable;
  import android.text.TextUtils;
  import android.util.Size;
  import android.view.ContextThemeWrapper;
  import android.view.Gravity;
  import android.view.LayoutInflater;
  import android.view.Menu;
  import android.view.MenuItem;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.View.MeasureSpec;
  import android.view.View.OnLayoutChangeListener;
  import android.view.ViewConfiguration;
  import android.view.ViewGroup;
  import android.view.ViewTreeObserver;
  import android.view.Window;
  import android.view.WindowManager;
  import android.view.animation.Animation;
  import android.view.animation.AnimationSet;
  import android.view.animation.AnimationUtils;
  import android.view.animation.Interpolator;
  import android.view.animation.Transformation;
  import android.widget.ArrayAdapter;
  import android.widget.ImageButton;
  import android.widget.ImageView;
  import android.widget.LinearLayout;
  import android.widget.ListView;
  import android.widget.PopupWindow;
  import android.widget.TextView;
  
  import com.android.internal.R;
  import com.android.internal.util.Preconditions;
  
  import java.util.ArrayList;
  import java.util.Comparator;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Objects;
  
  /**
   * A floating toolbar for showing contextual menu items.
   * This view shows as many menu item buttons as can fit in the horizontal toolbar and the
   * the remaining menu items in a vertical overflow view when the overflow button is clicked.
   * The horizontal toolbar morphs into the vertical overflow view.
   */
  public final class FloatingToolbar {
  
      // This class is responsible for the public API of the floating toolbar.
      // It delegates rendering operations to the FloatingToolbarPopup.
  
      public static final String FLOATING_TOOLBAR_TAG = "floating_toolbar";
  
      private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER =
              item -> false;
  
      private final Context mContext;
      private final Window mWindow;
      private final FloatingToolbarPopup mPopup;
  
      private final Rect mContentRect = new Rect();
      private final Rect mPreviousContentRect = new Rect();
  
      private Menu mMenu;
      private List<MenuItem> mShowingMenuItems = new ArrayList<>();
      private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
  
      private int mSuggestedWidth;
      private boolean mWidthChanged = true;
  
      private final OnLayoutChangeListener mOrientationChangeHandler = new OnLayoutChangeListener() {
  
          private final Rect mNewRect = new Rect();
          private final Rect mOldRect = new Rect();
  
          @Override
          public void onLayoutChange(
                  View view,
                  int newLeft, int newRight, int newTop, int newBottom,
                  int oldLeft, int oldRight, int oldTop, int oldBottom) {
              mNewRect.set(newLeft, newRight, newTop, newBottom);
              mOldRect.set(oldLeft, oldRight, oldTop, oldBottom);
              if (mPopup.isShowing() && !mNewRect.equals(mOldRect)) {
                  mWidthChanged = true;
                  updateLayout();
              }
          }
      };
  
      /**
       * Sorts the list of menu items to conform to certain requirements.
       */
      private final Comparator<MenuItem> mMenuItemComparator = (menuItem1, menuItem2) -> {
          // Ensure the assist menu item is always the first item:
          if (menuItem1.getItemId() == android.R.id.textAssist) {
              return menuItem2.getItemId() == android.R.id.textAssist ? 0 : -1;
          }
          if (menuItem2.getItemId() == android.R.id.textAssist) {
              return 1;
          }
  
          // Order by SHOW_AS_ACTION type:
          if (menuItem1.requiresActionButton()) {
              return menuItem2.requiresActionButton() ? 0 : -1;
          }
          if (menuItem2.requiresActionButton()) {
              return 1;
          }
          if (menuItem1.requiresOverflow()) {
              return menuItem2.requiresOverflow() ? 0 : 1;
          }
          if (menuItem2.requiresOverflow()) {
              return -1;
          }
  
          // Order by order value:
          return menuItem1.getOrder() - menuItem2.getOrder();
      };
  
      /**
       * Initializes a floating toolbar.
       */
      public FloatingToolbar(Window window) {
          // TODO(b/65172902): Pass context in constructor when DecorView (and other callers)
          // supports multi-display.
          mContext = applyDefaultTheme(window.getContext());
          mWindow = Preconditions.checkNotNull(window);
          mPopup = new FloatingToolbarPopup(mContext, window.getDecorView());
      }
  
      /**
       * Sets the menu to be shown in this floating toolbar.
       * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
       * toolbar.
       */
      public FloatingToolbar setMenu(Menu menu) {
          mMenu = Preconditions.checkNotNull(menu);
          return this;
      }
  
      /**
       * Sets the custom listener for invocation of menu items in this floating toolbar.
       */
      public FloatingToolbar setOnMenuItemClickListener(
              MenuItem.OnMenuItemClickListener menuItemClickListener) {
          if (menuItemClickListener != null) {
              mMenuItemClickListener = menuItemClickListener;
          } else {
              mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER;
          }
          return this;
      }
  
      /**
       * Sets the content rectangle. This is the area of the interesting content that this toolbar
       * should avoid obstructing.
       * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
       * toolbar.
       */
      public FloatingToolbar setContentRect(Rect rect) {
          mContentRect.set(Preconditions.checkNotNull(rect));
          return this;
      }
  
      /**
       * Sets the suggested width of this floating toolbar.
       * The actual width will be about this size but there are no guarantees that it will be exactly
       * the suggested width.
       * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the
       * toolbar.
       */
      public FloatingToolbar setSuggestedWidth(int suggestedWidth) {
          // Check if there's been a substantial width spec change.
          int difference = Math.abs(suggestedWidth - mSuggestedWidth);
          mWidthChanged = difference > (mSuggestedWidth * 0.2);
  
          mSuggestedWidth = suggestedWidth;
          return this;
      }
  
      /**
       * Shows this floating toolbar.
       */
      public FloatingToolbar show() {
          registerOrientationHandler();
          doShow();
          return this;
      }
  
      /**
       * Updates this floating toolbar to reflect recent position and view updates.
       * NOTE: This method is a no-op if the toolbar isn't showing.
       */
      public FloatingToolbar updateLayout() {
          if (mPopup.isShowing()) {
              doShow();
          }
          return this;
      }
  
      /**
       * Dismisses this floating toolbar.
       */
      public void dismiss() {
          unregisterOrientationHandler();
          mPopup.dismiss();
      }
  
      /**
       * Hides this floating toolbar. This is a no-op if the toolbar is not showing.
       * Use {@link #isHidden()} to distinguish between a hidden and a dismissed toolbar.
       */
      public void hide() {
          mPopup.hide();
      }
  
      /**
       * Returns {@code true} if this toolbar is currently showing. {@code false} otherwise.
       */
      public boolean isShowing() {
          return mPopup.isShowing();
      }
  
      /**
       * Returns {@code true} if this toolbar is currently hidden. {@code false} otherwise.
       */
      public boolean isHidden() {
          return mPopup.isHidden();
      }
  
      /**
       * If this is set to true, the action mode view will dismiss itself on touch events outside of
       * its window. If the toolbar is already showing, it will be re-shown so that this setting takes
       * effect immediately.
       *
       * @param outsideTouchable whether or not this action mode is "outside touchable"
       * @param onDismiss optional. Sets a callback for when this action mode popup dismisses itself
       */
      public void setOutsideTouchable(
              boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
          if (mPopup.setOutsideTouchable(outsideTouchable, onDismiss) && isShowing()) {
              dismiss();
              doShow();
          }
      }
  
      private void doShow() {
          List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu);
          menuItems.sort(mMenuItemComparator);
          if (!isCurrentlyShowing(menuItems) || mWidthChanged) {
              mPopup.dismiss();
              mPopup.layoutMenuItems(menuItems, mMenuItemClickListener, mSuggestedWidth);
              mShowingMenuItems = menuItems;
          }
          if (!mPopup.isShowing()) {
              mPopup.show(mContentRect);
          } else if (!mPreviousContentRect.equals(mContentRect)) {
              mPopup.updateCoordinates(mContentRect);
          }
          mWidthChanged = false;
          mPreviousContentRect.set(mContentRect);
      }
  
      /**
       * Returns true if this floating toolbar is currently showing the specified menu items.
       */
      private boolean isCurrentlyShowing(List<MenuItem> menuItems) {
          if (mShowingMenuItems == null || menuItems.size() != mShowingMenuItems.size()) {
              return false;
          }
  
          final int size = menuItems.size();
          for (int i = 0; i < size; i++) {
              final MenuItem menuItem = menuItems.get(i);
              final MenuItem showingItem = mShowingMenuItems.get(i);
              if (menuItem.getItemId() != showingItem.getItemId()
                      || !TextUtils.equals(menuItem.getTitle(), showingItem.getTitle())
                      || !Objects.equals(menuItem.getIcon(), showingItem.getIcon())
                      || menuItem.getGroupId() != showingItem.getGroupId()) {
                  return false;
              }
          }
  
          return true;
      }
  
      /**
       * Returns the visible and enabled menu items in the specified menu.
       * This method is recursive.
       */
      private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) {
          List<MenuItem> menuItems = new ArrayList<>();
          for (int i = 0; (menu != null) && (i < menu.size()); i++) {
              MenuItem menuItem = menu.getItem(i);
              if (menuItem.isVisible() && menuItem.isEnabled()) {
                  Menu subMenu = menuItem.getSubMenu();
                  if (subMenu != null) {
                      menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu));
                  } else {
                      menuItems.add(menuItem);
                  }
              }
          }
          return menuItems;
      }
  
      private void registerOrientationHandler() {
          unregisterOrientationHandler();
          mWindow.getDecorView().addOnLayoutChangeListener(mOrientationChangeHandler);
      }
  
      private void unregisterOrientationHandler() {
          mWindow.getDecorView().removeOnLayoutChangeListener(mOrientationChangeHandler);
      }
  
  
      /**
       * A popup window used by the floating toolbar.
       *
       * This class is responsible for the rendering/animation of the floating toolbar.
       * It holds 2 panels (i.e. main panel and overflow panel) and an overflow button
       * to transition between panels.
       */
      private static final class FloatingToolbarPopup {
  
          /* Minimum and maximum number of items allowed in the overflow. */
          private static final int MIN_OVERFLOW_SIZE = 2;
          private static final int MAX_OVERFLOW_SIZE = 4;
  
          private final Context mContext;
          private final View mParent;  // Parent for the popup window.
          private final PopupWindow mPopupWindow;
  
          /* Margins between the popup window and it's content. */
          private final int mMarginHorizontal;
          private final int mMarginVertical;
  
          /* View components */
          private final ViewGroup mContentContainer;  // holds all contents.
          private final ViewGroup mMainPanel;  // holds menu items that are initially displayed.
          private final OverflowPanel mOverflowPanel;  // holds menu items hidden in the overflow.
          private final ImageButton mOverflowButton;  // opens/closes the overflow.
          /* overflow button drawables. */
          private final Drawable mArrow;
          private final Drawable mOverflow;
          private final AnimatedVectorDrawable mToArrow;
          private final AnimatedVectorDrawable mToOverflow;
  
          private final OverflowPanelViewHelper mOverflowPanelViewHelper;
  
          /* Animation interpolators. */
          private final Interpolator mLogAccelerateInterpolator;
          private final Interpolator mFastOutSlowInInterpolator;
          private final Interpolator mLinearOutSlowInInterpolator;
          private final Interpolator mFastOutLinearInInterpolator;
  
          /* Animations. */
          private final AnimatorSet mShowAnimation;
          private final AnimatorSet mDismissAnimation;
          private final AnimatorSet mHideAnimation;
          private final AnimationSet mOpenOverflowAnimation;
          private final AnimationSet mCloseOverflowAnimation;
          private final Animation.AnimationListener mOverflowAnimationListener;
  
          private final Rect mViewPortOnScreen = new Rect();  // portion of screen we can draw in.
          private final Point mCoordsOnWindow = new Point();  // popup window coordinates.
          /* Temporary data holders. Reset values before using. */
          private final int[] mTmpCoords = new int[2];
  
          private final Region mTouchableRegion = new Region();
          private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
                  info -> {
                      info.contentInsets.setEmpty();
                      info.visibleInsets.setEmpty();
                      info.touchableRegion.set(mTouchableRegion);
                      info.setTouchableInsets(
                              ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
                  };
  
          private final int mLineHeight;
          private final int mIconTextSpacing;
  
          /**
           * @see OverflowPanelViewHelper#preparePopupContent().
           */
          private final Runnable mPreparePopupContentRTLHelper = new Runnable() {
              @Override
              public void run() {
                  setPanelsStatesAtRestingPosition();
                  setContentAreaAsTouchableSurface();
                  mContentContainer.setAlpha(1);
              }
          };
  
          private boolean mDismissed = true; // tracks whether this popup is dismissed or dismissing.
          private boolean mHidden; // tracks whether this popup is hidden or hiding.
  
          /* Calculated sizes for panels and overflow button. */
          private final Size mOverflowButtonSize;
          private Size mOverflowPanelSize;  // Should be null when there is no overflow.
          private Size mMainPanelSize;
  
          /* Item click listeners */
          private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
          private final View.OnClickListener mMenuItemButtonOnClickListener =
                  new View.OnClickListener() {
                      @Override
                      public void onClick(View v) {
                          if (v.getTag() instanceof MenuItem) {
                              if (mOnMenuItemClickListener != null) {
                                  mOnMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag());
                              }
                          }
                      }
                  };
  
          private boolean mOpenOverflowUpwards;  // Whether the overflow opens upwards or downwards.
          private boolean mIsOverflowOpen;
  
          private int mTransitionDurationScale;  // Used to scale the toolbar transition duration.
  
          /**
           * Initializes a new floating toolbar popup.
           *
           * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
           *      from.
           */
          public FloatingToolbarPopup(Context context, View parent) {
              mParent = Preconditions.checkNotNull(parent);
              mContext = Preconditions.checkNotNull(context);
              mContentContainer = createContentContainer(context);
              mPopupWindow = createPopupWindow(mContentContainer);
              mMarginHorizontal = parent.getResources()
                      .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
              mMarginVertical = parent.getResources()
                      .getDimensionPixelSize(R.dimen.floating_toolbar_vertical_margin);
              mLineHeight = context.getResources()
                      .getDimensionPixelSize(R.dimen.floating_toolbar_height);
              mIconTextSpacing = context.getResources()
                      .getDimensionPixelSize(R.dimen.floating_toolbar_icon_text_spacing);
  
              // Interpolators
              mLogAccelerateInterpolator = new LogAccelerateInterpolator();
              mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
                      mContext, android.R.interpolator.fast_out_slow_in);
              mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
                      mContext, android.R.interpolator.linear_out_slow_in);
              mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
                      mContext, android.R.interpolator.fast_out_linear_in);
  
              // Drawables. Needed for views.
              mArrow = mContext.getResources()
                      .getDrawable(R.drawable.ft_avd_tooverflow, mContext.getTheme());
              mArrow.setAutoMirrored(true);
              mOverflow = mContext.getResources()
                      .getDrawable(R.drawable.ft_avd_toarrow, mContext.getTheme());
              mOverflow.setAutoMirrored(true);
              mToArrow = (AnimatedVectorDrawable) mContext.getResources()
                      .getDrawable(R.drawable.ft_avd_toarrow_animation, mContext.getTheme());
              mToArrow.setAutoMirrored(true);
              mToOverflow = (AnimatedVectorDrawable) mContext.getResources()
                      .getDrawable(R.drawable.ft_avd_tooverflow_animation, mContext.getTheme());
              mToOverflow.setAutoMirrored(true);
  
              // Views
              mOverflowButton = createOverflowButton();
              mOverflowButtonSize = measure(mOverflowButton);
              mMainPanel = createMainPanel();
              mOverflowPanelViewHelper = new OverflowPanelViewHelper(mContext, mIconTextSpacing);
              mOverflowPanel = createOverflowPanel();
  
              // Animation. Need views.
              mOverflowAnimationListener = createOverflowAnimationListener();
              mOpenOverflowAnimation = new AnimationSet(true);
              mOpenOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
              mCloseOverflowAnimation = new AnimationSet(true);
              mCloseOverflowAnimation.setAnimationListener(mOverflowAnimationListener);
              mShowAnimation = createEnterAnimation(mContentContainer);
              mDismissAnimation = createExitAnimation(
                      mContentContainer,
                      150,  // startDelay
                      new AnimatorListenerAdapter() {
                          @Override
                          public void onAnimationEnd(Animator animation) {
                              mPopupWindow.dismiss();
                              mContentContainer.removeAllViews();
                          }
                      });
              mHideAnimation = createExitAnimation(
                      mContentContainer,
                      0,  // startDelay
                      new AnimatorListenerAdapter() {
                          @Override
                          public void onAnimationEnd(Animator animation) {
                              mPopupWindow.dismiss();
                          }
                      });
          }
  
          /**
           * Makes this toolbar "outside touchable" and sets the onDismissListener.
           * This will take effect the next time the toolbar is re-shown.
           *
           * @param outsideTouchable if true, the popup will be made "outside touchable" and
           *      "non focusable". The reverse will happen if false.
           * @param onDismiss
           *
           * @return true if the "outsideTouchable" setting was modified. Otherwise returns false
           *
           * @see PopupWindow#setOutsideTouchable(boolean)
           * @see PopupWindow#setFocusable(boolean)
           * @see PopupWindow.OnDismissListener
           */
          public boolean setOutsideTouchable(
                  boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
              boolean ret = false;
              if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) {
                  mPopupWindow.setOutsideTouchable(outsideTouchable);
                  mPopupWindow.setFocusable(!outsideTouchable);
                  ret = true;
              }
              mPopupWindow.setOnDismissListener(onDismiss);
              return ret;
          }
  
          /**
           * Lays out buttons for the specified menu items.
           * Requires a subsequent call to {@link #show()} to show the items.
           */
          public void layoutMenuItems(
                  List<MenuItem> menuItems,
                  MenuItem.OnMenuItemClickListener menuItemClickListener,
                  int suggestedWidth) {
              mOnMenuItemClickListener = menuItemClickListener;
              cancelOverflowAnimations();
              clearPanels();
              menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
              if (!menuItems.isEmpty()) {
                  // Add remaining items to the overflow.
                  layoutOverflowPanelItems(menuItems);
              }
              updatePopupSize();
          }
  
          /**
           * Shows this popup at the specified coordinates.
           * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
           */
          public void show(Rect contentRectOnScreen) {
              Preconditions.checkNotNull(contentRectOnScreen);
  
              if (isShowing()) {
                  return;
              }
  
              mHidden = false;
              mDismissed = false;
              cancelDismissAndHideAnimations();
              cancelOverflowAnimations();
  
              refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
              preparePopupContent();
              // We need to specify the position in window coordinates.
              // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
              // specify the popup position in screen coordinates.
              mPopupWindow.showAtLocation(
                      mParent, Gravity.NO_GRAVITY, mCoordsOnWindow.x, mCoordsOnWindow.y);
              setTouchableSurfaceInsetsComputer();
              runShowAnimation();
          }
  
          /**
           * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op.
           */
          public void dismiss() {
              if (mDismissed) {
                  return;
              }
  
              mHidden = false;
              mDismissed = true;
              mHideAnimation.cancel();
  
              runDismissAnimation();
              setZeroTouchableSurface();
          }
  
          /**
           * Hides this popup. This is a no-op if this popup is not showing.
           * Use {@link #isHidden()} to distinguish between a hidden and a dismissed popup.
           */
          public void hide() {
              if (!isShowing()) {
                  return;
              }
  
              mHidden = true;
              runHideAnimation();
              setZeroTouchableSurface();
          }
  
          /**
           * Returns {@code true} if this popup is currently showing. {@code false} otherwise.
           */
          public boolean isShowing() {
              return !mDismissed && !mHidden;
          }
  
          /**
           * Returns {@code true} if this popup is currently hidden. {@code false} otherwise.
           */
          public boolean isHidden() {
              return mHidden;
          }
  
          /**
           * Updates the coordinates of this popup.
           * The specified coordinates may be adjusted to make sure the popup is entirely on-screen.
           * This is a no-op if this popup is not showing.
           */
          public void updateCoordinates(Rect contentRectOnScreen) {
              Preconditions.checkNotNull(contentRectOnScreen);
  
              if (!isShowing() || !mPopupWindow.isShowing()) {
                  return;
              }
  
              cancelOverflowAnimations();
              refreshCoordinatesAndOverflowDirection(contentRectOnScreen);
              preparePopupContent();
              // We need to specify the position in window coordinates.
              // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can
              // specify the popup position in screen coordinates.
              mPopupWindow.update(
                      mCoordsOnWindow.x, mCoordsOnWindow.y,
                      mPopupWindow.getWidth(), mPopupWindow.getHeight());
          }
  
          private void refreshCoordinatesAndOverflowDirection(Rect contentRectOnScreen) {
              refreshViewPort();
  
              // Initialize x ensuring that the toolbar isn't rendered behind the nav bar in
              // landscape.
              final int x = Math.min(
                      contentRectOnScreen.centerX() - mPopupWindow.getWidth() / 2,
                      mViewPortOnScreen.right - mPopupWindow.getWidth());
  
              final int y;
  
              final int availableHeightAboveContent =
                      contentRectOnScreen.top - mViewPortOnScreen.top;
              final int availableHeightBelowContent =
                      mViewPortOnScreen.bottom - contentRectOnScreen.bottom;
  
              final int margin = 2 * mMarginVertical;
              final int toolbarHeightWithVerticalMargin = mLineHeight + margin;
  
              if (!hasOverflow()) {
                  if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin) {
                      // There is enough space at the top of the content.
                      y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
                  } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin) {
                      // There is enough space at the bottom of the content.
                      y = contentRectOnScreen.bottom;
                  } else if (availableHeightBelowContent >= mLineHeight) {
                      // Just enough space to fit the toolbar with no vertical margins.
                      y = contentRectOnScreen.bottom - mMarginVertical;
                  } else {
                      // Not enough space. Prefer to position as high as possible.
                      y = Math.max(
                              mViewPortOnScreen.top,
                              contentRectOnScreen.top - toolbarHeightWithVerticalMargin);
                  }
              } else {
                  // Has an overflow.
                  final int minimumOverflowHeightWithMargin =
                          calculateOverflowHeight(MIN_OVERFLOW_SIZE) + margin;
                  final int availableHeightThroughContentDown = mViewPortOnScreen.bottom -
                          contentRectOnScreen.top + toolbarHeightWithVerticalMargin;
                  final int availableHeightThroughContentUp = contentRectOnScreen.bottom -
                          mViewPortOnScreen.top + toolbarHeightWithVerticalMargin;
  
                  if (availableHeightAboveContent >= minimumOverflowHeightWithMargin) {
                      // There is enough space at the top of the content rect for the overflow.
                      // Position above and open upwards.
                      updateOverflowHeight(availableHeightAboveContent - margin);
                      y = contentRectOnScreen.top - mPopupWindow.getHeight();
                      mOpenOverflowUpwards = true;
                  } else if (availableHeightAboveContent >= toolbarHeightWithVerticalMargin
                          && availableHeightThroughContentDown >= minimumOverflowHeightWithMargin) {
                      // There is enough space at the top of the content rect for the main panel
                      // but not the overflow.
                      // Position above but open downwards.
                      updateOverflowHeight(availableHeightThroughContentDown - margin);
                      y = contentRectOnScreen.top - toolbarHeightWithVerticalMargin;
                      mOpenOverflowUpwards = false;
                  } else if (availableHeightBelowContent >= minimumOverflowHeightWithMargin) {
                      // There is enough space at the bottom of the content rect for the overflow.
                      // Position below and open downwards.
                      updateOverflowHeight(availableHeightBelowContent - margin);
                      y = contentRectOnScreen.bottom;
                      mOpenOverflowUpwards = false;
                  } else if (availableHeightBelowContent >= toolbarHeightWithVerticalMargin
                          && mViewPortOnScreen.height() >= minimumOverflowHeightWithMargin) {
                      // There is enough space at the bottom of the content rect for the main panel
                      // but not the overflow.
                      // Position below but open upwards.
                      updateOverflowHeight(availableHeightThroughContentUp - margin);
                      y = contentRectOnScreen.bottom + toolbarHeightWithVerticalMargin -
                              mPopupWindow.getHeight();
                      mOpenOverflowUpwards = true;
                  } else {
                      // Not enough space.
                      // Position at the top of the view port and open downwards.
                      updateOverflowHeight(mViewPortOnScreen.height() - margin);
                      y = mViewPortOnScreen.top;
                      mOpenOverflowUpwards = false;
                  }
              }
  
              // We later specify the location of PopupWindow relative to the attached window.
              // The idea here is that 1) we can get the location of a View in both window coordinates
              // and screen coordiantes, where the offset between them should be equal to the window
              // origin, and 2) we can use an arbitrary for this calculation while calculating the
              // location of the rootview is supposed to be least expensive.
              // TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can avoid
              // the following calculation.
              mParent.getRootView().getLocationOnScreen(mTmpCoords);
              int rootViewLeftOnScreen = mTmpCoords[0];
              int rootViewTopOnScreen = mTmpCoords[1];
              mParent.getRootView().getLocationInWindow(mTmpCoords);
              int rootViewLeftOnWindow = mTmpCoords[0];
              int rootViewTopOnWindow = mTmpCoords[1];
              int windowLeftOnScreen = rootViewLeftOnScreen - rootViewLeftOnWindow;
              int windowTopOnScreen = rootViewTopOnScreen - rootViewTopOnWindow;
              mCoordsOnWindow.set(
                      Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
          }
  
          /**
           * Performs the "show" animation on the floating popup.
           */
          private void runShowAnimation() {
              mShowAnimation.start();
          }
  
          /**
           * Performs the "dismiss" animation on the floating popup.
           */
          private void runDismissAnimation() {
              mDismissAnimation.start();
          }
  
          /**
           * Performs the "hide" animation on the floating popup.
           */
          private void runHideAnimation() {
              mHideAnimation.start();
          }
  
          private void cancelDismissAndHideAnimations() {
              mDismissAnimation.cancel();
              mHideAnimation.cancel();
          }
  
          private void cancelOverflowAnimations() {
              mContentContainer.clearAnimation();
              mMainPanel.animate().cancel();
              mOverflowPanel.animate().cancel();
              mToArrow.stop();
              mToOverflow.stop();
          }
  
          private void openOverflow() {
              final int targetWidth = mOverflowPanelSize.getWidth();
              final int targetHeight = mOverflowPanelSize.getHeight();
              final int startWidth = mContentContainer.getWidth();
              final int startHeight = mContentContainer.getHeight();
              final float startY = mContentContainer.getY();
              final float left = mContentContainer.getX();
              final float right = left + mContentContainer.getWidth();
              Animation widthAnimation = new Animation() {
                  @Override
                  protected void applyTransformation(float interpolatedTime, Transformation t) {
                      int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
                      setWidth(mContentContainer, startWidth + deltaWidth);
                      if (isInRTLMode()) {
                          mContentContainer.setX(left);
  
                          // Lock the panels in place.
                          mMainPanel.setX(0);
                          mOverflowPanel.setX(0);
                      } else {
                          mContentContainer.setX(right - mContentContainer.getWidth());
  
                          // Offset the panels' positions so they look like they're locked in place
                          // on the screen.
                          mMainPanel.setX(mContentContainer.getWidth() - startWidth);
                          mOverflowPanel.setX(mContentContainer.getWidth() - targetWidth);
                      }
                  }
              };
              Animation heightAnimation = new Animation() {
                  @Override
                  protected void applyTransformation(float interpolatedTime, Transformation t) {
                      int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
                      setHeight(mContentContainer, startHeight + deltaHeight);
                      if (mOpenOverflowUpwards) {
                          mContentContainer.setY(
                                  startY - (mContentContainer.getHeight() - startHeight));
                          positionContentYCoordinatesIfOpeningOverflowUpwards();
                      }
                  }
              };
              final float overflowButtonStartX = mOverflowButton.getX();
              final float overflowButtonTargetX = isInRTLMode() ?
                      overflowButtonStartX + targetWidth - mOverflowButton.getWidth() :
                      overflowButtonStartX - targetWidth + mOverflowButton.getWidth();
              Animation overflowButtonAnimation = new Animation() {
                  @Override
                  protected void applyTransformation(float interpolatedTime, Transformation t) {
                      float overflowButtonX = overflowButtonStartX
                              + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
                      float deltaContainerWidth = isInRTLMode() ?
                              0 :
                              mContentContainer.getWidth() - startWidth;
                      float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
                      mOverflowButton.setX(actualOverflowButtonX);
                  }
              };
              widthAnimation.setInterpolator(mLogAccelerateInterpolator);
              widthAnimation.setDuration(getAdjustedDuration(250));
              heightAnimation.setInterpolator(mFastOutSlowInInterpolator);
              heightAnimation.setDuration(getAdjustedDuration(250));
              overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
              overflowButtonAnimation.setDuration(getAdjustedDuration(250));
              mOpenOverflowAnimation.getAnimations().clear();
              mOpenOverflowAnimation.getAnimations().clear();
              mOpenOverflowAnimation.addAnimation(widthAnimation);
              mOpenOverflowAnimation.addAnimation(heightAnimation);
              mOpenOverflowAnimation.addAnimation(overflowButtonAnimation);
              mContentContainer.startAnimation(mOpenOverflowAnimation);
              mIsOverflowOpen = true;
              mMainPanel.animate()
                      .alpha(0).withLayer()
                      .setInterpolator(mLinearOutSlowInInterpolator)
                      .setDuration(250)
                      .start();
              mOverflowPanel.setAlpha(1); // fadeIn in 0ms.
          }
  
          private void closeOverflow() {
              final int targetWidth = mMainPanelSize.getWidth();
              final int startWidth = mContentContainer.getWidth();
              final float left = mContentContainer.getX();
              final float right = left + mContentContainer.getWidth();
              Animation widthAnimation = new Animation() {
                  @Override
                  protected void applyTransformation(float interpolatedTime, Transformation t) {
                      int deltaWidth = (int) (interpolatedTime * (targetWidth - startWidth));
                      setWidth(mContentContainer, startWidth + deltaWidth);
                      if (isInRTLMode()) {
                          mContentContainer.setX(left);
  
                          // Lock the panels in place.
                          mMainPanel.setX(0);
                          mOverflowPanel.setX(0);
                      } else {
                          mContentContainer.setX(right - mContentContainer.getWidth());
  
                          // Offset the panels' positions so they look like they're locked in place
                          // on the screen.
                          mMainPanel.setX(mContentContainer.getWidth() - targetWidth);
                          mOverflowPanel.setX(mContentContainer.getWidth() - startWidth);
                      }
                  }
              };
              final int targetHeight = mMainPanelSize.getHeight();
              final int startHeight = mContentContainer.getHeight();
              final float bottom = mContentContainer.getY() + mContentContainer.getHeight();
              Animation heightAnimation = new Animation() {
                  @Override
                  protected void applyTransformation(float interpolatedTime, Transformation t) {
                      int deltaHeight = (int) (interpolatedTime * (targetHeight - startHeight));
                      setHeight(mContentContainer, startHeight + deltaHeight);
                      if (mOpenOverflowUpwards) {
                          mContentContainer.setY(bottom - mContentContainer.getHeight());
                          positionContentYCoordinatesIfOpeningOverflowUpwards();
                      }
                  }
              };
              final float overflowButtonStartX = mOverflowButton.getX();
              final float overflowButtonTargetX = isInRTLMode() ?
                      overflowButtonStartX - startWidth + mOverflowButton.getWidth() :
                      overflowButtonStartX + startWidth - mOverflowButton.getWidth();
              Animation overflowButtonAnimation = new Animation() {
                  @Override
                  protected void applyTransformation(float interpolatedTime, Transformation t) {
                      float overflowButtonX = overflowButtonStartX
                              + interpolatedTime * (overflowButtonTargetX - overflowButtonStartX);
                      float deltaContainerWidth = isInRTLMode() ?
                              0 :
                              mContentContainer.getWidth() - startWidth;
                      float actualOverflowButtonX = overflowButtonX + deltaContainerWidth;
                      mOverflowButton.setX(actualOverflowButtonX);
                  }
              };
              widthAnimation.setInterpolator(mFastOutSlowInInterpolator);
              widthAnimation.setDuration(getAdjustedDuration(250));
              heightAnimation.setInterpolator(mLogAccelerateInterpolator);
              heightAnimation.setDuration(getAdjustedDuration(250));
              overflowButtonAnimation.setInterpolator(mFastOutSlowInInterpolator);
              overflowButtonAnimation.setDuration(getAdjustedDuration(250));
              mCloseOverflowAnimation.getAnimations().clear();
              mCloseOverflowAnimation.addAnimation(widthAnimation);
              mCloseOverflowAnimation.addAnimation(heightAnimation);
              mCloseOverflowAnimation.addAnimation(overflowButtonAnimation);
              mContentContainer.startAnimation(mCloseOverflowAnimation);
              mIsOverflowOpen = false;
              mMainPanel.animate()
                      .alpha(1).withLayer()
                      .setInterpolator(mFastOutLinearInInterpolator)
                      .setDuration(100)
                      .start();
              mOverflowPanel.animate()
                      .alpha(0).withLayer()
                      .setInterpolator(mLinearOutSlowInInterpolator)
                      .setDuration(150)
                      .start();
          }
  
          /**
           * Defines the position of the floating toolbar popup panels when transition animation has
           * stopped.
           */
          private void setPanelsStatesAtRestingPosition() {
              mOverflowButton.setEnabled(true);
              mOverflowPanel.awakenScrollBars();
  
              if (mIsOverflowOpen) {
                  // Set open state.
                  final Size containerSize = mOverflowPanelSize;
                  setSize(mContentContainer, containerSize);
                  mMainPanel.setAlpha(0);
                  mMainPanel.setVisibility(View.INVISIBLE);
                  mOverflowPanel.setAlpha(1);
                  mOverflowPanel.setVisibility(View.VISIBLE);
                  mOverflowButton.setImageDrawable(mArrow);
                  mOverflowButton.setContentDescription(mContext.getString(
                          R.string.floating_toolbar_close_overflow_description));
  
                  // Update x-coordinates depending on RTL state.
                  if (isInRTLMode()) {
                      mContentContainer.setX(mMarginHorizontal);  // align left
                      mMainPanel.setX(0);  // align left
                      mOverflowButton.setX(  // align right
                              containerSize.getWidth() - mOverflowButtonSize.getWidth());
                      mOverflowPanel.setX(0);  // align left
                  } else {
                      mContentContainer.setX(  // align right
                              mPopupWindow.getWidth() -
                                      containerSize.getWidth() - mMarginHorizontal);
                      mMainPanel.setX(-mContentContainer.getX());  // align right
                      mOverflowButton.setX(0);  // align left
                      mOverflowPanel.setX(0);  // align left
                  }
  
                  // Update y-coordinates depending on overflow's open direction.
                  if (mOpenOverflowUpwards) {
                      mContentContainer.setY(mMarginVertical);  // align top
                      mMainPanel.setY(  // align bottom
                              containerSize.getHeight() - mContentContainer.getHeight());
                      mOverflowButton.setY(  // align bottom
                              containerSize.getHeight() - mOverflowButtonSize.getHeight());
                      mOverflowPanel.setY(0);  // align top
                  } else {
                      // opens downwards.
                      mContentContainer.setY(mMarginVertical);  // align top
                      mMainPanel.setY(0);  // align top
                      mOverflowButton.setY(0);  // align top
                      mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
                  }
              } else {
                  // Overflow not open. Set closed state.
                  final Size containerSize = mMainPanelSize;
                  setSize(mContentContainer, containerSize);
                  mMainPanel.setAlpha(1);
                  mMainPanel.setVisibility(View.VISIBLE);
                  mOverflowPanel.setAlpha(0);
                  mOverflowPanel.setVisibility(View.INVISIBLE);
                  mOverflowButton.setImageDrawable(mOverflow);
                  mOverflowButton.setContentDescription(mContext.getString(
                          R.string.floating_toolbar_open_overflow_description));
  
                  if (hasOverflow()) {
                      // Update x-coordinates depending on RTL state.
                      if (isInRTLMode()) {
                          mContentContainer.setX(mMarginHorizontal);  // align left
                          mMainPanel.setX(0);  // align left
                          mOverflowButton.setX(0);  // align left
                          mOverflowPanel.setX(0);  // align left
                      } else {
                          mContentContainer.setX(  // align right
                                  mPopupWindow.getWidth() -
                                          containerSize.getWidth() - mMarginHorizontal);
                          mMainPanel.setX(0);  // align left
                          mOverflowButton.setX(  // align right
                                  containerSize.getWidth() - mOverflowButtonSize.getWidth());
                          mOverflowPanel.setX(  // align right
                                  containerSize.getWidth() - mOverflowPanelSize.getWidth());
                      }
  
                      // Update y-coordinates depending on overflow's open direction.
                      if (mOpenOverflowUpwards) {
                          mContentContainer.setY(  // align bottom
                                  mMarginVertical +
                                          mOverflowPanelSize.getHeight() - containerSize.getHeight());
                          mMainPanel.setY(0);  // align top
                          mOverflowButton.setY(0);  // align top
                          mOverflowPanel.setY(  // align bottom
                                  containerSize.getHeight() - mOverflowPanelSize.getHeight());
                      } else {
                          // opens downwards.
                          mContentContainer.setY(mMarginVertical);  // align top
                          mMainPanel.setY(0);  // align top
                          mOverflowButton.setY(0);  // align top
                          mOverflowPanel.setY(mOverflowButtonSize.getHeight());  // align bottom
                      }
                  } else {
                      // No overflow.
                      mContentContainer.setX(mMarginHorizontal);  // align left
                      mContentContainer.setY(mMarginVertical);  // align top
                      mMainPanel.setX(0);  // align left
                      mMainPanel.setY(0);  // align top
                  }
              }
          }
  
          private void updateOverflowHeight(int suggestedHeight) {
              if (hasOverflow()) {
                  final int maxItemSize = (suggestedHeight - mOverflowButtonSize.getHeight()) /
                          mLineHeight;
                  final int newHeight = calculateOverflowHeight(maxItemSize);
                  if (mOverflowPanelSize.getHeight() != newHeight) {
                      mOverflowPanelSize = new Size(mOverflowPanelSize.getWidth(), newHeight);
                  }
                  setSize(mOverflowPanel, mOverflowPanelSize);
                  if (mIsOverflowOpen) {
                      setSize(mContentContainer, mOverflowPanelSize);
                      if (mOpenOverflowUpwards) {
                          final int deltaHeight = mOverflowPanelSize.getHeight() - newHeight;
                          mContentContainer.setY(mContentContainer.getY() + deltaHeight);
                          mOverflowButton.setY(mOverflowButton.getY() - deltaHeight);
                      }
                  } else {
                      setSize(mContentContainer, mMainPanelSize);
                  }
                  updatePopupSize();
              }
          }
  
          private void updatePopupSize() {
              int width = 0;
              int height = 0;
              if (mMainPanelSize != null) {
                  width = Math.max(width, mMainPanelSize.getWidth());
                  height = Math.max(height, mMainPanelSize.getHeight());
              }
              if (mOverflowPanelSize != null) {
                  width = Math.max(width, mOverflowPanelSize.getWidth());
                  height = Math.max(height, mOverflowPanelSize.getHeight());
              }
              mPopupWindow.setWidth(width + mMarginHorizontal * 2);
              mPopupWindow.setHeight(height + mMarginVertical * 2);
              maybeComputeTransitionDurationScale();
          }
  
          private void refreshViewPort() {
              mParent.getWindowVisibleDisplayFrame(mViewPortOnScreen);
          }
  
          private int getAdjustedToolbarWidth(int suggestedWidth) {
              int width = suggestedWidth;
              refreshViewPort();
              int maximumWidth = mViewPortOnScreen.width() - 2 * mParent.getResources()
                      .getDimensionPixelSize(R.dimen.floating_toolbar_horizontal_margin);
              if (width <= 0) {
                  width = mParent.getResources()
                          .getDimensionPixelSize(R.dimen.floating_toolbar_preferred_width);
              }
              return Math.min(width, maximumWidth);
          }
  
          /**
           * Sets the touchable region of this popup to be zero. This means that all touch events on
           * this popup will go through to the surface behind it.
           */
          private void setZeroTouchableSurface() {
              mTouchableRegion.setEmpty();
          }
  
          /**
           * Sets the touchable region of this popup to be the area occupied by its content.
           */
          private void setContentAreaAsTouchableSurface() {
              Preconditions.checkNotNull(mMainPanelSize);
              final int width;
              final int height;
              if (mIsOverflowOpen) {
                  Preconditions.checkNotNull(mOverflowPanelSize);
                  width = mOverflowPanelSize.getWidth();
                  height = mOverflowPanelSize.getHeight();
              } else {
                  width = mMainPanelSize.getWidth();
                  height = mMainPanelSize.getHeight();
              }
              mTouchableRegion.set(
                      (int) mContentContainer.getX(),
                      (int) mContentContainer.getY(),
                      (int) mContentContainer.getX() + width,
                      (int) mContentContainer.getY() + height);
          }
  
          /**
           * Make the touchable area of this popup be the area specified by mTouchableRegion.
           * This should be called after the popup window has been dismissed (dismiss/hide)
           * and is probably being re-shown with a new content root view.
           */
          private void setTouchableSurfaceInsetsComputer() {
              ViewTreeObserver viewTreeObserver = mPopupWindow.getContentView()
                      .getRootView()
                      .getViewTreeObserver();
              viewTreeObserver.removeOnComputeInternalInsetsListener(mInsetsComputer);
              viewTreeObserver.addOnComputeInternalInsetsListener(mInsetsComputer);
          }
  
          private boolean isInRTLMode() {
              return mContext.getApplicationInfo().hasRtlSupport()
                      && mContext.getResources().getConfiguration().getLayoutDirection()
                              == View.LAYOUT_DIRECTION_RTL;
          }
  
          private boolean hasOverflow() {
              return mOverflowPanelSize != null;
          }
  
          /**
           * Fits as many menu items in the main panel and returns a list of the menu items that
           * were not fit in.
           *
           * @return The menu items that are not included in this main panel.
           */
          public List<MenuItem> layoutMainPanelItems(
                  List<MenuItem> menuItems, final int toolbarWidth) {
              Preconditions.checkNotNull(menuItems);
  
              int availableWidth = toolbarWidth;
  
              final LinkedList<MenuItem> remainingMenuItems = new LinkedList<>();
              // add the overflow menu items to the end of the remainingMenuItems list.
              final LinkedList<MenuItem> overflowMenuItems = new LinkedList();
              for (MenuItem menuItem : menuItems) {
                  if (menuItem.getItemId() != android.R.id.textAssist
                          && menuItem.requiresOverflow()) {
                      overflowMenuItems.add(menuItem);
                  } else {
                      remainingMenuItems.add(menuItem);
                  }
              }
              remainingMenuItems.addAll(overflowMenuItems);
  
              mMainPanel.removeAllViews();
              mMainPanel.setPaddingRelative(0, 0, 0, 0);
  
              int lastGroupId = -1;
              boolean isFirstItem = true;
              while (!remainingMenuItems.isEmpty()) {
                  final MenuItem menuItem = remainingMenuItems.peek();
  
                  // if this is the first item, regardless of requiresOverflow(), it should be
                  // displayed on the main panel. Otherwise all items including this one will be
                  // overflow items, and should be displayed in overflow panel.
                  if(!isFirstItem && menuItem.requiresOverflow()) {
                      break;
                  }
  
                  final boolean showIcon = isFirstItem && menuItem.getItemId() == R.id.textAssist;
                  final View menuItemButton = createMenuItemButton(
                          mContext, menuItem, mIconTextSpacing, showIcon);
                  if (!showIcon && menuItemButton instanceof LinearLayout) {
                      ((LinearLayout) menuItemButton).setGravity(Gravity.CENTER);
                  }
  
                  // Adding additional start padding for the first button to even out button spacing.
                  if (isFirstItem) {
                      menuItemButton.setPaddingRelative(
                              (int) (1.5 * menuItemButton.getPaddingStart()),
                              menuItemButton.getPaddingTop(),
                              menuItemButton.getPaddingEnd(),
                              menuItemButton.getPaddingBottom());
                  }
  
                  // Adding additional end padding for the last button to even out button spacing.
                  boolean isLastItem = remainingMenuItems.size() == 1;
                  if (isLastItem) {
                      menuItemButton.setPaddingRelative(
                              menuItemButton.getPaddingStart(),
                              menuItemButton.getPaddingTop(),
                              (int) (1.5 * menuItemButton.getPaddingEnd()),
                              menuItemButton.getPaddingBottom());
                  }
  
                  menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
                  final int menuItemButtonWidth = Math.min(
                          menuItemButton.getMeasuredWidth(), toolbarWidth);
  
                  // Check if we can fit an item while reserving space for the overflowButton.
                  final boolean canFitWithOverflow =
                          menuItemButtonWidth <=
                                  availableWidth - mOverflowButtonSize.getWidth();
                  final boolean canFitNoOverflow =
                          isLastItem && menuItemButtonWidth <= availableWidth;
                  if (canFitWithOverflow || canFitNoOverflow) {
                      setButtonTagAndClickListener(menuItemButton, menuItem);
                      // Set tooltips for main panel items, but not overflow items (b/35726766).
                      menuItemButton.setTooltipText(menuItem.getTooltipText());
                      mMainPanel.addView(menuItemButton);
                      final ViewGroup.LayoutParams params = menuItemButton.getLayoutParams();
                      params.width = menuItemButtonWidth;
                      menuItemButton.setLayoutParams(params);
                      availableWidth -= menuItemButtonWidth;
                      remainingMenuItems.pop();
                  } else {
                      break;
                  }
                  lastGroupId = menuItem.getGroupId();
                  isFirstItem = false;
              }
  
              if (!remainingMenuItems.isEmpty()) {
                  // Reserve space for overflowButton.
                  mMainPanel.setPaddingRelative(0, 0, mOverflowButtonSize.getWidth(), 0);
              }
  
              mMainPanelSize = measure(mMainPanel);
              return remainingMenuItems;
          }
  
          private void layoutOverflowPanelItems(List<MenuItem> menuItems) {
              ArrayAdapter<MenuItem> overflowPanelAdapter =
                      (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
              overflowPanelAdapter.clear();
              final int size = menuItems.size();
              for (int i = 0; i < size; i++) {
                  overflowPanelAdapter.add(menuItems.get(i));
              }
              mOverflowPanel.setAdapter(overflowPanelAdapter);
              if (mOpenOverflowUpwards) {
                  mOverflowPanel.setY(0);
              } else {
                  mOverflowPanel.setY(mOverflowButtonSize.getHeight());
              }
  
              int width = Math.max(getOverflowWidth(), mOverflowButtonSize.getWidth());
              int height = calculateOverflowHeight(MAX_OVERFLOW_SIZE);
              mOverflowPanelSize = new Size(width, height);
              setSize(mOverflowPanel, mOverflowPanelSize);
          }
  
          /**
           * Resets the content container and appropriately position it's panels.
           */
          private void preparePopupContent() {
              mContentContainer.removeAllViews();
  
              // Add views in the specified order so they stack up as expected.
              // Order: overflowPanel, mainPanel, overflowButton.
              if (hasOverflow()) {
                  mContentContainer.addView(mOverflowPanel);
              }
              mContentContainer.addView(mMainPanel);
              if (hasOverflow()) {
                  mContentContainer.addView(mOverflowButton);
              }
              setPanelsStatesAtRestingPosition();
              setContentAreaAsTouchableSurface();
  
              // The positioning of contents in RTL is wrong when the view is first rendered.
              // Hide the view and post a runnable to recalculate positions and render the view.
              // TODO: Investigate why this happens and fix.
              if (isInRTLMode()) {
                  mContentContainer.setAlpha(0);
                  mContentContainer.post(mPreparePopupContentRTLHelper);
              }
          }
  
          /**
           * Clears out the panels and their container. Resets their calculated sizes.
           */
          private void clearPanels() {
              mOverflowPanelSize = null;
              mMainPanelSize = null;
              mIsOverflowOpen = false;
              mMainPanel.removeAllViews();
              ArrayAdapter<MenuItem> overflowPanelAdapter =
                      (ArrayAdapter<MenuItem>) mOverflowPanel.getAdapter();
              overflowPanelAdapter.clear();
              mOverflowPanel.setAdapter(overflowPanelAdapter);
              mContentContainer.removeAllViews();
          }
  
          private void positionContentYCoordinatesIfOpeningOverflowUpwards() {
              if (mOpenOverflowUpwards) {
                  mMainPanel.setY(mContentContainer.getHeight() - mMainPanelSize.getHeight());
                  mOverflowButton.setY(mContentContainer.getHeight() - mOverflowButton.getHeight());
                  mOverflowPanel.setY(mContentContainer.getHeight() - mOverflowPanelSize.getHeight());
              }
          }
  
          private int getOverflowWidth() {
              int overflowWidth = 0;
              final int count = mOverflowPanel.getAdapter().getCount();
              for (int i = 0; i < count; i++) {
                  MenuItem menuItem = (MenuItem) mOverflowPanel.getAdapter().getItem(i);
                  overflowWidth =
                          Math.max(mOverflowPanelViewHelper.calculateWidth(menuItem), overflowWidth);
              }
              return overflowWidth;
          }
  
          private int calculateOverflowHeight(int maxItemSize) {
              // Maximum of 4 items, minimum of 2 if the overflow has to scroll.
              int actualSize = Math.min(
                      MAX_OVERFLOW_SIZE,
                      Math.min(
                              Math.max(MIN_OVERFLOW_SIZE, maxItemSize),
                              mOverflowPanel.getCount()));
              int extension = 0;
              if (actualSize < mOverflowPanel.getCount()) {
                  // The overflow will require scrolling to get to all the items.
                  // Extend the height so that part of the hidden items is displayed.
                  extension = (int) (mLineHeight * 0.5f);
              }
              return actualSize * mLineHeight
                      + mOverflowButtonSize.getHeight()
                      + extension;
          }
  
          private void setButtonTagAndClickListener(View menuItemButton, MenuItem menuItem) {
              menuItemButton.setTag(menuItem);
              menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener);
          }
  
          /**
           * NOTE: Use only in android.view.animation.* animations. Do not use in android.animation.*
           * animations. See comment about this in the code.
           */
          private int getAdjustedDuration(int originalDuration) {
              if (mTransitionDurationScale < 150) {
                  // For smaller transition, decrease the time.
                  return Math.max(originalDuration - 50, 0);
              } else if (mTransitionDurationScale > 300) {
                  // For bigger transition, increase the time.
                  return originalDuration + 50;
              }
  
              // Scale the animation duration with getDurationScale(). This allows
              // android.view.animation.* animations to scale just like android.animation.* animations
              // when  animator duration scale is adjusted in "Developer Options".
              // For this reason, do not use this method for android.animation.* animations.
              return (int) (originalDuration * ValueAnimator.getDurationScale());
          }
  
          private void maybeComputeTransitionDurationScale() {
              if (mMainPanelSize != null && mOverflowPanelSize != null) {
                  int w = mMainPanelSize.getWidth() - mOverflowPanelSize.getWidth();
                  int h = mOverflowPanelSize.getHeight() - mMainPanelSize.getHeight();
                  mTransitionDurationScale = (int) (Math.sqrt(w * w + h * h) /
                          mContentContainer.getContext().getResources().getDisplayMetrics().density);
              }
          }
  
          private ViewGroup createMainPanel() {
              ViewGroup mainPanel = new LinearLayout(mContext) {
                  @Override
                  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                      if (isOverflowAnimating()) {
                          // Update widthMeasureSpec to make sure that this view is not clipped
                          // as we offset it's coordinates with respect to it's parent.
                          widthMeasureSpec = MeasureSpec.makeMeasureSpec(
                                  mMainPanelSize.getWidth(),
                                  MeasureSpec.EXACTLY);
                      }
                      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                  }
  
                  @Override
                  public boolean onInterceptTouchEvent(MotionEvent ev) {
                      // Intercept the touch event while the overflow is animating.
                      return isOverflowAnimating();
                  }
              };
              return mainPanel;
          }
  
          private ImageButton createOverflowButton() {
              final ImageButton overflowButton = (ImageButton) LayoutInflater.from(mContext)
                      .inflate(R.layout.floating_popup_overflow_button, null);
              overflowButton.setImageDrawable(mOverflow);
              overflowButton.setOnClickListener(v -> {
                  if (mIsOverflowOpen) {
                      overflowButton.setImageDrawable(mToOverflow);
                      mToOverflow.start();
                      closeOverflow();
                  } else {
                      overflowButton.setImageDrawable(mToArrow);
                      mToArrow.start();
                      openOverflow();
                  }
              });
              return overflowButton;
          }
  
          private OverflowPanel createOverflowPanel() {
              final OverflowPanel overflowPanel = new OverflowPanel(this);
              overflowPanel.setLayoutParams(new ViewGroup.LayoutParams(
                      ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
              overflowPanel.setDivider(null);
              overflowPanel.setDividerHeight(0);
  
              final ArrayAdapter adapter =
                      new ArrayAdapter<MenuItem>(mContext, 0) {
                          @Override
                          public View getView(int position, View convertView, ViewGroup parent) {
                              return mOverflowPanelViewHelper.getView(
                                      getItem(position), mOverflowPanelSize.getWidth(), convertView);
                          }
                      };
              overflowPanel.setAdapter(adapter);
  
              overflowPanel.setOnItemClickListener((parent, view, position, id) -> {
                  MenuItem menuItem = (MenuItem) overflowPanel.getAdapter().getItem(position);
                  if (mOnMenuItemClickListener != null) {
                      mOnMenuItemClickListener.onMenuItemClick(menuItem);
                  }
              });
  
              return overflowPanel;
          }
  
          private boolean isOverflowAnimating() {
              final boolean overflowOpening = mOpenOverflowAnimation.hasStarted()
                      && !mOpenOverflowAnimation.hasEnded();
              final boolean overflowClosing = mCloseOverflowAnimation.hasStarted()
                      && !mCloseOverflowAnimation.hasEnded();
              return overflowOpening || overflowClosing;
          }
  
          private Animation.AnimationListener createOverflowAnimationListener() {
              Animation.AnimationListener listener = new Animation.AnimationListener() {
                  @Override
                  public void onAnimationStart(Animation animation) {
                      // Disable the overflow button while it's animating.
                      // It will be re-enabled when the animation stops.
                      mOverflowButton.setEnabled(false);
                      // Ensure both panels have visibility turned on when the overflow animation
                      // starts.
                      mMainPanel.setVisibility(View.VISIBLE);
                      mOverflowPanel.setVisibility(View.VISIBLE);
                  }
  
                  @Override
                  public void onAnimationEnd(Animation animation) {
                      // Posting this because it seems like this is called before the animation
                      // actually ends.
                      mContentContainer.post(() -> {
                          setPanelsStatesAtRestingPosition();
                          setContentAreaAsTouchableSurface();
                      });
                  }
  
                  @Override
                  public void onAnimationRepeat(Animation animation) {
                  }
              };
              return listener;
          }
  
          private static Size measure(View view) {
              Preconditions.checkState(view.getParent() == null);
              view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
              return new Size(view.getMeasuredWidth(), view.getMeasuredHeight());
          }
  
          private static void setSize(View view, int width, int height) {
              view.setMinimumWidth(width);
              view.setMinimumHeight(height);
              ViewGroup.LayoutParams params = view.getLayoutParams();
              params = (params == null) ? new ViewGroup.LayoutParams(0, 0) : params;
              params.width = width;
              params.height = height;
              view.setLayoutParams(params);
          }
  
          private static void setSize(View view, Size size) {
              setSize(view, size.getWidth(), size.getHeight());
          }
  
          private static void setWidth(View view, int width) {
              ViewGroup.LayoutParams params = view.getLayoutParams();
              setSize(view, width, params.height);
          }
  
          private static void setHeight(View view, int height) {
              ViewGroup.LayoutParams params = view.getLayoutParams();
              setSize(view, params.width, height);
          }
  
          /**
           * A custom ListView for the overflow panel.
           */
          private static final class OverflowPanel extends ListView {
  
              private final FloatingToolbarPopup mPopup;
  
              OverflowPanel(FloatingToolbarPopup popup) {
                  super(Preconditions.checkNotNull(popup).mContext);
                  this.mPopup = popup;
                  setScrollBarDefaultDelayBeforeFade(ViewConfiguration.getScrollDefaultDelay() * 3);
                  setScrollIndicators(View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
              }
  
              @Override
              protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                  // Update heightMeasureSpec to make sure that this view is not clipped
                  // as we offset it's coordinates with respect to it's parent.
                  int height = mPopup.mOverflowPanelSize.getHeight()
                          - mPopup.mOverflowButtonSize.getHeight();
                  heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
                  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
              }
  
              @Override
              public boolean dispatchTouchEvent(MotionEvent ev) {
                  if (mPopup.isOverflowAnimating()) {
                      // Eat the touch event.
                      return true;
                  }
                  return super.dispatchTouchEvent(ev);
              }
  
              @Override
              protected boolean awakenScrollBars() {
                  return super.awakenScrollBars();
              }
          }
  
          /**
           * A custom interpolator used for various floating toolbar animations.
           */
          private static final class LogAccelerateInterpolator implements Interpolator {
  
              private static final int BASE = 100;
              private static final float LOGS_SCALE = 1f / computeLog(1, BASE);
  
              private static float computeLog(float t, int base) {
                  return (float) (1 - Math.pow(base, -t));
              }
  
              @Override
              public float getInterpolation(float t) {
                  return 1 - computeLog(1 - t, BASE) * LOGS_SCALE;
              }
          }
  
          /**
           * A helper for generating views for the overflow panel.
           */
          private static final class OverflowPanelViewHelper {
  
              private final View mCalculator;
              private final int mIconTextSpacing;
              private final int mSidePadding;
  
              private final Context mContext;
  
              public OverflowPanelViewHelper(Context context, int iconTextSpacing) {
                  mContext = Preconditions.checkNotNull(context);
                  mIconTextSpacing = iconTextSpacing;
                  mSidePadding = context.getResources()
                          .getDimensionPixelSize(R.dimen.floating_toolbar_overflow_side_padding);
                  mCalculator = createMenuButton(null);
              }
  
              public View getView(MenuItem menuItem, int minimumWidth, View convertView) {
                  Preconditions.checkNotNull(menuItem);
                  if (convertView != null) {
                      updateMenuItemButton(
                              convertView, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
                  } else {
                      convertView = createMenuButton(menuItem);
                  }
                  convertView.setMinimumWidth(minimumWidth);
                  return convertView;
              }
  
              public int calculateWidth(MenuItem menuItem) {
                  updateMenuItemButton(
                          mCalculator, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
                  mCalculator.measure(
                          View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
                  return mCalculator.getMeasuredWidth();
              }
  
              private View createMenuButton(MenuItem menuItem) {
                  View button = createMenuItemButton(
                          mContext, menuItem, mIconTextSpacing, shouldShowIcon(menuItem));
                  button.setPadding(mSidePadding, 0, mSidePadding, 0);
                  return button;
              }
  
              private boolean shouldShowIcon(MenuItem menuItem) {
                  if (menuItem != null) {
                      return menuItem.getGroupId() == android.R.id.textAssist;
                  }
                  return false;
              }
          }
      }
  
      /**
       * Creates and returns a menu button for the specified menu item.
       */
      private static View createMenuItemButton(
              Context context, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
          final View menuItemButton = LayoutInflater.from(context)
                  .inflate(R.layout.floating_popup_menu_button, null);
          if (menuItem != null) {
              updateMenuItemButton(menuItemButton, menuItem, iconTextSpacing, showIcon);
          }
          return menuItemButton;
      }
  
      /**
       * Updates the specified menu item button with the specified menu item data.
       */
      private static void updateMenuItemButton(
              View menuItemButton, MenuItem menuItem, int iconTextSpacing, boolean showIcon) {
          final TextView buttonText = menuItemButton.findViewById(
                  R.id.floating_toolbar_menu_item_text);
          buttonText.setEllipsize(null);
          if (TextUtils.isEmpty(menuItem.getTitle())) {
              buttonText.setVisibility(View.GONE);
          } else {
              buttonText.setVisibility(View.VISIBLE);
              buttonText.setText(menuItem.getTitle());
          }
          final ImageView buttonIcon = menuItemButton.findViewById(
                  R.id.floating_toolbar_menu_item_image);
          if (menuItem.getIcon() == null || !showIcon) {
              buttonIcon.setVisibility(View.GONE);
              if (buttonText != null) {
                  buttonText.setPaddingRelative(0, 0, 0, 0);
              }
          } else {
              buttonIcon.setVisibility(View.VISIBLE);
              buttonIcon.setImageDrawable(menuItem.getIcon());
              if (buttonText != null) {
                  buttonText.setPaddingRelative(iconTextSpacing, 0, 0, 0);
              }
          }
          final CharSequence contentDescription = menuItem.getContentDescription();
          if (TextUtils.isEmpty(contentDescription)) {
              menuItemButton.setContentDescription(menuItem.getTitle());
          } else {
              menuItemButton.setContentDescription(contentDescription);
          }
      }
  
      private static ViewGroup createContentContainer(Context context) {
          ViewGroup contentContainer = (ViewGroup) LayoutInflater.from(context)
                  .inflate(R.layout.floating_popup_container, null);
          contentContainer.setLayoutParams(new ViewGroup.LayoutParams(
                  ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
          contentContainer.setTag(FLOATING_TOOLBAR_TAG);
          contentContainer.setClipToOutline(true);
          return contentContainer;
      }
  
      private static PopupWindow createPopupWindow(ViewGroup content) {
          ViewGroup popupContentHolder = new LinearLayout(content.getContext());
          PopupWindow popupWindow = new PopupWindow(popupContentHolder);
          // TODO: Use .setIsLaidOutInScreen(true) instead of .setClippingEnabled(false)
          // unless FLAG_LAYOUT_IN_SCREEN has any unintentional side-effects.
          popupWindow.setClippingEnabled(false);
          popupWindow.setWindowLayoutType(
                  WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
          popupWindow.setAnimationStyle(0);
          popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
          content.setLayoutParams(new ViewGroup.LayoutParams(
                  ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
          popupContentHolder.addView(content);
          return popupWindow;
      }
  
      /**
       * Creates an "appear" animation for the specified view.
       *
       * @param view  The view to animate
       */
      private static AnimatorSet createEnterAnimation(View view) {
          AnimatorSet animation = new AnimatorSet();
          animation.playTogether(
                  ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(150));
          return animation;
      }
  
      /**
       * Creates a "disappear" animation for the specified view.
       *
       * @param view  The view to animate
       * @param startDelay  The start delay of the animation
       * @param listener  The animation listener
       */
      private static AnimatorSet createExitAnimation(
              View view, int startDelay, Animator.AnimatorListener listener) {
          AnimatorSet animation =  new AnimatorSet();
          animation.playTogether(
                  ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(100));
          animation.setStartDelay(startDelay);
          animation.addListener(listener);
          return animation;
      }
  
      /**
       * Returns a re-themed context with controlled look and feel for views.
       */
      private static Context applyDefaultTheme(Context originalContext) {
          TypedArray a = originalContext.obtainStyledAttributes(new int[]{R.attr.isLightTheme});
          boolean isLightTheme = a.getBoolean(0, true);
          int themeId
                  = isLightTheme ? R.style.Theme_DeviceDefault_Light : R.style.Theme_DeviceDefault;
          a.recycle();
          return new ContextThemeWrapper(originalContext, themeId);
      }
  }
  