0

[WebLayer] Add WebContentsDelegate::ShouldPinTopControlsToContentTop()

This CL defines WebContentsDelegate::ShouldPinTopControlsToContentTop(),
which embedders can use to tell BrowserControlsOffsetManager to only
show the top browser controls if the page is scrolled to the top.

WebLayer's Browser#setTopView pinToContentTop parameter is hooked up to
this new method.

Bug: 1069498
Change-Id: I7a19656c0ee3219936c3279bfb89591b1f3abb56
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2284138
Commit-Queue: Robbie McElrath <rmcelrath@chromium.org>
Reviewed-by: John Abd-El-Malek <jam@chromium.org>
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Reviewed-by: Bo <boliu@chromium.org>
Reviewed-by: David Bokan <bokan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#790985}
This commit is contained in:
Robbie McElrath
2020-07-22 20:22:55 +00:00
committed by Commit Bot
parent fa60a76aa5
commit c727f0eb0f
24 changed files with 225 additions and 10 deletions

@ -7,6 +7,7 @@
#include <stdint.h>
#include <algorithm>
#include <utility>
#include "base/check_op.h"
#include "base/memory/ptr_util.h"
@ -369,7 +370,21 @@ gfx::Vector2dF BrowserControlsOffsetManager::ScrollBy(
pending_delta.y() < 0))
return pending_delta;
accumulated_scroll_delta_ += pending_delta.y();
// Scroll the page up before expanding the browser controls if
// ShouldPinTopControlsToContentTop() returns true.
float viewport_offset_y = client_->ViewportScrollOffset().y();
if (client_->ShouldPinTopControlsToContentTop() && pending_delta.y() < 0 &&
viewport_offset_y > 0) {
// Reset the baseline so the controls will immediately begin to scroll
// once we're at the top.
ResetBaseline();
// Only scroll the controls by the amount remaining after the page contents
// have been scrolled to the top.
accumulated_scroll_delta_ =
std::min(0.f, pending_delta.y() + viewport_offset_y);
} else {
accumulated_scroll_delta_ += pending_delta.y();
}
// We want to base our calculations on top or bottom controls. After consuming
// the scroll delta, we will calculate a shown ratio for the controls. The

