/*
 * Copyright (C) 2021 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.floatingtoolbar;

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.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
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.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * A popup window used by the floating toolbar to render menu items in the local app process.
 *
 * 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.
 */
public final class LocalFloatingToolbarPopup implements 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 its 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.
    // holds menu items hidden in the overflow.
    private final OverflowPanel mOverflowPanel;
    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;

    /* Menu items and click listeners */
    private final Map<MenuItemRepr, MenuItem> mMenuItems = new LinkedHashMap<>();
    private MenuItem.OnMenuItemClickListener mOnMenuItemClickListener;
    private final View.OnClickListener mMenuItemButtonOnClickListener =
            new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnMenuItemClickListener == null) {
                        return;
                    }
                    final Object tag = v.getTag();
                    if (!(tag instanceof MenuItemRepr)) {
                        return;
                    }
                    final MenuItem menuItem = mMenuItems.get((MenuItemRepr) tag);
                    if (menuItem == null) {
                        return;
                    }
                    mOnMenuItemClickListener.onMenuItemClick(menuItem);
                }
            };

    private boolean mOpenOverflowUpwards;  // Whether the overflow opens upwards or downwards.
    private boolean mIsOverflowOpen;

    private int mTransitionDurationScale;  // Used to scale the toolbar transition duration.

    private final Rect mPreviousContentRect = new Rect();
    private int mSuggestedWidth;
    private boolean mWidthChanged = true;

    /**
     * Initializes a new floating toolbar popup.
     *
     * @param parent  A parent view to get the {@link android.view.View#getWindowToken()} token
     *      from.
     */
    public LocalFloatingToolbarPopup(Context context, View parent) {
        mParent = Objects.requireNonNull(parent);
        mContext = applyDefaultTheme(context);
        mContentContainer = createContentContainer(mContext);
        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();
                    }
                });
    }

    @Override
    public boolean setOutsideTouchable(
            boolean outsideTouchable, @Nullable PopupWindow.OnDismissListener onDismiss) {
        boolean ret = false;
        if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) {
            mPopupWindow.setOutsideTouchable(outsideTouchable);
            mPopupWindow.setFocusable(!outsideTouchable);
            mPopupWindow.update();
            ret = true;
        }
        mPopupWindow.setOnDismissListener(onDismiss);
        return ret;
    }

    /**
     * Lays out buttons for the specified menu items.
     * Requires a subsequent call to {@link FloatingToolbar#show()} to show the items.
     */
    private void layoutMenuItems(
            List<MenuItem> menuItems,
            MenuItem.OnMenuItemClickListener menuItemClickListener,
            int suggestedWidth) {
        cancelOverflowAnimations();
        clearPanels();
        updateMenuItems(menuItems, menuItemClickListener);
        menuItems = layoutMainPanelItems(menuItems, getAdjustedToolbarWidth(suggestedWidth));
        if (!menuItems.isEmpty()) {
            // Add remaining items to the overflow.
            layoutOverflowPanelItems(menuItems);
        }
        updatePopupSize();
    }

    /**
     * Updates the popup's menu items without rebuilding the widget.
     * Use in place of layoutMenuItems() when the popup's views need not be reconstructed.
     *
     * @see #isLayoutRequired(List<MenuItem>)
     */
    private void updateMenuItems(
            List<MenuItem> menuItems, MenuItem.OnMenuItemClickListener menuItemClickListener) {
        mMenuItems.clear();
        for (MenuItem menuItem : menuItems) {
            mMenuItems.put(MenuItemRepr.of(menuItem), menuItem);
        }
        mOnMenuItemClickListener = menuItemClickListener;
    }

    /**
     * Returns true if this popup needs a relayout to properly render the specified menu items.
     */
    private boolean isLayoutRequired(List<MenuItem> menuItems) {
        return !MenuItemRepr.reprEquals(menuItems, mMenuItems.values());
    }

    @Override
    public void setWidthChanged(boolean widthChanged) {
        mWidthChanged = widthChanged;
    }

    @Override
    public void 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;
    }

    @Override
    public void show(List<MenuItem> menuItems,
            MenuItem.OnMenuItemClickListener menuItemClickListener, Rect contentRect) {
        if (isLayoutRequired(menuItems) || mWidthChanged) {
            dismiss();
            layoutMenuItems(menuItems, menuItemClickListener, mSuggestedWidth);
        } else {
            updateMenuItems(menuItems, menuItemClickListener);
        }
        if (!isShowing()) {
            show(contentRect);
        } else if (!mPreviousContentRect.equals(contentRect)) {
            updateCoordinates(contentRect);
        }
        mWidthChanged = false;
        mPreviousContentRect.set(contentRect);
    }

    private void show(Rect contentRectOnScreen) {
        Objects.requireNonNull(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();
    }

    @Override
    public void dismiss() {
        if (mDismissed) {
            return;
        }

        mHidden = false;
        mDismissed = true;
        mHideAnimation.cancel();

        runDismissAnimation();
        setZeroTouchableSurface();
    }

    @Override
    public void hide() {
        if (!isShowing()) {
            return;
        }

        mHidden = true;
        runHideAnimation();
        setZeroTouchableSurface();
    }

    @Override
    public boolean isShowing() {
        return !mDismissed && !mHidden;
    }

    @Override
    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.
     */
    private void updateCoordinates(Rect contentRectOnScreen) {
        Objects.requireNonNull(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 coordinates, 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() {
        Objects.requireNonNull(mMainPanelSize);
        final int width;
        final int height;
        if (mIsOverflowOpen) {
            Objects.requireNonNull(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) {
        Objects.requireNonNull(menuItems);

        int availableWidth = toolbarWidth;

        final ArrayList<MenuItem> remainingMenuItems = new ArrayList<>();
        // add the overflow menu items to the end of the remainingMenuItems list.
        final ArrayList<MenuItem> overflowMenuItems = new ArrayList<>();
        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.get(0);

            // 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.remove(0);
            } 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(MenuItemRepr.of(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 its coordinates with respect to its 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 LocalFloatingToolbarPopup mPopup;

        OverflowPanel(LocalFloatingToolbarPopup popup) {
            super(Objects.requireNonNull(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 its 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;

        OverflowPanelViewHelper(Context context, int iconTextSpacing) {
            mContext = Objects.requireNonNull(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) {
            Objects.requireNonNull(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(FloatingToolbar.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);
    }

    /**
     * Represents the identity of a MenuItem that is rendered in a FloatingToolbarPopup.
     */
    @VisibleForTesting
    public static final class MenuItemRepr {

        public final int itemId;
        public final int groupId;
        @Nullable public final String title;
        @Nullable private final Drawable mIcon;

        private MenuItemRepr(
                int itemId, int groupId, @Nullable CharSequence title, @Nullable Drawable icon) {
            this.itemId = itemId;
            this.groupId = groupId;
            this.title = (title == null) ? null : title.toString();
            mIcon = icon;
        }

        /**
         * Creates an instance of MenuItemRepr for the specified menu item.
         */
        public static MenuItemRepr of(MenuItem menuItem) {
            return new MenuItemRepr(
                    menuItem.getItemId(),
                    menuItem.getGroupId(),
                    menuItem.getTitle(),
                    menuItem.getIcon());
        }

        /**
         * Returns this object's hashcode.
         */
        @Override
        public int hashCode() {
            return Objects.hash(itemId, groupId, title, mIcon);
        }

        /**
         * Returns true if this object is the same as the specified object.
         */
        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            if (!(o instanceof MenuItemRepr)) {
                return false;
            }
            final MenuItemRepr other = (MenuItemRepr) o;
            return itemId == other.itemId
                    && groupId == other.groupId
                    && TextUtils.equals(title, other.title)
                    // Many Drawables (icons) do not implement equals(). Using equals() here instead
                    // of reference comparisons in case a Drawable subclass implements equals().
                    && Objects.equals(mIcon, other.mIcon);
        }

        /**
         * Returns true if the two menu item collections are the same based on MenuItemRepr.
         */
        public static boolean reprEquals(
                Collection<MenuItem> menuItems1, Collection<MenuItem> menuItems2) {
            if (menuItems1.size() != menuItems2.size()) {
                return false;
            }

            final Iterator<MenuItem> menuItems2Iter = menuItems2.iterator();
            for (MenuItem menuItem1 : menuItems1) {
                final MenuItem menuItem2 = menuItems2Iter.next();
                if (!MenuItemRepr.of(menuItem1).equals(MenuItemRepr.of(menuItem2))) {
                    return false;
                }
            }

            return true;
        }
    }
}
