chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayPanelBase.java

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.compositor.bottombar;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.view.ViewGroup;

import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.MathUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason;
import org.chromium.chrome.browser.ui.theme.ChromeSemanticColorUtils;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;

/** Base abstract class for the Overlay Panel. */
abstract class OverlayPanelBase implements OverlayPanelStateProvider {
    /** The side padding of Bar icons in dps. */
    private static final float BAR_ICON_SIDE_PADDING_DP = 12.f;

    /** The top padding of Bar icons in dps. */
    private static final float BAR_ICON_TOP_PADDING_DP = 10.f;

    /** The height of the Bar's border in dps. */
    private static final float BAR_BORDER_HEIGHT_DP = 1.f;

    /** The height of the expanded Overlay Panel relative to the height of the screen. */
    private static final float EXPANDED_PANEL_HEIGHT_PERCENTAGE = .7f;

    /** The width of the small version of the Overlay Panel in dps. */
    private static final float SMALL_PANEL_WIDTH_DP = 600.f;

    /**
     * The minimum width a screen should have in order to trigger the small version of the Panel.
     */
    private static final float SMALL_PANEL_WIDTH_THRESHOLD_DP = 680.f;

    /** The brightness of the base page when the Panel is peeking. */
    private static final float BASE_PAGE_BRIGHTNESS_STATE_PEEKED = 1.f;

    /** The brightness of the base page when the Panel is expanded. */
    private static final float BASE_PAGE_BRIGHTNESS_STATE_EXPANDED = .7f;

    /**
     * The brightness of the base page when the Panel is maximized. This value matches the alert
     * dialog brightness filter.
     */
    private static final float BASE_PAGE_BRIGHTNESS_STATE_MAXIMIZED = .4f;

    // -------------------------------------------------------------------------
    // TODO(donnd): crbug.com/1143472 - The close button from the legacy UI is
    // no longer used. Remove code related to the button here and in the native
    // interface.
    //
    /** The opacity of the Open-Tab icon when the Panel is peeking. */

    /** The opacity of the Open-Tab icon when the Panel is expanded. */

    /** The opacity of the Open-Tab icon when the Panel is maximized. */

    /** The opacity of the close icon when the Panel is peeking. */
    private static final float CLOSE_ICON_OPACITY_STATE_PEEKED = 0.f;

    /** The opacity of the close icon when the Panel is expanded. */
    private static final float CLOSE_ICON_OPACITY_STATE_EXPANDED = 1.f;

    /** The opacity of the close icon when the Panel is maximized. */
    private static final float CLOSE_ICON_OPACITY_STATE_MAXIMIZED = 1.f;

    /** The id of the close icon drawable. */
    public static final int CLOSE_ICON_DRAWABLE_ID = R.drawable.btn_close;

    // -------------------------------------------------------------------------

    /** The height of the Progress Bar in dps. */
    private static final float PROGRESS_BAR_HEIGHT_DP = 2.f;

    /**
     * The distance from the Progress Bar must be away from the bottom of the
     * screen in order to be completely visible. The closer the Progress Bar
     * gets to the bottom of the screen, the lower its opacity will be. When the
     * Progress Bar is at the very bottom of the screen (when the Overlay Panel
     * is peeking) it will be completely invisible.
     */
    private static final float PROGRESS_BAR_VISIBILITY_THRESHOLD_DP = 10.f;

    /** Ratio of dps per pixel. */
    protected final float mPxToDp;

    /** The height of the Toolbar in dp. */
    private float mToolbarHeightDp;

    /** The background color of the Bar. */
    private final @ColorInt int mBarBackgroundColor;

    /** The tint used for icons (e.g. close icon, etc). */
    private final @ColorInt int mIconColor;

    /** The tint used for drag handlebar. */
    private final @ColorInt int mDragHandlebarColor;

    /** The tint used for the progress bar background. */
    private final @ColorInt int mProgressBarBackgroundColor;

    /** The tint used for the progress bar. */
    private final @ColorInt int mProgressBarColor;

    /**
     * The Y coordinate to apply to the Base Page in order to keep the selection
     * in view when the Overlay Panel is in its EXPANDED state.
     */
    private float mBasePageTargetY;

    /** The current context. */
    protected final Context mContext;

    /** The current state of the Overlay Panel. */
    private @PanelState int mPanelState = PanelState.UNDEFINED;

    /** The padding on each side of the close and open-tab icons. */
    protected final int mButtonPaddingDps;

    /**
     * Indicates whether the Toolbar is allowed to hide vs cannot ever be hidden.
     *
     * @see OverlayPanel#shouldHideAndroidBrowserControls
     */
    protected boolean mCanHideAndroidBrowserControls = true;

