chromium/ash/capture_mode/user_nudge_controller.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/capture_mode/user_nudge_controller.h"

#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "ui/aura/window.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/animation/animation_sequence_block.h"

namespace ash {

namespace {

constexpr float kBaseRingOpacity = 0.21f;
constexpr float kRippleRingOpacity = 0.5f;

constexpr float kBaseRingScaleUpFactor = 1.1f;
constexpr float kRippleRingScaleUpFactor = 3.0f;
constexpr float kHighlightedViewScaleUpFactor = 1.2f;

constexpr base::TimeDelta kVisibilityChangeDuration = base::Milliseconds(200);
constexpr base::TimeDelta kScaleUpDuration = base::Milliseconds(500);
constexpr base::TimeDelta kScaleDownDelay = base::Milliseconds(650);
constexpr base::TimeDelta kScaleDownOffset = kScaleUpDuration + kScaleDownDelay;
constexpr base::TimeDelta kScaleDownDuration = base::Milliseconds(1350);
constexpr base::TimeDelta kRippleAnimationDuration = base::Milliseconds(2000);

constexpr base::TimeDelta kDelayToShowNudge = base::Milliseconds(1000);
constexpr base::TimeDelta kDelayToRepeatNudge = base::Milliseconds(2500);

// Returns the given `view`'s layer bounds in root coordinates ignoring any
// transforms it or any of its ancestors may have.
gfx::Rect GetViewLayerBoundsInRootNoTransform(views::View* view) {
  auto* layer = view->layer();
  DCHECK(layer);
  gfx::Point origin;
  while (layer) {
    const auto layer_origin = layer->bounds().origin();
    origin.Offset(layer_origin.x(), layer_origin.y());
    layer = layer->parent();
  }
  return gfx::Rect(origin, view->layer()->size());
}

}  // namespace

UserNudgeController::UserNudgeController(CaptureModeSession* session,
                                         views::View* view_to_be_highlighted)
    : capture_session_(session),
      view_to_be_highlighted_(view_to_be_highlighted) {
  view_to_be_highlighted_->SetPaintToLayer();
  view_to_be_highlighted_->layer()->SetFillsBoundsOpaquely(false);

  // Rings are created initially with 0 opacity. Calling SetVisible() will
  // animate them towards their correct state.
  const SkColor ring_color =
      DarkLightModeControllerImpl::Get()->IsDarkModeEnabled() ? SK_ColorWHITE
                                                              : SK_ColorBLACK;
  base_ring_.SetColor(ring_color);
  base_ring_.SetFillsBoundsOpaquely(false);
  base_ring_.SetOpacity(0);
  ripple_ring_.SetColor(ring_color);
  ripple_ring_.SetFillsBoundsOpaquely(false);
  ripple_ring_.SetOpacity(0);

  Reposition();
}

UserNudgeController::~UserNudgeController() {
  if (should_dismiss_nudge_forever_)
    CaptureModeController::Get()->DisableUserNudgeForever();
  capture_session_->capture_toast_controller()->MaybeDismissCaptureToast(
      CaptureToastType::kUserNudge,
      /*animate=*/false);
}

void UserNudgeController::Reposition() {
  auto* parent_window = GetParentWindow();

  auto* parent_layer = parent_window->layer();
  if (parent_layer != base_ring_.parent()) {
    parent_layer->Add(&base_ring_);
    parent_layer->Add(&ripple_ring_);
  }

  const auto view_bounds_in_root =
      GetViewLayerBoundsInRootNoTransform(view_to_be_highlighted_);
  base_ring_.SetBounds(view_bounds_in_root);
  base_ring_.SetRoundedCornerRadius(
      gfx::RoundedCornersF(view_bounds_in_root.width() / 2.f));
  ripple_ring_.SetBounds(view_bounds_in_root);
  ripple_ring_.SetRoundedCornerRadius(
      gfx::RoundedCornersF(view_bounds_in_root.width() / 2.f));
}

void UserNudgeController::SetVisible(bool visible) {
  if (is_visible_ == visible)
    return;

  is_visible_ = visible;
  auto* capture_toast_controller = capture_session_->capture_toast_controller();

  views::AnimationBuilder builder;
  builder.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);

