0

Add post-layout callback to the ScrollView

This change adds a new `post-layout` callback to the `ScrollView` to
allow the scroll view to invoke the callback at the end of every layout
operation. This is a useful feature to allow the user to explicitly
scroll the contents to a specific position after a layout and update the
scrollbars. It would allow e.g. to restore the position of the scrolled
view in a large content view when the contents is scrolled out of the
view.

Bug: b:413733359
Change-Id: I9e58136dbbedb3c128b31bba669db060900a87ac
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6498548
Auto-Submit: Leonid Baraz <lbaraz@chromium.org>
Commit-Queue: Keren Zhu <kerenzhu@chromium.org>
Reviewed-by: Keren Zhu <kerenzhu@chromium.org>
Commit-Queue: Leonid Baraz <lbaraz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1454078}
This commit is contained in:
Leonid Baraz
2025-04-30 11:32:35 -07:00
committed by Chromium LUCI CQ
parent 30fe1b3622
commit a4b960a7ad
3 changed files with 38 additions and 0 deletions

@ -834,6 +834,15 @@ void ScrollView::Layout(PassKey) {
if (contents_) {
UpdateOverflowIndicatorVisibility(CurrentOffset());
}
// If registered, run the post-layout callback. This is used to move the
// scroll view contents to the appropriate position that's different from the
// position assigned above.
if (post_layout_callback_) {
const bool layout_needed = needs_layout();
post_layout_callback_.Run(this);
CHECK_EQ(layout_needed, needs_layout());
}
}
bool ScrollView::OnKeyPressed(const ui::KeyEvent& event) {
@ -1362,6 +1371,11 @@ void ScrollView::UpdateOverflowIndicatorVisibility(const gfx::PointF& offset) {
offset.x() < horiz_sb_->GetMaxPosition() && draw_overflow_indicator_);
}
void ScrollView::RegisterPostLayoutCallback(
base::RepeatingCallback<void(ScrollView*)> post_layout_callback) {
post_layout_callback_ = post_layout_callback;
}
View* ScrollView::GetContentsViewportForTest() const {
return contents_viewport_;
}

@ -242,6 +242,15 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController {
bool is_positive) override;
void OnScrollEnded() override;
// Registers a callback to be called after the layout is complete. This
// callback can be used e.g. to scroll the view to the appropriate position
// in the contents by explicitly calling `ScrollToOffset` or `ScrollByOffset`
// and to update the scrollbars to reflect the new position.
// The callback should not trigger any new layouts on the scroll view,
// otherwise it will lead to a CHECK failure.
void RegisterPostLayoutCallback(
base::RepeatingCallback<void(ScrollView*)> post_layout_callback);
bool is_scrolling() const {
return horiz_sb_->is_scrolling() || vert_sb_->is_scrolling();
}
@ -399,6 +408,9 @@ class VIEWS_EXPORT ScrollView : public View, public ScrollBarController {
// Scrolling callbacks.
ScrollViewCallbackList on_contents_scrolled_;
ScrollViewCallbackList on_contents_scroll_ended_;
// Post-layout callback.
base::RepeatingCallback<void(ScrollView*)> post_layout_callback_;
};
// When building with GCC this ensures that an instantiation of the

@ -17,6 +17,7 @@
#include "base/task/single_thread_task_runner.h"
#include "base/test/gtest_util.h"
#include "base/test/icu_test_util.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/timer/timer.h"
@ -563,6 +564,17 @@ TEST_F(ScrollViewTest, BoundedViewportSizedToFit) {
EXPECT_EQ(96, contents->width());
}
// Verifies that the scroll view calls post-layout callback on every layout.
TEST_F(ScrollViewTest, LayoutCallbackCalledOnEveryLayout) {
auto mock_post_layout_cb_ =
base::MockCallback<base::RepeatingCallback<void(ScrollView*)>>();
scroll_view_->RegisterPostLayoutCallback(mock_post_layout_cb_.Get());
EXPECT_CALL(mock_post_layout_cb_, Run(scroll_view_.get())).Times(1);
InstallContents();
ASSERT_FALSE(scroll_view_->GetContentsBounds().IsEmpty());
views::test::RunScheduledLayout(scroll_view_.get());
}
// Verifies that the vertical scrollbar does not unnecessarily appear for a
// contents whose height always matches the height of the viewport.
TEST_F(ScrollViewTest, VerticalScrollbarDoesNotAppearUnnecessarily) {