chromium/ash/controls/scroll_view_gradient_helper.cc

// 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