chromium/ui/android/overscroll_glow.cc

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ui/android/overscroll_glow.h"

#include <stddef.h>

#include "cc/slim/layer.h"
#include "ui/android/edge_effect.h"
#include "ui/android/window_android_compositor.h"

using std::max;
using std::min;

namespace ui {

namespace {

const float kEpsilon = 1e-3f;

bool IsApproxZero(float value) {
  return std::abs(value) < kEpsilon;
}

gfx::Vector2dF ZeroSmallComponents(gfx::Vector2dF vector) {
  if (IsApproxZero(vector.x()))
    vector.set_x(0);
  if (IsApproxZero(vector.y()))
    vector.set_y(0);
  return vector;
}

}  // namespace

OverscrollGlow::OverscrollGlow(OverscrollGlowClient* client)
    : client_(client),
      edge_offsets_(),
      initialized_(false),
      allow_horizontal_overscroll_(true),
      allow_vertical_overscroll_(true) {
  DCHECK(client);
}

OverscrollGlow::~OverscrollGlow() {
  Detach();
  client_ = nullptr;
}

void OverscrollGlow::Reset() {
  if (!initialized_)
    return;
  Detach();
  for (size_t i = 0; i < EDGE_COUNT; ++i)
    edge_effects_[i]->Finish();
}

bool OverscrollGlow::IsActive() const {
  if (!initialized_)
    return false;
  for (size_t i = 0; i < EDGE_COUNT; ++i) {
    if (!edge_effects_[i]->IsFinished())
      return true;
  }
  return false;
}

float OverscrollGlow::GetVisibleAlpha() const {
  float max_alpha = 0;
  for (size_t i = 0; i < EDGE_COUNT; ++i) {
    if (!edge_effects_[i]->IsFinished())
      max_alpha = std::max(max_alpha, edge_effects_[i]->GetAlpha());
  }
  return std::min(max_alpha, 1.f);
}

bool OverscrollGlow::OnOverscrolled(base::TimeTicks current_time,
                                    gfx::Vector2dF accumulated_overscroll,
                                    gfx::Vector2dF overscroll_delta,
                                    gfx::Vector2dF velocity,
                                    gfx::Vector2dF overscroll_location) {
  // The size of the glow determines the relative effect of the inputs; an
  // empty-sized effect is effectively disabled.
  if (viewport_size_.IsEmpty())
    return false;

  if (!allow_horizontal_overscroll_) {
    overscroll_delta.set_x(0);
    velocity.set_x(0);
  }
  if (!allow_vertical_overscroll_) {
    overscroll_delta.set_y(0);
    velocity.set_y(0);
  }

  // Ignore sufficiently small values that won't meaningfuly affect animation.
  overscroll_delta = ZeroSmallComponents(overscroll_delta);
  if (overscroll_delta.IsZero()) {
    if (initialized_)
      Release(current_time);
    return CheckNeedsAnimate();
  }

  if (!InitializeIfNecessary())
    return false;

  gfx::Vector2dF old_overscroll = accumulated_overscroll - overscroll_delta;
  bool x_overscroll_started =
      !IsApproxZero(overscroll_delta.x()) && IsApproxZero(old_overscroll.x());
  bool y_overscroll_started =
      !IsApproxZero(overscroll_delta.y()) && IsApproxZero(old_overscroll.y());

  velocity = ZeroSmallComponents(velocity);
  if (!velocity.IsZero())
    Absorb(current_time, velocity, x_overscroll_started, y_overscroll_started);
  else
    Pull(current_time, overscroll_delta, overscroll_location);

  return CheckNeedsAnimate();
}

bool OverscrollGlow::Animate(base::TimeTicks current_time,
                             cc::slim::Layer* parent_layer) {
  DCHECK(parent_layer);
  if (!CheckNeedsAnimate())
    return false;

  UpdateLayerAttachment(parent_layer);

  for (size_t i = 0; i < EDGE_COUNT; ++i) {
    if (edge_effects_[i]->Update(current_time)) {
      EdgeEffect::Edge edge = static_cast<EdgeEffect::Edge>(i);
      edge_effects_[i]->ApplyToLayers(edge, viewport_size_, edge_offsets_[i]);
    }
  }

  return CheckNeedsAnimate();
}

void OverscrollGlow::OnFrameUpdated(const gfx::SizeF& viewport_size,
                                    const gfx::SizeF& content_size,
                                    const gfx::PointF& content_scroll_offset) {
  viewport_size_ = viewport_size;
  edge_offsets_[EdgeEffect::EDGE_TOP] = -content_scroll_offset.y();
  edge_offsets_[EdgeEffect::EDGE_LEFT] = -content_scroll_offset.x();
  edge_offsets_[EdgeEffect::EDGE_BOTTOM] = content_size.height() -
                                           content_scroll_offset.y() -
                                           viewport_size.height();
  edge_offsets_[EdgeEffect::EDGE_RIGHT] =
      content_size.width() - content_scroll_offset.x() - viewport_size.width();

  // Only allow overscroll on scrollable axes, matching platform behavior.
  allow_horizontal_overscroll_ =
      std::ceil(viewport_size_.width()) < std::floor(content_size.width());
  allow_vertical_overscroll_ =
      std::ceil(viewport_size_.height()) < std::floor(content_size.height());
}

bool OverscrollGlow::CheckNeedsAnimate() {
  if (!initialized_) {
    Detach();
    return false;
  }

  if (IsActive())
    return true;

  Detach();
  return false;
}

void OverscrollGlow::UpdateLayerAttachment(cc::slim::Layer* parent) {
  DCHECK(parent);
  if (!root_layer_.get())
    return;

  if (!CheckNeedsAnimate())
    return;

  if (root_layer_->parent() != parent)
    parent->AddChild(root_layer_);

  for (size_t i = 0; i < EDGE_COUNT; ++i)
    edge_effects_[i]->SetParent(root_layer_.get());
}

void OverscrollGlow::Detach() {
  if (root_layer_.get())
    root_layer_->RemoveFromParent();
}

bool OverscrollGlow::InitializeIfNecessary() {
  if (initialized_)
    return true;

  if (client_ == nullptr)
    return false;

  DCHECK(!root_layer_.get());
  root_layer_ = cc::slim::Layer::Create();
  for (size_t i = 0; i < EDGE_COUNT; ++i) {
    edge_effects_[i] = client_->CreateEdgeEffect();
    DCHECK(edge_effects_[i]);
  }

  initialized_ = true;
  return true;
}

void OverscrollGlow::Pull(base::TimeTicks current_time,
                          const gfx::Vector2dF& overscroll_delta,
                          const gfx::Vector2dF& overscroll_location) {
  DCHECK(initialized_);
  DCHECK(!overscroll_delta.IsZero());
  DCHECK(!viewport_size_.IsEmpty());
  const float inv_width = 1.f / viewport_size_.width();
  const float inv_height = 1.f / viewport_size_.height();

  gfx::Vector2dF overscroll_pull =
      gfx::ScaleVector2d(overscroll_delta, inv_width, inv_height);
  const float edge_pull[EDGE_COUNT] = {
      min(overscroll_pull.y(), 0.f),  // Top
      min(overscroll_pull.x(), 0.f),  // Left
      max(overscroll_pull.y(), 0.f),  // Bottom
      max(overscroll_pull.x(), 0.f)   // Right
  };

  gfx::Vector2dF displacement =
      gfx::ScaleVector2d(overscroll_location, inv_width, inv_height);
  displacement.set_x(max(0.f, min(1.f, displacement.x())));
  displacement.set_y(max(0.f, min(1.f, displacement.y())));
  const float edge_displacement[EDGE_COUNT] = {
      1.f - displacement.x(),  // Top
      displacement.y(),        // Left
      displacement.x(),        // Bottom
      1.f - displacement.y()   // Right
  };

  for (size_t i = 0; i < EDGE_COUNT; ++i) {
    if (!edge_pull[i])
      continue;

    edge_effects_[i]->Pull(
        current_time, std::abs(edge_pull[i]), edge_displacement[i]);
    GetOppositeEdge(i)->Release(current_time);
  }
}

void OverscrollGlow::Absorb(base::TimeTicks current_time,
                            const gfx::Vector2dF& velocity,
                            bool x_overscroll_started,
                            bool y_overscroll_started) {
  DCHECK(initialized_);
  DCHECK(!velocity.IsZero());

  // Only trigger on initial overscroll at a non-zero velocity
  const float overscroll_velocities[EDGE_COUNT] = {
      y_overscroll_started ? min(velocity.y(), 0.f) : 0,  // Top
      x_overscroll_started ? min(velocity.x(), 0.f) : 0,  // Left
      y_overscroll_started ? max(velocity.y(), 0.f) : 0,  // Bottom
      x_overscroll_started ? max(velocity.x(), 0.f) : 0   // Right
  };

  for (size_t i = 0; i < EDGE_COUNT; ++i) {
    if (!overscroll_velocities[i])
      continue;

    edge_effects_[i]->Absorb(current_time, std::abs(overscroll_velocities[i]));
    GetOppositeEdge(i)->Release(current_time);
  }
}

void OverscrollGlow::Release(base::TimeTicks current_time) {
  DCHECK(initialized_);
  for (size_t i = 0; i < EDGE_COUNT; ++i)
    edge_effects_[i]->Release(current_time);
}

EdgeEffect* OverscrollGlow::GetOppositeEdge(int edge_index) {
  DCHECK(initialized_);
  return edge_effects_[(edge_index + 2) % EDGE_COUNT].get();
}

}  // namespace ui