@ -5,6 +5,10 @@
#ifndef CC_INPUT_BROWSER_CONTROLS_OFFSET_MANAGER_CLIENT_H_
#define CC_INPUT_BROWSER_CONTROLS_OFFSET_MANAGER_CLIENT_H_
namespace gfx {
class ScrollOffset;
}
namespace cc {
class CC_EXPORT BrowserControlsOffsetManagerClient {
@ -17,7 +21,9 @@ class CC_EXPORT BrowserControlsOffsetManagerClient {
float bottom_ratio) = 0;
virtual float CurrentTopControlsShownRatio() const = 0;
virtual float CurrentBottomControlsShownRatio() const = 0;
virtual gfx::ScrollOffset ViewportScrollOffset() const = 0;
virtual void DidChangeBrowserControlsPosition() = 0;
virtual bool ShouldPinTopControlsToContentTop() const = 0;
virtual bool HaveRootScrollNode() const = 0;
virtual void SetNeedsCommit() = 0;

@ -6,6 +6,7 @@
#include <algorithm>
#include <cmath>
#include <limits>
#include <memory>
#include "base/time/time.h"
@ -30,7 +31,8 @@ class MockBrowserControlsOffsetManagerClient
: host_impl_(&task_runner_provider_, &task_graph_runner_),
redraw_needed_(false),
update_draw_properties_needed_(false),
browser_controls_params_({top_controls_height, 0, 0, 0, false, false}),
browser_controls_params_(
{top_controls_height, 0, 0, 0, false, false, false}),
bottom_controls_shown_ratio_(1.f),
top_controls_shown_ratio_(1.f),
browser_controls_show_threshold_(browser_controls_show_threshold),
@ -66,6 +68,14 @@ class MockBrowserControlsOffsetManagerClient
return browser_controls_params_.top_controls_min_height;
}
bool ShouldPinTopControlsToContentTop() const override {
return browser_controls_params_.pin_top_controls_to_content_top;
}
gfx::ScrollOffset ViewportScrollOffset() const override {
return viewport_scroll_offset_;
}
void SetCurrentBrowserControlsShownRatio(float top_ratio,
float bottom_ratio) override {
AssertAndClamp(&top_ratio);
@ -110,6 +120,15 @@ class MockBrowserControlsOffsetManagerClient
params.animate_browser_controls_height_changes);
}
void SetViewportScrollOffset(float x, float y) {
viewport_scroll_offset_ = gfx::ScrollOffset(x, y);
}
void ScrollVerticallyBy(float dy) {
gfx::Vector2dF viewport_scroll_delta = manager()->ScrollBy({0.f, dy});
viewport_scroll_offset_.Add(gfx::ScrollOffset(viewport_scroll_delta));
}
private:
FakeImplTaskRunnerProvider task_runner_provider_;
TestTaskGraphRunner task_graph_runner_;
@ -125,6 +144,7 @@ class MockBrowserControlsOffsetManagerClient
float top_controls_shown_ratio_;
float browser_controls_show_threshold_;
float browser_controls_hide_threshold_;
gfx::ScrollOffset viewport_scroll_offset_;
};
TEST(BrowserControlsOffsetManagerTest, EnsureScrollThresholdApplied) {
@ -1143,5 +1163,44 @@ TEST(BrowserControlsOffsetManagerTest,
EXPECT_FLOAT_EQ(1.f, client.CurrentBottomControlsShownRatio());
}
TEST(BrowserControlsOffsetManagerTest, PinTopControlsToContentTop) {
MockBrowserControlsOffsetManagerClient client(0.f, 0.5f, 0.5f);
client.SetBrowserControlsParams({/*top_controls_height=*/100.f, 0, 0, 0,
false, false,
/*pin_top_controls_to_content_top=*/true});
BrowserControlsOffsetManager* manager = client.manager();
// Scroll down to hide the controls entirely.
manager->ScrollBegin();
client.ScrollVerticallyBy(150.f);
EXPECT_FLOAT_EQ(-100.f, manager->ControlsTopOffset());
EXPECT_FLOAT_EQ(50.f, client.ViewportScrollOffset().y());
manager->ScrollEnd();
manager->ScrollBegin();
// Scroll back up a bit and ensure the controls don't move until we're at
// the top.
client.ScrollVerticallyBy(-20.f);
EXPECT_FLOAT_EQ(-100.f, manager->ControlsTopOffset());
EXPECT_FLOAT_EQ(30.f, client.ViewportScrollOffset().y());
client.ScrollVerticallyBy(-10.f);
EXPECT_FLOAT_EQ(-100.f, manager->ControlsTopOffset());
EXPECT_FLOAT_EQ(20.f, client.ViewportScrollOffset().y());
// After scrolling past the top, the top controls should start showing.
client.ScrollVerticallyBy(-30.f);
EXPECT_FLOAT_EQ(-90.f, manager->ControlsTopOffset());
EXPECT_FLOAT_EQ(0.f, client.ViewportScrollOffset().y());
client.ScrollVerticallyBy(-50.f);
EXPECT_FLOAT_EQ(-40.f, manager->ControlsTopOffset());
// The final offset is greater than gtest's epsilon.
EXPECT_GT(0.0001f, client.ViewportScrollOffset().y());
manager->ScrollEnd();
}
} // namespace
} // namespace cc

@ -32,6 +32,7 @@ IPC_STRUCT_TRAITS_BEGIN(cc::BrowserControlsParams)
IPC_STRUCT_TRAITS_MEMBER(bottom_controls_min_height)
IPC_STRUCT_TRAITS_MEMBER(animate_browser_controls_height_changes)
IPC_STRUCT_TRAITS_MEMBER(browser_controls_shrink_blink_size)
IPC_STRUCT_TRAITS_MEMBER(pin_top_controls_to_content_top)
IPC_STRUCT_TRAITS_END()
#endif // CC_IPC_CC_PARAM_TRAITS_MACROS_H_

