| // 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/ash_color_provider.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 = |
| AshColorProvider::Get()->GetInkDropBaseColorAndOpacity().first; |
| 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, |
| base::Unretained(this))) |
| .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, |
| base::Unretained(this))); |
| } |
| |
| 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 |