| // 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 "content/browser/android/overscroll_controller_android.h" |
| |
| #include "base/command_line.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "cc/layers/layer.h" |
| #include "components/viz/common/quads/compositor_frame_metadata.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_switches.h" |
| #include "third_party/blink/public/common/input/web_gesture_event.h" |
| #include "third_party/blink/public/common/input/web_input_event.h" |
| #include "ui/android/edge_effect.h" |
| #include "ui/android/resources/resource_manager.h" |
| #include "ui/android/window_android.h" |
| #include "ui/android/window_android_compositor.h" |
| #include "ui/base/l10n/l10n_util_android.h" |
| #include "ui/base/ui_base_switches.h" |
| #include "ui/base/ui_base_switches_util.h" |
| #include "ui/events/blink/did_overscroll_params.h" |
| |
| using ui::DidOverscrollParams; |
| using ui::EdgeEffect; |
| using ui::OverscrollGlow; |
| using ui::OverscrollGlowClient; |
| using ui::OverscrollRefresh; |
| |
| namespace content { |
| namespace { |
| |
| // If the glow effect alpha is greater than this value, the refresh effect will |
| // be suppressed. This value was experimentally determined to provide a |
| // reasonable balance between avoiding accidental refresh activation and |
| // minimizing the wait required to refresh after the glow has been triggered. |
| const float kMinGlowAlphaToDisableRefresh = 0.085f; |
| |
| std::unique_ptr<EdgeEffect> CreateGlowEdgeEffect( |
| ui::ResourceManager* resource_manager, |
| float dpi_scale) { |
| DCHECK(resource_manager); |
| return std::make_unique<EdgeEffect>(resource_manager); |
| } |
| |
| std::unique_ptr<OverscrollGlow> CreateGlowEffect(OverscrollGlowClient* client) { |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDisableOverscrollEdgeEffect)) { |
| return nullptr; |
| } |
| |
| // The elastic overscroll feature indicates when the user is scrolling beyond |
| // the range of the scrollable area. Showing a glow in addition would be |
| // redundant. |
| if (switches::IsElasticOverscrollEnabled()) |
| return nullptr; |
| |
| return std::make_unique<OverscrollGlow>(client); |
| } |
| |
| std::unique_ptr<OverscrollRefresh> CreateRefreshEffect( |
| ui::OverscrollRefreshHandler* overscroll_refresh_handler, |
| float dpi_scale) { |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDisablePullToRefreshEffect)) { |
| return nullptr; |
| } |
| |
| float edge_width = OverscrollRefresh::kDefaultNavigationEdgeWidth * dpi_scale; |
| return std::make_unique<OverscrollRefresh>(overscroll_refresh_handler, |
| edge_width); |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<OverscrollControllerAndroid> |
| OverscrollControllerAndroid::CreateForTests( |
| ui::WindowAndroidCompositor* compositor, |
| float dpi_scale, |
| std::unique_ptr<ui::OverscrollGlow> glow_effect, |
| std::unique_ptr<ui::OverscrollRefresh> refresh_effect) { |
| return std::unique_ptr<OverscrollControllerAndroid>( |
| new OverscrollControllerAndroid(compositor, dpi_scale, |
| std::move(glow_effect), |
| std::move(refresh_effect))); |
| } |
| |
| OverscrollControllerAndroid::OverscrollControllerAndroid( |
| ui::WindowAndroidCompositor* compositor, |
| float dpi_scale, |
| std::unique_ptr<ui::OverscrollGlow> glow_effect, |
| std::unique_ptr<ui::OverscrollRefresh> refresh_effect) |
| : compositor_(compositor), |
| dpi_scale_(dpi_scale), |
| enabled_(true), |
| glow_effect_(std::move(glow_effect)), |
| refresh_effect_(std::move(refresh_effect)) {} |
| |
| OverscrollControllerAndroid::OverscrollControllerAndroid( |
| ui::OverscrollRefreshHandler* overscroll_refresh_handler, |
| ui::WindowAndroidCompositor* compositor, |
| float dpi_scale) |
| : compositor_(compositor), |
| dpi_scale_(dpi_scale), |
| enabled_(true), |
| glow_effect_(CreateGlowEffect(this)), |
| refresh_effect_( |
| CreateRefreshEffect(overscroll_refresh_handler, dpi_scale_)) { |
| DCHECK(compositor_); |
| } |
| |
| OverscrollControllerAndroid::~OverscrollControllerAndroid() { |
| } |
| |
| bool OverscrollControllerAndroid::WillHandleGestureEvent( |
| const blink::WebGestureEvent& event) { |
| if (!enabled_) |
| return false; |
| |
| if (!refresh_effect_) |
| return false; |
| |
| // Suppress refresh detection if the glow effect is still prominent. |
| if (glow_effect_ && glow_effect_->IsActive()) { |
| if (glow_effect_->GetVisibleAlpha() > kMinGlowAlphaToDisableRefresh) |
| return false; |
| } |
| |
| bool handled = false; |
| switch (event.GetType()) { |
| case blink::WebInputEvent::Type::kGestureScrollBegin: |
| refresh_effect_->OnScrollBegin( |
| gfx::ScalePoint(event.PositionInWidget(), dpi_scale_)); |
| break; |
| |
| case blink::WebInputEvent::Type::kGestureScrollUpdate: { |
| gfx::Vector2dF scroll_delta(event.data.scroll_update.delta_x, |
| event.data.scroll_update.delta_y); |
| scroll_delta.Scale(dpi_scale_); |
| handled = refresh_effect_->WillHandleScrollUpdate(scroll_delta); |
| } break; |
| |
| case blink::WebInputEvent::Type::kGestureScrollEnd: |
| refresh_effect_->OnScrollEnd(gfx::Vector2dF()); |
| break; |
| |
| case blink::WebInputEvent::Type::kGestureFlingStart: { |
| if (refresh_effect_->IsActive()) { |
| gfx::Vector2dF scroll_velocity(event.data.fling_start.velocity_x, |
| event.data.fling_start.velocity_y); |
| scroll_velocity.Scale(dpi_scale_); |
| refresh_effect_->OnScrollEnd(scroll_velocity); |
| // TODO(jdduke): Figure out a cleaner way of suppressing a fling. |
| // It's important that the any downstream code sees a scroll-ending |
| // event (in this case GestureFlingStart) if it has seen a scroll begin. |
| // Thus, we cannot simply consume the fling. Changing the event type to |
| // a GestureScrollEnd might work in practice, but could lead to |
| // unexpected results. For now, simply truncate the fling velocity, but |
| // not to zero as downstream code may not expect a zero-velocity fling. |
| blink::WebGestureEvent& modified_event = |
| const_cast<blink::WebGestureEvent&>(event); |
| modified_event.data.fling_start.velocity_x = .01f; |
| modified_event.data.fling_start.velocity_y = .01f; |
| } |
| } break; |
| |
| case blink::WebInputEvent::Type::kGesturePinchBegin: |
| refresh_effect_->ReleaseWithoutActivation(); |
| break; |
| |
| default: |
| break; |
| } |
| |
| return handled; |
| } |
| |
| void OverscrollControllerAndroid::OnGestureEventAck( |
| const blink::WebGestureEvent& event, |
| blink::mojom::InputEventResultState ack_result) { |
| if (!enabled_) |
| return; |
| |
| // The overscroll effect requires an explicit release signal that may not be |
| // sent from the renderer compositor. |
| if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollEnd || |
| event.GetType() == blink::WebInputEvent::Type::kGestureFlingStart) { |
| OnOverscrolled(DidOverscrollParams()); |
| } |
| |
| if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollUpdate && |
| refresh_effect_) { |
| // The effect should only be allowed if the scroll events go unconsumed. |
| if (refresh_effect_->IsAwaitingScrollUpdateAck() && |
| ack_result == blink::mojom::InputEventResultState::kConsumed) { |
| refresh_effect_->Reset(); |
| } |
| } |
| } |
| |
| void OverscrollControllerAndroid::OnOverscrolled( |
| const DidOverscrollParams& params) { |
| if (!enabled_) |
| return; |
| |
| if (refresh_effect_) { |
| refresh_effect_->OnOverscrolled(params.overscroll_behavior); |
| |
| if (refresh_effect_->IsActive() || |
| refresh_effect_->IsAwaitingScrollUpdateAck()) { |
| // An active (or potentially active) refresh effect should always pre-empt |
| // the passive glow effect. |
| return; |
| } |
| } |
| |
| // When use-zoom-for-dsf is enabled, each value of params was already scaled |
| // by the device scale factor. |
| gfx::Vector2dF accumulated_overscroll = params.accumulated_overscroll; |
| gfx::Vector2dF latest_overscroll_delta = params.latest_overscroll_delta; |
| gfx::Vector2dF current_fling_velocity = params.current_fling_velocity; |
| gfx::Vector2dF overscroll_location = |
| params.causal_event_viewport_point.OffsetFromOrigin(); |
| |
| if (params.overscroll_behavior.x == cc::OverscrollBehavior::Type::kNone) { |
| accumulated_overscroll.set_x(0); |
| latest_overscroll_delta.set_x(0); |
| current_fling_velocity.set_x(0); |
| } |
| |
| if (params.overscroll_behavior.y == cc::OverscrollBehavior::Type::kNone) { |
| accumulated_overscroll.set_y(0); |
| latest_overscroll_delta.set_y(0); |
| current_fling_velocity.set_y(0); |
| } |
| |
| if (glow_effect_ && glow_effect_->OnOverscrolled( |
| base::TimeTicks::Now(), accumulated_overscroll, |
| latest_overscroll_delta, current_fling_velocity, |
| overscroll_location)) { |
| SetNeedsAnimate(); |
| } |
| } |
| |
| bool OverscrollControllerAndroid::Animate(base::TimeTicks current_time, |
| cc::slim::Layer* parent_layer) { |
| DCHECK(parent_layer); |
| if (!enabled_ || !glow_effect_) |
| return false; |
| |
| return glow_effect_->Animate(current_time, parent_layer); |
| } |
| |
| void OverscrollControllerAndroid::OnFrameMetadataUpdated( |
| float page_scale_factor, |
| float device_scale_factor, |
| const gfx::SizeF& scrollable_viewport_size, |
| const gfx::SizeF& root_layer_size, |
| const gfx::PointF& root_scroll_offset, |
| bool root_overflow_y_hidden) { |
| if (!refresh_effect_ && !glow_effect_) |
| return; |
| |
| // When use-zoom-for-dsf is enabled, frame_metadata.page_scale_factor was |
| // already scaled by the device scale factor. |
| float scale_factor = page_scale_factor; |
| gfx::SizeF viewport_size = |
| gfx::ScaleSize(scrollable_viewport_size, scale_factor); |
| gfx::SizeF content_size = gfx::ScaleSize(root_layer_size, scale_factor); |
| gfx::PointF content_scroll_offset = |
| gfx::ScalePoint(root_scroll_offset, scale_factor); |
| |
| if (refresh_effect_) { |
| refresh_effect_->OnFrameUpdated(viewport_size, content_scroll_offset, |
| root_overflow_y_hidden); |
| } |
| |
| if (glow_effect_) { |
| glow_effect_->OnFrameUpdated(viewport_size, content_size, |
| content_scroll_offset); |
| } |
| } |
| |
| void OverscrollControllerAndroid::Enable() { |
| enabled_ = true; |
| } |
| |
| void OverscrollControllerAndroid::Disable() { |
| if (!enabled_) |
| return; |
| enabled_ = false; |
| if (!enabled_) { |
| if (refresh_effect_) |
| refresh_effect_->Reset(); |
| if (glow_effect_) |
| glow_effect_->Reset(); |
| } |
| } |
| |
| std::unique_ptr<EdgeEffect> OverscrollControllerAndroid::CreateEdgeEffect() { |
| return CreateGlowEdgeEffect(&compositor_->GetResourceManager(), dpi_scale_); |
| } |
| |
| void OverscrollControllerAndroid::SetNeedsAnimate() { |
| compositor_->SetNeedsAnimate(); |
| } |
| |
| } // namespace content |