chromium/ash/app_list/views/top_icon_animation_view.cc

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/app_list/views/top_icon_animation_view.h"

#include <memory>
#include <utility>

#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/typography.h"
#include "base/task/single_thread_task_runner.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/background.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"

namespace ash {

TopIconAnimationView::TopIconAnimationView(AppsGridView* grid,
                                           const gfx::ImageSkia& icon,
                                           const gfx::ImageSkia& badge_icon,
                                           const std::u16string& title,
                                           const gfx::Rect& scaled_rect,
                                           bool open_folder,
                                           bool item_in_folder_icon)
    : grid_(grid),
      icon_(nullptr),
      title_(nullptr),
      scaled_rect_(scaled_rect),
      open_folder_(open_folder),
      item_in_folder_icon_(item_in_folder_icon) {
  const bool is_badged = !badge_icon.isNull();

  const AppListConfig* app_list_config = grid->app_list_config();
  // For badged icon, add the background view behind the icon to mimic
  // appearance of the main app shortcut icon.
  if (is_badged) {
    icon_background_ = AddChildView(std::make_unique<views::View>());
    if (item_in_folder_icon_) {
      icon_background_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
      icon_background_->layer()->SetFillsBoundsOpaquely(false);
    } else {
      const int background_diameter =
          app_list_config->GetShortcutBackgroundContainerDimension();
      const gfx::RoundedCornersF rounded_corners(
          background_diameter, background_diameter,
          app_list_config->GetShortcutHostBadgeIconContainerDimension() / 2,
          background_diameter);
      icon_background_->SetBackground(views::CreateThemedRoundedRectBackground(
          cros_tokens::kCrosSysSystemOnBaseOpaque, rounded_corners, 0));
    }
  }
  icon_size_ = is_badged ? app_list_config->GetShortcutIconSize()
                         : app_list_config->grid_icon_size();
  DCHECK(!icon.isNull());
  gfx::ImageSkia resized(gfx::ImageSkiaOperations::CreateResizedImage(
      icon, skia::ImageOperations::RESIZE_BEST, icon_size_));
  auto icon_image = std::make_unique<views::ImageView>();
  icon_image->SetImage(resized);
  icon_ = AddChildView(std::move(icon_image));
  if (icon_background_ && icon_background_->layer()) {
    icon_->SetPaintToLayer();
    icon_->layer()->SetFillsBoundsOpaquely(false);
  }

  // Add badge view if the item is badged.
  if (is_badged) {
    badge_container_ = AddChildView(std::make_unique<views::View>());
    badge_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
        views::BoxLayout::Orientation::kHorizontal,
        gfx::Insets(
            app_list_config->shortcut_host_badge_icon_border_margin())));
    badge_container_->SetBackground(views::CreateThemedRoundedRectBackground(
        cros_tokens::kCrosSysSystemOnBaseOpaque,
        app_list_config->GetShortcutHostBadgeIconContainerDimension() / 2));
    if (item_in_folder_icon_) {
      badge_container_->SetPaintToLayer();
      badge_container_->layer()->SetFillsBoundsOpaquely(false);
    }
    auto* badge_icon_view =
        badge_container_->AddChildView(std::make_unique<views::ImageView>());
    const gfx::Size badge_icon_size =
        gfx::Size(app_list_config->shortcut_host_badge_icon_dimension(),
                  app_list_config->shortcut_host_badge_icon_dimension());
    badge_icon_view->SetImage(gfx::ImageSkiaOperations::CreateResizedImage(
        badge_icon, skia::ImageOperations::RESIZE_BEST, badge_icon_size));
  }

  auto title_label = std::make_unique<views::Label>();
  title_label->SetBackgroundColor(SK_ColorTRANSPARENT);
  title_label->SetAutoColorReadabilityEnabled(false);
  title_label->SetHandlesTooltips(false);
  title_label->SetHorizontalAlignment(gfx::ALIGN_CENTER);

  TypographyProvider::Get()->StyleLabel(
      app_list_config->type() == AppListConfigType::kDense
          ? TypographyToken::kCrosAnnotation1
          : TypographyToken::kCrosButton2,
      *title_label);
  title_label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  title_label->SetLineHeight(app_list_config->app_title_max_line_height());
  title_label->SetText(title);
  if (item_in_folder_icon_) {
    // The title's opacity of the item should be changed separately if it is in
    // the folder item's icon.
    title_label->SetPaintToLayer();
    title_label->layer()->SetFillsBoundsOpaquely(false);
  }
  title_ = AddChildView(std::move(title_label));

  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
}

TopIconAnimationView::~TopIconAnimationView() {
  // Required due to RequiresNotificationWhenAnimatorDestroyed() returning true.
  // See ui::LayerAnimationObserver for details.
  StopObservingImplicitAnimations();
}

void TopIconAnimationView::AddObserver(TopIconAnimationObserver* observer) {
  observers_.AddObserver(observer);
}

void TopIconAnimationView::RemoveObserver(TopIconAnimationObserver* observer) {
  observers_.RemoveObserver(observer);
}

