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:

committed by
Chromium LUCI CQ

parent
2afc8d8af1
commit
a794b68057
ash
BUILD.gnash_prefs.cc
resources
system
video_conference
bubble
bubble_view.ccbubble_view_pixeltest.ccset_camera_background_view.ccset_value_effects_view.ccset_value_effects_view.h
resources
video_conference_common.hvideo_conference_tray_controller.ccvideo_conference_tray_controller.hvideo_conference_tray_pixeltest.ccvideo_conference_utils.ccvideo_conference_utils.hchrome
tools/gritsettings
@ -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;
|
||||
|
16
ash/system/video_conference/resources/BUILD.gn
Normal file
16
ash/system/video_conference/resources/BUILD.gn
Normal file
@ -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
18
ash/system/video_conference/resources/vc_resources.grd
Normal file
18
ash/system/video_conference/resources/vc_resources.grd
Normal file
@ -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],
|
||||
},
|
||||
|
Reference in New Issue
Block a user