@ -15,7 +15,9 @@ bool BrowserControlsParams::operator==(
animate_browser_controls_height_changes ==
other.animate_browser_controls_height_changes &&
browser_controls_shrink_blink_size ==
other.browser_controls_shrink_blink_size;
other.browser_controls_shrink_blink_size &&
pin_top_controls_to_content_top ==
other.pin_top_controls_to_content_top;
}
bool BrowserControlsParams::operator!=(

@ -35,6 +35,11 @@ struct CC_EXPORT BrowserControlsParams {
// URL-bar (always false on platforms where URL-bar hiding isn't supported).
bool browser_controls_shrink_blink_size = false;
// Whether or not the top controls should be pinned to the top of the page
// contents. If true, collapsed top controls won't begin scrolling into view
// until the page is scrolled to the top.
bool pin_top_controls_to_content_top = false;
bool operator==(const BrowserControlsParams& other) const;
bool operator!=(const BrowserControlsParams& other) const;
};

@ -3049,6 +3049,10 @@ void LayerTreeHostImpl::DidLoseLayerTreeFrameSink() {
client_->DidLoseLayerTreeFrameSinkOnImplThread();
}
bool LayerTreeHostImpl::ShouldPinTopControlsToContentTop() const {
return active_tree_->pin_top_controls_to_content_top();
}
bool LayerTreeHostImpl::HaveRootScrollNode() const {
return InnerViewportScrollNode();
}
@ -3779,6 +3783,10 @@ float LayerTreeHostImpl::CurrentBottomControlsShownRatio() const {
return active_tree_->CurrentBottomControlsShownRatio();
}
gfx::ScrollOffset LayerTreeHostImpl::ViewportScrollOffset() const {
return viewport_->TotalScrollOffset();
}
void LayerTreeHostImpl::BindToClient(InputHandlerClient* client) {
DCHECK(input_handler_client_ == nullptr);
input_handler_client_ = client;

@ -345,9 +345,11 @@ class CC_EXPORT LayerTreeHostImpl : public InputHandler,
float bottom_ratio) override;
float CurrentTopControlsShownRatio() const override;
float CurrentBottomControlsShownRatio() const override;
gfx::ScrollOffset ViewportScrollOffset() const override;
void DidChangeBrowserControlsPosition() override;
void DidObserveScrollDelay(base::TimeDelta scroll_delay,
base::TimeTicks scroll_timestamp);
bool ShouldPinTopControlsToContentTop() const override;
bool HaveRootScrollNode() const override;
void SetNeedsCommit() override;

@ -644,6 +644,9 @@ class CC_EXPORT LayerTreeImpl {
float bottom_controls_min_height() const {
return browser_controls_params_.bottom_controls_min_height;
}
bool pin_top_controls_to_content_top() const {
return browser_controls_params_.pin_top_controls_to_content_top;
}
void set_overscroll_behavior(const OverscrollBehavior& behavior);
OverscrollBehavior overscroll_behavior() const {

@ -38,6 +38,10 @@ bool RenderViewHostDelegateView::DoBrowserControlsShrinkRendererSize() const {
return false;
}
bool RenderViewHostDelegateView::ShouldPinTopControlsToContentTop() const {
return false;
}
void RenderViewHostDelegateView::GestureEventAck(
const blink::WebGestureEvent& event,
blink::mojom::InputEventResultState ack_result) {}

@ -93,6 +93,10 @@ class CONTENT_EXPORT RenderViewHostDelegateView {
// Returns true if the browser controls resize the renderer's view size.
virtual bool DoBrowserControlsShrinkRendererSize() const;
// Returns true if the top controls should be pinned to the top of the page,
// so they'll only be visible if the page is scrolled to the top.
virtual bool ShouldPinTopControlsToContentTop() const;
// Do post-event tasks for gesture events.
virtual void GestureEventAck(const blink::WebGestureEvent& event,
blink::mojom::InputEventResultState ack_result);

@ -871,6 +871,8 @@ blink::VisualProperties RenderWidgetHostImpl::GetVisualProperties() {
visual_properties.browser_controls_params
.animate_browser_controls_height_changes =
rvh_delegate_view->ShouldAnimateBrowserControlsHeightChanges();
visual_properties.browser_controls_params.pin_top_controls_to_content_top =
rvh_delegate_view->ShouldPinTopControlsToContentTop();
float top_controls_height = rvh_delegate_view->GetTopControlsHeight();
float top_controls_min_height = rvh_delegate_view->GetTopControlsMinHeight();

@ -518,6 +518,11 @@ bool WebContentsViewAndroid::DoBrowserControlsShrinkRendererSize() const {
delegate->DoBrowserControlsShrinkRendererSize(web_contents_);
}
bool WebContentsViewAndroid::ShouldPinTopControlsToContentTop() const {
auto* delegate = web_contents_->GetDelegate();
return delegate && delegate->ShouldPinTopControlsToContentTop();
}
bool WebContentsViewAndroid::OnTouchEvent(const ui::MotionEventAndroid& event) {
if (event.GetAction() == ui::MotionEventAndroid::Action::DOWN &&
ShouldRequestUnbufferedDispatch()) {

@ -109,6 +109,7 @@ class WebContentsViewAndroid : public WebContentsView,
int GetBottomControlsMinHeight() const override;
bool ShouldAnimateBrowserControlsHeightChanges() const override;
bool DoBrowserControlsShrinkRendererSize() const override;
bool ShouldPinTopControlsToContentTop() const override;
// ui::EventHandlerAndroid implementation.
bool OnTouchEvent(const ui::MotionEventAndroid& event) override;

@ -320,6 +320,10 @@ bool WebContentsDelegate::DoBrowserControlsShrinkRendererSize(
return false;
}
bool WebContentsDelegate::ShouldPinTopControlsToContentTop() {
return false;
}
PictureInPictureResult WebContentsDelegate::EnterPictureInPicture(
WebContents* web_contents,
const viz::SurfaceId&,

@ -643,6 +643,9 @@ class CONTENT_EXPORT WebContentsDelegate {
virtual bool ShouldAnimateBrowserControlsHeightChanges();
virtual bool DoBrowserControlsShrinkRendererSize(
const WebContents* web_contents);
// Returns true if the top controls should be pinned to the top of the page,
// so they'll only be visible if the page is scrolled to the top.
virtual bool ShouldPinTopControlsToContentTop();
// Propagates to the browser that gesture scrolling has changed state. This is
// used by the browser to assist in controlling the behavior of sliding the

@ -278,6 +278,46 @@ public class BrowserControlsTest {
});
}
// Disabled on L bots due to unexplained flakes. See crbug.com/1035894.
@MinAndroidSdkLevel(Build.VERSION_CODES.M)
@Test
@SmallTest
public void testPinTopControlsToContentTop() throws Exception {
InstrumentationActivity activity = mActivityTestRule.getActivity();
View topContents = activity.getTopContentsContainer();
TestThreadUtils.runOnUiThreadBlocking(
() -> activity.getBrowser().setTopView(topContents, 0, /*pinToContentTop=*/true));
// Scroll down past the top-controls, which should collapse the top-controls and change the
// page height.
EventUtils.simulateDragFromCenterOfView(
activity.getWindow().getDecorView(), 0, -2 * mTopViewHeight);
CriteriaHelper.pollInstrumentationThread(() -> {
Criteria.checkThat(
getVisiblePageHeight(), Matchers.greaterThan(mPageHeightWithTopView));
Criteria.checkThat(activity.getTopContentsContainer().getVisibility(),
Matchers.is(View.INVISIBLE));
});
// Scroll part of the way up again, which should not show the top controls.
int scrolledPageHeight = getVisiblePageHeight();
EventUtils.simulateDragFromCenterOfView(
activity.getWindow().getDecorView(), 0, mTopViewHeight);
CriteriaHelper.pollInstrumentationThread(() -> {
Criteria.checkThat(getVisiblePageHeight(), Matchers.is(scrolledPageHeight));
});
// Scroll to the top to show the top controls.
EventUtils.simulateDragFromCenterOfView(
activity.getWindow().getDecorView(), 0, 2 * mTopViewHeight);
CriteriaHelper.pollInstrumentationThread(() -> {
Criteria.checkThat(getVisiblePageHeight(), Matchers.is(mPageHeightWithTopView));
Criteria.checkThat(
activity.getTopContentsContainer().getVisibility(), Matchers.is(View.VISIBLE));
Criteria.checkThat(topContents.getTranslationY(), Matchers.is(0.f));
});
}
/**
* Makes sure that the top controls are shown when a js dialog is shown.
*

@ -199,6 +199,7 @@ public class BrowserImpl extends IBrowser.Stub implements View.OnAttachStateChan
getViewController().setTopView(ObjectWrapper.unwrap(viewWrapper, View.class));
getViewController().setTopControlsMinHeight(minHeight);
getViewController().setPinTopControlsToContentTop(pinToContentTop);
}
@Override

@ -57,6 +57,7 @@ public final class BrowserViewController
private final ModalDialogManager mModalDialogManager;
private int mTopControlsMinHeight;
private boolean mPinToContentTop;
private TabImpl mTab;
@ -210,6 +211,11 @@ public final class BrowserViewController
updateActiveTabScrollBehavior();
}
public void setPinTopControlsToContentTop(boolean pinToContentTop) {
mPinToContentTop = pinToContentTop;
updateActiveTabScrollBehavior();
}
public void setBottomView(View view) {
mBottomControlsContainerView.setView(view);
}
@ -303,6 +309,7 @@ public final class BrowserViewController
private void updateActiveTabScrollBehavior() {
if (mTab != null) {
mTab.setTopControlsMinHeight(mTopControlsMinHeight);
mTab.setPinTopControlsToContentTop(mPinToContentTop);
}
}

@ -871,6 +871,10 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer {
TabImplJni.get().setTopControlsMinHeight(mNativeTab, minHeight);
}
/* package */ void setPinTopControlsToContentTop(boolean pinToContentTop) {
TabImplJni.get().setPinTopControlsToContentTop(mNativeTab, pinToContentTop);
}
@CalledByNative
private boolean doBrowserControlsShrinkRendererSize() {
BrowserViewController viewController = getViewController();
@ -993,5 +997,6 @@ public final class TabImpl extends ITab.Stub implements LoginPrompt.Observer {
boolean canTranslate(long nativeTabImpl);
void showTranslateUi(long nativeTabImpl);
void setTopControlsMinHeight(long nativeTabImpl, int minHeight);
void setPinTopControlsToContentTop(long nativeTabImpl, boolean pinToContentTop);
}
}

@ -780,6 +780,12 @@ void TabImpl::ShowTranslateUi(JNIEnv* env) {
void TabImpl::SetTopControlsMinHeight(JNIEnv* env, int min_height) {
top_controls_min_height_ = min_height;
}
void TabImpl::SetPinTopControlsToContentTop(
JNIEnv* env,
jboolean pin_top_controls_to_content_top) {
pin_top_controls_to_content_top_ = pin_top_controls_to_content_top;
}
#endif // OS_ANDROID
content::WebContents* TabImpl::OpenURLFromTab(
@ -920,6 +926,14 @@ bool TabImpl::DoBrowserControlsShrinkRendererSize(
#endif
}
bool TabImpl::ShouldPinTopControlsToContentTop() {
#if defined(OS_ANDROID)
return pin_top_controls_to_content_top_;
#else
return false;
#endif
}
bool TabImpl::EmbedsFullscreenWidget() {
return true;
}

@ -194,6 +194,8 @@ class TabImpl : public Tab,
jboolean CanTranslate(JNIEnv* env);
void ShowTranslateUi(JNIEnv* env);
void SetTopControlsMinHeight(JNIEnv* env, int min_height);
void SetPinTopControlsToContentTop(JNIEnv* env,
jboolean pin_top_controls_to_content_top);
#endif
ErrorPageDelegate* error_page_delegate() { return error_page_delegate_; }
@ -262,6 +264,7 @@ class TabImpl : public Tab,
int GetBottomControlsHeight() override;
bool DoBrowserControlsShrinkRendererSize(
const content::WebContents* web_contents) override;
bool ShouldPinTopControlsToContentTop() override;
bool EmbedsFullscreenWidget() override;
void RequestMediaAccessPermission(
content::WebContents* web_contents,
@ -367,6 +370,7 @@ class TabImpl : public Tab,
std::unique_ptr<BrowserControlsNavigationStateHandler>
browser_controls_navigation_state_handler_;
int top_controls_min_height_ = 0;
bool pin_top_controls_to_content_top_ = false;
// Last value supplied to UpdateBrowserControlsConstraint(). This *constraint*
// can be SHOWN, if for example a modal dialog is forcing the controls to be

@ -15,9 +15,16 @@
<item android:id="@+id/toggle_bottom_view_id"
android:checkable="true"
android:title="Bottom view" />
<item android:id="@+id/toggle_top_view_min_height_id"
android:checkable="true"
android:title="Set top view min height" />
<item android:title="Scrolling">
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/toggle_top_view_min_height_id"
android:checkable="true"
android:title="Set top view min height" />
<item android:id="@+id/toggle_top_view_pinned_to_top_id"
android:checkable="true"
android:title="Pin top view to content top" />
</menu>
</item>
<item android:id="@+id/site_settings_menu_id"
android:title="Site Settings" />
<item android:id="@+id/translate_menu_id"

@ -140,6 +140,7 @@ public class WebLayerShellActivity extends FragmentActivity {
private Runnable mExitFullscreenRunnable;
private View mBottomView;
private int mTopViewMinHeight;
private boolean mTopViewPinnedToContentTop;
private boolean mInIncognitoMode;
@Override
@ -181,8 +182,12 @@ public class WebLayerShellActivity extends FragmentActivity {
popup.getMenuInflater().inflate(R.menu.app_menu, popup.getMenu());
MenuItem bottomMenuItem = popup.getMenu().findItem(R.id.toggle_bottom_view_id);
bottomMenuItem.setChecked(mBottomView != null);
MenuItem topMenuItem = popup.getMenu().findItem(R.id.toggle_top_view_min_height_id);
topMenuItem.setChecked(mTopViewMinHeight > 0);
popup.getMenu()
.findItem(R.id.toggle_top_view_min_height_id)
.setChecked(mTopViewMinHeight > 0);
popup.getMenu()
.findItem(R.id.toggle_top_view_pinned_to_top_id)
.setChecked(mTopViewPinnedToContentTop);
popup.getMenu()
.findItem(R.id.translate_menu_id)
.setVisible(mBrowser.getActiveTab().canTranslate());
@ -219,7 +224,15 @@ public class WebLayerShellActivity extends FragmentActivity {
if (item.getItemId() == R.id.toggle_top_view_min_height_id) {
mTopViewMinHeight = (mTopViewMinHeight == 0) ? 50 : 0;
mBrowser.setTopView(mTopContentsContainer, mTopViewMinHeight, false);
mBrowser.setTopView(
mTopContentsContainer, mTopViewMinHeight, mTopViewPinnedToContentTop);
return true;
}
if (item.getItemId() == R.id.toggle_top_view_pinned_to_top_id) {
mTopViewPinnedToContentTop = !mTopViewPinnedToContentTop;
mBrowser.setTopView(
mTopContentsContainer, mTopViewMinHeight, mTopViewPinnedToContentTop);
return true;
}
@ -294,7 +307,7 @@ public class WebLayerShellActivity extends FragmentActivity {
mProfile.setBooleanSetting(SettingType.UKM_ENABLED, true);
setTabCallbacks(mBrowser.getActiveTab(), fragment);
mBrowser.setTopView(mTopContentsContainer);
mBrowser.setTopView(mTopContentsContainer, /*minHeight=*/0, /*pinToContentTop=*/false);
mTabListCallback = new TabListCallback() {
@Override
public void onActiveTabChanged(Tab activeTab) {