Skip to content

Commit

Permalink
[Phone Hub][Eche] Fix loading animation for more apps button
Browse files Browse the repository at this point in the history
The PhoneHubMoreAppsButton's loading animation does not match the
"Skeleton Loaders" spec, so this CL fixes that by factoring out the
LoadingView's animation code into AppLoadingIcon and using it in both
places.

A "StopLoadingAnimation" method is also introduced, which will be
useful later to pause the animation before fading in the loaded view.

The size of the app icons / buttons has also been increased to 42px.
This brings us into spec, and makes the icons match the size of the
more apps button.

Screen capture:
https://drive.google.com/file/d/1vM3lbcRV4zm1-1tbhB-hf6Gku_twmUBd/view?usp=sharing&resourcekey=0-1gsylB4dHxAKV6VRqWgRNQ

TEST=Manual testing with the loading view forced on, manual testing
for regressions of the non-loading view.

Bug: b/271478560
Change-Id: I4704403c9c8ac047de560dec64c40cc54327b834
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4347745
Reviewed-by: Jon Mann <jonmann@chromium.org>
Commit-Queue: Curt Clemens <cclem@google.com>
Reviewed-by: Crisrael Lucero <crisrael@google.com>
Reviewed-by: Pu Shi <pushi@google.com>
Cr-Commit-Position: refs/heads/main@{#1118902}
  • Loading branch information
Curt Clemens authored and Chromium LUCI CQ committed Mar 17, 2023
1 parent 416e3af commit 6dc633c
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 76 deletions.
4 changes: 2 additions & 2 deletions ash/system/phonehub/phone_hub_app_icon.h
Expand Up @@ -4,6 +4,7 @@

#ifndef ASH_SYSTEM_PHONEHUB_PHONE_HUB_APP_ICON_H_
#define ASH_SYSTEM_PHONEHUB_PHONE_HUB_APP_ICON_H_

#include "ash/ash_export.h"
#include "ui/views/controls/image_view.h"

Expand All @@ -13,7 +14,7 @@ class ASH_EXPORT AppIcon : public views::ImageView {
public:
// Measured in DIPs.
static constexpr int kSizeSmall = 20;
static constexpr int kSizeNormal = 32;
static constexpr int kSizeNormal = 42;

static constexpr gfx::Size GetRecommendedImageSize(int icon_size) {
// Leave 1 DP of space around the image to avoid the appearance of clipping.
Expand All @@ -26,7 +27,6 @@ class ASH_EXPORT AppIcon : public views::ImageView {
AppIcon(const gfx::Image& icon, int size);
AppIcon(const AppIcon&) = delete;
AppIcon& operator=(const AppIcon&) = delete;

~AppIcon() override = default;

// views::View:
Expand Down
34 changes: 34 additions & 0 deletions ash/system/phonehub/phone_hub_app_loading_icon.cc
Expand Up @@ -13,6 +13,12 @@ namespace ash {

namespace {

// Constants for loading animation
constexpr float kAnimationOpacityHigh = 1.0f;
constexpr float kAnimationOpacityLow = 0.5f;
constexpr int kAnimationFadeDownDurationInMs = 500;
constexpr int kAnimationFadeUpDurationInMs = 500;

class LoadingCircle : public gfx::CanvasImageSource {
public:
explicit LoadingCircle(int size)
Expand Down Expand Up @@ -43,4 +49,32 @@ AppLoadingIcon::AppLoadingIcon(int size)
layer()->SetFillsBoundsCompletely(false);
}

AppLoadingIcon::~AppLoadingIcon() = default;

void AppLoadingIcon::StartLoadingAnimation(
absl::optional<base::TimeDelta> initial_delay) {
if (initial_delay) {
animation_initial_delay_timer_.Start(
FROM_HERE, *initial_delay,
base::BindOnce(&AppLoadingIcon::StartLoadingAnimation,
base::Unretained(this),
/*initial_delay=*/absl::nullopt));
return;
}

views::AnimationBuilder builder;
builder.Repeatedly()
.SetDuration(base::Milliseconds(kAnimationFadeDownDurationInMs))
.SetOpacity(this, kAnimationOpacityLow, gfx::Tween::ACCEL_30_DECEL_20_85)
.Then()
.SetDuration(base::Milliseconds(kAnimationFadeUpDurationInMs))
.SetOpacity(this, kAnimationOpacityHigh, gfx::Tween::LINEAR);
animation_abort_handle_ = builder.GetAbortHandle();
}

void AppLoadingIcon::StopLoadingAnimation() {
animation_abort_handle_.reset();
animation_initial_delay_timer_.Stop();
}

} // namespace ash
13 changes: 13 additions & 0 deletions ash/system/phonehub/phone_hub_app_loading_icon.h
Expand Up @@ -7,12 +7,25 @@

#include "ash/ash_export.h"
#include "ash/system/phonehub/phone_hub_app_icon.h"
#include "base/timer/timer.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/views/animation/animation_abort_handle.h"

namespace ash {

class ASH_EXPORT AppLoadingIcon : public AppIcon {
public:
explicit AppLoadingIcon(int size);
AppLoadingIcon(const AppLoadingIcon&) = delete;
AppLoadingIcon& operator=(const AppLoadingIcon&) = delete;
~AppLoadingIcon() override;

void StartLoadingAnimation(absl::optional<base::TimeDelta> initial_delay);
void StopLoadingAnimation();

private:
std::unique_ptr<views::AnimationAbortHandle> animation_abort_handle_;
base::OneShotTimer animation_initial_delay_timer_;
};

} // namespace ash
Expand Down
55 changes: 27 additions & 28 deletions ash/system/phonehub/phone_hub_more_apps_button.cc
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

#include "ash/system/phonehub/phone_hub_more_apps_button.h"

#include <memory>

#include "ash/strings/grit/ash_strings.h"
Expand All @@ -28,11 +29,9 @@ constexpr int kMoreAppsButtonRowPadding = 20;
constexpr int kMoreAppsButtonColumnPadding = 2;
constexpr int kMoreAppsButtonBackgroundRadius = 18;

// Animation constants for loading card
constexpr float kAnimationLoadingCardOpacity = 1.0f;
constexpr int kAnimationLoadingCardDelayInMs = 83;
constexpr int kAnimationLoadingCardTransitDurationInMs = 200;
constexpr int kAnimationLoadingCardFreezeDurationInMs = 150;
// The app icons in the LoadingView stagger the start of the loading animation
// to make the appearance of a ripple.
constexpr int kAnimationLoadingIconStaggerDelayInMs = 100;

class MoreAppsButtonBackground : public views::Background {
public:
Expand Down Expand Up @@ -100,13 +99,12 @@ void PhoneHubMoreAppsButton::InitLayout() {
SetEnabled(false);
SetBackground(std::make_unique<MoreAppsButtonBackground>());
if (!app_stream_launcher_data_model_) {
AddLoadingAppIcons(/*animate=*/false);
return;
}

if (app_stream_launcher_data_model_->GetAppsListSortedByName()->empty()) {
load_app_list_latency_ = base::TimeTicks::Now();
AddLoadingAppIcons(/*animate=*/true);
StartLoadingAnimation(/*initial_delay=*/absl::nullopt);
SetEnabled(false);
phone_hub_metrics::LogMoreAppsButtonAnimationOnShow(
phone_hub_metrics::MoreAppsButtonLoadingState::kAnimationShown);
Expand All @@ -123,30 +121,30 @@ void PhoneHubMoreAppsButton::InitLayout() {
}
}

void PhoneHubMoreAppsButton::AddLoadingAppIcons(bool animate) {
for (auto i = 0; i < 4; i++) {
auto* app_loading_icon =
void PhoneHubMoreAppsButton::StartLoadingAnimation(
absl::optional<base::TimeDelta> initial_delay) {
app_loading_icons_.clear();
RemoveAllChildViews();
for (size_t i = 0; i < 4; i++) {
AppLoadingIcon* app_loading_icon =
AddChildView(new AppLoadingIcon(AppIcon::kSizeSmall));
if (!animate) {
continue;
app_loading_icons_.push_back(app_loading_icon);

size_t x = i % 2;
size_t y = i / 2;
base::TimeDelta stagger_delay =
(x + y) * base::Milliseconds(kAnimationLoadingIconStaggerDelayInMs);
if (initial_delay) {
stagger_delay += *initial_delay;
}

views::AnimationBuilder animation_builder;
animation_builder.Once().SetOpacity(app_loading_icon,
kAnimationLoadingCardOpacity);

animation_builder.Repeatedly()
.Offset(base::Milliseconds(kAnimationLoadingCardDelayInMs))
.SetDuration(
base::Milliseconds(kAnimationLoadingCardTransitDurationInMs))
.SetOpacity(app_loading_icon, 0.0f, gfx::Tween::LINEAR)
.Then()
.Offset(base::Milliseconds(kAnimationLoadingCardFreezeDurationInMs))
.Then()
.SetDuration(
base::Milliseconds(kAnimationLoadingCardTransitDurationInMs))
.SetOpacity(app_loading_icon, kAnimationLoadingCardOpacity,
gfx::Tween::LINEAR);
app_loading_icon->StartLoadingAnimation(stagger_delay);
}
}

void PhoneHubMoreAppsButton::StopLoadingAnimation() {
for (AppLoadingIcon* app_loading_icon : app_loading_icons_) {
app_loading_icon->StopLoadingAnimation();
}
}

Expand All @@ -164,6 +162,7 @@ void PhoneHubMoreAppsButton::OnAppListChanged() {

void PhoneHubMoreAppsButton::LoadAppList() {
CHECK(app_stream_launcher_data_model_);
app_loading_icons_.clear();
RemoveAllChildViews();
const std::vector<phonehub::Notification::AppMetadata>* app_list =
app_stream_launcher_data_model_->GetAppsListSortedByName();
Expand Down
13 changes: 10 additions & 3 deletions ash/system/phonehub/phone_hub_more_apps_button.h
Expand Up @@ -5,13 +5,17 @@
#ifndef ASH_SYSTEM_PHONEHUB_PHONE_HUB_MORE_APPS_BUTTON_H_
#define ASH_SYSTEM_PHONEHUB_PHONE_HUB_MORE_APPS_BUTTON_H_

#include "base/timer/timer.h"
#include <vector>

#include "chromeos/ash/components/phonehub/app_stream_launcher_data_model.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/layout/table_layout.h"

namespace ash {

class AppLoadingIcon;

// A view in phone hub that displays the first three apps in a user's app
// drawer, as well as a count of how many apps they have on their phone.
class VIEWS_EXPORT PhoneHubMoreAppsButton
Expand All @@ -32,19 +36,22 @@ class VIEWS_EXPORT PhoneHubMoreAppsButton
PhoneHubMoreAppsButton& operator=(const PhoneHubMoreAppsButton&) = delete;
~PhoneHubMoreAppsButton() override;

// phonehub::AppStreamLauncherDataModel
void StartLoadingAnimation(absl::optional<base::TimeDelta> initial_delay);
void StopLoadingAnimation();

// phonehub::AppStreamLauncherDataModel::Observer:
void OnShouldShowMiniLauncherChanged() override;
void OnAppListChanged() override;

private:
void InitLayout();
void LoadAppList();
void AddLoadingAppIcons(bool animate);

base::TimeTicks load_app_list_latency_ = base::TimeTicks();
views::TableLayout* table_layout_ = nullptr;
phonehub::AppStreamLauncherDataModel* app_stream_launcher_data_model_ =
nullptr;
std::vector<AppLoadingIcon*> app_loading_icons_;
};

} // namespace ash
Expand Down
2 changes: 1 addition & 1 deletion ash/system/phonehub/phone_hub_recent_app_button.cc
Expand Up @@ -18,7 +18,7 @@ namespace ash {
namespace {

// Appearance in DIPs.
constexpr int kRecentAppButtonSize = 32;
constexpr int kRecentAppButtonSize = 42;

} // namespace

Expand Down
63 changes: 24 additions & 39 deletions ash/system/phonehub/phone_hub_recent_apps_view.cc
Expand Up @@ -29,7 +29,6 @@
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/background.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/button/image_button.h"
Expand Down Expand Up @@ -67,15 +66,12 @@ constexpr int kMaxAppsWithMoreAppsButton = 5;
constexpr gfx::Rect kMoreAppsButtonArea = gfx::Rect(57, 32);
constexpr int kMoreAppsButtonRadius = 16;

// Animation constants for loading view
constexpr float kAnimationLoadingIconOpacityHigh = 1.0f;
constexpr float kAnimationLoadingIconOpacityLow = 0.5f;
constexpr int kAnimationLoadingIconStaggerDelayInMs = 100;
constexpr int kAnimationLoadingIconFadeDownDurationInMs = 500;
constexpr int kAnimationLoadingIconFadeUpDurationInMs = 500;

constexpr int kRecentAppsHeaderSpacing = 220;

// The app icons in the LoadingView stagger the start of the loading animation
// to make the appearance of a ripple.
constexpr int kAnimationLoadingIconStaggerDelayInMs = 100;

void LayoutAppButtonsView(views::View* buttons_view) {
const gfx::Rect child_area = buttons_view->GetContentsBounds();
views::View::Views visible_children;
Expand Down Expand Up @@ -114,18 +110,6 @@ void LayoutAppButtonsView(views::View* buttons_view) {
}
}

void ApplyLoadingAnimation(views::View* view) {
views::AnimationBuilder()
.Repeatedly()
.SetDuration(
base::Milliseconds(kAnimationLoadingIconFadeDownDurationInMs))
.SetOpacity(view, kAnimationLoadingIconOpacityLow,
gfx::Tween::ACCEL_30_DECEL_20_85)
.Then()
.SetDuration(base::Milliseconds(kAnimationLoadingIconFadeUpDurationInMs))
.SetOpacity(view, kAnimationLoadingIconOpacityHigh, gfx::Tween::LINEAR);
}

class HeaderView : public views::View {
public:
explicit HeaderView(views::ImageButton::PressedCallback callback) {
Expand Down Expand Up @@ -294,7 +278,13 @@ PhoneHubRecentAppsView::LoadingView::LoadingView() {
SetMainAxisAlignment(views::BoxLayout::MainAxisAlignment::kCenter);
SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kCenter);

PopulateLoadingView();
for (size_t i = 0; i < 5; i++) {
app_loading_icons_.push_back(
AddChildView(new AppLoadingIcon(AppIcon::kSizeNormal)));
}
more_apps_button_ = AddChildView(new PhoneHubMoreAppsButton());

StartLoadingAnimation();
}

PhoneHubRecentAppsView::LoadingView::~LoadingView() = default;
Expand All @@ -319,26 +309,21 @@ const char* PhoneHubRecentAppsView::LoadingView::GetClassName() const {
return "RecentAppLoadingView";
}

void PhoneHubRecentAppsView::LoadingView::PopulateLoadingView() {
for (size_t i = 0; i < 6; i++) {
views::View* empty_app_loading_icon = nullptr;
if (i == 5) {
empty_app_loading_icon = AddChildView(new PhoneHubMoreAppsButton());

// TODO(b/271478560): Animation fails for more apps button.
continue;
} else {
empty_app_loading_icon =
AddChildView(new AppLoadingIcon(AppIcon::kSizeNormal));
}
void PhoneHubRecentAppsView::LoadingView::StartLoadingAnimation() {
for (size_t i = 0; i < app_loading_icons_.size(); i++) {
app_loading_icons_[i]->StartLoadingAnimation(
/*initial_delay=*/base::Milliseconds(
i * kAnimationLoadingIconStaggerDelayInMs));
}
more_apps_button_->StartLoadingAnimation(/*initial_delay=*/base::Milliseconds(
5 * kAnimationLoadingIconStaggerDelayInMs));
}

auto timer = std::make_unique<base::OneShotTimer>();
timer->Start(
FROM_HERE,
/*delay=*/base::Milliseconds(i * kAnimationLoadingIconStaggerDelayInMs),
base::BindOnce(&ApplyLoadingAnimation, empty_app_loading_icon));
timers_.emplace_back(std::move(timer));
void PhoneHubRecentAppsView::LoadingView::StopLoadingAnimation() {
for (AppLoadingIcon* app_loading_icon : app_loading_icons_) {
app_loading_icon->StopLoadingAnimation();
}
more_apps_button_->StopLoadingAnimation();
}

void PhoneHubRecentAppsView::Update() {
Expand Down
11 changes: 8 additions & 3 deletions ash/system/phonehub/phone_hub_recent_apps_view.h
Expand Up @@ -10,15 +10,18 @@
#include "ash/ash_export.h"
#include "ash/system/phonehub/phone_connected_view.h"
#include "base/gtest_prod_util.h"
#include "base/timer/timer.h"
#include "chromeos/ash/components/phonehub/recent_apps_interaction_handler.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/view.h"
#include "ui/views/view_model.h"

namespace ash {

class AppLoadingIcon;
class PhoneHubMoreAppsButton;

namespace phonehub {
class PhoneHubManager;
}
Expand Down Expand Up @@ -83,10 +86,12 @@ class ASH_EXPORT PhoneHubRecentAppsView
void Layout() override;
const char* GetClassName() const override;

void PopulateLoadingView();
void StartLoadingAnimation();
void StopLoadingAnimation();

private:
std::vector<std::unique_ptr<base::OneShotTimer>> timers_;
std::vector<AppLoadingIcon*> app_loading_icons_;
PhoneHubMoreAppsButton* more_apps_button_ = nullptr;
};

// Update the view to reflect the most recently opened apps.
Expand Down

0 comments on commit 6dc633c

Please sign in to comment.