0

Reland "VC Background: Add animation on Image and Create with AI buttons"

This is a reland of commit 583ac9dda2

There's missing dependency and resource files for the animations in the
test environment which caused tests failure. It's not caught in the
first CL because ash_unittests is disabled by dry run bot, b/333572800.
I've manually ran all ash_unittests locally to make sure this fix works.

Original change's description:
> VC Background: Add animation on Image and Create with AI buttons
>
> The animation is to help user to discover the feature `Create with AI`.
> Once user clicks on the button, animation won't be shown anymore.
>
> Before user tries out the new feature, animation will only be played
> once which is 3120 milliseconds during the lifetime of the VC tray.
>
> Animation on the `Image` and `Create with AI` button won't be shown at
> the same time. For user who first tries out the feature, animation will
> be shown on the image button first. Once user clicks on the images
> button, and selects a generated background, the animation will be moved
> to the `Create with AI` button.
>
> Screen record:
> https://screencast.googleplex.com/cast/NDg1NDA3ODI3MzIyNDcwNHw5YWJlOGFjOC0xNg
>
> Bug: b/324608665
> Change-Id: I0cfc41f2ec547b3ddc09373866da9bfde224cbf8
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5435600
> Reviewed-by: John Lee <johntlee@chromium.org>
> Reviewed-by: Jiaming Cheng <jiamingc@chromium.org>
> Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
> Commit-Queue: Connie Xu <conniekxu@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1287451}

Bug: b/324608665
Change-Id: I7a4e39aba73a4087dc4fd33a3577cc912cea4f00
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5454045
Reviewed-by: Jiaming Cheng <jiamingc@chromium.org>
Commit-Queue: Connie Xu <conniekxu@chromium.org>
Reviewed-by: John Lee <johntlee@chromium.org>
Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1287665}
This commit is contained in:
conniekxu
2024-04-15 22:00:15 +00:00
committed by Chromium LUCI CQ
parent 2afc8d8af1
commit a794b68057
20 changed files with 497 additions and 40 deletions

