| // Copyright 2020 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/capture_label_view.h" |
| |
| #include "ash/capture_mode/capture_mode_controller.h" |
| #include "ash/capture_mode/capture_mode_session.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/style/ash_color_provider.h" |
| #include "ash/wm/tablet_mode/tablet_mode_controller.h" |
| #include "base/i18n/number_formatting.h" |
| #include "base/task_runner.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/callback_layer_animation_observer.h" |
| #include "ui/compositor/layer_animation_element.h" |
| #include "ui/compositor/layer_animation_sequence.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/gfx/text_constants.h" |
| #include "ui/gfx/transform.h" |
| #include "ui/gfx/transform_util.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/metadata/metadata_impl_macros.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // Capture label button rounded corner radius. |
| constexpr int kCaptureLabelRadius = 18; |
| |
| constexpr int kCountDownStartSeconds = 3; |
| constexpr int kCountDownEndSeconds = 1; |
| |
| constexpr base::TimeDelta kCaptureLabelOpacityFadeoutDuration = |
| base::TimeDelta::FromMilliseconds(33); |
| // Opacity fade in animation duration and scale up animation duration when the |
| // timeout label enters 3. |
| constexpr base::TimeDelta kCountDownEnter3Duration = |
| base::TimeDelta::FromMilliseconds(267); |
| // Opacity fade out animation duration and scale down animation duration when |
| // the timeout label exits 1. |
| constexpr base::TimeDelta kCountDownExit1Duration = |
| base::TimeDelta::FromMilliseconds(333); |
| // For other number enter/exit fade in/out, scale up/down animation duration. |
| constexpr base::TimeDelta kCountDownEnterExitDuration = |
| base::TimeDelta::FromMilliseconds(167); |
| |
| // Delay to enter number 3 to start count down. |
| constexpr base::TimeDelta kStartCountDownDelay = |
| base::TimeDelta::FromMilliseconds(233); |
| // Delay to exit a number after entering animation is completed. |
| constexpr base::TimeDelta kCountDownExitDelay = |
| base::TimeDelta::FromMilliseconds(667); |
| |
| // Different scales for enter/exiting countdown numbers. |
| constexpr float kEnterLabelScaleDown = 0.8f; |
| constexpr float kExitLabelScaleUp = 1.2f; |
| // Scale when exiting the number 1, unlike the other numbers, it will shrink |
| // down a bit and fade out. |
| constexpr float kExitLabel1ScaleDown = 0.8f; |
| |
| void GetOpacityCountDownAnimationSetting(int count_down_number, |
| bool enter, |
| base::TimeDelta* duration, |
| gfx::Tween::Type* tween_type) { |
| if (count_down_number == kCountDownStartSeconds && enter) { |
| *duration = kCountDownEnter3Duration; |
| *tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN; |
| } else if (count_down_number == kCountDownEndSeconds && !enter) { |
| *duration = kCountDownExit1Duration; |
| *tween_type = gfx::Tween::EASE_OUT_3; |
| } else { |
| *duration = kCountDownEnterExitDuration; |
| *tween_type = gfx::Tween::LINEAR; |
| } |
| } |
| |
| void GetTransformCountDownAnimationSetting(int count_down_number, |
| bool enter, |
| base::TimeDelta* duration, |
| gfx::Tween::Type* tween_type) { |
| if (count_down_number == kCountDownStartSeconds && enter) { |
| *duration = kCountDownEnter3Duration; |
| *tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN; |
| } else if (count_down_number == kCountDownEndSeconds && !enter) { |
| *duration = kCountDownExit1Duration; |
| *tween_type = gfx::Tween::EASE_OUT_3; |
| } else if (enter) { |
| *duration = kCountDownEnterExitDuration; |
| *tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN; |
| } else { |
| *duration = kCountDownEnterExitDuration; |
| *tween_type = gfx::Tween::FAST_OUT_LINEAR_IN; |
| } |
| } |
| |
| std::unique_ptr<ui::LayerAnimationElement> CreateOpacityLayerAnimationElement( |
| float target_opacity, |
| base::TimeDelta duration, |
| gfx::Tween::Type tween_type) { |
| std::unique_ptr<ui::LayerAnimationElement> opacity_element = |
| ui::LayerAnimationElement::CreateOpacityElement(target_opacity, duration); |
| opacity_element->set_tween_type(tween_type); |
| return opacity_element; |
| } |
| |
| std::unique_ptr<ui::LayerAnimationElement> CreateTransformLayerAnimationElement( |
| const gfx::Transform& target_transform, |
| base::TimeDelta duration, |
| gfx::Tween::Type tween_type) { |
| std::unique_ptr<ui::LayerAnimationElement> transform_element = |
| ui::LayerAnimationElement::CreateTransformElement(target_transform, |
| duration); |
| transform_element->set_tween_type(tween_type); |
| return transform_element; |
| } |
| |
| // Returns the transform that can scale |bounds| around its center point. |
| gfx::Transform GetScaleTransform(const gfx::Rect& bounds, float scale) { |
| const gfx::Point center_point = bounds.CenterPoint(); |
| return gfx::GetScaleTransform( |
| gfx::Point(center_point.x() - bounds.x(), center_point.y() - bounds.y()), |
| scale); |
| } |
| |
| } // namespace |
| |
| CaptureLabelView::CaptureLabelView(CaptureModeSession* capture_mode_session) |
| : timeout_count_down_(kCountDownStartSeconds), |
| capture_mode_session_(capture_mode_session) { |
| SetPaintToLayer(); |
| layer()->SetFillsBoundsOpaquely(false); |
| |
| auto* color_provider = AshColorProvider::Get(); |
| SkColor background_color = color_provider->GetBaseLayerColor( |
| AshColorProvider::BaseLayerType::kTransparent80); |
| SetBackground(views::CreateSolidBackground(background_color)); |
| layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(kCaptureLabelRadius)); |
| layer()->SetBackgroundBlur( |
| static_cast<float>(AshColorProvider::LayerBlurSigma::kBlurDefault)); |
| |
| SkColor text_color = color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kTextColorPrimary); |
| label_button_ = AddChildView(std::make_unique<views::LabelButton>( |
| base::BindRepeating(&CaptureLabelView::OnButtonPressed, |
| base::Unretained(this)), |
| base::string16())); |
| label_button_->SetPaintToLayer(); |
| label_button_->layer()->SetFillsBoundsOpaquely(false); |
| label_button_->SetEnabledTextColors(text_color); |
| label_button_->SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| label_button_->SetNotifyEnterExitOnChild(true); |
| |
| label_button_->SetInkDropMode(views::InkDropHostView::InkDropMode::ON); |
| const auto ripple_attributes = |
| color_provider->GetRippleAttributes(background_color); |
| label_button_->SetInkDropVisibleOpacity(ripple_attributes.inkdrop_opacity); |
| label_button_->SetInkDropBaseColor(ripple_attributes.base_color); |
| label_button_->SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY); |
| |
| label_ = AddChildView(std::make_unique<views::Label>(base::string16())); |
| label_->SetPaintToLayer(); |
| label_->layer()->SetFillsBoundsOpaquely(false); |
| label_->SetEnabledColor(text_color); |
| label_->SetBackgroundColor(SK_ColorTRANSPARENT); |
| |
| UpdateIconAndText(); |
| } |
| |
| CaptureLabelView::~CaptureLabelView() = default; |
| |
| void CaptureLabelView::UpdateIconAndText() { |
| CaptureModeController* controller = CaptureModeController::Get(); |
| const CaptureModeSource source = controller->source(); |
| const bool is_capturing_image = controller->type() == CaptureModeType::kImage; |
| const bool in_tablet_mode = TabletModeController::Get()->InTabletMode(); |
| auto* color_provider = AshColorProvider::Get(); |
| SkColor icon_color = color_provider->GetContentLayerColor( |
| AshColorProvider::ContentLayerType::kIconColorPrimary); |
| |
| gfx::ImageSkia icon; |
| base::string16 text; |
| switch (source) { |
| case CaptureModeSource::kFullscreen: |
| text = l10n_util::GetStringUTF16( |
| is_capturing_image |
| ? (in_tablet_mode |
| ? IDS_ASH_SCREEN_CAPTURE_LABEL_FULLSCREEN_IMAGE_CAPTURE_TABLET |
| : IDS_ASH_SCREEN_CAPTURE_LABEL_FULLSCREEN_IMAGE_CAPTURE_CLAMSHELL) |
| : (in_tablet_mode |
| ? IDS_ASH_SCREEN_CAPTURE_LABEL_FULLSCREEN_VIDEO_RECORD_TABLET |
| : IDS_ASH_SCREEN_CAPTURE_LABEL_FULLSCREEN_VIDEO_RECORD_CLAMSHELL)); |
| break; |
| case CaptureModeSource::kWindow: { |
| if (in_tablet_mode) { |
| text = l10n_util::GetStringUTF16( |
| is_capturing_image |
| ? IDS_ASH_SCREEN_CAPTURE_LABEL_WINDOW_IMAGE_CAPTURE |
| : IDS_ASH_SCREEN_CAPTURE_LABEL_WINDOW_VIDEO_RECORD); |
| } |
| break; |
| } |
| case CaptureModeSource::kRegion: { |
| if (!capture_mode_session_->is_selecting_region()) { |
| if (CaptureModeController::Get()->user_capture_region().IsEmpty()) { |
| // We're now in waiting to select a capture region phase. |
| text = l10n_util::GetStringUTF16( |
| is_capturing_image |
| ? IDS_ASH_SCREEN_CAPTURE_LABEL_REGION_IMAGE_CAPTURE |
| : IDS_ASH_SCREEN_CAPTURE_LABEL_REGION_VIDEO_RECORD); |
| } else { |
| // We're now in fine-tuning phase. |
| icon = is_capturing_image |
| ? gfx::CreateVectorIcon(kCaptureModeImageIcon, icon_color) |
| : gfx::CreateVectorIcon(kCaptureModeVideoIcon, icon_color); |
| text = l10n_util::GetStringUTF16( |
| is_capturing_image ? IDS_ASH_SCREEN_CAPTURE_LABEL_IMAGE_CAPTURE |
| : IDS_ASH_SCREEN_CAPTURE_LABEL_VIDEO_RECORD); |
| } |
| } |
| break; |
| } |
| } |
| |
| if (!icon.isNull()) { |
| label_->SetVisible(false); |
| label_button_->SetVisible(true); |
| label_button_->SetImage(views::Button::STATE_NORMAL, icon); |
| label_button_->SetText(text); |
| } else if (!text.empty()) { |
| label_button_->SetVisible(false); |
| label_->SetVisible(true); |
| label_->SetText(text); |
| } else { |
| label_button_->SetVisible(false); |
| label_->SetVisible(false); |
| } |
| } |
| |
| bool CaptureLabelView::ShouldHandleEvent() { |
| return label_button_->GetVisible() && !IsInCountDownAnimation(); |
| } |
| |
| void CaptureLabelView::StartCountDown( |
| base::OnceClosure countdown_finished_callback) { |
| countdown_finished_callback_ = std::move(countdown_finished_callback); |
| |
| // Depending on the visibility of |label_button_| and |label_|, decide which |
| // view needs to fade out. |
| ui::Layer* animation_layer = nullptr; |
| if (label_button_->GetVisible()) |
| animation_layer = label_button_->layer(); |
| if (label_->GetVisible()) |
| animation_layer = label_->layer(); |
| if (animation_layer) { |
| // Fade out the opacity. |
| animation_layer->SetOpacity(1.f); |
| ui::ScopedLayerAnimationSettings settings(animation_layer->GetAnimator()); |
| settings.SetTweenType(gfx::Tween::LINEAR); |
| settings.SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); |
| settings.SetTransitionDuration(kCaptureLabelOpacityFadeoutDuration); |
| animation_layer->SetOpacity(0.f); |
| } |
| |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&CaptureLabelView::ScheduleCountDownAnimation, |
| weak_factory_.GetWeakPtr()), |
| kStartCountDownDelay); |
| } |
| |
| bool CaptureLabelView::IsInCountDownAnimation() const { |
| return !!countdown_finished_callback_; |
| } |
| |
| void CaptureLabelView::Layout() { |
| label_button_->SetBoundsRect(GetLocalBounds()); |
| |
| gfx::Rect label_bounds = GetLocalBounds(); |
| label_bounds.ClampToCenteredSize(label_->GetPreferredSize()); |
| label_->SetBoundsRect(label_bounds); |
| } |
| |
| gfx::Size CaptureLabelView::CalculatePreferredSize() const { |
| if (countdown_finished_callback_) |
| return gfx::Size(kCaptureLabelRadius * 2, kCaptureLabelRadius * 2); |
| |
| const bool is_label_button_visible = label_button_->GetVisible(); |
| const bool is_label_visible = label_->GetVisible(); |
| |
| if (!is_label_button_visible && !is_label_visible) |
| return gfx::Size(); |
| |
| if (is_label_button_visible) { |
| DCHECK(!is_label_visible); |
| return gfx::Size( |
| label_button_->GetPreferredSize().width() + kCaptureLabelRadius * 2, |
| kCaptureLabelRadius * 2); |
| } |
| |
| DCHECK(is_label_visible && !is_label_button_visible); |
| return gfx::Size(label_->GetPreferredSize().width() + kCaptureLabelRadius * 2, |
| kCaptureLabelRadius * 2); |
| } |
| |
| void CaptureLabelView::ScheduleCountDownAnimation() { |
| label_->SetVisible(true); |
| label_->SetText(base::FormatNumber(timeout_count_down_)); |
| |
| // Initial setup for entering |timeout_count_down_|: |
| ui::Layer* label_layer = label_->layer(); |
| label_layer->SetOpacity(0.f); |
| // Use target bounds as when this function is called, we're still in bounds |
| // change animation, Widget::GetBoundsInScreen() won't return correct value. |
| gfx::Rect bounds = GetWidget()->GetLayer()->GetTargetBounds(); |
| bounds.ClampToCenteredSize(label_->GetPreferredSize()); |
| label_layer->SetTransform(GetScaleTransform(bounds, kEnterLabelScaleDown)); |
| if (!animation_observer_) { |
| animation_observer_ = std::make_unique<ui::CallbackLayerAnimationObserver>( |
| base::BindRepeating(&CaptureLabelView::OnCountDownAnimationCompleted, |
| base::Unretained(this))); |
| } |
| |
| StartLabelLayerAnimationSequences(); |
| StartWidgetLayerAnimationSequences(); |
| animation_observer_->SetActive(); |
| } |
| |
| bool CaptureLabelView::OnCountDownAnimationCompleted( |
| const ui::CallbackLayerAnimationObserver& observer) { |
| // If animation was aborted, return directly to avoid crash as |this| may |
| // no longer be valid. |
| if (observer.aborted_count()) |
| return false; |
| |
| if (timeout_count_down_ == kCountDownEndSeconds) { |
| std::move(countdown_finished_callback_).Run(); // |this| is destroyed here. |
| } else { |
| timeout_count_down_--; |
| ScheduleCountDownAnimation(); |
| } |
| |
| // Return false to prevent the observer from destroying itself. |
| return false; |
| } |
| |
| void CaptureLabelView::StartLabelLayerAnimationSequences() { |
| // Create |label_opacity_sequence|. Note we don't need the exit animation for |
| // the last countdown number 1, since when exiting number 1, we'll fade out |
| // the entire widget, not just the label. |
| std::unique_ptr<ui::LayerAnimationSequence> label_opacity_sequence = |
| std::make_unique<ui::LayerAnimationSequence>(); |
| base::TimeDelta enter_duration, exit_duration; |
| gfx::Tween::Type enter_type, exit_type; |
| GetOpacityCountDownAnimationSetting(timeout_count_down_, /*enter=*/true, |
| &enter_duration, &enter_type); |
| GetOpacityCountDownAnimationSetting(timeout_count_down_, /*enter=*/false, |
| &exit_duration, &exit_type); |
| |
| label_opacity_sequence->AddElement( |
| CreateOpacityLayerAnimationElement(1.f, enter_duration, enter_type)); |
| label_opacity_sequence->AddElement( |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::OPACITY, kCountDownExitDelay)); |
| const bool is_final_second = timeout_count_down_ == kCountDownEndSeconds; |
| if (!is_final_second) { |
| label_opacity_sequence->AddElement( |
| CreateOpacityLayerAnimationElement(0.f, exit_duration, exit_type)); |
| } |
| |
| // Construct |label_transfrom_sequence|. Same reason above, we don't need |
| // the exit animation for the last countdown number 1. |
| std::unique_ptr<ui::LayerAnimationSequence> label_transfrom_sequence = |
| std::make_unique<ui::LayerAnimationSequence>(); |
| GetTransformCountDownAnimationSetting(timeout_count_down_, /*enter=*/true, |
| &enter_duration, &enter_type); |
| GetTransformCountDownAnimationSetting(timeout_count_down_, /*enter=*/false, |
| &exit_duration, &exit_type); |
| |
| label_transfrom_sequence->AddElement(CreateTransformLayerAnimationElement( |
| gfx::Transform(), enter_duration, enter_type)); |
| label_transfrom_sequence->AddElement( |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::TRANSFORM, kCountDownExitDelay)); |
| if (!is_final_second) { |
| gfx::Rect bounds = GetWidget()->GetLayer()->GetTargetBounds(); |
| bounds.ClampToCenteredSize(label_->GetPreferredSize()); |
| label_transfrom_sequence->AddElement(CreateTransformLayerAnimationElement( |
| GetScaleTransform(bounds, kExitLabelScaleUp), exit_duration, |
| exit_type)); |
| } |
| |
| label_opacity_sequence->AddObserver(animation_observer_.get()); |
| label_transfrom_sequence->AddObserver(animation_observer_.get()); |
| label_->layer()->GetAnimator()->StartTogether( |
| {label_opacity_sequence.release(), label_transfrom_sequence.release()}); |
| } |
| |
| void CaptureLabelView::StartWidgetLayerAnimationSequences() { |
| // Only need animate the widget layer when exiting the last countdown number. |
| if (timeout_count_down_ != kCountDownEndSeconds) |
| return; |
| |
| std::unique_ptr<ui::LayerAnimationSequence> widget_opacity_sequence = |
| std::make_unique<ui::LayerAnimationSequence>(); |
| base::TimeDelta exit_duration; |
| gfx::Tween::Type exit_type; |
| GetOpacityCountDownAnimationSetting(timeout_count_down_, /*enter=*/false, |
| &exit_duration, &exit_type); |
| widget_opacity_sequence->AddElement( |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::OPACITY, |
| kCountDownEnterExitDuration + kCountDownExitDelay)); |
| widget_opacity_sequence->AddElement( |
| CreateOpacityLayerAnimationElement(0.f, exit_duration, exit_type)); |
| |
| std::unique_ptr<ui::LayerAnimationSequence> widget_transform_sequence = |
| std::make_unique<ui::LayerAnimationSequence>(); |
| GetTransformCountDownAnimationSetting(timeout_count_down_, /*enter=*/false, |
| &exit_duration, &exit_type); |
| widget_transform_sequence->AddElement( |
| ui::LayerAnimationElement::CreatePauseElement( |
| ui::LayerAnimationElement::TRANSFORM, |
| kCountDownEnterExitDuration + kCountDownExitDelay)); |
| const gfx::Rect bounds = GetWidget()->GetLayer()->GetTargetBounds(); |
| widget_transform_sequence->AddElement(CreateTransformLayerAnimationElement( |
| GetScaleTransform(bounds, kExitLabel1ScaleDown), exit_duration, |
| exit_type)); |
| |
| widget_opacity_sequence->AddObserver(animation_observer_.get()); |
| widget_transform_sequence->AddObserver(animation_observer_.get()); |
| GetWidget()->GetLayer()->GetAnimator()->StartTogether( |
| {widget_opacity_sequence.release(), widget_transform_sequence.release()}); |
| } |
| |
| void CaptureLabelView::OnButtonPressed() { |
| CaptureModeController::Get()->PerformCapture(); |
| } |
| |
| BEGIN_METADATA(CaptureLabelView, views::View) |
| END_METADATA |
| |
| } // namespace ash |