| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/controls/scroll_view_gradient_helper.h" |
| |
| #include <memory> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/views/animation/animation_builder.h" |
| |
| namespace ash { |
| |
| const base::TimeDelta kAnimationDuration = base::Milliseconds(50); |
| |
| ScrollViewGradientHelper::ScrollViewGradientHelper( |
| views::ScrollView* scroll_view, |
| int gradient_height) |
| : scroll_view_(scroll_view), gradient_height_(gradient_height) { |
| DCHECK(scroll_view_); |
| DCHECK(scroll_view_->layer()); |
| on_contents_scrolled_subscription_ = |
| scroll_view_->AddContentsScrolledCallback( |
| base::BindRepeating(&ScrollViewGradientHelper::UpdateGradientMask, |
| base::Unretained(this))); |
| on_contents_scroll_ended_subscription_ = |
| scroll_view_->AddContentsScrollEndedCallback( |
| base::BindRepeating(&ScrollViewGradientHelper::UpdateGradientMask, |
| base::Unretained(this))); |
| scroll_view_->SetPreferredViewportMargins( |
| gfx::Insets::VH(gradient_height_, 0)); |
| } |
| |
| ScrollViewGradientHelper::~ScrollViewGradientHelper() { |
| RemoveMaskLayer(); |
| scroll_view_->SetPreferredViewportMargins(gfx::Insets()); |
| } |
| |
| void ScrollViewGradientHelper::UpdateGradientMask() { |
| DCHECK(scroll_view_->contents()); |
| |
| const gfx::Rect visible_rect = scroll_view_->GetVisibleRect(); |
| // Show the top gradient if the scroll view is not scrolled to the top. |
| const bool show_top_gradient = visible_rect.y() > 0; |
| // Show the bottom gradient if the scroll view is not scrolled to the bottom. |
| const bool show_bottom_gradient = |
| visible_rect.bottom() < scroll_view_->contents()->bounds().bottom(); |
| |
| // If no gradient is needed, remove the gradient mask. |
| if (scroll_view_->contents()->bounds().IsEmpty()) { |
| RemoveMaskLayer(); |
| return; |
| } |
| if (!show_top_gradient && !show_bottom_gradient) { |
| RemoveMaskLayer(); |
| return; |
| } |
| |
| // Vertical linear gradient, from top to bottom. |
| gfx::LinearGradient gradient_mask(/*angle=*/-90); |
| // Clamp fade_position to the ~middle. If we don't do this, then in degenerate |
| // cases (where the gradients are larger than the scroll view itself) we would |
| // end up passing bogus values to the gradient mask. |
| const float fade_position = std::min( |
| static_cast<float>(gradient_height_) / scroll_view_->bounds().height(), |
| 0.49f); |
| |
| // Top fade in section. |
| if (show_top_gradient) { |
| gradient_mask.AddStep(/*fraction=*/0, /*alpha=*/0); |
| gradient_mask.AddStep(fade_position, 255); |
| } |
| |
| // Bottom fade out section. |
| if (show_bottom_gradient) { |
| gradient_mask.AddStep(/*fraction=*/(1 - fade_position), /*alpha=*/255); |
| gradient_mask.AddStep(1, 0); |
| } |
| |
| // If a gradient update is needed, add the gradient mask to the scroll view |
| // layer. |
| if (scroll_view_->layer()->gradient_mask() != gradient_mask) { |
| DVLOG(1) << "Adding gradient mask"; |
| |
| if (first_time_update_) { |
| scroll_view_->layer()->SetGradientMask(gradient_mask); |
| } else { |
| // On first call to UpdateGradientMask, animate in the gradient. |
| AnimateMaskLayer(gradient_mask); |
| first_time_update_ = true; |
| } |
| } |
| } |
| |
| void ScrollViewGradientHelper::AnimateMaskLayer( |
| const gfx::LinearGradient& target_gradient) { |
| // Instead of starting the animation with fully transparent frame, |
| // use an initial value so the first frame is opaque. |
| gfx::LinearGradient start_gradient(target_gradient); |
| for (auto& step : start_gradient.steps()) { |
| if (step.alpha < 255) |
| step.alpha = 255; |
| } |
| scroll_view_->layer()->SetGradientMask(start_gradient); |
| views::AnimationBuilder() |
| .SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET) |
| .Once() |
| .SetDuration(kAnimationDuration) |
| .SetGradientMask(scroll_view_, target_gradient); |
| } |
| |
| void ScrollViewGradientHelper::RemoveMaskLayer() { |
| if (!scroll_view_->layer()->HasGradientMask()) |
| return; |
| |
| DVLOG(1) << "Removing gradient mask"; |
| scroll_view_->layer()->SetGradientMask(gfx::LinearGradient::GetEmpty()); |
| } |
| |
| } // namespace ash |