    protected ObserverList<OverlayPanelStateProvider.Observer> mObservers = new ObserverList<>();

    // ============================================================================================
    // Constructor
    // ============================================================================================

    /**
     * @param context The current Android {@link Context}.
     * @param toolbarHeightDp The current height of the toolbar in dp.
     */
    public OverlayPanelBase(Context context, float toolbarHeightDp) {
        mContext = context;
        mToolbarHeightDp = toolbarHeightDp;
        mPxToDp = 1.f / mContext.getResources().getDisplayMetrics().density;

        mBarMarginSide = BAR_ICON_SIDE_PADDING_DP;
        mBarMarginTop = BAR_ICON_TOP_PADDING_DP;
        mProgressBarHeight = PROGRESS_BAR_HEIGHT_DP;
        mBarBorderHeight = BAR_BORDER_HEIGHT_DP;

        int bar_height_dimen = R.dimen.overlay_panel_bar_height;
        mBarHeight = mContext.getResources().getDimension(bar_height_dimen) * mPxToDp;

        final Resources resources = mContext.getResources();
        mBarBackgroundColor = ChromeSemanticColorUtils.getOverlayPanelBarBackgroundColor(mContext);
        mIconColor = SemanticColorUtils.getDefaultIconColor(context);
        mDragHandlebarColor = SemanticColorUtils.getDragHandlebarColor(context);
        mProgressBarBackgroundColor = SemanticColorUtils.getDefaultControlColorActive(context);
        mProgressBarColor = SemanticColorUtils.getDefaultControlColorActive(context);
        mButtonPaddingDps =
                (int) (mPxToDp * resources.getDimension(R.dimen.overlay_panel_button_padding));
    }

    // ============================================================================================
    // General API
    // ============================================================================================

    /**
     * @return An Android {@link Context}.
     */
    public Context getContext() {
        return mContext;
    }

    /** Tracks whether the panel has been hidden. {@See #showPanel, #hidePanel}.  */
    protected boolean mPanelHidden;

    /**
     * Animates the Overlay Panel to its closed state.
     * @param reason The reason for the change of panel state.
     * @param animate If the panel should animate closed.
     */
    protected abstract void closePanel(@StateChangeReason int reason, boolean animate);

    /**
     * Event notification that the Panel did get closed.
     * @param reason The reason the panel is closing.
     */
    protected abstract void onClosed(@StateChangeReason int reason);

    /** Temporarily hides a peeking panel for the given reason.  Does nothing if not peeking. */
    public abstract void hidePanel(@StateChangeReason int reason);

    /** Shows a previously hidden panel again.  {@See #hidePanel}. */
    public abstract void showPanel(@StateChangeReason int reason);

    /**
     * Handles when the Panel's container view size changes.
     *
     * @param width The new width of the Panel's container view.
     * @param height The new height of the Panel's container view.
     * @param previousWidth The previous width of the Panel's container view.
     */
    protected abstract void handleSizeChanged(float width, float height, float previousWidth);

    // ============================================================================================
    // OverlayPanelStateProvider
    // ============================================================================================

