// Copyright 2021 The Chromium Authors. All rights reserved. // 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/bind.h" #include "base/check.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