@ -3095,6 +3095,7 @@ component("ash") {
"//ash/quick_pair/ui",
"//ash/style",
"//ash/system/mahi/resources:mahi_resources_grit",
"//ash/system/video_conference/resources:vc_resources_grit",
"//ash/webui/common/mojom:sea_pen",
"//ash/webui/diagnostics_ui/mojom:mojom",
"//ash/webui/eche_app_ui:eche_connection_status",

@ -72,6 +72,7 @@
#include "ash/system/unified/quick_settings_footer.h"
#include "ash/system/unified/unified_system_tray_controller.h"
#include "ash/system/usb_peripheral/usb_peripheral_notification_controller.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/touch/touch_devices_controller.h"
#include "ash/user_education/user_education_controller.h"
#include "ash/wallpaper/sea_pen_wallpaper_manager.h"
@ -166,6 +167,7 @@ void RegisterProfilePrefs(PrefRegistrySimple* registry,
UserEducationController::RegisterProfilePrefs(registry);
MediaTray::RegisterProfilePrefs(registry);
UsbPeripheralNotificationController::RegisterProfilePrefs(registry);
VideoConferenceTrayController::RegisterProfilePrefs(registry);
VpnDetailedView::RegisterProfilePrefs(registry);
WallpaperDailyRefreshScheduler::RegisterProfilePrefs(registry);
WallpaperTimeOfDayScheduler::RegisterProfilePrefs(registry);

@ -17,10 +17,12 @@ repack("ash_test_resources_unscaled") {
sources = [
"$root_gen_dir/ash/public/cpp/resources/ash_public_unscaled_resources.pak",
"$root_gen_dir/ash/system/mahi/resources/mahi_resources.pak",
"$root_gen_dir/ash/system/video_conference/resources/vc_resources.pak",
]
deps = [
"//ash/public/cpp/resources:ash_public_unscaled_resources",
"//ash/system/mahi/resources:mahi_resources",
"//ash/system/video_conference/resources:vc_resources",
]
if (include_ash_ambient_animation_resources) {

@ -45,6 +45,7 @@ namespace {
constexpr int kLinuxAppWarningViewTopPadding = 12;
constexpr int kLinuxAppWarningViewSpacing = 1;
constexpr int kLinuxAppWarningIconSize = 16;
constexpr int kScrollViewBetweenChildSpacing = 10;
CameraEffectsController* GetCameraEffectsController() {
return Shell::Get()->camera_effects_controller();
@ -161,6 +162,7 @@ void BubbleView::AddedToWidget() {
views::BoxLayout::CrossAxisAlignment::kStretch);
scroll_contents_view->SetInsideBorderInsets(
gfx::Insets::VH(16, kVideoConferenceBubbleHorizontalPadding));
scroll_contents_view->SetBetweenChildSpacing(kScrollViewBetweenChildSpacing);
// Make the effects sections children of the `views::FlexLayoutView`, so that
// they scroll (if more effects are present than can fit in the available

@ -69,7 +69,10 @@ class BubbleViewPixelTest
features::kFeatureManagementVideoConference,
::features::kChromeRefresh2023, ::features::kChromeRefreshSecondary2023,
::features::kChromeRefresh2023NTB};
std::vector<base::test::FeatureRef> disabled_features{};
// TODO(b/334375880): Add a specific pixel test for the feature
// VcBackgroundReplace.
std::vector<base::test::FeatureRef> disabled_features{
features::kVcBackgroundReplace};
if (IsVcDlcUiEnabled()) {
enabled_features.push_back(features::kVcDlcUi);
}
@ -219,7 +222,7 @@ TEST_P(BubbleViewPixelTest, Basic) {
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"video_conference_bubble_view_basic",
/*revision_number=*/10, bubble_view()));
/*revision_number=*/11, bubble_view()));
}
// Pixel test that tests toggled on/off and focused/not focused for the toggle
@ -292,7 +295,7 @@ TEST_P(BubbleViewPixelTest, ReturnToApp) {
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"video_conference_tray_return_to_app_one_app",
/*revision_number=*/4, GetReturnToAppPanel()));
/*revision_number=*/5, GetReturnToAppPanel()));
controller()->AddMediaApp(CreateFakeMediaApp(
/*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
@ -308,7 +311,7 @@ TEST_P(BubbleViewPixelTest, ReturnToApp) {
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"video_conference_tray_return_to_app_two_apps_collapsed",
/*revision_number=*/4, return_to_app_panel));
/*revision_number=*/5, return_to_app_panel));
// Click the summary row to expand the panel.
auto* summary_row = static_cast<video_conference::ReturnToAppButton*>(
@ -318,7 +321,7 @@ TEST_P(BubbleViewPixelTest, ReturnToApp) {
EXPECT_TRUE(GetPixelDiffer()->CompareUiComponentsOnPrimaryScreen(
"video_conference_tray_return_to_app_two_apps_expanded",
/*revision_number=*/4, return_to_app_panel));
/*revision_number=*/5, return_to_app_panel));
}
TEST_P(BubbleViewPixelTest, ReturnToAppLinux) {

@ -11,7 +11,9 @@
#include "ash/system/camera/camera_effects_controller.h"
#include "ash/system/video_conference/bubble/bubble_view.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/resources/grit/vc_resources.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_utils.h"
#include "ash/wallpaper/wallpaper_utils/sea_pen_metadata_utils.h"
#include "ash/webui/common/mojom/sea_pen.mojom.h"
#include "base/functional/callback_helpers.h"
@ -29,9 +31,12 @@
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/views/background.h"
#include "ui/views/controls/animated_image_view.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
namespace ash::video_conference {
@ -49,9 +54,8 @@ constexpr char kCreateWithAiButtonHistogramName[] =
constexpr gfx::Insets kSetCameraBackgroundViewInsideBorderInsets =
gfx::Insets::TLBR(10, 0, 0, 0);
// This extra border is added to `CreateImageButton` to make it consistent with
// other buttons in the video conference bubble.
constexpr gfx::Insets kCreateImageButtonBorderInsets = gfx::Insets::VH(8, 0);
constexpr gfx::Insets kImageLabelContainerInsideBorderInsets =
gfx::Insets::TLBR(6, 0, 6, 0);
constexpr int kCreateImageButtonBetweenChildSpacing = 12;
constexpr int kSetCameraBackgroundViewBetweenChildSpacing = 10;
@ -90,6 +94,21 @@ CameraEffectsController* GetCameraEffectsController() {
return Shell::Get()->camera_effects_controller();
}
// Returns a gradient lottie animation defined in the resource file for the
// `Create with AI` button.
std::unique_ptr<lottie::Animation> GetGradientAnimation(
const ui::ColorProvider* color_provider) {
std::optional<std::vector<uint8_t>> lottie_data =
ui::ResourceBundle::GetSharedInstance().GetLottieData(
IDR_VC_CREATE_WITH_AI_BUTTON_ANIMATION);
CHECK(lottie_data.has_value());
return std::make_unique<lottie::Animation>(
cc::SkottieWrapper::UnsafeCreateSerializable(lottie_data.value()),
video_conference_utils::CreateColorMapForGradientAnimation(
color_provider));
}
// Image button for the recently used images as camera background.
class RecentlyUsedImageButton : public views::ImageButton {
METADATA_HEADER(RecentlyUsedImageButton, views::ImageButton)
@ -299,26 +318,44 @@ BEGIN_METADATA(RecentlyUsedBackgroundView)
END_METADATA
// Button for "Create with AI".
class CreateImageButton : public views::LabelButton {
METADATA_HEADER(CreateImageButton, views::LabelButton)
class CreateImageButton : public views::Button {
METADATA_HEADER(CreateImageButton, views::Button)
public:
CreateImageButton(VideoConferenceTrayController* controller)
: views::LabelButton(
base::BindRepeating(&CreateImageButton::OnButtonClicked,
base::Unretained(this)),
l10n_util::GetStringUTF16(
IDS_ASH_VIDEO_CONFERENCE_CREAT_WITH_AI_NAME)),
explicit CreateImageButton(VideoConferenceTrayController* controller)
: views::Button(base::BindRepeating(&CreateImageButton::OnButtonClicked,
base::Unretained(this))),
controller_(controller) {
// TODO(b/334205690): Use view builder pattern.
SetID(BubbleViewID::kCreateWithAiButton);
SetBorder(views::CreateEmptyBorder(kCreateImageButtonBorderInsets));
SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
SetImageLabelSpacing(kCreateImageButtonBetweenChildSpacing);
SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ASH_VIDEO_CONFERENCE_CREAT_WITH_AI_NAME));
SetLayoutManager(std::make_unique<views::FillLayout>());
SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysSystemOnBase, kSetCameraBackgroundViewRadius));
SetImageModel(ButtonState::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(
kAiWandIcon, ui::kColorMenuIcon, kButtonHeight));
lottie_animation_view_ =
AddChildView(std::make_unique<views::AnimatedImageView>());
auto* image_label_view_container =
AddChildView(std::make_unique<views::BoxLayoutView>());
image_label_view_container->SetBetweenChildSpacing(
kCreateImageButtonBetweenChildSpacing);
image_label_view_container->SetOrientation(
views::BoxLayout::Orientation::kHorizontal);
image_label_view_container->SetMainAxisAlignment(
views::LayoutAlignment::kCenter);
image_label_view_container->SetInsideBorderInsets(
kImageLabelContainerInsideBorderInsets);
image_label_view_container->SetMainAxisAlignment(
views::LayoutAlignment::kCenter);
image_label_view_container->AddChildView(
std::make_unique<views::ImageView>(ui::ImageModel::FromVectorIcon(
kAiWandIcon, ui::kColorMenuIcon, kButtonHeight)));
image_label_view_container->AddChildView(
std::make_unique<views::Label>(l10n_util::GetStringUTF16(
IDS_ASH_VIDEO_CONFERENCE_CREAT_WITH_AI_NAME)));
}
CreateImageButton(const CreateImageButton&) = delete;
@ -326,14 +363,116 @@ class CreateImageButton : public views::LabelButton {
~CreateImageButton() override = default;
private:
// views::Button:
// Reset the animated image on theme changed to get correct color for the
// animation if the `lottie_animation_view_` should be shown and is visible.
void OnThemeChanged() override {
views::Button::OnThemeChanged();
// Don't need to reset the animated image when animation shouldn't be shown
// or `lottie_animation_view_` is invisible, or this button's bounds is
// empty.
// TODO(b/334205691): Set visibility correctly to make the check for bounds
// no longer needed.
if (!controller_->ShouldShowCreateWithAiButtonAnimation() ||
!lottie_animation_view_->GetVisible() ||
GetBoundsInScreen().IsEmpty()) {
return;
}
lottie_animation_view_->SetAnimatedImage(
GetGradientAnimation(GetColorProvider()));
lottie_animation_view_->Play();
}
// CreateImageButton could be not laid out if there's no recently used images.
// We don't want to play the animation if the button is not shown yet.
void AddedToWidget() override {
// TODO(b/334205691): Set visibility correctly to make the check for bounds
// no longer needed.
if (!controller_->ShouldShowCreateWithAiButtonAnimation() ||
GetBoundsInScreen().IsEmpty()) {
lottie_animation_view_->SetVisible(false);
if (lottie_animation_view_->animated_image()) {
lottie_animation_view_->Stop();
}
return;
}
PlayAnimation();
}
// Used to indicate when the button is laid out and shown.
// TODO(b/334205691): Set visibility correctly. Change this function to
// VisibilityChanged();
void OnBoundsChanged(const gfx::Rect& previous_bounds) override {
UpdateAnimationViewVisibility();
}
void UpdateAnimationViewVisibility() {
// TODO(b/334205691): Set visibility correctly to make the check for bounds
// no longer needed.
if (!is_first_time_animation_ ||
!controller_->ShouldShowCreateWithAiButtonAnimation() ||
GetBoundsInScreen().IsEmpty()) {
lottie_animation_view_->SetVisible(false);
if (lottie_animation_view_->animated_image()) {
lottie_animation_view_->Stop();
}
return;
}
PlayAnimation();
}
void OnButtonClicked(const ui::Event& event) {
HideAnimationView();
controller_->CreateBackgroundImage();
controller_->DismissCreateWithAiButtonAnimationForever();
base::UmaHistogramBoolean(kCreateWithAiButtonHistogramName, true);
}
void PlayAnimation() {
if (!lottie_animation_view_->animated_image()) {
lottie_animation_view_->SetAnimatedImage(
GetGradientAnimation(GetColorProvider()));
}
lottie_animation_view_->SetVisible(true);
lottie_animation_view_->Play();
stop_animation_timer_.Start(FROM_HERE, kGradientAnimationDuration, this,
&CreateImageButton::StopAnimation);
}
void StopAnimation() {
is_first_time_animation_ = false;
stop_animation_timer_.Stop();
lottie_animation_view_->Stop();
lottie_animation_view_->SetVisible(false);
}
void HideAnimationView() {
if (!lottie_animation_view_->GetVisible()) {
return;
}
stop_animation_timer_.Stop();
lottie_animation_view_->Stop();
lottie_animation_view_->SetVisible(false);
}
// Unowned by `CreateImageButton`.
const raw_ptr<VideoConferenceTrayController> controller_;
// Owned by the View's hierarchy. Used to play the animation on the button.
raw_ptr<views::AnimatedImageView> lottie_animation_view_ = nullptr;
// It's set false when the animation has been played during the lifetime of
// `this`. When it's false, we shouldn't play animation animation anymore.
bool is_first_time_animation_ = true;
// Started when `lottie_animation_view_` starts playing the animation. It's
// used to stop the animation after the animation duration.
base::OneShotTimer stop_animation_timer_;
};
BEGIN_METADATA(CreateImageButton)

@ -9,27 +9,190 @@
#include "ash/style/tab_slider.h"
#include "ash/style/tab_slider_button.h"
#include "ash/style/typography.h"
#include "ash/system/camera/camera_effects_controller.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_delegate.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager_types.h"
#include "ash/system/video_conference/resources/grit/vc_resources.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_utils.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/views/controls/animated_image_view.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/view.h"
namespace ash::video_conference {
SetValueEffectSlider::SetValueEffectSlider(const VcHostedEffect* effect)
namespace {
constexpr int kIconSize = 20;
// Returns a gradient lottie animation defined in the resource file for the
// `Image` button.
std::unique_ptr<lottie::Animation> GetGradientAnimation(
const ui::ColorProvider* color_provider) {
std::optional<std::vector<uint8_t>> lottie_data =
ui::ResourceBundle::GetSharedInstance().GetLottieData(
IDR_VC_IMAGE_BUTTON_ANIMATION);
CHECK(lottie_data.has_value());
return std::make_unique<lottie::Animation>(
cc::SkottieWrapper::UnsafeCreateSerializable(lottie_data.value()),
video_conference_utils::CreateColorMapForGradientAnimation(
color_provider));
}
// Button for "Create with AI".
class AnimatedImageButton : public TabSliderButton {
METADATA_HEADER(AnimatedImageButton, views::Button)
public:
explicit AnimatedImageButton(VideoConferenceTrayController* controller,
const VcHostedEffect* effect,
const VcEffectState* state)
: TabSliderButton(
base::BindRepeating(&AnimatedImageButton::OnButtonClicked,
base::Unretained(this)),
state->label_text()),
controller_(controller),
effect_(effect),
state_(state) {
// TODO(b/334205690): Use view builder pattern.
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
/*inside_border_insets=*/gfx::Insets(8)));
auto* animated_view_container =
AddChildView(std::make_unique<views::View>());
animated_view_container->SetLayoutManager(
std::make_unique<views::FillLayout>());
lottie_animation_view_ = animated_view_container->AddChildView(
std::make_unique<views::AnimatedImageView>());
auto* image_view_container =
animated_view_container->AddChildView(std::make_unique<views::View>());
image_view_container->SetLayoutManager(
std::make_unique<views::FillLayout>());
image_view_container->SetBorder(views::CreateEmptyBorder(gfx::Insets(4)));
auto* image_view = image_view_container->AddChildView(
std::make_unique<views::ImageView>());
image_view->SetImage(ui::ImageModel::FromImageGenerator(
base::BindRepeating(
[](TabSliderButton* tab_slider_button,
const gfx::VectorIcon* vector_icon, const ui::ColorProvider*) {
return gfx::CreateVectorIcon(
*vector_icon, kIconSize,
tab_slider_button->GetColorProvider()->GetColor(
tab_slider_button->GetColorIdOnButtonState()));
},
/*tab_slider_button=*/this, state->icon()),
gfx::Size(kIconSize, kIconSize)));
label_ = AddChildView(std::make_unique<views::Label>(state->label_text()));
label_->SetAutoColorReadabilityEnabled(false);
TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2,
*label_);
}
AnimatedImageButton(const AnimatedImageButton&) = delete;
AnimatedImageButton& operator=(const AnimatedImageButton&) = delete;
~AnimatedImageButton() override = default;
// Reset the animated image on theme changed to get correct color for the
// animation if the `lottie_animation_view_` should be shown and is visible.
void OnThemeChanged() override {
TabSliderButton::OnThemeChanged();
if (!controller_->ShouldShowImageButtonAnimation() ||
!lottie_animation_view_->GetVisible()) {
return;
}
lottie_animation_view_->SetAnimatedImage(
GetGradientAnimation(GetColorProvider()));
lottie_animation_view_->Play();
}
// We should only play the animation when animation view should be shown.
void AddedToWidget() override {
if (!controller_->ShouldShowImageButtonAnimation()) {
lottie_animation_view_->SetVisible(false);
return;
}
if (!lottie_animation_view_->animated_image()) {
lottie_animation_view_->SetAnimatedImage(
GetGradientAnimation(GetColorProvider()));
}
lottie_animation_view_->Play();
stop_animation_timer_.Start(FROM_HERE, kGradientAnimationDuration, this,
&AnimatedImageButton::HideAnimationView);
}
void OnButtonClicked(const ui::Event& event) {
HideAnimationView();
if (effect_->delegate()) {
effect_->delegate()->RecordMetricsForSetValueEffectOnClick(
effect_->id(), state_->state_value().value());
}
state_->button_callback().Run();
controller_->DismissImageButtonAnimationForever();
}
// Update label and image color on selected state changed.
void OnSelectedChanged() override {
label_->SetEnabledColorId(GetColorIdOnButtonState());
// `SchedulePaint()` will result in the `gfx::VectorIcon` for `image_view_`
// getting re-generated with the proper color.
SchedulePaint();
}
void HideAnimationView() {
if (!lottie_animation_view_->GetVisible()) {
return;
}
stop_animation_timer_.Stop();
lottie_animation_view_->Stop();
lottie_animation_view_->SetVisible(false);
}
private:
raw_ptr<VideoConferenceTrayController> controller_;
// Information about the associated video conferencing effect needed to
// display the UI of the tile controlled by this controller.
const raw_ptr<const VcHostedEffect> effect_;
const raw_ptr<const VcEffectState> state_;
// Owned by the View's hierarchy. Used to play the animation on the image.
raw_ptr<views::AnimatedImageView> lottie_animation_view_ = nullptr;
// Owned by the View's hierarchy. It's the text shown on `this`.
raw_ptr<views::Label> label_ = nullptr;
// Started when `lottie_animation_view_` starts playing the animation. It's
// used to stop the animation after the animation duration.
base::OneShotTimer stop_animation_timer_;
};
BEGIN_METADATA(AnimatedImageButton)
END_METADATA
} // namespace
SetValueEffectSlider::SetValueEffectSlider(
VideoConferenceTrayController* controller,
const VcHostedEffect* effect)
: effect_id_(effect->id()) {
SetID(BubbleViewID::kSingleSetValueEffectView);
auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
/*inside_border_insets=*/gfx::Insets::TLBR(8, 0, 0, 0),
/*between_child_spacing=*/8));
/*inside_border_insets=*/gfx::Insets(), /*between_child_spacing=*/8));
layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kStretch);
@ -79,19 +242,27 @@ SetValueEffectSlider::SetValueEffectSlider(const VcHostedEffect* effect)
for (int i = 0; i < num_states; ++i) {
const VcEffectState* state = effect->GetState(/*index=*/i);
DCHECK(state->state_value());
auto* slider_button =
tab_slider->AddButton(std::make_unique<IconLabelSliderButton>(
base::BindRepeating(
[](const VcHostedEffect* effect, const VcEffectState* state) {
if (effect->delegate()) {
effect->delegate()->RecordMetricsForSetValueEffectOnClick(
effect->id(), state->state_value().value());
}
state->button_callback().Run();
},
base::Unretained(effect), base::Unretained(state)),
state->icon(), state->label_text()));
TabSliderButton* slider_button;
if (state->state_value() ==
CameraEffectsController::BackgroundBlurPrefValue::kImage) {
slider_button = tab_slider->AddButton(
std::make_unique<AnimatedImageButton>(controller, effect, state));
} else {
slider_button =
tab_slider->AddButton(std::make_unique<IconLabelSliderButton>(
base::BindRepeating(
[](const VcHostedEffect* effect, const VcEffectState* state) {
if (effect->delegate()) {
effect->delegate()->RecordMetricsForSetValueEffectOnClick(
effect->id(), state->state_value().value());
}
state->button_callback().Run();
},
base::Unretained(effect), base::Unretained(state)),
state->icon(), state->label_text()));
}
slider_button->SetSelected(state->state_value().value() == current_state);
@ -125,7 +296,7 @@ SetValueEffectsView::SetValueEffectsView(
continue;
}
AddChildView(std::make_unique<SetValueEffectSlider>(effect));
AddChildView(std::make_unique<SetValueEffectSlider>(controller, effect));
}
}
}