  if (!is_visible_) {
    // We should no longer repeat the nudge animation.
    timer_.Stop();
    // We should also stop any ongoing animation on the `base_ring_` in
    // particular since we observe this animation ending to schedule a repeat.
    // See OnBaseRingAnimationEnded().
    base_ring_.GetAnimator()->AbortAllAnimations();

    // Animate all animation layers and the toast widget to 0 opacity.
    builder.Once()
        .SetDuration(kVisibilityChangeDuration)
        .SetOpacity(&base_ring_, 0, gfx::Tween::FAST_OUT_SLOW_IN)
        .SetOpacity(&ripple_ring_, 0, gfx::Tween::FAST_OUT_SLOW_IN);
    capture_toast_controller->MaybeDismissCaptureToast(
        CaptureToastType::kUserNudge);
    return;
  }

  // Animate the `base_ring_` and the `toast_widget_` to their default shown
  // opacity. Note that we don't need to show the `ripple_ring_` since it only
  // shows as the nudge animation is being performed.
  // Once those elements reach their default shown opacity, we perform the nudge
  // animation.
  builder
      .OnEnded(base::BindOnce(&UserNudgeController::PerformNudgeAnimations,
                              weak_ptr_factory_.GetWeakPtr()))
      .Once()
      .SetDuration(kDelayToShowNudge)
      .SetOpacity(&base_ring_, kBaseRingOpacity, gfx::Tween::FAST_OUT_SLOW_IN);
  capture_toast_controller->ShowCaptureToast(CaptureToastType::kUserNudge);
}

void UserNudgeController::PerformNudgeAnimations() {
  PerformBaseRingAnimation();
  PerformRippleRingAnimation();
  PerformViewScaleAnimation();
}

void UserNudgeController::PerformBaseRingAnimation() {
  // The `base_ring_` should scale up around the center of the
  // `view_to_be_highlighted_` to grab the user's attention, and then scales
  // back down to its original size.
  const gfx::Transform scale_up_transform =
      capture_mode_util::GetScaleTransformAboutCenter(&base_ring_,
                                                      kBaseRingScaleUpFactor);
  views::AnimationBuilder()
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .OnEnded(base::BindOnce(&UserNudgeController::OnBaseRingAnimationEnded,
                              base::Unretained(this)))
      .Once()
      .SetDuration(kScaleUpDuration)
      .SetTransform(&base_ring_, scale_up_transform,
                    gfx::Tween::ACCEL_40_DECEL_20)
      .Offset(kScaleDownOffset)
      .SetDuration(kScaleDownDuration)
      .SetTransform(&base_ring_, gfx::Transform(),
                    gfx::Tween::FAST_OUT_SLOW_IN_3);
}

void UserNudgeController::PerformRippleRingAnimation() {
  // The ripple scales up to 3x the size of the `view_to_be_highlighted_` and
  // around its center while fading out.
  ripple_ring_.SetOpacity(kRippleRingOpacity);
  ripple_ring_.SetTransform(gfx::Transform());
  const gfx::Transform scale_up_transform =
      capture_mode_util::GetScaleTransformAboutCenter(&ripple_ring_,
                                                      kRippleRingScaleUpFactor);
  views::AnimationBuilder()
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .Once()
      .SetDuration(kRippleAnimationDuration)
      .SetOpacity(&ripple_ring_, 0, gfx::Tween::ACCEL_0_80_DECEL_80)
      .SetTransform(&ripple_ring_, scale_up_transform,
                    gfx::Tween::ACCEL_0_40_DECEL_100);
}

void UserNudgeController::PerformViewScaleAnimation() {
  // The `view_to_be_highlighted_` scales up and down around its own center in
  // a similar fashion to that of the `base_ring_`.
  auto* view_layer = view_to_be_highlighted_->layer();
  const gfx::Transform scale_up_transform =
      capture_mode_util::GetScaleTransformAboutCenter(
          view_layer, kHighlightedViewScaleUpFactor);
  views::AnimationBuilder()
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .Once()
      .SetDuration(kScaleUpDuration)
      .SetTransform(view_layer, scale_up_transform,
                    gfx::Tween::ACCEL_40_DECEL_20)
      .Offset(kScaleDownOffset)
      .SetDuration(kScaleDownDuration)
      .SetTransform(view_layer, gfx::Transform(),
                    gfx::Tween::FAST_OUT_SLOW_IN_3);
}

void UserNudgeController::OnBaseRingAnimationEnded() {
  timer_.Start(FROM_HERE, kDelayToRepeatNudge,
               base::BindOnce(&UserNudgeController::PerformNudgeAnimations,
                              weak_ptr_factory_.GetWeakPtr()));
}

aura::Window* UserNudgeController::GetParentWindow() const {
  auto* root_window =
      view_to_be_highlighted_->GetWidget()->GetNativeWindow()->GetRootWindow();
  DCHECK(root_window);
  return root_window->GetChildById(kShellWindowId_OverlayContainer);
}

}  // namespace ash