blob: 9976acc543fdbd832b3da4f26bc0cbd333579084 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/web_contents/aura/gesture_nav_simple.h"
#include <utility>
#include "base/macros.h"
#include "cc/paint/paint_flags.h"
#include "content/browser/frame_host/navigation_controller_impl.h"
#include "content/browser/renderer_host/overscroll_controller.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/overscroll_configuration.h"
#include "third_party/skia/include/core/SkDrawLooper.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_delegate.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/gfx/animation/animation_delegate.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/shadow_value.h"
#include "ui/gfx/skia_paint_util.h"
#include "ui/vector_icons/vector_icons.h"
namespace content {
namespace {
// Parameters defining the arrow icon inside the affordance.
const int kArrowSize = 16;
const SkColor kArrowColor = gfx::kGoogleBlue500;
const uint8_t kArrowInitialOpacity = 0x4D;
// The arrow opacity remains constant until progress reaches this threshold,
// then increases quickly as the progress increases beyond the threshold
const float kArrowOpacityProgressThreshold = .9f;
// Parameters defining the background circle of the affordance.
const int kBackgroundRadius = 18;
const SkColor kBackgroundColor = SK_ColorWHITE;
const int kBgShadowOffsetY = 2;
const int kBgShadowBlurRadius = 8;
const SkColor kBgShadowColor = SkColorSetA(SK_ColorBLACK, 0x4D);
// Parameters defining the affordance ripple. The ripple fades in and grows as
// the user drags the affordance until it reaches |kMaxRippleRadius|. If the
// overscroll is successful, the ripple will burst by fading out and growing to
// |kMaxRippleBurstRadius|.
const int kMaxRippleRadius = 54;
const SkColor kRippleColor = SkColorSetA(gfx::kGoogleBlue500, 0x66);
const int kMaxRippleBurstRadius = 72;
const gfx::Tween::Type kBurstAnimationTweenType = gfx::Tween::EASE_IN;
const int kRippleBurstAnimationDuration = 160;
// Offset of the affordance when it is at the maximum distance with content
// border. Since the affordance is initially out of content bounds, this is the
// offset of the farther side of the affordance (which equals 128 + 18).
const int kMaxAffordanceOffset = 146;
// Parameters defining animation when the affordance is aborted.
const gfx::Tween::Type kAbortAnimationTweenType = gfx::Tween::EASE_IN;
const int kAbortAnimationDuration = 300;
bool ShouldNavigateForward(const NavigationController& controller,
OverscrollMode mode) {
return mode == (base::i18n::IsRTL() ? OVERSCROLL_EAST : OVERSCROLL_WEST) &&
controller.CanGoForward();
}
bool ShouldNavigateBack(const NavigationController& controller,
OverscrollMode mode) {
return mode == (base::i18n::IsRTL() ? OVERSCROLL_WEST : OVERSCROLL_EAST) &&
controller.CanGoBack();
}
} // namespace
// This class is responsible for creating, painting, and positioning the layer
// for the gesture nav affordance.
class Affordance : public ui::LayerDelegate, public gfx::AnimationDelegate {
public:
Affordance(GestureNavSimple* owner,
OverscrollMode mode,
const gfx::Rect& content_bounds);
~Affordance() override;
// Sets progress of affordance drag as a value between 0 and 1.
void SetDragProgress(float progress);
// Aborts the affordance and animates it back. This will delete |this|
// instance after the animation.
void Abort();
// Completes the affordance by doing a ripple burst animation. This will
// delete |this| instance after the animation.
void Complete();
// Returns the root layer of the affordance.
ui::Layer* root_layer() const { return root_layer_.get(); }
private:
enum class State { DRAGGING, ABORTING, COMPLETING };
void UpdateTransform();
void SchedulePaint();
void SetAbortProgress(float progress);
void SetCompleteProgress(float progress);
// ui::LayerDelegate:
void OnPaintLayer(const ui::PaintContext& context) override;
void OnDelegatedFrameDamage(const gfx::Rect& damage_rect_in_dip) override;
void OnDeviceScaleFactorChanged(float device_scale_factor) override;
// gfx::AnimationDelegate:
void AnimationEnded(const gfx::Animation* animation) override;
void AnimationProgressed(const gfx::Animation* animation) override;
void AnimationCanceled(const gfx::Animation* animation) override;
GestureNavSimple* const owner_;
const OverscrollMode mode_;
// Root layer of the affordance. This is used to clip the affordance to the
// content bounds.
std::unique_ptr<ui::Layer> root_layer_;
// Layer that actually paints the affordance.
std::unique_ptr<ui::Layer> painted_layer_;
// Arrow image to be used for the affordance.
const gfx::Image image_;
// Values that determine current state of the affordance.
State state_ = State::DRAGGING;
float drag_progress_ = 0.f;
float abort_progress_ = 0.f;
float complete_progress_ = 0.f;
std::unique_ptr<gfx::LinearAnimation> animation_;
DISALLOW_COPY_AND_ASSIGN(Affordance);
};
Affordance::Affordance(GestureNavSimple* owner,
OverscrollMode mode,
const gfx::Rect& content_bounds)
: owner_(owner),
mode_(mode),
root_layer_(base::MakeUnique<ui::Layer>(ui::LAYER_NOT_DRAWN)),
painted_layer_(base::MakeUnique<ui::Layer>(ui::LAYER_TEXTURED)),
image_(gfx::CreateVectorIcon(
mode == OVERSCROLL_EAST ? ui::kBackArrowIcon : ui::kForwardArrowIcon,
kArrowSize,
kArrowColor)) {
DCHECK(mode == OVERSCROLL_EAST || mode == OVERSCROLL_WEST);
DCHECK(!image_.IsEmpty());
root_layer_->SetBounds(content_bounds);
root_layer_->SetMasksToBounds(true);
painted_layer_->SetFillsBoundsOpaquely(false);
int x =
mode_ == OVERSCROLL_EAST
? -kMaxRippleBurstRadius - kBackgroundRadius
: content_bounds.width() - kMaxRippleBurstRadius + kBackgroundRadius;
int y = std::max(0, content_bounds.height() / 2 - kMaxRippleBurstRadius);
painted_layer_->SetBounds(
gfx::Rect(x, y, 2 * kMaxRippleBurstRadius, 2 * kMaxRippleBurstRadius));
painted_layer_->set_delegate(this);
root_layer_->Add(painted_layer_.get());
}
Affordance::~Affordance() {}
void Affordance::SetDragProgress(float progress) {
DCHECK_EQ(State::DRAGGING, state_);
DCHECK_LE(0.f, progress);
DCHECK_GE(1.f, progress);
if (drag_progress_ == progress)
return;
drag_progress_ = progress;
UpdateTransform();
SchedulePaint();
}
void Affordance::Abort() {
DCHECK_EQ(State::DRAGGING, state_);
state_ = State::ABORTING;
animation_.reset(
new gfx::LinearAnimation(drag_progress_ * kAbortAnimationDuration,
gfx::LinearAnimation::kDefaultFrameRate, this));
animation_->Start();
}
void Affordance::Complete() {
DCHECK_EQ(State::DRAGGING, state_);
DCHECK_EQ(1.f, drag_progress_);
state_ = State::COMPLETING;
animation_.reset(
new gfx::LinearAnimation(kRippleBurstAnimationDuration,
gfx::LinearAnimation::kDefaultFrameRate, this));
animation_->Start();
}
void Affordance::UpdateTransform() {
float offset = (1 - abort_progress_) * drag_progress_ * kMaxAffordanceOffset;
gfx::Transform transform;
transform.Translate(mode_ == OVERSCROLL_EAST ? offset : -offset, 0);
painted_layer_->SetTransform(transform);
}
void Affordance::SchedulePaint() {
painted_layer_->SchedulePaint(gfx::Rect(painted_layer_->size()));
}
void Affordance::SetAbortProgress(float progress) {
DCHECK_EQ(State::ABORTING, state_);
DCHECK_LE(0.f, progress);
DCHECK_GE(1.f, progress);
if (abort_progress_ == progress)
return;
abort_progress_ = progress;
UpdateTransform();
SchedulePaint();
}
void Affordance::SetCompleteProgress(float progress) {
DCHECK_EQ(State::COMPLETING, state_);
DCHECK_LE(0.f, progress);
DCHECK_GE(1.f, progress);
if (complete_progress_ == progress)
return;
complete_progress_ = progress;
painted_layer_->SetOpacity(1 - complete_progress_);
SchedulePaint();
}
void Affordance::OnPaintLayer(const ui::PaintContext& context) {
DCHECK(drag_progress_ == 1.f || state_ != State::COMPLETING);
DCHECK(abort_progress_ == 0.f || state_ == State::ABORTING);
DCHECK(complete_progress_ == 0.f || state_ == State::COMPLETING);
ui::PaintRecorder recorder(context, painted_layer_->size());
gfx::Canvas* canvas = recorder.canvas();
gfx::PointF center_point(kMaxRippleBurstRadius, kMaxRippleBurstRadius);
float progress = (1 - abort_progress_) * drag_progress_;
// Draw the ripple.
cc::PaintFlags ripple_flags;
ripple_flags.setAntiAlias(true);
ripple_flags.setStyle(cc::PaintFlags::kFill_Style);
ripple_flags.setColor(kRippleColor);
float ripple_radius;
if (state_ == State::COMPLETING) {
ripple_radius =
kMaxRippleRadius +
complete_progress_ * (kMaxRippleBurstRadius - kMaxRippleRadius);
} else {
ripple_radius =
kBackgroundRadius + progress * (kMaxRippleRadius - kBackgroundRadius);
}
canvas->DrawCircle(center_point, ripple_radius, ripple_flags);
// Draw the arrow background circle with the shadow.
cc::PaintFlags bg_flags;
bg_flags.setAntiAlias(true);
bg_flags.setStyle(cc::PaintFlags::kFill_Style);
bg_flags.setColor(kBackgroundColor);
gfx::ShadowValues shadow;
shadow.emplace_back(gfx::Vector2d(0, kBgShadowOffsetY), kBgShadowBlurRadius,
kBgShadowColor);
bg_flags.setLooper(gfx::CreateShadowDrawLooper(shadow));
canvas->DrawCircle(center_point, kBackgroundRadius, bg_flags);
// Draw the arrow.
float arrow_x = center_point.x() - kArrowSize / 2.f;
float arrow_y = center_point.y() - kArrowSize / 2.f;
// Calculate the offset for the arrow relative to its circular background.
float arrow_x_offset =
(1 - progress) * (-kBackgroundRadius + kArrowSize / 2.f);
arrow_x += mode_ == OVERSCROLL_EAST ? arrow_x_offset : -arrow_x_offset;
// Calculate arrow opacity. Opacity is fixed before progress reaches
// kArrowOpacityProgressThreshold and after that increases linearly to 1;
// essentially, making a quick bump at the end.
uint8_t arrow_opacity = kArrowInitialOpacity;
if (progress > kArrowOpacityProgressThreshold) {
const uint8_t max_opacity_bump = 0xFF - kArrowInitialOpacity;
const float opacity_bump_ratio =
std::min(1.f, (progress - kArrowOpacityProgressThreshold) /
(1.f - kArrowOpacityProgressThreshold));
arrow_opacity +=
static_cast<uint8_t>(opacity_bump_ratio * max_opacity_bump);
}
canvas->DrawImageInt(*image_.ToImageSkia(), static_cast<int>(arrow_x),
static_cast<int>(arrow_y), arrow_opacity);
}
void Affordance::OnDelegatedFrameDamage(const gfx::Rect& damage_rect_in_dip) {}
void Affordance::OnDeviceScaleFactorChanged(float device_scale_factor) {}
void Affordance::AnimationEnded(const gfx::Animation* animation) {
owner_->OnAffordanceAnimationEnded();
}
void Affordance::AnimationProgressed(const gfx::Animation* animation) {
switch (state_) {
case State::DRAGGING:
NOTREACHED();
break;
case State::ABORTING:
SetAbortProgress(gfx::Tween::CalculateValue(
kAbortAnimationTweenType, animation->GetCurrentValue()));
break;
case State::COMPLETING:
SetCompleteProgress(gfx::Tween::CalculateValue(
kBurstAnimationTweenType, animation->GetCurrentValue()));
break;
}
}
void Affordance::AnimationCanceled(const gfx::Animation* animation) {
NOTREACHED();
}
GestureNavSimple::GestureNavSimple(WebContentsImpl* web_contents)
: web_contents_(web_contents),
completion_threshold_(0.f) {}
GestureNavSimple::~GestureNavSimple() {}
void GestureNavSimple::AbortGestureAnimation() {
if (affordance_)
affordance_->Abort();
}
void GestureNavSimple::CompleteGestureAnimation() {
if (affordance_)
affordance_->Complete();
}
void GestureNavSimple::OnAffordanceAnimationEnded() {
affordance_.reset();
}
gfx::Rect GestureNavSimple::GetVisibleBounds() const {
return web_contents_->GetNativeView()->bounds();
}
bool GestureNavSimple::OnOverscrollUpdate(float delta_x, float delta_y) {
if (!affordance_)
return false;
affordance_->SetDragProgress(
std::min(1.f, std::abs(delta_x) / completion_threshold_));
return true;
}
void GestureNavSimple::OnOverscrollComplete(OverscrollMode overscroll_mode) {
CompleteGestureAnimation();
NavigationControllerImpl& controller = web_contents_->GetController();
if (ShouldNavigateForward(controller, overscroll_mode))
controller.GoForward();
else if (ShouldNavigateBack(controller, overscroll_mode))
controller.GoBack();
}
void GestureNavSimple::OnOverscrollModeChange(OverscrollMode old_mode,
OverscrollMode new_mode,
OverscrollSource source) {
NavigationControllerImpl& controller = web_contents_->GetController();
if (!ShouldNavigateForward(controller, new_mode) &&
!ShouldNavigateBack(controller, new_mode)) {
AbortGestureAnimation();
return;
}
aura::Window* window = web_contents_->GetNativeView();
const gfx::Rect& window_bounds = window->bounds();
DCHECK_NE(source, OverscrollSource::NONE);
float start_threshold = GetOverscrollConfig(
source == OverscrollSource::TOUCHPAD
? OVERSCROLL_CONFIG_HORIZ_THRESHOLD_START_TOUCHPAD
: OVERSCROLL_CONFIG_HORIZ_THRESHOLD_START_TOUCHSCREEN);
completion_threshold_ =
window_bounds.width() *
GetOverscrollConfig(OVERSCROLL_CONFIG_HORIZ_THRESHOLD_COMPLETE) -
start_threshold;
affordance_.reset(new Affordance(this, new_mode, window_bounds));
// Adding the affordance as a child of the content window is not sufficient,
// because it is possible for a new layer to be parented on top of the
// affordance layer (e.g. when the navigated-to page is displayed while the
// completion animation is in progress). So instead, it is installed on top of
// the content window as its sibling. Note that the affordance itself makes
// sure that its contents are clipped to the bounds given to it.
ui::Layer* parent = window->layer()->parent();
parent->Add(affordance_->root_layer());
parent->StackAtTop(affordance_->root_layer());
}
} // namespace content