| // Copyright 2014 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 "chrome/browser/ui/views/tabs/alert_indicator.h" |
| |
| #include "chrome/app/vector_icons/vector_icons.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/views/tabs/tab.h" |
| #include "ui/base/material_design/material_design_controller.h" |
| #include "ui/gfx/animation/animation_delegate.h" |
| #include "ui/gfx/animation/multi_animation.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| |
| namespace { |
| |
| // Fade-in/out duration for the tab indicator animations. Fade-in is quick to |
| // immediately notify the user. Fade-out is more gradual, so that the user has |
| // a chance of finding a tab that has quickly "blipped" on and off. |
| constexpr int kIndicatorFadeInDurationMs = 200; |
| constexpr int kIndicatorFadeOutDurationMs = 1000; |
| |
| // Interval between frame updates of the tab indicator animations. This is not |
| // the usual 60 FPS because a trade-off must be made between tab UI animation |
| // smoothness and media recording/playback performance on low-end hardware. |
| constexpr base::TimeDelta kIndicatorFrameInterval = |
| base::TimeDelta::FromMilliseconds(50); // 20 FPS |
| |
| // Animation that throbs in (towards 1.0) and out (towards 0.0), and ends in the |
| // "in" state. |
| class TabRecordingIndicatorAnimation : public gfx::MultiAnimation { |
| public: |
| ~TabRecordingIndicatorAnimation() override {} |
| |
| // Overridden to provide alternating "towards in" and "towards out" behavior. |
| double GetCurrentValue() const override; |
| |
| static std::unique_ptr<TabRecordingIndicatorAnimation> Create(); |
| |
| private: |
| TabRecordingIndicatorAnimation(const gfx::MultiAnimation::Parts& parts, |
| const base::TimeDelta interval) |
| : MultiAnimation(parts, interval) {} |
| |
| // Number of times to "toggle throb" the recording and tab capture indicators |
| // when they first appear. |
| static const int kCaptureIndicatorThrobCycles = 5; |
| }; |
| |
| double TabRecordingIndicatorAnimation::GetCurrentValue() const { |
| return current_part_index() % 2 ? 1.0 - MultiAnimation::GetCurrentValue() |
| : MultiAnimation::GetCurrentValue(); |
| } |
| |
| std::unique_ptr<TabRecordingIndicatorAnimation> |
| TabRecordingIndicatorAnimation::Create() { |
| MultiAnimation::Parts parts; |
| static_assert( |
| kCaptureIndicatorThrobCycles % 2 != 0, |
| "odd number of cycles required so animation finishes in showing state"); |
| for (int i = 0; i < kCaptureIndicatorThrobCycles; ++i) { |
| parts.push_back(MultiAnimation::Part( |
| i % 2 ? kIndicatorFadeOutDurationMs : kIndicatorFadeInDurationMs, |
| gfx::Tween::EASE_IN)); |
| } |
| |
| std::unique_ptr<TabRecordingIndicatorAnimation> animation( |
| new TabRecordingIndicatorAnimation(parts, kIndicatorFrameInterval)); |
| animation->set_continuous(false); |
| return animation; |
| } |
| |
| // Returns a cached image, to be shown by the alert indicator for the given |
| // |alert_state|. Uses the global ui::ResourceBundle shared instance. |
| gfx::Image GetTabAlertIndicatorImage(TabAlertState alert_state, |
| SkColor button_color) { |
| const gfx::VectorIcon* icon = nullptr; |
| int image_width = GetLayoutConstant(TAB_ALERT_INDICATOR_ICON_WIDTH); |
| const bool is_touch_optimized_ui = |
| ui::MaterialDesignController::IsTouchOptimizedUiEnabled(); |
| switch (alert_state) { |
| case TabAlertState::AUDIO_PLAYING: |
| icon = is_touch_optimized_ui ? &kTabAudioRoundedIcon : &kTabAudioIcon; |
| break; |
| case TabAlertState::AUDIO_MUTING: |
| icon = is_touch_optimized_ui ? &kTabAudioMutingRoundedIcon |
| : &kTabAudioMutingIcon; |
| break; |
| case TabAlertState::MEDIA_RECORDING: |
| case TabAlertState::DESKTOP_CAPTURING: |
| icon = &kTabMediaRecordingIcon; |
| break; |
| case TabAlertState::TAB_CAPTURING: |
| icon = is_touch_optimized_ui ? &kTabMediaCapturingWithArrowIcon |
| : &kTabMediaCapturingIcon; |
| // Tab capturing and presenting icon uses a different width compared to |
| // the other tab alert indicator icons. |
| image_width = GetLayoutConstant(TAB_ALERT_INDICATOR_CAPTURE_ICON_WIDTH); |
| break; |
| case TabAlertState::BLUETOOTH_CONNECTED: |
| icon = &kTabBluetoothConnectedIcon; |
| break; |
| case TabAlertState::USB_CONNECTED: |
| icon = &kTabUsbConnectedIcon; |
| break; |
| case TabAlertState::PIP_PLAYING: |
| icon = &kPictureInPictureAltIcon; |
| break; |
| case TabAlertState::NONE: |
| return gfx::Image(); |
| } |
| DCHECK(icon); |
| return gfx::Image(gfx::CreateVectorIcon(*icon, image_width, button_color)); |
| } |
| |
| // Returns a non-continuous Animation that performs a fade-in or fade-out |
| // appropriate for the given |next_alert_state|. This is used by the tab alert |
| // indicator to alert the user that recording, tab capture, or audio playback |
| // has started/stopped. |
| std::unique_ptr<gfx::Animation> CreateTabAlertIndicatorFadeAnimation( |
| TabAlertState alert_state) { |
| if (alert_state == TabAlertState::MEDIA_RECORDING || |
| alert_state == TabAlertState::TAB_CAPTURING || |
| alert_state == TabAlertState::DESKTOP_CAPTURING) { |
| return TabRecordingIndicatorAnimation::Create(); |
| } |
| |
| // Note: While it seems silly to use a one-part MultiAnimation, it's the only |
| // gfx::Animation implementation that lets us control the frame interval. |
| gfx::MultiAnimation::Parts parts; |
| const bool is_for_fade_in = (alert_state != TabAlertState::NONE); |
| parts.push_back(gfx::MultiAnimation::Part( |
| is_for_fade_in ? kIndicatorFadeInDurationMs : kIndicatorFadeOutDurationMs, |
| gfx::Tween::EASE_IN)); |
| std::unique_ptr<gfx::MultiAnimation> animation( |
| new gfx::MultiAnimation(parts, kIndicatorFrameInterval)); |
| animation->set_continuous(false); |
| return std::move(animation); |
| } |
| |
| } // namespace |
| |
| class AlertIndicator::FadeAnimationDelegate : public gfx::AnimationDelegate { |
| public: |
| explicit FadeAnimationDelegate(AlertIndicator* indicator) |
| : indicator_(indicator) {} |
| ~FadeAnimationDelegate() override {} |
| |
| private: |
| // gfx::AnimationDelegate |
| void AnimationProgressed(const gfx::Animation* animation) override { |
| indicator_->SchedulePaint(); |
| } |
| |
| void AnimationCanceled(const gfx::Animation* animation) override { |
| AnimationEnded(animation); |
| } |
| |
| void AnimationEnded(const gfx::Animation* animation) override { |
| indicator_->showing_alert_state_ = indicator_->alert_state_; |
| indicator_->SchedulePaint(); |
| indicator_->parent_tab_->AlertStateChanged(); |
| } |
| |
| AlertIndicator* const indicator_; |
| |
| DISALLOW_COPY_AND_ASSIGN(FadeAnimationDelegate); |
| }; |
| |
| AlertIndicator::AlertIndicator(Tab* parent_tab) |
| : views::ImageView(), |
| parent_tab_(parent_tab), |
| alert_state_(TabAlertState::NONE), |
| showing_alert_state_(TabAlertState::NONE) { |
| DCHECK(parent_tab_); |
| } |
| |
| AlertIndicator::~AlertIndicator() {} |
| |
| void AlertIndicator::OnPaint(gfx::Canvas* canvas) { |
| double opaqueness = 1.0; |
| if (fade_animation_) { |
| opaqueness = fade_animation_->GetCurrentValue(); |
| if (alert_state_ == TabAlertState::NONE) |
| opaqueness = 1.0 - opaqueness; // Fading out, not in. |
| } |
| if (opaqueness < 1.0) |
| canvas->SaveLayerAlpha(opaqueness * SK_AlphaOPAQUE); |
| ImageView::OnPaint(canvas); |
| if (opaqueness < 1.0) |
| canvas->Restore(); |
| } |
| |
| void AlertIndicator::TransitionToAlertState(TabAlertState next_state) { |
| if (next_state == alert_state_) |
| return; |
| |
| TabAlertState previous_alert_showing_state = showing_alert_state_; |
| |
| if (next_state != TabAlertState::NONE) |
| ResetImage(next_state); |
| |
| if ((alert_state_ == TabAlertState::AUDIO_PLAYING && |
| next_state == TabAlertState::AUDIO_MUTING) || |
| (alert_state_ == TabAlertState::AUDIO_MUTING && |
| next_state == TabAlertState::AUDIO_PLAYING)) { |
| // Instant user feedback: No fade animation. |
| showing_alert_state_ = next_state; |
| fade_animation_.reset(); |
| } else { |
| if (next_state == TabAlertState::NONE) |
| showing_alert_state_ = alert_state_; // Fading-out indicator. |
| else |
| showing_alert_state_ = next_state; // Fading-in to next indicator. |
| fade_animation_ = CreateTabAlertIndicatorFadeAnimation(next_state); |
| if (!fade_animation_delegate_) |
| fade_animation_delegate_.reset(new FadeAnimationDelegate(this)); |
| fade_animation_->set_delegate(fade_animation_delegate_.get()); |
| fade_animation_->Start(); |
| } |
| |
| alert_state_ = next_state; |
| |
| if (previous_alert_showing_state != showing_alert_state_) |
| parent_tab_->AlertStateChanged(); |
| } |
| |
| void AlertIndicator::OnParentTabButtonColorChanged() { |
| if (alert_state_ == TabAlertState::AUDIO_PLAYING || |
| alert_state_ == TabAlertState::AUDIO_MUTING) |
| ResetImage(alert_state_); |
| } |
| |
| views::View* AlertIndicator::GetTooltipHandlerForPoint( |
| const gfx::Point& point) { |
| return nullptr; // Tab (the parent View) provides the tooltip. |
| } |
| |
| void AlertIndicator::ResetImage(TabAlertState state) { |
| SkColor color = parent_tab_->GetAlertIndicatorColor(state); |
| gfx::ImageSkia image = GetTabAlertIndicatorImage(state, color).AsImageSkia(); |
| SetImage(&image); |
| } |