blob: 3770f3e886998f1e0957c9cdd0cc96dbba9269b1 [file]
// 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 <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/notreached.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 fling in the opposite direction of current active
// overscroll action (e.g. a small upward fling after pulling down to refresh,
// quite common during a slow scroll)
const float kMinFlingVelocityForActivation = -500.f;
// Minimum velocity in the active navigation direction required to force-trigger
// navigation on gesture end. According to UX, the most common scale factor
// is 1.625, so 1100 dp(fling-to-start threshold used on chrome desktop) is
// about 1788 pixel.
const float kMinFlingVelocityForForceActivation = 1788.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),
edge_width_(edge_width),
handler_(handler) {
DCHECK(handler);
}
OverscrollRefresh::OverscrollRefresh()
: scrolled_to_top_(true),
scrolled_to_bottom_(false),
overflow_y_hidden_(false),
edge_width_(kDefaultNavigationEdgeWidth * 1.f),
handler_(nullptr) {}
OverscrollRefresh::~OverscrollRefresh() {
}
void OverscrollRefresh::Reset() {
scroll_state_.Reset();
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_state_.StartAwaitingAck();
}
void OverscrollRefresh::OnScrollEnd(const gfx::Vector2dF& scroll_velocity) {
// Reached when a user scrolls but not overscrolls
if (!IsActive()) {
Release(OverscrollActivationStatus::kReset);
return;
}
Release(GetActivationStatus(scroll_velocity));
}
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 (!IsAwaitingScrollUpdateAck()) {
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) {
// Check overscroll-behavior-y and source device: pull-to-refresh should
// only work on touchscreen overscrolls, in particular, not by touchpad or
// mousewheel scrolls.
if (!behavior.PropagatesYScroll() ||
source_device != blink::WebGestureDevice::kTouchscreen) {
Reset();
return;
}
// Pull-to-refresh
if (ydelta > 0) {
type = OverscrollAction::kPullToRefresh;
} else if (scrolled_to_bottom_) { // ydelta < 0
type = OverscrollAction::kPullFromBottomEdge;
}
} else if (in_x_direction) {
DCHECK_GE(viewport_width_, 0);
DCHECK_LE(scroll_begin_x_, viewport_width_);
DCHECK_GE(scroll_begin_x_, 0);
bool scroll_from_edge = scroll_begin_x_ < edge_width_ ||
viewport_width_ - scroll_begin_x_ < edge_width_;
bool touchpad_swipe_to_navigate =
(source_device == blink::WebGestureDevice::kTouchpad &&
touchpad_overscroll_history_navigation_enabled_ &&
base::FeatureList::IsEnabled(
ui::kAndroidTouchpadOverscrollHistoryNavigation));
// Check overscroll-behavior-x and other activation conditions for history
// navigation depending on the input device:
// - touchscreen: iff system is not in gesture navigation mode;
// only activated by swipes near the horizontal edges
// - touchpad (possibly converted from mousewheel): iff the feature is
// enabled; activated by swipes everywhere
if (!(behavior.PropagatesXScroll() &&
((scroll_from_edge && !is_gesture_navigation_mode_) ||
touchpad_swipe_to_navigate))) {
Reset();
return;
}
// Swipe-to-navigate.
type = OverscrollAction::kHistoryNavigation;
overscroll_edge = xdelta < 0 ? BackGestureEventSwipeEdge::RIGHT
: BackGestureEventSwipeEdge::LEFT;
}
CHECK_EQ(overscroll_edge.has_value(),
type == OverscrollAction::kHistoryNavigation);
if (type != OverscrollAction::kNone) {
if (handler_->PullStart(type, overscroll_edge)) {
scroll_state_.SetEnabled(
ActiveAction{type, overscroll_edge, source_device});
} else {
scroll_state_.Reset();
}
}
}
void OverscrollRefresh::MaybeDisableScrollConsumption(
const gfx::Vector2dF& scroll_delta) {
if (IsActive() && scroll_state_.GetAction().action ==
OverscrollAction::kHistoryNavigation) {
return;
}
// This check is meant for kPullToRefresh or kPullFromBottomEdge.
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_state_.Reset();
}
}
}
bool OverscrollRefresh::WillHandleScrollUpdate(
const gfx::Vector2dF& scroll_delta) {
if (scroll_state_.IsDisabled()) {
return false;
}
if (IsAwaitingScrollUpdateAck()) {
MaybeDisableScrollConsumption(scroll_delta);
return false;
}
if (IsActive()) {
handler_->PullUpdate(scroll_delta.x(), scroll_delta.y());
return true;
}
NOTREACHED() << "Invalid overscroll state";
}
void OverscrollRefresh::ReleaseWithoutActivation() {
Release(OverscrollActivationStatus::kReset);
}
bool OverscrollRefresh::IsActive() const {
return scroll_state_.IsEnabled();
}
bool OverscrollRefresh::IsAwaitingScrollUpdateAck() const {
return scroll_state_.IsAwaitingAck();
}
void OverscrollRefresh::OnFrameUpdated(float view_width,
float scrollable_viewport_height,
const gfx::PointF& content_scroll_offset,
const gfx::SizeF& content_size,
bool root_overflow_y_hidden) {
viewport_width_ = view_width;
scrolled_to_top_ = content_scroll_offset.y() == 0;
if (base::FeatureList::IsEnabled(kReportBottomOverscrolls)) {
scrolled_to_bottom_ =
content_size.height() <=
content_scroll_offset.y() + scrollable_viewport_height;
}
overflow_y_hidden_ = root_overflow_y_hidden;
}
void OverscrollRefresh::SetTouchpadOverscrollHistoryNavigation(bool enabled) {
touchpad_overscroll_history_navigation_enabled_ = enabled;
}
void OverscrollRefresh::SetIsGestureNavigationMode(
bool is_gesture_navigation_mode) {
is_gesture_navigation_mode_ = is_gesture_navigation_mode;
}
void OverscrollRefresh::Release(OverscrollActivationStatus activation_status) {
if (scroll_state_.IsEnabled()) {
handler_->PullRelease(activation_status);
}
scroll_state_.Reset();
}
float OverscrollRefresh::GetVelocityInActiveActionDirection(
const gfx::Vector2dF& velocity) {
const ActiveAction& active_action = scroll_state_.GetAction();
switch (active_action.action) {
case OverscrollAction::kPullToRefresh:
return velocity.y();
case OverscrollAction::kPullFromBottomEdge:
return -velocity.y();
case OverscrollAction::kHistoryNavigation:
if (active_action.edge == BackGestureEventSwipeEdge::LEFT) {
return velocity.x();
} else {
return -velocity.x();
}
default:
NOTREACHED();
}
}
OverscrollActivationStatus OverscrollRefresh::GetActivationStatus(
const gfx::Vector2dF& velocity) {
float velocity_in_direction = GetVelocityInActiveActionDirection(velocity);
const ActiveAction& active_action = scroll_state_.GetAction();
switch (active_action.action) {
case OverscrollAction::kHistoryNavigation: {
if (active_action.device == blink::WebGestureDevice::kTouchpad &&
velocity_in_direction > kMinFlingVelocityForForceActivation) {
return OverscrollActivationStatus::kForceActivation;
}
[[fallthrough]];
}
case OverscrollAction::kPullToRefresh:
case OverscrollAction::kPullFromBottomEdge:
return velocity_in_direction > kMinFlingVelocityForActivation
? OverscrollActivationStatus::kAllowActivation
: OverscrollActivationStatus::kDisallowActivation;
default:
NOTREACHED();
}
}
} // namespace ui