    @Override
    public void addObserver(Observer observer) {
        mObservers.addObserver(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        mObservers.removeObserver(observer);
    }

    // ============================================================================================
    // Layout Integration
    // ============================================================================================

    private float mLayoutWidth;
    private float mLayoutHeight;
    private float mLayoutYOffset;

    private float mMaximumWidth;
    private float mMaximumHeight;

    private boolean mIsFullWidthSizePanelForTesting;
    private boolean mOverrideIsFullWidthSizePanelForTesting;

    /**
     * Called when the layout has changed.
     *
     * @param width  The new width in dp.
     * @param height The new height in dp.
     * @param visibleViewportOffsetY The Y offset of the content in dp.
     */
    public void onLayoutChanged(float width, float height, float visibleViewportOffsetY) {
        if (width == mLayoutWidth
                && height == mLayoutHeight
                && visibleViewportOffsetY == mLayoutYOffset) {
            return;
        }

        float previousLayoutWidth = mLayoutWidth;

        mLayoutWidth = width;
        mLayoutHeight = height;
        mLayoutYOffset = visibleViewportOffsetY;

        mMaximumWidth = calculateOverlayPanelWidth();
        mMaximumHeight = getPanelHeightFromState(PanelState.MAXIMIZED);

        handleSizeChanged(width, height, previousLayoutWidth);
    }

    /**
     * @return Whether the Panel is in full width size.
     */
    @Override
    public boolean isFullWidthSizePanel() {
        return doesMatchFullWidthCriteria(getFullscreenWidth());
    }

    /**
     * @param containerWidth The width of the panel's container.
     * @return Whether the given width matches the criteria required for a full width Panel.
     */
    protected boolean doesMatchFullWidthCriteria(float containerWidth) {
        if (mOverrideIsFullWidthSizePanelForTesting) return mIsFullWidthSizePanelForTesting;

        return containerWidth <= SMALL_PANEL_WIDTH_THRESHOLD_DP;
    }

    /**
     * @return The current X-position of the Overlay Panel.
     */
    protected float calculateOverlayPanelX() {
        return isFullWidthSizePanel()
                ? 0.f
                : Math.round((getFullscreenWidth() - calculateOverlayPanelWidth()) / 2.f);
    }

    /**
     * @return The current Y-position of the Overlay Panel.
     */
    protected float calculateOverlayPanelY() {
        return getTabHeight() + heightForNeverHideBrowserControls() - mHeight;
    }

    /**
     * @return The current width of the Overlay Panel.
     */
    protected float calculateOverlayPanelWidth() {
        return isFullWidthSizePanel() ? getFullscreenWidth() : SMALL_PANEL_WIDTH_DP;
    }

    /**
     * @return Whether the Panel is showing.
     */
    public boolean isShowing() {
        return mHeight > 0;
    }

    /**
     * @return Supplier of whether the Panel is showing.
     */
    public ObservableSupplier<Boolean> isShowingSupplier() {
        return mIsShowingSupplier;
    }

    /**
     * @return Whether the Overlay Panel is opened. That is, whether the current height is greater
     * than the peeking height.
     */
    public boolean isPanelOpened() {
        return mHeight > getBarContainerHeight();
    }

    /**
     * @return The fullscreen width.
     */
    public float getFullscreenWidth() {
        return mLayoutWidth;
    }

    /**
     * @return The height of the tab the panel is displayed on top of.
     */
    public float getTabHeight() {
        return mLayoutHeight - heightForNeverHideBrowserControls();
    }

    /**
     * @return The maximum width of the Overlay Panel in pixels.
     */
    public int getMaximumWidthPx() {
        return Math.round(mMaximumWidth / mPxToDp);
    }

    /**
     * The Panel Bar Container is a abstract container that groups the Controls
     * that will be visible when the Panel is in the peeked state.
     *
     * @return The Panel Bar Container in dps.
     */
    public float getBarContainerHeight() {
        return getBarHeight();
    }

    /** @return The width of the Overlay Panel Content View in pixels. */
    public int getContentViewWidthPx() {
        return getMaximumWidthPx();
    }

    /** @return The height of the Overlay Panel Content View in pixels. */
    public int getContentViewHeightPx() {
        return Math.round(mMaximumHeight / mPxToDp);
    }

    /** @return The offset for the page content in DPs. */
    protected float getLayoutOffsetYDps() {
        return mLayoutYOffset * mPxToDp;
    }

    // ============================================================================================
    // Controls for a never hidden Toolbar.
    // ============================================================================================

    /**
     * Tells this Panel whether it can ever hide the Browser Controls (Toolbar).
     * This is set to false by a Partial-height Chrome Custom Tab, and defaults to true.
     * @param canHideAndroidBrowserControls whether hiding is ever allowed.
     */
    public void setCanHideAndroidBrowserControls(boolean canHideAndroidBrowserControls) {
        mCanHideAndroidBrowserControls = canHideAndroidBrowserControls;
    }

    /**
     * @return The Tab height adjustment needed for Android Browser controls that can never hide,
     * or 0 if the Toolbar is allowed to hide. When the Toolbar cannot hide, it obscures part of
     * the Base Page so the Overlay cannot use that part of the page height. Value in pixels.
     */
    private float heightForNeverHideBrowserControls() {
        return mCanHideAndroidBrowserControls ? 0.f : mToolbarHeightDp * mPxToDp;
    }

    @VisibleForTesting
    protected boolean getCanHideAndroidBrowserControls() {
        return mCanHideAndroidBrowserControls;
    }

    // --------------------------------------------------------------------------------------------
    // Overlay Panel states
    // --------------------------------------------------------------------------------------------

    private float mOffsetX;
    private float mOffsetY;
    private float mHeight;
    private boolean mIsMaximized;
    private final ObservableSupplierImpl<Boolean> mIsShowingSupplier =
            new ObservableSupplierImpl<>();

    /**
     * @return The horizontal offset of the Overlay Panel in DPs.
     */
    public float getOffsetX() {
        return mOffsetX;
    }

    /**
     * @return The vertical offset of the Overlay Panel in DPs.
     */
    public float getOffsetY() {
        return mOffsetY;
    }

    /**
     * @return The width of the Overlay Panel in dps.
     */
    public float getWidth() {
        return mMaximumWidth;
    }

    /**
     * @return The height of the Overlay Panel in dps.
     */
    public float getHeight() {
        return mHeight;
    }

    /**
     * @return Whether the Overlay Panel is fully maximized.
     */
    public boolean isMaximized() {
        return mIsMaximized;
    }

    // --------------------------------------------------------------------------------------------
    // Panel Bar states
    // --------------------------------------------------------------------------------------------
    private final float mBarMarginSide;
    private final float mBarMarginTop;

    private float mBarHeight;
    private boolean mIsBarBorderVisible;
    private float mBarBorderHeight;

    private float mCloseIconOpacity;
    private float mCloseIconWidth;

    private float mOpenTabIconWidth;

    /**
     * @return The side margin of the Bar.
     */
    public float getBarMarginSide() {
        return mBarMarginSide;
    }

    /**
     * @return The top margin of the Bar.
     */
    public float getBarMarginTop() {
        return mBarMarginTop;
    }

    /**
     * @return The bottom margin of the Bar in pixels.
     */
    public float getBarMarginBottomPx() {
        return 0;
    }

    /**
     * @return The height of the Bar in dp.
     */
    public float getBarHeight() {
        return mBarHeight;
    }

    /**
     * @return Whether the Bar border is visible.
     */
    public boolean isBarBorderVisible() {
        return mIsBarBorderVisible;
    }

    /**
     * @return The height of the Bar border.
     */
    public float getBarBorderHeight() {
        return mBarBorderHeight;
    }

    /**
     * @return The background color of the Bar.
     */
    public int getBarBackgroundColor() {
        return mBarBackgroundColor;
    }

    /**
     * @return The tint used for icons.
     */
    public int getIconColor() {
        return mIconColor;
    }

    /**
     * @return The tint used for drag handlebar.
     */
    public int getDragHandlebarColor() {
        return mDragHandlebarColor;
    }

    /** @return the color to use to draw the separator between the Bar and Content. */
    public int getSeparatorLineColor() {
        return SemanticColorUtils.getOverlayPanelSeparatorLineColor(mContext);
    }

    /**
     * @return The opacity of the close icon.
     */
    public float getCloseIconOpacity() {
        return mCloseIconOpacity;
    }

    /**
     * @return The width/height of the close icon.
     */
    public float getCloseIconDimension() {
        if (mCloseIconWidth == 0) {
            mCloseIconWidth =
                    ApiCompatibilityUtils.getDrawable(
                                            mContext.getResources(), CLOSE_ICON_DRAWABLE_ID)
                                    .getIntrinsicWidth()
                            * mPxToDp;
        }
        return mCloseIconWidth;
    }

    /**
     * @return The left X coordinate of the close icon.
     */
    public float getCloseIconX() {
        if (LocalizationUtils.isLayoutRtl()) {
            return getOffsetX() + getBarMarginSide();
        } else {
            return getOffsetX() + getWidth() - getBarMarginSide() - getCloseIconDimension();
        }
    }

    /**
     * @return The width/height of the open tab icon in DPs.
     */
    public float getOpenTabIconDimension() {
        if (mOpenTabIconWidth == 0) {
            Drawable icon =
                    ApiCompatibilityUtils.getDrawable(
                            mContext.getResources(), R.drawable.open_in_new_tab);
            mOpenTabIconWidth = icon.getIntrinsicWidth() * mPxToDp;
        }
        return mOpenTabIconWidth;
    }

    /**
     * @return The left X coordinate of the open new tab icon in DPs.
     */
    public float getOpenTabIconX() {
        float offset = getCloseIconDimension() + 2 * mButtonPaddingDps;
        if (LocalizationUtils.isLayoutRtl()) {
            return getCloseIconX() + offset;
        } else {
            return getCloseIconX() - offset;
        }
    }

    // --------------------------------------------------------------------------------------------
    // Base Page states
    // --------------------------------------------------------------------------------------------

    private float mBasePageY;
    private float mBasePageBrightness = 1.0f;

    /**
     * @return The vertical offset of the base page.
     */
    public float getBasePageY() {
        return mBasePageY;
    }

    /**
     * @return The brightness of the base page.
     */
    public float getBasePageBrightness() {
        return mBasePageBrightness;
    }

    // --------------------------------------------------------------------------------------------
    // Progress Bar states
    // --------------------------------------------------------------------------------------------

    private float mProgressBarOpacity;
    private boolean mIsProgressBarVisible;
    private float mProgressBarHeight;
    private float mProgressBarCompletion;

    /**
     * @return Whether the Progress Bar is visible.
     */
    public boolean isProgressBarVisible() {
        return mIsProgressBarVisible;
    }

    /**
     * @param isVisible Whether the Progress Bar should be visible.
     */
    protected void setProgressBarVisible(boolean isVisible) {
        mIsProgressBarVisible = isVisible;
    }

    /**
     * @return The Progress Bar height.
     */
    public float getProgressBarHeight() {
        return mProgressBarHeight;
    }

    /**
     * @return The Progress Bar opacity.
     */
    public float getProgressBarOpacity() {
        return mProgressBarOpacity;
    }

    /**
     * @return The completion percentage of the Progress Bar.
     */
    public float getProgressBarCompletion() {
        return mProgressBarCompletion;
    }

    /**
     * @param completion The completion to be set.
     */
    protected void setProgressBarCompletion(float completion) {
        mProgressBarCompletion = completion;
    }

    /** Returns the progress bar background color. */
    public @ColorInt int getProgressBarBackgroundColor() {
        return mProgressBarBackgroundColor;
    }

    /** Returns the progress bar color. */
    public @ColorInt int getProgressBarColor() {
        return mProgressBarColor;
    }

    // ============================================================================================
    // State Handler
    // ============================================================================================

    /**
     * @return The panel's state.
     */
    public @PanelState int getPanelState() {
        return mPanelState;
    }

    /**
     * Sets the panel's state.
     * @param state The panel state to transition to.
     * @param reason The reason for a change in the panel's state.
     */
    protected void setPanelState(@PanelState int state, @StateChangeReason int reason) {
        if (mPanelHidden) return;

        if (state == PanelState.CLOSED) {
            mHeight = 0;
            mIsShowingSupplier.set(isShowing());
            onClosed(reason);
        }

        // We should only set the state at the end of this method, in oder to make sure that
        // all callbacks will be fired before changing the state of the Panel. This prevents
        // some flakiness on tests since they rely on changes of state to determine when a
        // particular action has been completed.
        mPanelState = state;
        for (Observer observer : mObservers) {
            observer.onOverlayPanelStateChanged(state, mBarBackgroundColor);
        }
    }

    /**
     * Determines if a given {@code PanelState} is a valid UI state. The UNDEFINED state should
     * never be considered a valid UI state.
     *
     * @param state The given state.
     * @return Whether the state is valid.
     */
    private boolean isValidUiState(@PanelState int state) {
        // TODO(pedrosimonetti): consider removing the UNDEFINED state
        // which would allow removing this method.
        return state != PanelState.UNDEFINED;
    }

    /**
     * Gets the panel's state that is before the given {@code PanelState} in the order of states.
     * @param state The given state.
     * @return The previous state.
     */
    private @PanelState int getPreviousPanelState(@PanelState int state) {
        @Nullable
        @PanelState
        Integer prevState =
                state >= PanelState.PEEKED && state <= PanelState.MAXIMIZED ? state - 1 : null;

        return prevState != null ? prevState : PanelState.UNDEFINED;
    }

    // ============================================================================================
    // Helpers
    // ============================================================================================

    /**
     * Gets the height of the Overlay Panel in dps for a given |state|.
     *
     * @param state The state whose height will be calculated.
     * @return The height of the Overlay Panel in dps for a given |state|.
     */
    public float getPanelHeightFromState(@Nullable @PanelState Integer state) {
        if (state == null) {
            return 0;
        } else if (state == PanelState.PEEKED) {
            return getPeekedHeight();
        } else if (state == PanelState.EXPANDED) {
            return getExpandedHeight();
        } else if (state == PanelState.MAXIMIZED) {
            return getMaximizedHeight();
        }
        return 0;
    }

    /**
     * @return The peeked height of the panel in dps.
     */
    protected float getPeekedHeight() {
        return getBarHeight();
    }

    /**
     * @return The expanded height of the panel in dps.
     */
    protected float getExpandedHeight() {
        if (isFullWidthSizePanel()) {
            return getTabHeight() * EXPANDED_PANEL_HEIGHT_PERCENTAGE;
        } else {
            return (getTabHeight() - mToolbarHeightDp * mPxToDp) * EXPANDED_PANEL_HEIGHT_PERCENTAGE;
        }
    }

    /**
     * @return The maximized height of the panel in dps.
     */
    protected float getMaximizedHeight() {
        return getTabHeight();
    }

    /**
     * @return The fraction of the distance the panel has to be to its next state before animating
     *         itself there. Default is the panel must be half of the way to the next state.
     */
    protected float getThresholdToNextState() {
        return 0.5f;
    }

    /**
     * Finds the state which has the nearest height compared to a given
     * |desiredPanelHeight|.
     *
     * @param desiredPanelHeight The height to compare to.
     * @param velocity The velocity of the swipe if applicable. The swipe is upward if less than 0.
     * @return The nearest panel state.
     */
    protected @PanelState int findNearestPanelStateFromHeight(
            float desiredPanelHeight, float velocity) {
        // If the panel was flung hard enough to make the desired height negative, it's closed.
        if (desiredPanelHeight < 0) return PanelState.CLOSED;

        // First, find the two states that the desired panel height is between.
        @PanelState int nextState = PanelState.UNDEFINED;
        @PanelState int prevState = nextState;
        for (@PanelState int state = PanelState.UNDEFINED;
                state < PanelState.NUM_ENTRIES;
                state++) {
            if (!isValidUiState(state)) continue;
            prevState = nextState;
            nextState = state;
            // The values in PanelState are ascending, they should be kept that way in order for
            // this to work.
            if (desiredPanelHeight >= getPanelHeightFromState(prevState)
                    && desiredPanelHeight < getPanelHeightFromState(nextState)) {
                break;
            }
        }

        // If the desired height is close enough to a certain state, depending on the direction of
        // the velocity, move to that state.
        float lowerBound = getPanelHeightFromState(prevState);
        float distance = getPanelHeightFromState(nextState) - lowerBound;
        float thresholdToNextState =
                velocity < 0.0f ? getThresholdToNextState() : 1.0f - getThresholdToNextState();
        if ((desiredPanelHeight - lowerBound) / distance > thresholdToNextState) {
            return nextState;
        } else {
            return prevState;
        }
    }

    /**
     * Sets the last panel height within the limits allowable by our UI.
     *
     * @param height The height of the panel in dps.
     */
    protected void setClampedPanelHeight(float height) {
        final float clampedHeight =
                MathUtils.clamp(
                        height,
                        getPanelHeightFromState(PanelState.MAXIMIZED),
                        getPanelHeightFromState(PanelState.PEEKED));
        setPanelHeight(clampedHeight);
    }

    /**
     * Sets the panel height.
     *
     * @param height The height of the panel in dps.
     */
    protected void setPanelHeight(float height) {
        updatePanelForHeight(height);
    }

    /**
     * @param state The Panel state.
     * @return Whether the Panel height matches the one from the given state.
     */
    protected boolean doesPanelHeightMatchState(@PanelState int state) {
        return state == getPanelState()
                && MathUtils.areFloatsEqual(getHeight(), getPanelHeightFromState(state));
    }

    // ============================================================================================
    // UI Update Handling
    // ============================================================================================

    /**
     * Updates the UI state for a given |height|.
     *
     * @param height The Overlay Panel height.
     */
    private void updatePanelForHeight(float height) {
        @PanelState int endState = findLargestPanelStateFromHeight(height);
        @PanelState int startState = getPreviousPanelState(endState);
        float percentage = getStateCompletion(height, startState, endState);

        updatePanelSize(height);

        if (endState == PanelState.CLOSED || endState == PanelState.PEEKED) {
            updatePanelForCloseOrPeek(percentage);
        } else if (endState == PanelState.EXPANDED) {
            updatePanelForExpansion(percentage);
        } else if (endState == PanelState.MAXIMIZED) {
            updatePanelForMaximization(percentage);
        }

        updateStatusBar();
    }

    /**
     * Updates the Panel size information.
     *
     * @param height The Overlay Panel height.
     */
    private void updatePanelSize(float height) {
        mHeight = height;
        mOffsetX = calculateOverlayPanelX();
        mOffsetY = calculateOverlayPanelY();
        mIsMaximized = height == getPanelHeightFromState(PanelState.MAXIMIZED);
        mIsShowingSupplier.set(isShowing());
    }

    /**
     * Finds the largest Panel state which is being transitioned to/from.
     * Whenever the Panel is in between states, let's say, when resizing the
     * Panel from its peeked to expanded state, we need to know those two states
     * in order to calculate how closely we are from one of them. This method
     * will always return the nearest state with the largest height, and
     * together with the state preceding it, it's possible to calculate how far
     * the Panel is from them.
     *
     * @param panelHeight The height to compare to.
     * @return The panel state which is being transitioned to/from.
     */
    private @PanelState int findLargestPanelStateFromHeight(float panelHeight) {
        @PanelState int stateFound = PanelState.CLOSED;

        // Iterate over all states and find the largest one which is being
        // transitioned to/from.
        for (@PanelState int state = PanelState.UNDEFINED;
                state < PanelState.NUM_ENTRIES;
                state++) {
            if (!isValidUiState(state)) continue;
            if (panelHeight <= getPanelHeightFromState(state)) {
                stateFound = state;
                break;
            }
        }

        return stateFound;
    }

    /**
     * Gets the state completion percentage, taking into consideration the |height| of the Overlay
     * Panel, and the initial and final states. A completion of 0 means the Panel is in the initial
     * state and a completion of 1 means the Panel is in the final state.
     *
     * @param height The height of the Overlay Panel.
     * @param startState The initial state of the Panel.
     * @param endState The final state of the Panel.
     * @return The completion percentage.
     */
    private float getStateCompletion(
            float height, @PanelState int startState, @PanelState int endState) {
        float startSize = getPanelHeightFromState(startState);
        float endSize = getPanelHeightFromState(endState);
        // NOTE(pedrosimonetti): Handle special case from PanelState.UNDEFINED
        // to PanelState.CLOSED, where both have a height of zero. Returning
        // zero here means the Panel will be reset to its CLOSED state.
        float completionPercent =
                startSize == 0.f && endSize == 0.f
                        ? 0.f
                        : (height - startSize) / (endSize - startSize);

        return completionPercent;
    }

    /**
     * Updates the UI state for the closed to peeked transition (and vice
     * versa), according to a completion |percentage|.
     *
     * Note that this method may be called when the panel is going from expanded to peeked because
     * the end panel state for the transitions is calculated based on the panel height. When the
     * panel reaches the peeking height, the calculated end state is peeked.
     *
     * @param percentage The completion percentage.
     */
    protected void updatePanelForCloseOrPeek(float percentage) {
        // Base page offset.
        mBasePageY = 0.f;

        // Base page brightness.
        mBasePageBrightness = BASE_PAGE_BRIGHTNESS_STATE_PEEKED;

        // Bar border.
        mIsBarBorderVisible = false;

        // Close icon opacity.
        mCloseIconOpacity = CLOSE_ICON_OPACITY_STATE_PEEKED;

        // Progress Bar.
        mProgressBarOpacity = 0.f;
    }

    /**
     * Updates the UI state for the peeked to expanded transition (and vice
     * versa), according to a completion |percentage|.
     *
     * Note that this method will never be called with percentage = 0.f. Once the panel
     * reaches the peeked state #updatePanelForCloseOrPeek() will be called instead of this method
     * because the end panel state for transitions is calculated based on the panel height.
     *
     * @param percentage The completion percentage.
     */
    protected void updatePanelForExpansion(float percentage) {
        // Base page offset.
        mBasePageY = MathUtils.interpolate(0.f, getBasePageTargetY(), percentage);

        // Base page brightness.
        mBasePageBrightness =
                MathUtils.interpolate(
                        BASE_PAGE_BRIGHTNESS_STATE_PEEKED,
                        BASE_PAGE_BRIGHTNESS_STATE_EXPANDED,
                        percentage);

        // Bar border.
        mIsBarBorderVisible = true;

        // Determine fading element opacities. The arrow icon needs to finish fading out before
        // the close icon starts fading in. Any other elements fading in or fading out should use
        // the same percentage.
        float fadingInPercentage = Math.max(percentage - .5f, 0.f) / .5f;

        // Close Icon.
        mCloseIconOpacity =
                MathUtils.interpolate(
                        CLOSE_ICON_OPACITY_STATE_PEEKED,
                        CLOSE_ICON_OPACITY_STATE_EXPANDED,
                        fadingInPercentage);

        // Progress Bar.
        float peekedHeight = getPanelHeightFromState(PanelState.PEEKED);
        float threshold = PROGRESS_BAR_VISIBILITY_THRESHOLD_DP / mPxToDp;
        float diff = Math.min(mHeight - peekedHeight, threshold);
        // Fades the Progress Bar the closer it gets to the bottom of the
        // screen.
        mProgressBarOpacity = MathUtils.interpolate(0.f, 1.f, diff / threshold);
    }

    /**
     * Updates the UI state for the expanded to maximized transition (and vice
     * versa), according to a completion |percentage|.
     *
     * @param percentage The completion percentage.
     */
    protected void updatePanelForMaximization(float percentage) {
        // Base page offset.
        mBasePageY = getBasePageTargetY();

        // Base page brightness.
        mBasePageBrightness =
                MathUtils.interpolate(
                        BASE_PAGE_BRIGHTNESS_STATE_EXPANDED,
                        BASE_PAGE_BRIGHTNESS_STATE_MAXIMIZED,
                        percentage);

        // Bar border.
        mIsBarBorderVisible = true;

        // Close Icon.
        mCloseIconOpacity = CLOSE_ICON_OPACITY_STATE_MAXIMIZED;

        // Progress Bar.
        mProgressBarOpacity = 1.f;
    }

    /** Updates the Status Bar. */
    protected void updateStatusBar() {}

    /** @return the maximum brightness of the base page. */
    protected float getMaxBasePageBrightness() {
        return BASE_PAGE_BRIGHTNESS_STATE_PEEKED;
    }

    /** @return the minimum brightness of the base page. */
    protected float getMinBasePageBrightness() {
        return BASE_PAGE_BRIGHTNESS_STATE_MAXIMIZED;
    }

    // ============================================================================================
    // Base Page Offset
    // ============================================================================================

    /**
     * Calculates the desired offset for the Base Page. The purpose of this method is to allow
     * subclasses to provide an specific offset, which can be useful for keeping a certain
     * portion of the Base Page visible when a Panel is in expanded state. To facilitate the
     * calculation, the first argument contains the height of the Panel in the expanded state.
     *
     * @return The desired offset for the Base Page in DPs
     */
    protected float calculateBasePageDesiredOffset() {
        return 0.f;
    }

    /**
     * Updates the target offset of the Base Page in order to keep the selection in view
     * after expanding the Panel.
     */
    protected void updateBasePageTargetY() {
        mBasePageTargetY = calculateBasePageTargetY();
    }

    /**
     * Calculates the target offset of the Base Page in order to achieve the desired offset
     * specified by {@link #calculateBasePageDesiredOffset} while assuring that the Base
     * Page will always fill the gap between the Panel and the top of the screen, because
     * there's nothing to see below the Base Page layer. This method will take into
     * consideration the Toolbar height, and adjust the offset accordingly, in order to
     * move the Toolbar out of the view as the Panel expands.
     *
     * @return The target offset Y in DPs.
     */
    private float calculateBasePageTargetY() {
        // Only a fullscreen wide Panel should offset the base page. A small panel should
        // always return zero to ensure the Base Page remains in the same position.
        if (!isFullWidthSizePanel()) return 0.f;

        // Start with the desired offset taking viewport offset into consideration and make sure
        // the result is <= 0 so the page moves up and not down.
        float offset = Math.min(calculateBasePageDesiredOffset() - getLayoutOffsetYDps(), 0.0f);

        // Make sure the offset is not greater than the expanded height, because
        // there's nothing to render below the Page.
        offset = Math.max(offset, -getExpandedHeight());

        return offset;
    }

    /**
     * @return The Y coordinate to apply to the Base Page in order to keep the selection
     *         in view when the Overlay Panel is in EXPANDED state.
     */
    private float getBasePageTargetY() {
        return mBasePageTargetY;
    }

    // ============================================================================================
    // Resource Loader
    // ============================================================================================

    protected ViewGroup mContainerView;
    protected DynamicResourceLoader mResourceLoader;

    /**
     * @param resourceLoader The {@link DynamicResourceLoader} to register and unregister the view.
     */
    public void setDynamicResourceLoader(DynamicResourceLoader resourceLoader) {
        mResourceLoader = resourceLoader;
    }

    /**
     * Sets the container ViewGroup to which the auxiliary Views will be attached to.
     *
     * @param container The {@link ViewGroup} container.
     */
    public void setContainerView(ViewGroup container) {
        mContainerView = container;
    }

    // ============================================================================================
    // Test Infrastructure
    // ============================================================================================

    /**
     * @param height The height of the Overlay Panel to be set.
     */
    public void setHeightForTesting(float height) {
        mHeight = height;
        mIsShowingSupplier.set(isShowing());
    }

    /**
     * @param offsetY The vertical offset of the Overlay Panel to be
     *            set.
     */
    public void setOffsetYForTesting(float offsetY) {
        mOffsetY = offsetY;
    }

    /**
     * @param isMaximized The setting for whether the Overlay Panel is fully
     *            maximized.
     */
    public void setMaximizedForTesting(boolean isMaximized) {
        mIsMaximized = isMaximized;
    }

    /**
     * @param barHeight The height of the Overlay Bar to be set.
     */
    public void setSearchBarHeightForTesting(float barHeight) {
        mBarHeight = barHeight;
    }

    /**
     * Overrides the FullWidthSizePanel state for testing.
     *
     * @param isFullWidthSizePanel Whether the Panel has a full width size.
     */
    public void setIsFullWidthSizePanelForTesting(boolean isFullWidthSizePanel) {
        mOverrideIsFullWidthSizePanelForTesting = true;
        mIsFullWidthSizePanelForTesting = isFullWidthSizePanel;
    }
}