// Copyright 2014 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.fullscreen;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.ObserverList;
import org.chromium.base.TraceEvent;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.cc.input.BrowserControlsOffsetTagsInfo;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabTabObserver;
import org.chromium.chrome.browser.ActivityUtils;
import org.chromium.chrome.browser.browser_controls.BrowserControlsSizer;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.browser_controls.BrowserControlsUtils;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.tab.SadTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabBrowserControlsConstraintsHelper;
import org.chromium.chrome.browser.tab.TabBrowserControlsOffsetHelper;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.chrome.browser.toolbar.ControlContainer;
import org.chromium.chrome.browser.toolbar.ToolbarFeatures;
import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.util.TokenHolder;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** A class that manages browser control visibility and positioning. */
public class BrowserControlsManager implements ActivityStateListener, BrowserControlsSizer {
// The amount of time to delay the control show request after returning to a once visible
// activity. This delay is meant to allow Android to run its Activity focusing animation and
// have the controls scroll back in smoothly once that has finished.
private static final long ACTIVITY_RETURN_SHOW_REQUEST_DELAY_MS = 100;
/**
* Maximum duration for the control container slide-in animation and the duration for the
* browser controls height change animation. Note that this value matches the one in
* browser_controls_offset_manager.cc.
*/
private static final int CONTROLS_ANIMATION_DURATION_MS = 200;
private final Activity mActivity;
private final BrowserStateBrowserControlsVisibilityDelegate mBrowserVisibilityDelegate;
@ControlsPosition private final int mControlsPosition;
private final TokenHolder mHidingTokenHolder = new TokenHolder(this::scheduleVisibilityUpdate);
/**
* An observable for browser controls being at its minimum height or not.
* This is as good as the controls being hidden when both min heights are 0.
*/
private final ObservableSupplierImpl<Boolean> mControlsAtMinHeight =
new ObservableSupplierImpl<>();
private TabModelSelectorTabObserver mTabControlsObserver;
@Nullable private ControlContainer mControlContainer;
private int mTopControlContainerHeight;
private int mTopControlsMinHeight;
private int mBottomControlContainerHeight;
private int mBottomControlsMinHeight;
private boolean mAnimateBrowserControlsHeightChanges;
private int mRendererTopControlOffset;
private int mRendererBottomControlOffset;
private int mRendererTopContentOffset;
private int mRendererTopControlsMinHeightOffset;
private int mRendererBottomControlsMinHeightOffset;
private float mControlOffsetRatio;
private ActivityTabTabObserver mActiveTabObserver;
private final ObserverList<BrowserControlsStateProvider.Observer> mControlsObservers =
new ObserverList<>();
private FullscreenHtmlApiHandlerBase mHtmlApiHandler;
@Nullable private Tab mTab;
/** The animator for the Android browser controls. */
private ValueAnimator mControlsAnimator;
/**
* Indicates if control offset is in the overridden state by animation. Stays {@code true}
* from animation start till the next offset update from compositor arrives.
*/
private boolean mOffsetOverridden;
private boolean mContentViewScrolling;
@IntDef({ControlsPosition.TOP, ControlsPosition.NONE})
@Retention(RetentionPolicy.SOURCE)
public @interface ControlsPosition {
/** Controls are at the top, eg normal ChromeTabbedActivity. */
int TOP = 0;
/** Controls are not present, eg NoTouchActivity. */
int NONE = 1;
}
private final Runnable mUpdateVisibilityRunnable =
new Runnable() {
@Override
public void run() {
int visibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE;
if (mControlContainer == null
|| mControlContainer.getView().getVisibility() == visibility) {
return;
} else if (visibility == View.VISIBLE
&& mContentViewScrolling
&& ToolbarFeatures.shouldSuppressCaptures()
&& mBrowserVisibilityDelegate.get() == BrowserControlsState.BOTH) {
// Don't make the controls visible until scrolling has stopped to avoid
// doing it more often than we need to. onContentViewScrollingStateChanged
// will schedule us again when scrolling ceases.
return;
}
try (TraceEvent e =
TraceEvent.scoped(
"BrowserControlsManager.onAndroidVisibilityChanged")) {
mControlContainer.getView().setVisibility(visibility);
for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
obs.onAndroidControlsVisibilityChanged(visibility);
}
if (!ToolbarFeatures.shouldSuppressCaptures()) {
// requestLayout is required to trigger a new gatherTransparentRegion(),
// which only occurs together with a layout and let's SurfaceFlinger
// trim overlays.
// This may be almost equivalent to using View.GONE, but we still use
// View.INVISIBLE since drawing caches etc. won't be destroyed, and the
// layout may be less expensive. The overlay trimming optimization
// only works pre-Android N (see https://crbug.com/725453), so this
// call should be removed entirely once it's confirmed to be safe.
ViewUtils.requestLayout(
mControlContainer.getView(),
"BrowserControlsManager.mUpdateVisibilityRunnable Runnable");
}
}
}
};
/**
* Creates an instance of the browser controls manager.
* @param activity The activity that supports browser controls.
* @param controlsPosition Where the browser controls are.
*/
public BrowserControlsManager(Activity activity, @ControlsPosition int controlsPosition) {
this(activity, controlsPosition, true);
}
/**
* Creates an instance of the browser controls manager.
* @param activity The activity that supports browser controls.
* @param controlsPosition Where the browser controls are.
* @param exitFullscreenOnStop Whether fullscreen mode should exit on stop - should be
* true for Activities that are not always fullscreen.
*/
public BrowserControlsManager(
Activity activity,
@ControlsPosition int controlsPosition,
boolean exitFullscreenOnStop) {
mActivity = activity;
mControlsPosition = controlsPosition;
mControlsAtMinHeight.set(false);
mHtmlApiHandler =
FullscreenHtmlApiHandlerFactory.createInstance(
activity, mControlsAtMinHeight, exitFullscreenOnStop);
mBrowserVisibilityDelegate =
new BrowserStateBrowserControlsVisibilityDelegate(
mHtmlApiHandler.getPersistentFullscreenModeSupplier());
mBrowserVisibilityDelegate.addObserver(
(constraints) -> {
if (constraints == BrowserControlsState.SHOWN) {
setPositionsForTabToNonFullscreen();
// If controls become locked, it's possible we've previously delayed
// actually setting visibility until a touch event is over. In this case, we
// need to trigger an update again now, which should go through due to
// constraints.
scheduleVisibilityUpdate();
}
});
}
/**
* Initializes the browser controls manager with the required dependencies.
*
* @param controlContainer Container holding the controls (Toolbar).
* @param activityTabProvider Provider of the current activity tab.
* @param modelSelector The tab model selector that will be monitored for tab changes.
* @param resControlContainerHeight The dimension resource ID for the control container height.
*/
public void initialize(
@Nullable ControlContainer controlContainer,
ActivityTabProvider activityTabProvider,
final TabModelSelector modelSelector,
int resControlContainerHeight) {
mHtmlApiHandler.initialize(activityTabProvider, modelSelector);
ApplicationStatus.registerStateListenerForActivity(this, mActivity);
mActiveTabObserver =
new ActivityTabTabObserver(activityTabProvider) {
@Override
protected void onObservingDifferentTab(Tab tab, boolean hint) {
setTab(tab);
// The tab that's been switched away from is never going to update us that
// the scroll event stopped.
mTabControlsObserver.onContentViewScrollingStateChanged(false);
}
};
mTabControlsObserver =
new TabModelSelectorTabObserver(modelSelector) {
@Override
public void onInteractabilityChanged(Tab tab, boolean interactable) {
if (!interactable || tab != getTab()) return;
TabBrowserControlsOffsetHelper helper =
TabBrowserControlsOffsetHelper.get(tab);
if (!helper.offsetInitialized()) return;
onOffsetsChanged(
helper.topControlsOffset(),
helper.bottomControlsOffset(),
helper.contentOffset(),
helper.topControlsMinHeightOffset(),
helper.bottomControlsMinHeightOffset());
}
@Override
public void onContentChanged(Tab tab) {
if (tab.isShowingCustomView()) {
showAndroidControls(false);
}
}
@Override
public void onRendererResponsiveStateChanged(Tab tab, boolean isResponsive) {
if (tab == getTab() && !isResponsive) showAndroidControls(false);
}
@Override
public void onBrowserControlsOffsetChanged(
Tab tab,
int topControlsOffset,
int bottomControlsOffset,
int contentOffset,
int topControlsMinHeightOffset,
int bottomControlsMinHeightOffset) {
if (tab == getTab() && tab.isUserInteractable() && !tab.isNativePage()) {
onOffsetsChanged(
topControlsOffset,
bottomControlsOffset,
contentOffset,
topControlsMinHeightOffset,
bottomControlsMinHeightOffset);
}
}
@Override
public void onBrowserControlsConstraintsChanged(
Tab tab,
BrowserControlsOffsetTagsInfo oldOffsetTagsInfo,
BrowserControlsOffsetTagsInfo offsetTagsInfo,
@BrowserControlsState int constraints) {
WebContents webContents = tab.getWebContents();
if (webContents == null) {
return;
}
// TODO(peilinwang) Refactor so this this function only gets passed
// OffsetTags as only this class needs to know/use the height for
// creating the OffsetTagConstraint.
offsetTagsInfo.mTopControlsHeight = mTopControlContainerHeight;
webContents.notifyControlsConstraintsChanged(
oldOffsetTagsInfo, offsetTagsInfo);
notifyConstraintsChanged(oldOffsetTagsInfo, offsetTagsInfo, constraints);
}
@Override
public void onContentViewScrollingStateChanged(boolean scrolling) {
mContentViewScrolling = scrolling;
if (!scrolling
&& ToolbarFeatures.shouldSuppressCaptures()
&& shouldShowAndroidControls()
&& mControlContainer.getView().getVisibility() != View.VISIBLE) {
scheduleVisibilityUpdate();
}
}
};
assert controlContainer != null || mControlsPosition == ControlsPosition.NONE;
mControlContainer = controlContainer;
switch (mControlsPosition) {
case ControlsPosition.TOP:
assert resControlContainerHeight != ActivityUtils.NO_RESOURCE_ID;
mTopControlContainerHeight =
mActivity.getResources().getDimensionPixelSize(resControlContainerHeight);
break;
case ControlsPosition.NONE:
// Treat the case of no controls as controls always being totally offscreen.
mControlOffsetRatio = 1.0f;
break;
}
mRendererTopContentOffset = mTopControlContainerHeight;
updateControlOffset();
scheduleVisibilityUpdate();
}
/**
* @return {@link FullscreenManager} object.
*/
public FullscreenManager getFullscreenManager() {
return mHtmlApiHandler;
}
@Override
public BrowserStateBrowserControlsVisibilityDelegate getBrowserVisibilityDelegate() {
return mBrowserVisibilityDelegate;
}
/**
* @return The currently selected tab for fullscreen.
*/
@Nullable
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public Tab getTab() {
return mTab;
}
private void setTab(@Nullable Tab tab) {
Tab previousTab = getTab();
mTab = tab;
if (previousTab != tab) {
if (tab != null) {
mBrowserVisibilityDelegate.showControlsTransient();
if (tab.isUserInteractable()) restoreControlsPositions();
}
}
if (tab == null && mBrowserVisibilityDelegate.get() != BrowserControlsState.HIDDEN) {
setPositionsForTabToNonFullscreen();
}
}
// ActivityStateListener
@Override
public void onActivityStateChange(Activity activity, int newState) {
if (newState == ActivityState.STARTED) {
PostTask.postDelayedTask(
TaskTraits.UI_DEFAULT,
mBrowserVisibilityDelegate::showControlsTransient,
ACTIVITY_RETURN_SHOW_REQUEST_DELAY_MS);
} else if (newState == ActivityState.DESTROYED) {
ApplicationStatus.unregisterActivityStateListener(this);
}
}
@Override
public float getBrowserControlHiddenRatio() {
return mControlOffsetRatio;
}
/**
* @return True if the browser controls are showing as much as the min height. Note that this is
* the same as
* {@link BrowserControlsUtils#areBrowserControlsOffScreen(BrowserControlsStateProvider)} when
* both min-heights are 0.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public boolean areBrowserControlsAtMinHeight() {
return mControlsAtMinHeight.get();
}
@Override
public void setBottomControlsHeight(int bottomControlsHeight, int bottomControlsMinHeight) {
if (mBottomControlContainerHeight == bottomControlsHeight
&& mBottomControlsMinHeight == bottomControlsMinHeight) {
return;
}
try (TraceEvent e = TraceEvent.scoped("BrowserControlsManager.setBottomControlsHeight")) {
final int oldBottomControlsHeight = mBottomControlContainerHeight;
final int oldBottomControlsMinHeight = mBottomControlsMinHeight;
mBottomControlContainerHeight = bottomControlsHeight;
mBottomControlsMinHeight = bottomControlsMinHeight;
if (!canAnimateNativeBrowserControls()) {
if (shouldAnimateBrowserControlsHeightChanges()) {
runBrowserDrivenBottomControlsHeightChangeAnimation(
oldBottomControlsHeight, oldBottomControlsMinHeight);
} else {
updateBrowserControlsOffsets(
/* toNonFullscreen= */ false,
0,
0,
getTopControlsHeight(),
getTopControlsMinHeight(),
getBottomControlsMinHeight());
}
}
for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
obs.onBottomControlsHeightChanged(
mBottomControlContainerHeight, mBottomControlsMinHeight);
}
}
}
@Override
public void setTopControlsHeight(int topControlsHeight, int topControlsMinHeight) {
if (mTopControlContainerHeight == topControlsHeight
&& mTopControlsMinHeight == topControlsMinHeight) {
return;
}
try (TraceEvent e = TraceEvent.scoped("BrowserControlsManager.setTopControlsHeight")) {
final int oldTopHeight = mTopControlContainerHeight;
final int oldTopMinHeight = mTopControlsMinHeight;
mTopControlContainerHeight = topControlsHeight;
mTopControlsMinHeight = topControlsMinHeight;
if (!canAnimateNativeBrowserControls()) {
if (shouldAnimateBrowserControlsHeightChanges()) {
runBrowserDrivenTopControlsHeightChangeAnimation(oldTopHeight, oldTopMinHeight);
} else {
showAndroidControls(false);
}
}
for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
obs.onTopControlsHeightChanged(mTopControlContainerHeight, mTopControlsMinHeight);
}
}
}
@Override
public void setAnimateBrowserControlsHeightChanges(
boolean animateBrowserControlsHeightChanges) {
mAnimateBrowserControlsHeightChanges = animateBrowserControlsHeightChanges;
}
@Override
public int getTopControlsHeight() {
return mTopControlContainerHeight;
}
@Override
public int getTopControlsMinHeight() {
return mTopControlsMinHeight;
}
@Override
public int getBottomControlsHeight() {
return mBottomControlContainerHeight;
}
@Override
public int getBottomControlsMinHeight() {
return mBottomControlsMinHeight;
}
@Override
public boolean shouldAnimateBrowserControlsHeightChanges() {
return mAnimateBrowserControlsHeightChanges;
}
@Override
public int getContentOffset() {
return mRendererTopContentOffset;
}
@Override
public int getTopControlOffset() {
return mRendererTopControlOffset;
}
@Override
public int getTopControlsMinHeightOffset() {
return mRendererTopControlsMinHeightOffset;
}
private int getBottomContentOffset() {
return BrowserControlsUtils.getBottomContentOffset(this);
}
@Override
public int getBottomControlOffset() {
// If the height is currently 0, the offset generated by the bottom controls should be too.
// TODO(crbug.com/40112494): Send a offset update from the browser controls manager when the
// height changes to ensure correct offsets (removing the need for min()).
return Math.min(mRendererBottomControlOffset, mBottomControlContainerHeight);
}
@Override
public int getBottomControlsMinHeightOffset() {
return mRendererBottomControlsMinHeightOffset;
}
private void updateControlOffset() {
if (mControlsPosition == ControlsPosition.NONE) return;
if (getTopControlsHeight() == 0) {
// Treat the case of 0 height as controls being totally offscreen.
mControlOffsetRatio = 1.0f;
} else {
mControlOffsetRatio =
Math.abs((float) mRendererTopControlOffset / getTopControlsHeight());
}
}
@Override
public float getTopVisibleContentOffset() {
return getTopControlsHeight() + getTopControlOffset();
}
@Override
public int getAndroidControlsVisibility() {
return mControlContainer == null
? View.INVISIBLE
: mControlContainer.getView().getVisibility();
}
@Override
public void notifyBackgroundColor(@ColorInt int color) {
for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
obs.onBottomControlsBackgroundColorChanged(color);
}
}
@Override
public void addObserver(BrowserControlsStateProvider.Observer obs) {
mControlsObservers.addObserver(obs);
}
@Override
public void removeObserver(BrowserControlsStateProvider.Observer obs) {
mControlsObservers.removeObserver(obs);
}
/**
* Utility routine for ensuring visibility updates are synchronized with animation, preventing
* message loop stalls due to untimely invalidation.
*/
private void scheduleVisibilityUpdate() {
if (mControlContainer == null) {
return;
}
final int desiredVisibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE;
if (mControlContainer.getView().getVisibility() == desiredVisibility) return;
mControlContainer.getView().removeCallbacks(mUpdateVisibilityRunnable);
mControlContainer.getView().postOnAnimation(mUpdateVisibilityRunnable);
}
/**
* Forces the Android controls to hide. While there are acquired tokens the browser controls
* Android view will always be hidden, otherwise they will show/hide based on position.
*
* NB: this only affects the Android controls. For controlling composited toolbar visibility,
* implement {@link BrowserControlsVisibilityDelegate#canShowBrowserControls()}.
*/
private int hideAndroidControls() {
return mHidingTokenHolder.acquireToken();
}
@Override
public int hideAndroidControlsAndClearOldToken(int oldToken) {
int newToken = hideAndroidControls();
mHidingTokenHolder.releaseToken(oldToken);
return newToken;
}
@Override
public void releaseAndroidControlsHidingToken(int token) {
mHidingTokenHolder.releaseToken(token);
}
private boolean shouldShowAndroidControls() {
if (mControlContainer == null) return false;
if (mHidingTokenHolder.hasTokens()) {
return false;
}
if (offsetOverridden()) return true;
return !BrowserControlsUtils.drawControlsAsTexture(this);
}
/**
* Updates the positions of the browser controls and content to the default non fullscreen
* values.
*/
private void setPositionsForTabToNonFullscreen() {
Tab tab = getTab();
if (tab == null
|| !tab.isInitialized()
|| TabBrowserControlsConstraintsHelper.getConstraints(tab)
!= BrowserControlsState.HIDDEN) {
setPositionsForTab(
0,
0,
getTopControlsHeight(),
getTopControlsMinHeight(),
getBottomControlsMinHeight());
} else {
// Tab isn't null and the BrowserControlsState is HIDDEN. In this case, set the offsets
// to values that will position the browser controls at the min-height.
setPositionsForTab(
getTopControlsMinHeight() - getTopControlsHeight(),
getBottomControlsHeight() - getBottomControlsMinHeight(),
getTopControlsMinHeight(),
getTopControlsMinHeight(),
getBottomControlsMinHeight());
}
}
/**
* Updates the positions of the browser controls and content based on the desired position of
* the current tab.
* @param topControlsOffset The Y offset of the top controls in px.
* @param bottomControlsOffset The Y offset of the bottom controls in px.
* @param topContentOffset The Y offset for the content in px.
* @param topControlsMinHeightOffset The Y offset for the top controls min-height in px.
* @param bottomControlsMinHeightOffset The Y offset for the bottom controls min-height in px.
*/
private void setPositionsForTab(
int topControlsOffset,
int bottomControlsOffset,
int topContentOffset,
int topControlsMinHeightOffset,
int bottomControlsMinHeightOffset) {
// This min/max logic is here to handle changes in the browser controls height. For example,
// if we change either height to 0, the offsets of the controls should also be 0. This works
// assuming we get an event from the renderer after the browser control heights change.
int rendererTopControlOffset = Math.max(topControlsOffset, -getTopControlsHeight());
int rendererBottomControlOffset = Math.min(bottomControlsOffset, getBottomControlsHeight());
int rendererTopContentOffset =
Math.min(topContentOffset, rendererTopControlOffset + getTopControlsHeight());
if (rendererTopControlOffset == mRendererTopControlOffset
&& rendererBottomControlOffset == mRendererBottomControlOffset
&& rendererTopContentOffset == mRendererTopContentOffset
&& topControlsMinHeightOffset == mRendererTopControlsMinHeightOffset
&& bottomControlsMinHeightOffset == mRendererBottomControlsMinHeightOffset) {
return;
}
mRendererTopControlOffset = rendererTopControlOffset;
mRendererBottomControlOffset = rendererBottomControlOffset;
mRendererTopControlsMinHeightOffset = topControlsMinHeightOffset;
mRendererBottomControlsMinHeightOffset = bottomControlsMinHeightOffset;
mRendererTopContentOffset = rendererTopContentOffset;
mControlsAtMinHeight.set(
getContentOffset() == getTopControlsMinHeight()
&& getBottomContentOffset() == getBottomControlsMinHeight());
updateControlOffset();
notifyControlOffsetChanged();
}
private void notifyControlOffsetChanged() {
try (TraceEvent e =
TraceEvent.scoped("BrowserControlsManager.notifyControlOffsetChanged")) {
scheduleVisibilityUpdate();
if (shouldShowAndroidControls()) {
// TODO(crbug.com/40941730): Fix frame mismatch between Android view with cc layer.
mControlContainer.getView().setTranslationY(getTopControlOffset());
}
// Whether we need the compositor to draw again to update our animation.
// Should be |false| when the browser controls are only moved through the page
// scrolling.
boolean needsAnimate = shouldShowAndroidControls();
// With BCIV enabled, renderer scrolling will not update the control offsets of the
// browser's compositor frame, but we still want this update to happen if the browser
// is controlling the controls.
@BrowserControlsState
int constraints = TabBrowserControlsConstraintsHelper.getConstraints(getTab());
boolean isVisibilityForced =
constraints == BrowserControlsState.HIDDEN
|| constraints == BrowserControlsState.SHOWN;
for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
obs.onControlsOffsetChanged(
getTopControlOffset(),
getTopControlsMinHeightOffset(),
getBottomControlOffset(),
getBottomControlsMinHeightOffset(),
needsAnimate,
isVisibilityForced);
}
}
}
private void notifyConstraintsChanged(
BrowserControlsOffsetTagsInfo oldOffsetTagsInfo,
BrowserControlsOffsetTagsInfo offsetTagsInfo,
@BrowserControlsState int constraints) {
for (BrowserControlsStateProvider.Observer obs : mControlsObservers) {
obs.onControlsConstraintsChanged(oldOffsetTagsInfo, offsetTagsInfo, constraints);
}
}
/**
* Called when offset values related with fullscreen functionality has been changed by the
* compositor.
*
* @param topControlsOffsetY The Y offset of the top controls in physical pixels.
* @param bottomControlsOffsetY The Y offset of the bottom controls in physical pixels.
* @param contentOffsetY The Y offset of the content in physical pixels.
* @param topControlsMinHeightOffsetY The current offset of the top controls min-height.
* @param bottomControlsMinHeightOffsetY The current offset of the bottom controls min-height.
*/
private void onOffsetsChanged(
int topControlsOffsetY,
int bottomControlsOffsetY,
int contentOffsetY,
int topControlsMinHeightOffsetY,
int bottomControlsMinHeightOffsetY) {
// Cancel any animation on the Android controls and let compositor drive the offset updates.
resetControlsOffsetOverridden();
Tab tab = getTab();
if (SadTab.isShowing(tab) || tab.isNativePage()) {
showAndroidControls(false);
} else {
updateBrowserControlsOffsets(
false,
topControlsOffsetY,
bottomControlsOffsetY,
contentOffsetY,
topControlsMinHeightOffsetY,
bottomControlsMinHeightOffsetY);
}
}
@Override
public void showAndroidControls(boolean animate) {
if (animate) {
runBrowserDrivenShowAnimation();
} else {
updateBrowserControlsOffsets(
true,
0,
0,
getTopControlsHeight(),
getTopControlsMinHeight(),
getBottomControlsMinHeight());
}
}
@Override
public void restoreControlsPositions() {
resetControlsOffsetOverridden();
// Make sure the dominant control offsets have been set.
Tab tab = getTab();
TabBrowserControlsOffsetHelper offsetHelper = null;
if (tab != null) offsetHelper = TabBrowserControlsOffsetHelper.get(tab);
// Browser controls should always be shown on native pages and restoring offsets might cause
// the controls to get stuck in an invalid position.
if (offsetHelper != null
&& offsetHelper.offsetInitialized()
&& tab != null
&& !tab.isNativePage()) {
updateBrowserControlsOffsets(
false,
offsetHelper.topControlsOffset(),
offsetHelper.bottomControlsOffset(),
offsetHelper.contentOffset(),
offsetHelper.topControlsMinHeightOffset(),
offsetHelper.bottomControlsMinHeightOffset());
} else {
showAndroidControls(false);
}
TabBrowserControlsConstraintsHelper.updateEnabledState(tab);
}
/** Helper method to update offsets and notify offset changes to observers if necessary. */
private void updateBrowserControlsOffsets(
boolean toNonFullscreen,
int topControlsOffset,
int bottomControlsOffset,
int topContentOffset,
int topControlsMinHeightOffset,
int bottomControlsMinHeightOffset) {
if (toNonFullscreen) {
setPositionsForTabToNonFullscreen();
} else {
setPositionsForTab(
topControlsOffset,
bottomControlsOffset,
topContentOffset,
topControlsMinHeightOffset,
bottomControlsMinHeightOffset);
}
}
@Override
public boolean offsetOverridden() {
return mOffsetOverridden;
}
/**
* Sets the flat indicating if browser control offset is overridden by animation.
* @param flag Boolean flag of the new offset overridden state.
*/
private void setOffsetOverridden(boolean flag) {
mOffsetOverridden = flag;
}
/** Helper method to cancel overridden offset on Android browser controls. */
private void resetControlsOffsetOverridden() {
if (!offsetOverridden()) return;
if (mControlsAnimator != null) mControlsAnimator.cancel();
setOffsetOverridden(false);
}
/** Helper method to run slide-in animations on the Android browser controls views. */
private void runBrowserDrivenShowAnimation() {
if (mControlsAnimator != null) return;
setOffsetOverridden(true);
final float hiddenRatio = getBrowserControlHiddenRatio();
final int topControlHeight = getTopControlsHeight();
final int topControlOffset = getTopControlOffset();
// Set animation start value to current renderer controls offset.
mControlsAnimator = ValueAnimator.ofInt(topControlOffset, 0);
mControlsAnimator.setDuration(
(long) Math.abs(hiddenRatio * CONTROLS_ANIMATION_DURATION_MS));
mControlsAnimator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mControlsAnimator = null;
}
@Override
public void onAnimationCancel(Animator animation) {
updateBrowserControlsOffsets(
false,
0,
0,
topControlHeight,
getTopControlsMinHeight(),
getBottomControlsMinHeight());
}
});
mControlsAnimator.addUpdateListener(
(animator) -> {
updateBrowserControlsOffsets(
false,
(int) animator.getAnimatedValue(),
0,
topControlHeight,
getTopControlsMinHeight(),
getBottomControlsMinHeight());
});
mControlsAnimator.start();
}
private void runBrowserDrivenTopControlsHeightChangeAnimation(
int oldTopControlsHeight, int oldTopControlsMinHeight) {
runBrowserDrivenControlsAnimation(
oldTopControlsHeight,
oldTopControlsMinHeight,
getBottomControlsHeight(),
getBottomControlsMinHeight());
}
private void runBrowserDrivenBottomControlsHeightChangeAnimation(
int oldBottomControlsHeight, int oldBottomControlsMinHeight) {
runBrowserDrivenControlsAnimation(
getTopControlsHeight(),
getTopControlsMinHeight(),
oldBottomControlsHeight,
oldBottomControlsMinHeight);
}
private void runBrowserDrivenControlsAnimation(
int oldTopControlsHeight,
int oldTopControlsMinHeight,
int oldBottomControlsHeight,
int oldBottomControlsMinHeight) {
if (mControlsAnimator != null) return;
assert getContentOffset() == oldTopControlsHeight
: "Height change animations are implemented for fully shown controls only!";
setOffsetOverridden(true);
final int newTopControlsHeight = getTopControlsHeight();
final int newTopControlsMinHeight = getTopControlsMinHeight();
final int newBottomControlsHeight = getBottomControlsHeight();
final int newBottomControlsMinHeight = getBottomControlsMinHeight();
mControlsAnimator = ValueAnimator.ofFloat(0.f, 1.f);
mControlsAnimator.addUpdateListener(
(animator) -> {
final float topValue = (float) animator.getAnimatedValue();
final float topControlsMinHeightOffset =
interpolate(topValue, oldTopControlsMinHeight, newTopControlsMinHeight);
final float topContentOffset =
interpolate(topValue, oldTopControlsHeight, newTopControlsHeight);
final float topControlsOffset = topContentOffset - newTopControlsHeight;
// Bottom controls offsets need to change in the opposite direction, so use the
// same calculations but with animation progress going from 1 to 0 instead of 0
// to 1.
final float bottomValue = 1.f - topValue;
final float bottomControlsMinHeightOffset =
interpolate(
bottomValue,
oldBottomControlsMinHeight,
newBottomControlsMinHeight);
final float bottomControlsOffset =
interpolate(
bottomValue, oldBottomControlsHeight, newBottomControlsHeight);
updateBrowserControlsOffsets(
false,
(int) topControlsOffset,
(int) bottomControlsOffset,
(int) topContentOffset,
(int) topControlsMinHeightOffset,
(int) bottomControlsMinHeightOffset);
});
mControlsAnimator.setDuration(CONTROLS_ANIMATION_DURATION_MS);
mControlsAnimator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
updateBrowserControlsOffsets(
false,
0,
0,
getTopControlsHeight(),
getTopControlsMinHeight(),
getBottomControlsMinHeight());
mControlsAnimator = null;
}
});
mControlsAnimator.start();
}
private static float interpolate(float progress, float oldValue, float newValue) {
return oldValue + progress * (newValue - oldValue);
}
private boolean canAnimateNativeBrowserControls() {
final Tab tab = getTab();
return tab != null && tab.isUserInteractable() && !tab.isNativePage();
}
/** Destroys the BrowserControlsManager */
public void destroy() {
mTab = null;
mHtmlApiHandler.destroy();
if (mActiveTabObserver != null) mActiveTabObserver.destroy();
mBrowserVisibilityDelegate.destroy();
if (mTabControlsObserver != null) mTabControlsObserver.destroy();
}
public TabModelSelectorTabObserver getTabControlsObserverForTesting() {
return mTabControlsObserver;
}
ValueAnimator getControlsAnimatorForTesting() {
return mControlsAnimator;
}
int getControlsAnimationDurationMsForTesting() {
return CONTROLS_ANIMATION_DURATION_MS;
}
}