| // 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. |
| |
| #include "ui/android/overscroll_refresh.h" |
| |
| #include <ostream> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/feature_list.h" |
| #include "base/notreached.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "cc/input/overscroll_behavior.h" |
| #include "ui/android/overscroll_refresh_handler.h" |
| #include "ui/android/ui_android_features.h" |
| #include "ui/events/back_gesture_event.h" |
| #include "ui/gfx/geometry/point_f.h" |
| |
| namespace ui { |
| namespace { |
| |
| // Experimentally determined constant used to allow activation even if touch |
| // release results in a small upward fling (quite common during a slow scroll). |
| const float kMinFlingVelocityForActivation = -500.f; |
| |
| // Weighted value used to determine whether a scroll should trigger vertical |
| // scroll or horizontal navigation. |
| const float kWeightAngle30 = 1.73f; |
| |
| } // namespace |
| |
| OverscrollRefresh::OverscrollRefresh(OverscrollRefreshHandler* handler, |
| float edge_width) |
| : scrolled_to_top_(true), |
| scrolled_to_bottom_(false), |
| top_at_scroll_start_(true), |
| bottom_at_scroll_start_(false), |
| overflow_y_hidden_(false), |
| scroll_consumption_state_(ScrollConsumptionState::kDisabled), |
| edge_width_(edge_width), |
| handler_(handler) { |
| DCHECK(handler); |
| } |
| |
| OverscrollRefresh::OverscrollRefresh() |
| : scrolled_to_top_(true), |
| scrolled_to_bottom_(false), |
| overflow_y_hidden_(false), |
| scroll_consumption_state_(ScrollConsumptionState::kDisabled), |
| edge_width_(kDefaultNavigationEdgeWidth * 1.f), |
| handler_(nullptr) {} |
| |
| OverscrollRefresh::~OverscrollRefresh() { |
| } |
| |
| void OverscrollRefresh::Reset() { |
| scroll_consumption_state_ = ScrollConsumptionState::kDisabled; |
| handler_->PullReset(); |
| } |
| |
| void OverscrollRefresh::OnScrollBegin(const gfx::PointF& pos) { |
| scroll_begin_x_ = pos.x(); |
| scroll_begin_y_ = pos.y(); |
| top_at_scroll_start_ = scrolled_to_top_; |
| bottom_at_scroll_start_ = scrolled_to_bottom_; |
| ReleaseWithoutActivation(); |
| scroll_consumption_state_ = ScrollConsumptionState::kAwaitingScrollUpdateAck; |
| } |
| |
| void OverscrollRefresh::OnScrollEnd(const gfx::Vector2dF& scroll_velocity) { |
| bool allow_activation = scroll_velocity.y() > kMinFlingVelocityForActivation; |
| Release(allow_activation); |
| } |
| |
| void OverscrollRefresh::OnOverscrolled(const cc::OverscrollBehavior& behavior, |
| gfx::Vector2dF accumulated_overscroll, |
| blink::WebGestureDevice source_device) { |
| // `accumulated_overscroll` is in the opposite direction of the scroll_deltas |
| // sent to the renderer. |
| MaybeDisableScrollConsumption(-accumulated_overscroll); |
| if (scroll_consumption_state_ != |
| ScrollConsumptionState::kAwaitingScrollUpdateAck) { |
| return; |
| } |
| float ydelta = -accumulated_overscroll.y(); |
| float xdelta = -accumulated_overscroll.x(); |
| bool in_y_direction = std::abs(ydelta) > std::abs(xdelta); |
| bool in_x_direction = std::abs(ydelta) * kWeightAngle30 < std::abs(xdelta); |
| OverscrollAction type = OverscrollAction::kNone; |
| std::optional<BackGestureEventSwipeEdge> overscroll_edge; |
| if (in_y_direction) { |
| if (behavior.y != cc::OverscrollBehavior::Type::kAuto || |
| // Pull-to-refresh should only work on touchscreen overscrolls. In |
| // particular, not by touchpad or mousewheel scrolls. |
| source_device != blink::WebGestureDevice::kTouchscreen) { |
| Reset(); |
| return; |
| } |
| // Pull-to-refresh. Check overscroll-behavior-y |
| if (ydelta > 0) { |
| type = OverscrollAction::kPullToRefresh; |
| } else if (scrolled_to_bottom_) { // ydelta < 0 |
| type = OverscrollAction::kPullFromBottomEdge; |
| } |
| } else if (in_x_direction) { |
| DCHECK(source_device == blink::WebGestureDevice::kTouchpad || |
| source_device == blink::WebGestureDevice::kTouchscreen); |
| DCHECK_GE(viewport_width_, 0); |
| bool scroll_from_edge = scroll_begin_x_ < edge_width_ || |
| viewport_width_ - scroll_begin_x_ < edge_width_; |
| // Swipe-to-navigate. Check overscroll-behavior-x and scroll start position. |
| if (behavior.x != cc::OverscrollBehavior::Type::kAuto || |
| !scroll_from_edge) { |
| Reset(); |
| return; |
| } |
| type = OverscrollAction::kHistoryNavigation; |
| overscroll_edge = xdelta < 0 ? BackGestureEventSwipeEdge::RIGHT |
| : BackGestureEventSwipeEdge::LEFT; |
| } |
| |
| CHECK_EQ(overscroll_edge.has_value(), |
| type == OverscrollAction::kHistoryNavigation); |
| |
| if (type != OverscrollAction::kNone) { |
| scroll_consumption_state_ = handler_->PullStart(type, overscroll_edge) |
| ? ScrollConsumptionState::kEnabled |
| : ScrollConsumptionState::kDisabled; |
| } |
| } |
| |
| void OverscrollRefresh::MaybeDisableScrollConsumption( |
| const gfx::Vector2dF& scroll_delta) { |
| if (std::abs(scroll_delta.y()) > std::abs(scroll_delta.x())) { |
| // Check applies for the pull-to-refresh. |
| bool is_pull_to_refresh = scroll_delta.y() > 0 && top_at_scroll_start_; |
| // Check applies for the pull-from-bottom-edge. |
| bool is_pull_from_bottom_edge = scroll_delta.y() < 0 && |
| bottom_at_scroll_start_ && |
| !top_at_scroll_start_; |
| |
| // If the activation shouldn't have happened, stop here. |
| if (overflow_y_hidden_ || |
| (!is_pull_to_refresh && !is_pull_from_bottom_edge)) { |
| scroll_consumption_state_ = ScrollConsumptionState::kDisabled; |
| } |
| } |
| } |
| |
| bool OverscrollRefresh::WillHandleScrollUpdate( |
| const gfx::Vector2dF& scroll_delta) { |
| switch (scroll_consumption_state_) { |
| case ScrollConsumptionState::kDisabled: |
| return false; |
| |
| case ScrollConsumptionState::kAwaitingScrollUpdateAck: |
| MaybeDisableScrollConsumption(scroll_delta); |
| return false; |
| |
| case ScrollConsumptionState::kEnabled: |
| handler_->PullUpdate(scroll_delta.x(), scroll_delta.y()); |
| return true; |
| } |
| |
| NOTREACHED() << "Invalid overscroll state: " |
| << base::to_underlying(scroll_consumption_state_); |
| } |
| |
| void OverscrollRefresh::ReleaseWithoutActivation() { |
| bool allow_activation = false; |
| Release(allow_activation); |
| } |
| |
| bool OverscrollRefresh::IsActive() const { |
| return scroll_consumption_state_ == ScrollConsumptionState::kEnabled; |
| } |
| |
| bool OverscrollRefresh::IsAwaitingScrollUpdateAck() const { |
| return scroll_consumption_state_ == ScrollConsumptionState::kAwaitingScrollUpdateAck; |
| } |
| |
| void OverscrollRefresh::OnFrameUpdated(const gfx::SizeF& viewport_size, |
| const gfx::PointF& content_scroll_offset, |
| const gfx::SizeF& content_size, |
| bool root_overflow_y_hidden) { |
| viewport_width_ = viewport_size.width(); |
| scrolled_to_top_ = content_scroll_offset.y() == 0; |
| if (base::FeatureList::IsEnabled(kReportBottomOverscrolls)) { |
| scrolled_to_bottom_ = content_size.height() <= |
| content_scroll_offset.y() + viewport_size.height(); |
| } |
| overflow_y_hidden_ = root_overflow_y_hidden; |
| } |
| |
| void OverscrollRefresh::Release(bool allow_refresh) { |
| if (scroll_consumption_state_ == ScrollConsumptionState::kEnabled) |
| handler_->PullRelease(allow_refresh); |
| scroll_consumption_state_ = ScrollConsumptionState::kDisabled; |
| } |
| |
| } // namespace ui |