Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(128)

Unified Diff: content/browser/android/overscroll_refresh.cc

Issue 679493002: [Android] Add a native pull-to-refresh overscroll effect (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix findbugs... Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
« no previous file with comments | « content/browser/android/overscroll_refresh.h ('k') | content/browser/android/overscroll_refresh_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698