diff --git a/ash/system/video_conference/bubble/return_to_app_panel.cc b/ash/system/video_conference/bubble/return_to_app_panel.cc index 14b607dae5620a..f7436029a9e46b 100644 --- a/ash/system/video_conference/bubble/return_to_app_panel.cc +++ b/ash/system/video_conference/bubble/return_to_app_panel.cc @@ -11,6 +11,8 @@ #include "ash/strings/grit/ash_strings.h" #include "ash/system/video_conference/bubble/bubble_view_ids.h" #include "ash/system/video_conference/video_conference_tray_controller.h" +#include "base/functional/callback_helpers.h" +#include "base/memory/weak_ptr.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "base/unguessable_token.h" @@ -18,11 +20,13 @@ #include "ui/base/l10n/l10n_util.h" #include "ui/base/models/image_model.h" #include "ui/chromeos/styles/cros_tokens_color_mappings.h" +#include "ui/compositor/layer.h" #include "ui/compositor/scoped_animation_duration_scale_mode.h" #include "ui/gfx/animation/linear_animation.h" #include "ui/gfx/animation/tween.h" #include "ui/gfx/canvas.h" #include "ui/gfx/scoped_canvas.h" +#include "ui/views/animation/animation_builder.h" #include "ui/views/background.h" #include "ui/views/controls/image_view.h" #include "ui/views/controls/label.h" @@ -41,6 +45,58 @@ const int kReturnToAppIconSize = 20; constexpr auto kPanelBoundsChangeAnimationDuration = base::Milliseconds(200); +// Performs fade in/fade out animation using `AnimationBuilder`. +void FadeInView(views::View* view, int delay_in_ms, int duration_in_ms) { + // If we are in testing with animation (non zero duration), we shouldn't have + // delays so that we can properly track when animation is completed in test. + if (ui::ScopedAnimationDurationScaleMode::duration_multiplier() == + ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION) { + delay_in_ms = 0; + } + + // The view must have a layer to perform animation. + CHECK(view->layer()); + + views::AnimationBuilder() + .SetPreemptionStrategy( + ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET) + .Once() + .SetDuration(base::TimeDelta()) + .SetOpacity(view, 0.0f) + .At(base::Milliseconds(delay_in_ms)) + .SetDuration(base::Milliseconds(duration_in_ms)) + .SetOpacity(view, 1.0f); +} + +void FadeOutView(views::View* view, + base::WeakPtr parent_weak_ptr) { + auto on_animation_ended = base::BindOnce( + [](base::WeakPtr parent_weak_ptr, views::View* view) { + if (parent_weak_ptr) { + view->layer()->SetOpacity(1.0f); + view->SetVisible(false); + } + }, + parent_weak_ptr, view); + + std::pair split = + base::SplitOnceCallback(std::move(on_animation_ended)); + + // The view must have a layer to perform animation. + CHECK(view->layer()); + + view->SetVisible(true); + views::AnimationBuilder() + .SetPreemptionStrategy( + ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET) + .OnEnded(std::move(split.first)) + .OnAborted(std::move(split.second)) + .Once() + .SetDuration(base::Milliseconds(50)) + .SetVisibility(view, false) + .SetOpacity(view, 0.0f); +} + // Creates a view containing camera, microphone, and screen share icons that // shows capturing state of a media app. std::unique_ptr CreateReturnToAppIconsContainer( @@ -199,6 +255,10 @@ ReturnToAppButton::ReturnToAppButton(ReturnToAppPanel* panel, expand_indicator->SetTooltipText(l10n_util::GetStringUTF16( IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SHOW_TOOLTIP)); expand_indicator_ = AddChildView(std::move(expand_indicator)); + + // Add a layer for icons container in the top row to perform animation. + icons_container_->SetPaintToLayer(); + icons_container_->layer()->SetFillsBoundsOpaquely(false); } // TODO(b/253646076): Double check accessible name for this button. @@ -206,6 +266,12 @@ ReturnToAppButton::ReturnToAppButton(ReturnToAppPanel* panel, // When we show the bubble for the first time, only the top row is visible. SetVisible(is_top_row); + + if (!is_top_row) { + // Add a layer to perform fade in animation. + SetPaintToLayer(); + layer()->SetFillsBoundsOpaquely(false); + } } ReturnToAppButton::~ReturnToAppButton() = default; @@ -244,6 +310,10 @@ void ReturnToAppButton::OnButtonClicked(const base::UnguessableToken& id) { expanded_ ? IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_HIDE_TOOLTIP : IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SHOW_TOOLTIP; expand_indicator_->SetTooltipText(l10n_util::GetStringUTF16(tooltip_text_id)); + + if (icons_container_->GetVisible()) { + FadeInView(icons_container_, /*delay_in_ms=*/100, /*duration_in_ms=*/100); + } } // ----------------------------------------------------------------------------- @@ -356,6 +426,12 @@ void ReturnToAppPanel::OnExpandedStateChanged(bool expanded) { continue; } child->SetVisible(expanded); + + if (expanded) { + FadeInView(child, /*delay_in_ms=*/50, /*duration_in_ms=*/150); + } else { + FadeOutView(child, weak_ptr_factory_.GetWeakPtr()); + } } // In tests, widget might be null and the animation, in some cases, might be diff --git a/ash/system/video_conference/bubble/return_to_app_panel_unittest.cc b/ash/system/video_conference/bubble/return_to_app_panel_unittest.cc index 6cbaffff06110d..8cf377e5b7fb2d 100644 --- a/ash/system/video_conference/bubble/return_to_app_panel_unittest.cc +++ b/ash/system/video_conference/bubble/return_to_app_panel_unittest.cc @@ -21,6 +21,7 @@ #include "chromeos/crosapi/mojom/video_conference.mojom.h" #include "ui/base/l10n/l10n_util.h" #include "ui/compositor/scoped_animation_duration_scale_mode.h" +#include "ui/compositor/test/layer_animation_stopped_waiter.h" #include "ui/gfx/animation/linear_animation.h" #include "ui/views/controls/image_view.h" #include "ui/views/controls/label.h" @@ -443,6 +444,18 @@ TEST_F(ReturnToAppPanelTest, CollapseAnimation) { LeftClickOn(summary_row); EXPECT_TRUE(GetBoundsChangeAnimation()->is_animating()); + // Normally, a layer animation will be performed to fade out the return to app + // buttons. However, since we are simulating different stage of the bounds + // change animation, we will set visibility right away here to prevent the + // layer animation from interfering with the bounds change animation + // simulation. + auto* first_app_row = + static_cast(return_to_app_container->children()[1]); + auto* second_app_row = + static_cast(return_to_app_container->children()[2]); + first_app_row->SetVisible(false); + second_app_row->SetVisible(false); + AnimateToValue(0.5); auto panel_mid_animation_height = return_to_app_panel->size().height(); @@ -465,4 +478,69 @@ TEST_F(ReturnToAppPanelTest, CollapseAnimation) { bubble_end_animation_height - bubble_mid_animation_height); } +// Verify that the layer animations to show/hide the view are performed with +// the expected visibility and opacity before and after the animation. +TEST_F(ReturnToAppPanelTest, LayerAnimations) { + ui::ScopedAnimationDurationScaleMode scoped_animation_duration_scale_mode( + ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION); + + controller()->ClearMediaApps(); + controller()->AddMediaApp(CreateFakeMediaApp( + /*is_capturing_camera=*/true, /*is_capturing_microphone=*/false, + /*is_capturing_screen=*/false, /*title=*/u"Meet", + /*url=*/kMeetTestUrl)); + controller()->AddMediaApp(CreateFakeMediaApp( + /*is_capturing_camera=*/false, /*is_capturing_microphone=*/true, + /*is_capturing_screen=*/true, /*title=*/u"Zoom", + /*url=*/"")); + + LeftClickOn(toggle_bubble_button()); + + auto* return_to_app_panel = GetReturnToAppPanel(); + auto* return_to_app_container = GetReturnToAppContainer(return_to_app_panel); + auto* summary_row = static_cast( + return_to_app_container->children().front()); + + // Expand animation: The return to app buttons should fade in. + LeftClickOn(summary_row); + + auto* first_app_row = + static_cast(return_to_app_container->children()[1]); + auto* second_app_row = + static_cast(return_to_app_container->children()[2]); + + EXPECT_EQ(0, first_app_row->layer()->opacity()); + EXPECT_EQ(0, second_app_row->layer()->opacity()); + + ui::LayerAnimationStoppedWaiter layer_animation_waiter; + layer_animation_waiter.Wait(first_app_row->layer()); + layer_animation_waiter.Wait(second_app_row->layer()); + + EXPECT_EQ(1, first_app_row->layer()->opacity()); + EXPECT_EQ(1, second_app_row->layer()->opacity()); + + // End the rest of the animation to test collapse animation. + SimulateAnimationEnded(); + ASSERT_TRUE(summary_row->expanded()); + + // Collapse animation: The return to app buttons should fade out and the + // summary icons should fade in. + LeftClickOn(summary_row); + EXPECT_TRUE(GetBoundsChangeAnimation()->is_animating()); + + auto* summary_icons = summary_row->icons_container(); + EXPECT_EQ(0, summary_icons->layer()->opacity()); + + EXPECT_TRUE(first_app_row->GetVisible()); + EXPECT_TRUE(second_app_row->GetVisible()); + + layer_animation_waiter.Wait(summary_icons->layer()); + layer_animation_waiter.Wait(first_app_row->layer()); + layer_animation_waiter.Wait(second_app_row->layer()); + + EXPECT_EQ(1, summary_icons->layer()->opacity()); + EXPECT_FALSE(first_app_row->GetVisible()); + EXPECT_FALSE(second_app_row->GetVisible()); +} + } // namespace ash::video_conference \ No newline at end of file