| /* |
| * Copyright (c) 2011, Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/core/scroll/scroll_animator.h" |
| |
| #include <memory> |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "cc/animation/scroll_offset_animation_curve.h" |
| #include "cc/layers/picture_layer.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/renderer/core/scroll/scrollable_area.h" |
| #include "third_party/blink/renderer/platform/animation/compositor_keyframe_model.h" |
| #include "third_party/blink/renderer/platform/graphics/graphics_layer.h" |
| #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" |
| #include "third_party/blink/renderer/platform/scroll/main_thread_scrolling_reason.h" |
| #include "third_party/blink/renderer/platform/wtf/time.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| cc::PictureLayer* ToCcLayer(GraphicsLayer* layer) { |
| return layer ? layer->CcLayer() : nullptr; |
| } |
| |
| } // namespace |
| |
| ScrollAnimatorBase* ScrollAnimatorBase::Create( |
| ScrollableArea* scrollable_area) { |
| if (scrollable_area && scrollable_area->ScrollAnimatorEnabled()) |
| return MakeGarbageCollected<ScrollAnimator>(scrollable_area); |
| return MakeGarbageCollected<ScrollAnimatorBase>(scrollable_area); |
| } |
| |
| ScrollAnimator::ScrollAnimator(ScrollableArea* scrollable_area, |
| WTF::TimeFunction time_function) |
| : ScrollAnimatorBase(scrollable_area), |
| time_function_(time_function), |
| last_granularity_(kScrollByPixel) {} |
| |
| ScrollAnimator::~ScrollAnimator() = default; |
| |
| ScrollOffset ScrollAnimator::DesiredTargetOffset() const { |
| if (run_state_ == RunState::kWaitingToCancelOnCompositor) |
| return CurrentOffset(); |
| return (animation_curve_ || |
| run_state_ == RunState::kWaitingToSendToCompositor) |
| ? target_offset_ |
| : CurrentOffset(); |
| } |
| |
| bool ScrollAnimator::HasRunningAnimation() const { |
| return run_state_ != RunState::kPostAnimationCleanup && |
| (animation_curve_ || |
| run_state_ == RunState::kWaitingToSendToCompositor); |
| } |
| |
| ScrollOffset ScrollAnimator::ComputeDeltaToConsume( |
| const ScrollOffset& delta) const { |
| ScrollOffset pos = DesiredTargetOffset(); |
| ScrollOffset new_pos = scrollable_area_->ClampScrollOffset(pos + delta); |
| return new_pos - pos; |
| } |
| |
| void ScrollAnimator::ResetAnimationState() { |
| ScrollAnimatorCompositorCoordinator::ResetAnimationState(); |
| if (animation_curve_) |
| animation_curve_.reset(); |
| start_time_ = 0.0; |
| } |
| |
| ScrollResult ScrollAnimator::UserScroll(ScrollGranularity granularity, |
| const ScrollOffset& delta) { |
| if (!scrollable_area_->ScrollAnimatorEnabled()) |
| return ScrollAnimatorBase::UserScroll(granularity, delta); |
| |
| TRACE_EVENT0("blink", "ScrollAnimator::scroll"); |
| |
| if (granularity == kScrollByPrecisePixel) { |
| // Cancel scroll animation because asked to instant scroll. |
| if (HasRunningAnimation()) |
| CancelAnimation(); |
| return ScrollAnimatorBase::UserScroll(granularity, delta); |
| } |
| |
| bool needs_post_animation_cleanup = |
| run_state_ == RunState::kPostAnimationCleanup; |
| if (run_state_ == RunState::kPostAnimationCleanup) |
| ResetAnimationState(); |
| |
| ScrollOffset consumed_delta = ComputeDeltaToConsume(delta); |
| ScrollOffset target_offset = DesiredTargetOffset(); |
| target_offset += consumed_delta; |
| |
| if (WillAnimateToOffset(target_offset)) { |
| last_granularity_ = granularity; |
| // Report unused delta only if there is no animation running. See |
| // comment below regarding scroll latching. |
| // TODO(bokan): Need to standardize how ScrollAnimators report |
| // unusedDelta. This differs from ScrollAnimatorMac currently. |
| return ScrollResult(true, true, 0, 0); |
| } |
| |
| // If the run state when this method was called was PostAnimationCleanup and |
| // we're not starting an animation, stay in PostAnimationCleanup state so |
| // that the main thread scrolling reason can be removed. |
| if (needs_post_animation_cleanup) |
| run_state_ = RunState::kPostAnimationCleanup; |
| |
| // Report unused delta only if there is no animation and we are not |
| // starting one. This ensures we latch for the duration of the |
| // animation rather than animating multiple scrollers at the same time. |
| return ScrollResult(false, false, delta.Width(), delta.Height()); |
| } |
| |
| bool ScrollAnimator::WillAnimateToOffset(const ScrollOffset& target_offset) { |
| if (run_state_ == RunState::kPostAnimationCleanup) |
| ResetAnimationState(); |
| |
| if (run_state_ == RunState::kWaitingToCancelOnCompositor || |
| run_state_ == RunState::kWaitingToCancelOnCompositorButNewScroll) { |
| DCHECK(animation_curve_); |
| target_offset_ = target_offset; |
| if (RegisterAndScheduleAnimation()) |
| run_state_ = RunState::kWaitingToCancelOnCompositorButNewScroll; |
| return true; |
| } |
| |
| if (animation_curve_) { |
| if ((target_offset - target_offset_).IsZero()) |
| return true; |
| |
| target_offset_ = target_offset; |
| DCHECK(run_state_ == RunState::kRunningOnMainThread || |
| run_state_ == RunState::kRunningOnCompositor || |
| run_state_ == RunState::kRunningOnCompositorButNeedsUpdate || |
| run_state_ == RunState::kRunningOnCompositorButNeedsTakeover || |
| run_state_ == RunState::kRunningOnCompositorButNeedsAdjustment); |
| |
| // Running on the main thread, simply update the target offset instead |
| // of sending to the compositor. |
| if (run_state_ == RunState::kRunningOnMainThread) { |
| animation_curve_->UpdateTarget( |
| TimeDelta::FromSecondsD(time_function_() - start_time_), |
| CompositorOffsetFromBlinkOffset(target_offset)); |
| return true; |
| } |
| |
| if (RegisterAndScheduleAnimation()) |
| run_state_ = RunState::kRunningOnCompositorButNeedsUpdate; |
| return true; |
| } |
| |
| if ((target_offset - CurrentOffset()).IsZero()) |
| return false; |
| |
| target_offset_ = target_offset; |
| start_time_ = time_function_(); |
| |
| if (RegisterAndScheduleAnimation()) |
| run_state_ = RunState::kWaitingToSendToCompositor; |
| |
| return true; |
| } |
| |
| void ScrollAnimator::AdjustAnimationAndSetScrollOffset( |
| const ScrollOffset& offset, |
| ScrollType scroll_type) { |
| IntSize adjustment = RoundedIntSize(offset) - |
| RoundedIntSize(scrollable_area_->GetScrollOffset()); |
| ScrollOffsetChanged(offset, scroll_type); |
| |
| if (run_state_ == RunState::kIdle) { |
| AdjustImplOnlyScrollOffsetAnimation(adjustment); |
| } else if (HasRunningAnimation()) { |
| target_offset_ += ScrollOffset(adjustment); |
| if (animation_curve_) { |
| animation_curve_->ApplyAdjustment(adjustment); |
| if (run_state_ != RunState::kRunningOnMainThread && |
| RegisterAndScheduleAnimation()) |
| run_state_ = RunState::kRunningOnCompositorButNeedsAdjustment; |
| } |
| } |
| } |
| |
| void ScrollAnimator::ScrollToOffsetWithoutAnimation( |
| const ScrollOffset& offset) { |
| current_offset_ = offset; |
| |
| ResetAnimationState(); |
| NotifyOffsetChanged(); |
| } |
| |
| void ScrollAnimator::TickAnimation(double monotonic_time) { |
| if (run_state_ != RunState::kRunningOnMainThread) |
| return; |
| |
| TRACE_EVENT0("blink", "ScrollAnimator::tickAnimation"); |
| double elapsed_time = monotonic_time - start_time_; |
| |
| bool is_finished = (elapsed_time > animation_curve_->Duration()); |
| ScrollOffset offset = BlinkOffsetFromCompositorOffset( |
| is_finished ? animation_curve_->TargetValue() |
| : animation_curve_->GetValue(elapsed_time)); |
| |
| offset = scrollable_area_->ClampScrollOffset(offset); |
| |
| current_offset_ = offset; |
| |
| if (is_finished) |
| run_state_ = RunState::kPostAnimationCleanup; |
| else |
| GetScrollableArea()->ScheduleAnimation(); |
| |
| TRACE_EVENT0("blink", "ScrollAnimator::notifyOffsetChanged"); |
| NotifyOffsetChanged(); |
| } |
| |
| void ScrollAnimator::PostAnimationCleanupAndReset() { |
| // Remove the temporary main thread scrolling reason that was added while |
| // main thread had scheduled an animation. |
| RemoveMainThreadScrollingReason(); |
| |
| ResetAnimationState(); |
| } |
| |
| bool ScrollAnimator::SendAnimationToCompositor() { |
| if (scrollable_area_->ShouldScrollOnMainThread()) |
| return false; |
| |
| std::unique_ptr<CompositorKeyframeModel> animation = |
| CompositorKeyframeModel::Create( |
| *animation_curve_, compositor_target_property::SCROLL_OFFSET, 0, 0); |
| // Being here means that either there is an animation that needs |
| // to be sent to the compositor, or an animation that needs to |
| // be updated (a new scroll event before the previous animation |
| // is finished). In either case, the start time is when the |
| // first animation was initiated. This re-targets the animation |
| // using the current time on main thread. |
| animation->SetStartTime(start_time_); |
| |
| int animation_id = animation->Id(); |
| int animation_group_id = animation->Group(); |
| |
| bool sent_to_compositor = AddAnimation(std::move(animation)); |
| if (sent_to_compositor) { |
| run_state_ = RunState::kRunningOnCompositor; |
| compositor_animation_id_ = animation_id; |
| compositor_animation_group_id_ = animation_group_id; |
| } |
| |
| return sent_to_compositor; |
| } |
| |
| void ScrollAnimator::CreateAnimationCurve() { |
| DCHECK(!animation_curve_); |
| animation_curve_ = CompositorScrollOffsetAnimationCurve::Create( |
| CompositorOffsetFromBlinkOffset(target_offset_), |
| last_granularity_ == kScrollByPixel |
| ? CompositorScrollOffsetAnimationCurve::kScrollDurationInverseDelta |
| : CompositorScrollOffsetAnimationCurve::kScrollDurationConstant); |
| animation_curve_->SetInitialValue( |
| CompositorOffsetFromBlinkOffset(CurrentOffset())); |
| } |
| |
| void ScrollAnimator::UpdateCompositorAnimations() { |
| ScrollAnimatorCompositorCoordinator::UpdateCompositorAnimations(); |
| |
| if (run_state_ == RunState::kPostAnimationCleanup) { |
| PostAnimationCleanupAndReset(); |
| return; |
| } |
| |
| if (run_state_ == RunState::kWaitingToCancelOnCompositor) { |
| DCHECK(compositor_animation_id_); |
| AbortAnimation(); |
| PostAnimationCleanupAndReset(); |
| return; |
| } |
| |
| if (run_state_ == RunState::kRunningOnCompositorButNeedsTakeover) { |
| // The call to ::takeOverCompositorAnimation aborted the animation and |
| // put us in this state. The assumption is that takeOver is called |
| // because a main thread scrolling reason is added, and simply trying |
| // to ::sendAnimationToCompositor will fail and we will run on the main |
| // thread. |
| ResetAnimationIds(); |
| run_state_ = RunState::kWaitingToSendToCompositor; |
| } |
| |
| if (run_state_ == RunState::kRunningOnCompositorButNeedsUpdate || |
| run_state_ == RunState::kWaitingToCancelOnCompositorButNewScroll || |
| run_state_ == RunState::kRunningOnCompositorButNeedsAdjustment) { |
| // Abort the running animation before a new one with an updated |
| // target is added. |
| AbortAnimation(); |
| ResetAnimationIds(); |
| |
| if (run_state_ != RunState::kRunningOnCompositorButNeedsAdjustment) { |
| // When in RunningOnCompositorButNeedsAdjustment, the call to |
| // ::adjustScrollOffsetAnimation should have made the necessary |
| // adjustment to the curve. |
| animation_curve_->UpdateTarget( |
| TimeDelta::FromSecondsD(time_function_() - start_time_), |
| CompositorOffsetFromBlinkOffset(target_offset_)); |
| } |
| |
| if (run_state_ == RunState::kWaitingToCancelOnCompositorButNewScroll) { |
| animation_curve_->SetInitialValue( |
| CompositorOffsetFromBlinkOffset(CurrentOffset())); |
| } |
| |
| run_state_ = RunState::kWaitingToSendToCompositor; |
| } |
| |
| if (run_state_ == RunState::kWaitingToSendToCompositor) { |
| if (!element_id_) |
| ReattachCompositorAnimationIfNeeded( |
| GetScrollableArea()->GetCompositorAnimationTimeline()); |
| |
| if (!animation_curve_) |
| CreateAnimationCurve(); |
| |
| bool running_on_main_thread = false; |
| bool sent_to_compositor = SendAnimationToCompositor(); |
| if (!sent_to_compositor) { |
| running_on_main_thread = RegisterAndScheduleAnimation(); |
| if (running_on_main_thread) |
| run_state_ = RunState::kRunningOnMainThread; |
| } |
| |
| // Main thread should deal with the scroll animations it started. |
| if (sent_to_compositor || running_on_main_thread) |
| AddMainThreadScrollingReason(); |
| else |
| RemoveMainThreadScrollingReason(); |
| } |
| } |
| |
| void ScrollAnimator::AddMainThreadScrollingReason() { |
| // Usually main thread scrolling reasons should be updated from |
| // one frame to all its descendants. khandlingScrollFromMainThread |
| // is a special case because its subframes cannot be scrolled |
| // when the reason is set. When the subframes are ready to scroll |
| // the reason has benn reset. |
| if (cc::Layer* scroll_layer = |
| ToCcLayer(GetScrollableArea()->LayerForScrolling())) { |
| scroll_layer->AddMainThreadScrollingReasons( |
| MainThreadScrollingReason::kHandlingScrollFromMainThread); |
| } |
| } |
| |
| void ScrollAnimator::RemoveMainThreadScrollingReason() { |
| if (cc::Layer* scroll_layer = |
| ToCcLayer(GetScrollableArea()->LayerForScrolling())) { |
| scroll_layer->ClearMainThreadScrollingReasons( |
| MainThreadScrollingReason::kHandlingScrollFromMainThread); |
| } |
| } |
| |
| void ScrollAnimator::NotifyCompositorAnimationAborted(int group_id) { |
| // An animation aborted by the compositor is treated as a finished |
| // animation. |
| ScrollAnimatorCompositorCoordinator::CompositorAnimationFinished(group_id); |
| } |
| |
| void ScrollAnimator::NotifyCompositorAnimationFinished(int group_id) { |
| ScrollAnimatorCompositorCoordinator::CompositorAnimationFinished(group_id); |
| } |
| |
| void ScrollAnimator::NotifyAnimationTakeover( |
| double monotonic_time, |
| double animation_start_time, |
| std::unique_ptr<cc::AnimationCurve> curve) { |
| // If there is already an animation running and the compositor asks to take |
| // over an animation, do nothing to avoid judder. |
| if (HasRunningAnimation()) |
| return; |
| |
| cc::ScrollOffsetAnimationCurve* scroll_offset_animation_curve = |
| curve->ToScrollOffsetAnimationCurve(); |
| ScrollOffset target_value(scroll_offset_animation_curve->target_value().x(), |
| scroll_offset_animation_curve->target_value().y()); |
| if (WillAnimateToOffset(target_value)) { |
| animation_curve_ = CompositorScrollOffsetAnimationCurve::Create( |
| scroll_offset_animation_curve); |
| start_time_ = animation_start_time; |
| } |
| } |
| |
| void ScrollAnimator::CancelAnimation() { |
| ScrollAnimatorCompositorCoordinator::CancelAnimation(); |
| } |
| |
| void ScrollAnimator::TakeOverCompositorAnimation() { |
| if (run_state_ == RunState::kRunningOnCompositor || |
| run_state_ == RunState::kRunningOnCompositorButNeedsUpdate) |
| RemoveMainThreadScrollingReason(); |
| |
| ScrollAnimatorCompositorCoordinator::TakeOverCompositorAnimation(); |
| } |
| |
| void ScrollAnimator::LayerForCompositedScrollingDidChange( |
| CompositorAnimationTimeline* timeline) { |
| if (ReattachCompositorAnimationIfNeeded(timeline) && animation_curve_) |
| AddMainThreadScrollingReason(); |
| } |
| |
| bool ScrollAnimator::RegisterAndScheduleAnimation() { |
| GetScrollableArea()->RegisterForAnimation(); |
| if (!scrollable_area_->ScheduleAnimation()) { |
| ScrollToOffsetWithoutAnimation(target_offset_); |
| ResetAnimationState(); |
| return false; |
| } |
| return true; |
| } |
| |
| void ScrollAnimator::Trace(blink::Visitor* visitor) { |
| ScrollAnimatorBase::Trace(visitor); |
| } |
| |
| } // namespace blink |