@ -26,7 +26,8 @@ class SetValueEffectSlider : public views::View {
METADATA_HEADER(SetValueEffectSlider, views::View)
public:
explicit SetValueEffectSlider(const VcHostedEffect* effect);
SetValueEffectSlider(VideoConferenceTrayController* controller,
const VcHostedEffect* effect);
SetValueEffectSlider(const SetValueEffectSlider&) = delete;
SetValueEffectSlider& operator=(const SetValueEffectSlider&) = delete;

@ -0,0 +1,16 @@
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//build/config/chromeos/ui_mode.gni")
import("//tools/grit/grit_rule.gni")
assert(is_chromeos_ash)
grit("vc_resources") {
source = "vc_resources.grd"
outputs = [
"grit/vc_resources.h",
"vc_resources.pak",
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--This file contains the gradient animation json files for image button and create
with ai button on the VC tray.
-->
<grit latest_public_release="0" current_release="1" output_all_resource_defines="false">
<outputs>
<output filename="grit/vc_resources.h" type="rc_header">
<emit emit_type='prepend'></emit>
</output>
<output filename="vc_resources.pak" type="data_package" />
</outputs>
<release seq="1">
<structures>
<structure type="lottie" name="IDR_VC_CREATE_WITH_AI_BUTTON_ANIMATION" file="create_with_ai_button_gradient.json" compress="gzip" />
<structure type="lottie" name="IDR_VC_IMAGE_BUTTON_ANIMATION" file="image_button_gradient.json" compress="gzip" />
</structures>
</release>
</grit>

@ -8,6 +8,7 @@
#include <vector>
#include "base/functional/callback.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chromeos/crosapi/mojom/video_conference.mojom-forward.h"
@ -17,6 +18,10 @@ constexpr int kVideoConferenceBubbleHorizontalPadding = 12;
const int kReturnToAppIconSize = 20;
// The duration for the gradient animation on the Image and Create with AI
// buttons.
const base::TimeDelta kGradientAnimationDuration = base::Milliseconds(3120);
// This struct provides aggregated attributes of media apps
// from one or more clients.
struct VideoConferenceMediaState {

@ -72,6 +72,15 @@ constexpr char kVideoConferenceTrayCameraUseWhileSWDisabledNudgeId[] =
constexpr char kVideoConferenceTrayBothUseWhileDisabledNudgeId[] =
"video_conference_tray_nudge_ids.camera_microphone_use_while_disabled";
// Boolean prefs used to determine whether to show the gradient animation on the
// buttons. When the value is false, it means that we haved showed the animation
// at some point and the user has clicked on the button in such a way that the
// animation no longer needs to be displayed again.
constexpr char kShowImageButtonAnimation[] =
"ash.vc.show_inmage_button_animation";
constexpr char kShowCreateWithAiButtonAnimation[] =
"ash.vc.show_create_with_ai_button_animation";
// VC nudge ids vector that is iterated whenever `CloseAllVcNudges()` is
// called. Please keep in sync whenever adding/removing/updating a nudge id.
const char* const kNudgeIds[] = {
@ -127,6 +136,15 @@ VideoConferenceTray* GetVcTrayInActiveWindow() {
return status_area_widget->video_conference_tray();
}
PrefService* GetActiveUserPrefService() {
DCHECK(Shell::Get()->session_controller()->IsActiveUserSessionStarted());
auto* pref_service =
Shell::Get()->session_controller()->GetActivePrefService();
DCHECK(pref_service);
return pref_service;
}
} // namespace
VideoConferenceTrayController::VideoConferenceTrayController()
@ -151,6 +169,12 @@ VideoConferenceTrayController::~VideoConferenceTrayController() {
}
// static
void VideoConferenceTrayController::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(kShowImageButtonAnimation, true);
registry->RegisterBooleanPref(kShowCreateWithAiButtonAnimation, true);
}
VideoConferenceTrayController* VideoConferenceTrayController::Get() {
return g_controller_instance;
}
@ -288,6 +312,26 @@ void VideoConferenceTrayController::MaybeShowSpeakOnMuteOptInNudge() {
}
}
void VideoConferenceTrayController::DismissImageButtonAnimationForever() {
GetActiveUserPrefService()->SetBoolean(kShowImageButtonAnimation, false);
}
void VideoConferenceTrayController::
DismissCreateWithAiButtonAnimationForever() {
GetActiveUserPrefService()->SetBoolean(kShowCreateWithAiButtonAnimation,
false);
}
bool VideoConferenceTrayController::ShouldShowImageButtonAnimation() const {
return GetActiveUserPrefService()->GetBoolean(kShowImageButtonAnimation);
}
bool VideoConferenceTrayController::ShouldShowCreateWithAiButtonAnimation()
const {
return GetActiveUserPrefService()->GetBoolean(
kShowCreateWithAiButtonAnimation);
}
void VideoConferenceTrayController::OnSpeakOnMuteNudgeOptInAction(bool opt_in) {
auto* pref_service =
Shell::Get()->session_controller()->GetActivePrefService();

@ -16,6 +16,7 @@
#include "base/timer/timer.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "chromeos/crosapi/mojom/video_conference.mojom-forward.h"
#include "components/prefs/pref_registry_simple.h"
#include "media/capture/video/chromeos/camera_hal_dispatcher_impl.h"
namespace base {
@ -72,6 +73,9 @@ class ASH_EXPORT VideoConferenceTrayController
~VideoConferenceTrayController() override;
// Called inside ash/ash_prefs.cc to register related prefs.
static void RegisterProfilePrefs(PrefRegistrySimple* registry);
// Returns the singleton instance.
static VideoConferenceTrayController* Get();
@ -103,6 +107,17 @@ class ASH_EXPORT VideoConferenceTrayController
// Attempts showing the speak-on-mute opt-in nudge.
void MaybeShowSpeakOnMuteOptInNudge();
// Returns true if we can show the animation to help users to discover the new
// feature.
bool ShouldShowImageButtonAnimation() const;
bool ShouldShowCreateWithAiButtonAnimation() const;
// Disables showing the animation for the button from now on. Calling the
// above ShouldShow...() will return false for the current active user going
// forward.
void DismissImageButtonAnimationForever();
void DismissCreateWithAiButtonAnimationForever();
// Callback used to update prefs whenever a user opts in or out of the
// speak-on-mute feature. An `opt_in` value of false means the user opted out.
void OnSpeakOnMuteNudgeOptInAction(bool opt_in);

@ -40,7 +40,9 @@ class VideoConferenceTrayPixelTest : public AshTestBase {
features::kVcStopAllScreenShare,
chromeos::features::kJelly,
features::kFeatureManagementVideoConference},
/*disabled_features=*/{});
/*disabled_features=*/{features::kVcBackgroundReplace});
// TODO(b/334375880): Add a specific pixel test for the feature
// VcBackgroundReplace.
// Instantiates a fake controller (the real one is created in
// ChromeBrowserMainExtraPartsAsh::PreProfileInit() which is not called in

@ -6,9 +6,11 @@
#include <string>
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager_types.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/crosapi/mojom/video_conference.mojom.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
namespace ash::video_conference_utils {
@ -59,4 +61,23 @@ std::u16string GetMediaAppDisplayText(
return std::u16string();
}
cc::SkottieColorMap CreateColorMapForGradientAnimation(
const ui::ColorProvider* color_provider) {
cc::SkottieColorMap map;
if (DarkLightModeController::Get()->IsDarkModeEnabled()) {
map[cc::HashSkottieResourceId("cros.sys.illo.complement")] =
color_provider->GetColor(
cros_tokens::CrosRefColorIds::kCrosRefSparkleComplement20);
map[cc::HashSkottieResourceId("cros.sys.illo.analog")] =
color_provider->GetColor(
cros_tokens::CrosRefColorIds::kCrosRefSparkleAnalog30);
} else {
map[cc::HashSkottieResourceId("cros.sys.illo.complement")] =
color_provider->GetColor(ui::kColorNativeComplementColor);
map[cc::HashSkottieResourceId("cros.sys.illo.analog")] =
color_provider->GetColor(ui::kColorNativeAnalogColor);
}
return map;
}
} // namespace ash::video_conference_utils

@ -8,7 +8,9 @@
#include <string>
#include "ash/ash_export.h"
#include "cc/paint/skottie_color_map.h"
#include "chromeos/crosapi/mojom/video_conference.mojom-forward.h"
#include "ui/color/color_provider.h"
namespace ash {
@ -29,6 +31,12 @@ std::u16string GetMediaAppDisplayText(
const mojo::StructPtr<crosapi::mojom::VideoConferenceMediaAppInfo>&
media_app);
// Lottie animation doesn't support dark mode color, in order to make the
// animation look good in both dark and light modes, we manually override the
// colors used in the animation.
cc::SkottieColorMap CreateColorMapForGradientAnimation(
const ui::ColorProvider* color_provider);
} // namespace video_conference_utils
} // namespace ash

@ -231,6 +231,7 @@ template("chrome_extra_paks") {
sources += [
"$root_gen_dir/ash/public/cpp/resources/ash_public_unscaled_resources.pak",
"$root_gen_dir/ash/system/mahi/resources/mahi_resources.pak",
"$root_gen_dir/ash/system/video_conference/resources/vc_resources.pak",
"$root_gen_dir/ash/webui/ash_camera_app_resources.pak",
"$root_gen_dir/ash/webui/ash_color_internals_resources.pak",
"$root_gen_dir/ash/webui/ash_demo_mode_app_resources.pak",
@ -313,6 +314,7 @@ template("chrome_extra_paks") {
"//ash/components/arc/input_overlay/resources",
"//ash/public/cpp/resources:ash_public_unscaled_resources",
"//ash/system/mahi/resources:mahi_resources",
"//ash/system/video_conference/resources:vc_resources",
"//ash/webui/color_internals/resources:resources",
"//ash/webui/common/resources:resources",
"//ash/webui/common/resources/office_fallback:resources",

@ -1161,6 +1161,9 @@
"ash/system/mahi/resources/mahi_resources.grd": {
"structures":[7620],
},
"ash/system/video_conference/resources/vc_resources.grd": {
"structures":[7630],
},
"base/tracing/protos/resources.grd": {
"includes": [7640],
},