void TopIconAnimationView::TransformView(base::TimeDelta duration) {
  // Transform used for scaling down the icon and move it back inside to the
  // original folder icon. The transform's origin is this view's origin.
  gfx::Transform transform;
  transform.Translate(scaled_rect_.x() - GetMirroredX(),
                      scaled_rect_.y() - bounds().y());
  transform.Scale(
      static_cast<double>(scaled_rect_.width()) / bounds().width(),
      static_cast<double>(scaled_rect_.height()) / bounds().height());
  if (open_folder_) {
    // Transform to a scaled down icon inside the original folder icon.
    layer()->SetTransform(transform);
  }

  if (!item_in_folder_icon_)
    layer()->SetOpacity(open_folder_ ? 0.0f : 1.0f);

  // Animate the icon to its target location and scale when opening or
  // closing a folder.
  ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator());
  settings.AddObserver(this);
  settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
  settings.SetTransitionDuration(duration);
  layer()->SetTransform(open_folder_ ? gfx::Transform() : transform);
  if (!item_in_folder_icon_)
    layer()->SetOpacity(open_folder_ ? 1.0f : 0.0f);

  // Animate the badge opacity - the item icon within the folder icon is not
  // badged, while the icon in the folder view is badged.
  if (item_in_folder_icon_ && badge_container_) {
    badge_container_->layer()->SetOpacity(open_folder_ ? 0.0f : 1.0f);
    ui::ScopedLayerAnimationSettings badge_settings(
        badge_container_->layer()->GetAnimator());
    badge_settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
    badge_settings.SetTransitionDuration(duration);
    badge_container_->layer()->SetOpacity(open_folder_ ? 1.0f : 0.0f);
  }

  // Animate the background size and shape - in the folder view UI, the icon
  // background (set for badge items only) is tearshaped, with bottom right
  // rounded corner smaller than other corners, and is larger than the icon
  // itself. Within the folder icon, the item icon does not have a background.
  // The animation makes the background bounds and shape match the icon bounds
  // and shape.
  if (item_in_folder_icon_ && icon_background_) {
    const gfx::Size background_size = icon_background_->layer()->size();
    const gfx::Rect collapsed_background = gfx::Rect(
        gfx::Point(
            std::round((background_size.width() - icon_size_.width()) / 2.0f),
            std::round((background_size.height() - icon_size_.height()) /
                       2.0f)),
        icon_size_);
    const gfx::Rect expanded_background = gfx::Rect(background_size);
    icon_background_->layer()->SetClipRect(open_folder_ ? collapsed_background
                                                        : expanded_background);

    const int background_diameter = background_size.width() / 2;
    const gfx::RoundedCornersF collapsed_rounded_corners(background_diameter);
    const gfx::RoundedCornersF expanded_rounded_corners(
        background_diameter, background_diameter,
        grid_->app_list_config()->GetShortcutHostBadgeIconContainerDimension() /
            2,
        background_diameter);
    icon_background_->layer()->SetRoundedCornerRadius(
        open_folder_ ? collapsed_rounded_corners : expanded_rounded_corners);

    ui::ScopedLayerAnimationSettings background_settings(
        icon_background_->layer()->GetAnimator());
    background_settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
    background_settings.SetTransitionDuration(duration);

    icon_background_->layer()->SetClipRect(open_folder_ ? expanded_background
                                                        : collapsed_background);
    icon_background_->layer()->SetRoundedCornerRadius(
        open_folder_ ? expanded_rounded_corners : collapsed_rounded_corners);
  }

  if (item_in_folder_icon_) {
    // Animate the opacity of the title.
    title_->layer()->SetOpacity(open_folder_ ? 0.0f : 1.0f);
    ui::ScopedLayerAnimationSettings title_settings(
        title_->layer()->GetAnimator());
    title_settings.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
    title_settings.SetTransitionDuration(duration);
    title_->layer()->SetOpacity(open_folder_ ? 1.0f : 0.0f);
  }
}

gfx::Size TopIconAnimationView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return gfx::Size(grid_->app_list_config()->grid_tile_width(),
                   grid_->app_list_config()->grid_tile_height());
}

void TopIconAnimationView::OnThemeChanged() {
  views::View::OnThemeChanged();

  if (icon_background_ && icon_background_->layer()) {
    icon_background_->layer()->SetColor(
        GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemOnBaseOpaque));
  }
}

void TopIconAnimationView::Layout(PassKey) {
  // This view's layout should be the same as AppListItemView's.
  gfx::Rect rect(GetContentsBounds());
  if (rect.IsEmpty())
    return;

  icon_->SetBoundsRect(AppListItemView::GetIconBoundsForTargetViewBounds(
      grid_->app_list_config(), rect, icon_->GetImage().size(),
      /*icon_scale=*/1.0f));
  if (icon_background_) {
    const int background_diameter =
        grid_->app_list_config()->GetShortcutBackgroundContainerDimension();
    icon_background_->SetBoundsRect(
        AppListItemView::GetIconBoundsForTargetViewBounds(
            grid_->app_list_config(), rect,
            gfx::Size(background_diameter, background_diameter),
            /*icon_scale=*/1.0f));
  }
  if (badge_container_) {
    CHECK(icon_background_);
    const int badge_container_diameter =
        grid_->app_list_config()->GetShortcutHostBadgeIconContainerDimension();
    badge_container_->SetBoundsRect(gfx::Rect(
        icon_background_->bounds().CenterPoint(),
        gfx::Size(badge_container_diameter, badge_container_diameter)));
  }
  title_->SetBoundsRect(AppListItemView::GetTitleBoundsForTargetViewBounds(
      grid_->app_list_config(), rect,
      title_->GetPreferredSize(views::SizeBounds(title_->width(), {})),
      /*icon_scale=*/1.0f));
}

void TopIconAnimationView::OnImplicitAnimationsCompleted() {
  SetVisible(false);
  for (auto& observer : observers_)
    observer.OnTopIconAnimationsComplete(this);
  DCHECK(parent());
  base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(
      FROM_HERE, parent()->RemoveChildViewT(this));
}

bool TopIconAnimationView::RequiresNotificationWhenAnimatorDestroyed() const {
  return true;
}

BEGIN_METADATA(TopIconAnimationView)
END_METADATA

}  // namespace ash