chromium/ui/android/overscroll_refresh.cc

// 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 "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_(DISABLED),
      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_(DISABLED),
      edge_width_(kDefaultNavigationEdgeWidth * 1.f),
      handler_(nullptr) {}

OverscrollRefresh::~OverscrollRefresh() {
}

void OverscrollRefresh::Reset() {
  scroll_consumption_state_ = DISABLED;
  cumulative_scroll_.set_x(0);
  cumulative_scroll_.set_y(0);
  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_ = AWAITING_SCROLL_UPDATE_ACK;
}

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) {
  if (scroll_consumption_state_ != AWAITING_SCROLL_UPDATE_ACK)
    return;

  float ydelta = cumulative_scroll_.y();
  float xdelta = cumulative_scroll_.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::NONE;
  std::optional<BackGestureEventSwipeEdge> overscroll_edge;
  if (in_y_direction) {
    if (behavior.y != cc::OverscrollBehavior::Type::kAuto) {
      Reset();
      return;
    }
    // Pull-to-refresh. Check overscroll-behavior-y
    if (ydelta > 0) {
      type = OverscrollAction::PULL_TO_REFRESH;
    } else if (scrolled_to_bottom_) {  // ydelta < 0
      type = OverscrollAction::PULL_FROM_BOTTOM_EDGE;
    }
  } else if (in_x_direction &&
             (scroll_begin_x_ < edge_width_ ||
              viewport_width_ - scroll_begin_x_ < edge_width_)) {
    // Swipe-to-navigate. Check overscroll-behavior-x
    if (behavior.x != cc::OverscrollBehavior::Type::kAuto) {
      Reset();
      return;
    }
    type = OverscrollAction::HISTORY_NAVIGATION;
    overscroll_edge = xdelta < 0 ? BackGestureEventSwipeEdge::RIGHT
                                 : BackGestureEventSwipeEdge::LEFT;
  }

  CHECK_EQ(overscroll_edge.has_value(),
           type == OverscrollAction::HISTORY_NAVIGATION);

  if (type != OverscrollAction::NONE) {
    scroll_consumption_state_ =
        handler_->PullStart(type, overscroll_edge) ? ENABLED : DISABLED;
  }
}

bool OverscrollRefresh::WillHandleScrollUpdate(
    const gfx::Vector2dF& scroll_delta) {
  switch (scroll_consumption_state_) {
    case DISABLED:
      return false;

    case AWAITING_SCROLL_UPDATE_ACK:
      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_ = DISABLED;
          return false;
        }
      }
      cumulative_scroll_.Add(scroll_delta);
      return false;

    case ENABLED:
      handler_->PullUpdate(scroll_delta.x(), scroll_delta.y());
      return true;
  }

  NOTREACHED_IN_MIGRATION()
      << "Invalid overscroll state: " << scroll_consumption_state_;
  return false;
}

void OverscrollRefresh::ReleaseWithoutActivation() {
  bool allow_activation = false;
  Release(allow_activation);
}

bool OverscrollRefresh::IsActive() const {
  return scroll_consumption_state_ == ENABLED;
}

bool OverscrollRefresh::IsAwaitingScrollUpdateAck() const {
  return scroll_consumption_state_ == AWAITING_SCROLL_UPDATE_ACK;
}

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_ == ENABLED)
    handler_->PullRelease(allow_refresh);
  scroll_consumption_state_ = DISABLED;
  cumulative_scroll_.set_x(0);
  cumulative_scroll_.set_y(0);
}

}  // namespace ui