Index: content/browser/android/overscroll_refresh.cc |
diff --git a/content/browser/android/overscroll_refresh.cc b/content/browser/android/overscroll_refresh.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..49145143e7215e9452cd8c25c6395c99487a6a9e |
--- /dev/null |
+++ b/content/browser/android/overscroll_refresh.cc |
@@ -0,0 +1,418 @@ |
+// 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/android/overscroll_refresh.h" |
+ |
+#include "cc/layers/ui_resource_layer.h" |
+#include "cc/trees/layer_tree_host.h" |
+#include "content/browser/android/animation_utils.h" |
+#include "ui/base/android/system_ui_resource_manager.h" |
+ |
+using std::max; |
+using std::min; |
+ |
+namespace content { |
+namespace { |
+ |
+const ui::SystemUIResourceType kIdleResourceType = ui::OVERSCROLL_REFRESH_IDLE; |
+const ui::SystemUIResourceType kActiveResourceType = |
+ ui::OVERSCROLL_REFRESH_ACTIVE; |
+ |
+// Animation duration after the effect is released without triggering a refresh. |
+const int kRecedeTimeMs = 300; |
+ |
+// Animation duration after the effect is released and triggers a refresh. |
+const int kActivationTimeMs = 1000; |
+ |
+// Max animation duration after the effect is released and triggers a refresh. |
+const int kMaxActivationTimeMs = kActivationTimeMs * 3; |
+ |
+// Animation duration after the refresh activated phase has completed. |
+const int kActivationRecedeTimeMs = 300; |
+ |
+// Input threshold required to activate the refresh. |
+const float kPullActivationThreshold = .35f; |
+ |
+// Input threshold required to start glowing. |
+const float kGlowActivationThreshold = kPullActivationThreshold * 0.85f; |
+ |
+// Useful for avoiding accidental triggering when a scroll janks (is delayed), |
+// capping the impulse per event. |
+const float kMaxNormalizedDeltaPerPull = kPullActivationThreshold / 4.f; |
+ |
+// Maximum offset of the effect relative to the content size. |
+const float kMaxRelativeOffset = .3f; |
+ |
+// Minimum alpha for the effect layer. |
+const float kMinAlpha = 0.25f; |
+ |
+// Controls spin velocity. |
+const float kPullRotationMultiplier = 180.f * (1.f / kPullActivationThreshold); |
+ |
+// 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; |
+ |
+const float kEpsilon = 0.005f; |
+ |
+void UpdateLayer(cc::UIResourceLayer* layer, |
+ cc::Layer* parent, |
+ cc::UIResourceId res_id, |
+ const gfx::SizeF& viewport_size, |
+ float relative_offset, |
+ float opacity, |
+ float rotation) { |
+ if (layer->parent() != parent) |
+ parent->AddChild(layer); |
+ |
+ if (!layer->layer_tree_host()) |
+ return; |
+ |
+ // An empty window size, while meaningless, is also relatively harmless, and |
+ // will simply prevent any drawing of the layers. |
+ if (viewport_size.IsEmpty()) { |
+ layer->SetIsDrawable(false); |
+ return; |
+ } |
+ |
+ if (!res_id) { |
+ layer->SetIsDrawable(false); |
+ return; |
+ } |
+ |
+ if (opacity == 0) { |
+ layer->SetIsDrawable(false); |
+ layer->SetOpacity(0); |
+ return; |
+ } |
+ |
+ gfx::Size image_size = layer->layer_tree_host()->GetUIResourceSize(res_id); |
+ layer->SetUIResourceId(res_id); |
+ layer->SetIsDrawable(true); |
+ layer->SetTransformOrigin( |
+ gfx::Point3F(image_size.width() * 0.5f, image_size.height() * 0.5f, 0)); |
+ layer->SetBounds(image_size); |
+ layer->SetContentsOpaque(false); |
+ layer->SetOpacity(Clamp(opacity, 0.f, 1.f)); |
+ |
+ float min_viewport_size = min(viewport_size.width(), viewport_size.height()); |
+ float offset_x = (viewport_size.width() - image_size.width()) * 0.5f; |
+ float offset_y = |
+ Damp(relative_offset, 1.2f) * min_viewport_size * kMaxRelativeOffset - |
+ image_size.height(); |
+ gfx::Transform transform; |
+ transform.Translate(offset_x, offset_y); |
+ transform.Rotate(rotation); |
+ layer->SetTransform(transform); |
+} |
+ |
+} // namespace |
+ |
+class OverscrollRefresh::Effect { |
+ public: |
+ Effect(ui::SystemUIResourceManager* resource_manager) |
+ : resource_manager_(resource_manager), |
+ idle_layer_(cc::UIResourceLayer::Create()), |
+ active_layer_(cc::UIResourceLayer::Create()), |
+ idle_alpha_(0), |
+ active_alpha_(0), |
+ offset_(0), |
+ rotation_(0), |
+ idle_alpha_start_(0), |
+ idle_alpha_finish_(0), |
+ active_alpha_start_(0), |
+ active_alpha_finish_(0), |
+ offset_start_(0), |
+ offset_finish_(0), |
+ rotation_start_(0), |
+ rotation_finish_(0), |
+ state_(STATE_IDLE) { |
+ idle_layer_->SetIsDrawable(false); |
+ active_layer_->SetIsDrawable(false); |
+ } |
+ |
+ ~Effect() { Detach(); } |
+ |
+ void Pull(float normalized_delta) { |
+ if (state_ != STATE_PULL) |
+ offset_ = 0; |
+ |
+ state_ = STATE_PULL; |
+ |
+ normalized_delta = Clamp(normalized_delta, -kMaxNormalizedDeltaPerPull, |
+ kMaxNormalizedDeltaPerPull); |
+ |
+ offset_ += normalized_delta; |
+ offset_ = Clamp(offset_, 0.f, 1.f); |
+ |
+ idle_alpha_ = |
+ kMinAlpha + (1.f - kMinAlpha) * offset_ / kGlowActivationThreshold; |
+ active_alpha_ = (offset_ - kGlowActivationThreshold) / |
+ (kPullActivationThreshold - kGlowActivationThreshold); |
+ idle_alpha_ = Clamp(idle_alpha_, 0.f, 1.f); |
+ active_alpha_ = Clamp(active_alpha_, 0.f, 1.f); |
+ |
+ rotation_ = kPullRotationMultiplier * Damp(offset_, 1.f); |
+ } |
+ |
+ bool Animate(base::TimeTicks current_time, bool still_refreshing) { |
+ if (IsFinished()) |
+ return false; |
+ |
+ if (state_ == STATE_PULL) |
+ return true; |
+ |
+ const double dt = (current_time - start_time_).InMilliseconds(); |
+ const double t = min(dt / duration_.InMilliseconds(), 1.); |
+ const float interp = static_cast<float>(Damp(t, 1.)); |
+ |
+ idle_alpha_ = Lerp(idle_alpha_start_, idle_alpha_finish_, interp); |
+ active_alpha_ = Lerp(active_alpha_start_, active_alpha_finish_, interp); |
+ offset_ = Lerp(offset_start_, offset_finish_, interp); |
+ rotation_ = Lerp(rotation_start_, rotation_finish_, interp); |
+ |
+ if (t < 1.f - kEpsilon) |
+ return true; |
+ |
+ switch (state_) { |
+ case STATE_IDLE: |
+ case STATE_PULL: |
+ NOTREACHED() << "Invalidate state for animation."; |
+ break; |
+ case STATE_ACTIVATED: |
+ start_time_ = current_time; |
+ if (still_refreshing && |
+ (current_time - activated_start_time_ < |
+ base::TimeDelta::FromMilliseconds(kMaxActivationTimeMs))) { |
+ offset_start_ = offset_finish_ = offset_; |
+ rotation_start_ = rotation_; |
+ rotation_finish_ = rotation_start_ + 360.f; |
+ break; |
+ } |
+ state_ = STATE_ACTIVATED_RECEDE; |
+ duration_ = base::TimeDelta::FromMilliseconds(kActivationRecedeTimeMs); |
+ idle_alpha_start_ = idle_alpha_; |
+ active_alpha_start_ = active_alpha_; |
+ idle_alpha_finish_ = 0; |
+ active_alpha_finish_ = 0; |
+ rotation_start_ = rotation_finish_ = rotation_; |
+ offset_start_ = offset_finish_ = offset_; |
+ break; |
+ case STATE_ACTIVATED_RECEDE: |
+ Finish(); |
+ break; |
+ case STATE_RECEDE: |
+ Finish(); |
+ break; |
+ }; |
+ |
+ return !IsFinished(); |
+ } |
+ |
+ bool Release(base::TimeTicks current_time, bool allow_activation) { |
+ if (state_ != STATE_ACTIVATED && state_ != STATE_PULL) |
+ return false; |
+ |
+ if (state_ == STATE_ACTIVATED && allow_activation) |
+ return false; |
+ |
+ start_time_ = current_time; |
+ idle_alpha_start_ = idle_alpha_; |
+ active_alpha_start_ = active_alpha_; |
+ offset_start_ = offset_; |
+ rotation_start_ = rotation_; |
+ |
+ if (offset_ < kPullActivationThreshold || !allow_activation) { |
+ state_ = STATE_RECEDE; |
+ duration_ = base::TimeDelta::FromMilliseconds(kRecedeTimeMs); |
+ idle_alpha_finish_ = 0; |
+ active_alpha_finish_ = 0; |
+ offset_finish_ = 0; |
+ rotation_finish_ = rotation_start_ - 180.f; |
+ return false; |
+ } |
+ |
+ state_ = STATE_ACTIVATED; |
+ duration_ = base::TimeDelta::FromMilliseconds(kActivationTimeMs); |
+ activated_start_time_ = current_time; |
+ idle_alpha_finish_ = idle_alpha_start_; |
+ active_alpha_finish_ = active_alpha_start_; |
+ offset_finish_ = kPullActivationThreshold; |
+ rotation_finish_ = rotation_start_ + 360.f; |
+ return true; |
+ } |
+ |
+ void Finish() { |
+ Detach(); |
+ idle_layer_->SetIsDrawable(false); |
+ active_layer_->SetIsDrawable(false); |
+ offset_ = 0; |
+ idle_alpha_ = 0; |
+ active_alpha_ = 0; |
+ rotation_ = 0; |
+ state_ = STATE_IDLE; |
+ } |
+ |
+ void ApplyToLayers(const gfx::SizeF& size, cc::Layer* parent) { |
+ if (IsFinished()) |
+ return; |
+ |
+ UpdateLayer(idle_layer_.get(), |
+ parent, |
+ resource_manager_->GetUIResourceId(kIdleResourceType), |
+ size, |
+ offset_, |
+ idle_alpha_, |
+ rotation_); |
+ UpdateLayer(active_layer_.get(), |
+ parent, |
+ resource_manager_->GetUIResourceId(kActiveResourceType), |
+ size, |
+ offset_, |
+ active_alpha_, |
+ rotation_); |
+ } |
+ |
+ bool IsFinished() const { return state_ == STATE_IDLE; } |
+ |
+ private: |
+ enum State { |
+ STATE_IDLE = 0, |
+ STATE_PULL, |
+ STATE_ACTIVATED, |
+ STATE_ACTIVATED_RECEDE, |
+ STATE_RECEDE |
+ }; |
+ |
+ void Detach() { |
+ idle_layer_->RemoveFromParent(); |
+ active_layer_->RemoveFromParent(); |
+ } |
+ |
+ ui::SystemUIResourceManager* const resource_manager_; |
+ |
+ scoped_refptr<cc::UIResourceLayer> idle_layer_; |
+ scoped_refptr<cc::UIResourceLayer> active_layer_; |
+ |
+ float idle_alpha_; |
+ float active_alpha_; |
+ float offset_; |
+ float rotation_; |
+ |
+ float idle_alpha_start_; |
+ float idle_alpha_finish_; |
+ float active_alpha_start_; |
+ float active_alpha_finish_; |
+ float offset_start_; |
+ float offset_finish_; |
+ float rotation_start_; |
+ float rotation_finish_; |
+ |
+ base::TimeTicks start_time_; |
+ base::TimeTicks activated_start_time_; |
+ base::TimeDelta duration_; |
+ |
+ State state_; |
+}; |
+ |
+OverscrollRefresh::OverscrollRefresh( |
+ ui::SystemUIResourceManager* resource_manager, |
+ OverscrollRefreshClient* client) |
+ : client_(client), |
+ scrolled_to_top_(true), |
+ scroll_consumption_state_(DISABLED), |
+ effect_(new Effect(resource_manager)) { |
+ DCHECK(client); |
+} |
+ |
+OverscrollRefresh::~OverscrollRefresh() { |
+} |
+ |
+void OverscrollRefresh::Reset() { |
+ scroll_consumption_state_ = DISABLED; |
+ effect_->Finish(); |
+} |
+ |
+void OverscrollRefresh::OnScrollBegin() { |
+ bool allow_activation = false; |
+ Release(allow_activation); |
+ if (scrolled_to_top_) |
+ 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::OnScrollUpdateAck(bool was_consumed) { |
+ if (scroll_consumption_state_ != AWAITING_SCROLL_UPDATE_ACK) |
+ return; |
+ |
+ scroll_consumption_state_ = was_consumed ? DISABLED : ENABLED; |
+} |
+ |
+bool OverscrollRefresh::WillHandleScrollUpdate( |
+ const gfx::Vector2dF& scroll_delta) { |
+ if (viewport_size_.IsEmpty()) |
+ return false; |
+ |
+ switch (scroll_consumption_state_) { |
+ case DISABLED: |
+ return false; |
+ |
+ case AWAITING_SCROLL_UPDATE_ACK: |
+ // If the initial scroll motion is downward, never allow activation. |
+ if (scroll_delta.y() <= 0) |
+ scroll_consumption_state_ = DISABLED; |
+ return false; |
+ |
+ case ENABLED: { |
+ float normalized_delta = scroll_delta.y() / min(viewport_size_.height(), |
+ viewport_size_.width()); |
+ effect_->Pull(normalized_delta); |
+ return true; |
+ } |
+ } |
+ |
+ NOTREACHED() << "Invalid overscroll state: " << scroll_consumption_state_; |
+ return false; |
+} |
+ |
+bool OverscrollRefresh::Animate(base::TimeTicks current_time, |
+ cc::Layer* parent_layer) { |
+ DCHECK(parent_layer); |
+ if (effect_->IsFinished()) |
+ return false; |
+ |
+ if (effect_->Animate(current_time, client_->IsStillRefreshing())) |
+ effect_->ApplyToLayers(viewport_size_, parent_layer); |
+ |
+ return !effect_->IsFinished(); |
+} |
+ |
+bool OverscrollRefresh::IsActive() const { |
+ return scroll_consumption_state_ == ENABLED || !effect_->IsFinished(); |
+} |
+ |
+bool OverscrollRefresh::IsAwaitingScrollUpdateAck() const { |
+ return scroll_consumption_state_ == AWAITING_SCROLL_UPDATE_ACK; |
+} |
+ |
+void OverscrollRefresh::UpdateDisplay( |
+ const gfx::SizeF& viewport_size, |
+ const gfx::Vector2dF& content_scroll_offset) { |
+ viewport_size_ = viewport_size; |
+ scrolled_to_top_ = content_scroll_offset.y() == 0; |
+} |
+ |
+void OverscrollRefresh::Release(bool allow_activation) { |
+ if (scroll_consumption_state_ == ENABLED) { |
+ if (effect_->Release(base::TimeTicks::Now(), allow_activation)) |
+ client_->TriggerRefresh(); |
+ } |
+ scroll_consumption_state_ = DISABLED; |
+} |
+ |
+} // namespace content |