blob: 9c8905927053fa2084bca6df809597568d3f4424 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/holding_space/holding_space_tray_icon_preview.h"
#include "ash/public/cpp/holding_space/holding_space_constants.h"
#include "ash/public/cpp/holding_space/holding_space_image.h"
#include "ash/public/cpp/holding_space/holding_space_item.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/shelf/shelf.h"
#include "ash/system/holding_space/holding_space_tray_icon.h"
#include "ash/system/tray/tray_constants.h"
#include "base/i18n/rtl.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_sequence.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_skia_source.h"
#include "ui/gfx/skia_paint_util.h"
namespace ash {
namespace {
// The duration of each of the preview icon bounce animation.
constexpr base::TimeDelta kBounceAnimationSegmentDuration =
base::TimeDelta::FromMilliseconds(250);
// The delay with which preview icon is dropped into the holding space tray
// icon.
constexpr base::TimeDelta kBounceAnimationBaseDelay =
base::TimeDelta::FromMilliseconds(150);
// The duration of shift animation.
constexpr base::TimeDelta kShiftAnimationDuration =
base::TimeDelta::FromMilliseconds(250);
// Helpers ---------------------------------------------------------------------
// Returns the preview icon contents size.
gfx::Size GetPreviewSize() {
return gfx::Size(kTrayItemSize, kTrayItemSize);
}
// Returns whether the specified `shelf_alignment` is horizontal.
bool IsHorizontal(ShelfAlignment shelf_alignment) {
switch (shelf_alignment) {
case ShelfAlignment::kBottom:
case ShelfAlignment::kBottomLocked:
return true;
case ShelfAlignment::kLeft:
case ShelfAlignment::kRight:
return false;
}
}
// Performs set up of the specified `animation_settings`.
void SetUpAnimation(ui::ScopedLayerAnimationSettings* animation_settings) {
animation_settings->SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
animation_settings->SetTransitionDuration(
ShelfConfig::Get()->shelf_animation_duration());
animation_settings->SetTweenType(gfx::Tween::EASE_OUT);
}
// ContentsImageSource ---------------------------------------------------------
class ContentsImageSource : public gfx::ImageSkiaSource {
public:
explicit ContentsImageSource(gfx::ImageSkia item_image)
: item_image_(item_image) {}
ContentsImageSource(const ContentsImageSource&) = delete;
ContentsImageSource& operator=(const ContentsImageSource&) = delete;
~ContentsImageSource() override = default;
private:
// gfx::ImageSkiaSource:
gfx::ImageSkiaRep GetImageForScale(float scale) override {
gfx::ImageSkia image = item_image_;
// Crop to square (if necessary).
gfx::Size square_size = image.size();
square_size.SetToMin(gfx::Size(square_size.height(), square_size.width()));
if (image.size() != square_size) {
gfx::Rect square_rect(image.size());
square_rect.ClampToCenteredSize(square_size);
image = gfx::ImageSkiaOperations::ExtractSubset(image, square_rect);
}
// Resize to contents size (if necessary).
gfx::Size contents_size = GetPreviewSize();
if (image.size() != contents_size) {
image = gfx::ImageSkiaOperations::CreateResizedImage(
image, skia::ImageOperations::ResizeMethod::RESIZE_BEST,
contents_size);
}
// Clip to circle.
// NOTE: Since `image` has already been cropped to a square, the center
// x-coordinate, center y-coordinate, and radius all equal the same value.
const int radius = image.width() / 2;
gfx::Canvas canvas(image.size(), scale, /*is_opaque=*/false);
canvas.ClipPath(SkPath::Circle(/*cx=*/radius, /*cy=*/radius, radius),
/*anti_alias=*/true);
canvas.DrawImageInt(image, /*x=*/0, /*y=*/0);
return gfx::ImageSkiaRep(canvas.GetBitmap(), scale);
}
const gfx::ImageSkia item_image_;
};
} // namespace
// HoldingSpaceTrayIconPreview -------------------------------------------------
HoldingSpaceTrayIconPreview::HoldingSpaceTrayIconPreview(
Shelf* shelf,
views::View* container,
const HoldingSpaceItem* item)
: shelf_(shelf), container_(container), item_(item) {
contents_image_ = gfx::ImageSkia(
std::make_unique<ContentsImageSource>(item->image().image_skia()),
GetPreviewSize());
image_subscription_ =
item->image().AddImageSkiaChangedCallback(base::BindRepeating(
&HoldingSpaceTrayIconPreview::OnHoldingSpaceItemImageChanged,
base::Unretained(this)));
container_observer_.Observe(container_);
}
HoldingSpaceTrayIconPreview::~HoldingSpaceTrayIconPreview() = default;
void HoldingSpaceTrayIconPreview::AnimateIn(base::TimeDelta additional_delay) {
DCHECK(transform_.IsIdentity());
DCHECK(!index_.has_value());
DCHECK(pending_index_.has_value());
index_ = *pending_index_;
pending_index_.reset();
const gfx::Size preview_size = GetPreviewSize();
if (*index_ > 0u) {
gfx::Vector2dF translation(*index_ * preview_size.width() / 2, 0);
AdjustForShelfAlignmentAndTextDirection(&translation);
transform_.Translate(translation);
}
if (!NeedsLayer())
return;
gfx::Transform pre_transform;
pre_transform.Translate(transform_.To2dTranslation().x(),
-preview_size.height());
CreateLayer(pre_transform);
gfx::Transform mid_transform(transform_);
mid_transform.Translate(0, preview_size.height() * 0.25f);
ui::ScopedLayerAnimationSettings scoped_settings(layer_->GetAnimator());
scoped_settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
std::unique_ptr<ui::LayerAnimationSequence> sequence =
std::make_unique<ui::LayerAnimationSequence>();
sequence->AddElement(ui::LayerAnimationElement::CreatePauseElement(
ui::LayerAnimationElement::TRANSFORM,
kBounceAnimationBaseDelay + additional_delay));
std::unique_ptr<ui::LayerAnimationElement> initial_drop =
ui::LayerAnimationElement::CreateTransformElement(
mid_transform, kBounceAnimationSegmentDuration);
initial_drop->set_tween_type(gfx::Tween::EASE_OUT_4);
sequence->AddElement(std::move(initial_drop));
std::unique_ptr<ui::LayerAnimationElement> rebound =
ui::LayerAnimationElement::CreateTransformElement(
transform_, kBounceAnimationSegmentDuration);
rebound->set_tween_type(gfx::Tween::FAST_OUT_SLOW_IN_3);
sequence->AddElement(std::move(rebound));
layer_->GetAnimator()->StartAnimation(sequence.release());
}
void HoldingSpaceTrayIconPreview::AnimateOut(
base::OnceClosure animate_out_closure) {
animate_out_closure_ = std::move(animate_out_closure);
pending_index_.reset();
index_.reset();
if (!layer_) {
std::move(animate_out_closure_).Run();
return;
}
ui::ScopedLayerAnimationSettings animation_settings(layer_->GetAnimator());
SetUpAnimation(&animation_settings);
animation_settings.AddObserver(this);
layer_->SetOpacity(0.f);
layer_->SetVisible(false);
}
void HoldingSpaceTrayIconPreview::AnimateShift(base::TimeDelta delay) {
DCHECK(index_.has_value());
DCHECK(pending_index_.has_value());
index_ = *pending_index_;
pending_index_.reset();
if (!layer_ && NeedsLayer())
CreateLayer(transform_);
// Calculate the target preview transform for the new position in the icon.
// Avoid adjustments based on relative index change, as the current transform
// may not match the previous index in case the icon view has been resized
// since last update - see `AdjustTransformForContainerSizeChange()`.
transform_ = gfx::Transform();
gfx::Vector2dF translation(index_.value() * GetPreviewSize().width() / 2, 0);
AdjustForShelfAlignmentAndTextDirection(&translation);
transform_.Translate(translation);
if (!layer_)
return;
ui::ScopedLayerAnimationSettings scoped_settings(layer_->GetAnimator());
scoped_settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
std::unique_ptr<ui::LayerAnimationSequence> sequence =
std::make_unique<ui::LayerAnimationSequence>();
sequence->AddElement(ui::LayerAnimationElement::CreatePauseElement(
ui::LayerAnimationElement::TRANSFORM, delay));
std::unique_ptr<ui::LayerAnimationElement> shift =
ui::LayerAnimationElement::CreateTransformElement(
transform_, kShiftAnimationDuration);
shift->set_tween_type(gfx::Tween::FAST_OUT_SLOW_IN);
sequence->AddElement(std::move(shift));
layer_->GetAnimator()->StartAnimation(sequence.release());
}
void HoldingSpaceTrayIconPreview::AdjustTransformForContainerSizeChange(
const gfx::Vector2d& size_change) {
if (!index_.has_value())
return;
int direction = base::i18n::IsRTL() ? -1 : 1;
transform_.Translate(direction * size_change.x(), size_change.y());
if (layer()) {
// Update the layer transform. The current layer transform may be different
// from `transform_` if a transform animation is in progress, so calculate
// the new target transform using the current layer transform as the base.
gfx::Transform layer_transform = layer()->transform();
layer_transform.Translate(direction * size_change.x(), size_change.y());
layer()->SetTransform(layer_transform);
}
}
void HoldingSpaceTrayIconPreview::OnShelfAlignmentChanged(
ShelfAlignment old_shelf_alignment,
ShelfAlignment new_shelf_alignment) {
// If shelf orientation has not changed, no action needs to be taken.
if (IsHorizontal(old_shelf_alignment) == IsHorizontal(new_shelf_alignment))
return;
// Since shelf orientation has changed, the target `transform_` needs to be
// updated. First stop the current animation to immediately advance to target
// end values.
const auto& weak_ptr = weak_factory_.GetWeakPtr();
if (layer_ && layer_->GetAnimator()->is_animating())
layer_->GetAnimator()->StopAnimating();
// This instance may have been deleted as a result of stopping the current
// animation if it was in the process of animating out.
if (!weak_ptr)
return;
// Swap x-coordinate and y-coordinate of the target `transform_` since the
// shelf has changed orientation from horizontal to vertical or vice versa.
gfx::Vector2dF translation = transform_.To2dTranslation();
// In LTR, `translation` is always a positive offset. With a horizontal shelf,
// offset is relative to the parent layer's left bound while with a vertical
// shelf, offset is relative to the parent layer's top bound. In RTL, positive
// offset is still used for vertical shelf but with a horizontal shelf the
// `translation` is a negative offset from the parent layer's right bound. For
// this reason, a change in shelf orientation in RTL requires a negation of
// the current `translation`.
if (base::i18n::IsRTL())
translation = -translation;
gfx::Transform swapped_transform;
swapped_transform.Translate(translation.y(), translation.x());
transform_ = swapped_transform;
if (layer_) {
UpdateLayerBounds();
layer_->SetTransform(transform_);
}
}
// TODO(crbug.com/1142572): Support theming.
void HoldingSpaceTrayIconPreview::OnPaintLayer(
const ui::PaintContext& context) {
const gfx::Rect contents_bounds = gfx::Rect(GetPreviewSize());
ui::PaintRecorder recorder(context, contents_bounds.size());
gfx::Canvas* canvas = recorder.canvas();
// Background.
// NOTE: The background radius is shrunk by a single pixel to avoid being
// painted outside `contents_image_` bounds as might otherwise occur due to
// pixel rounding. Failure to do so could result in white paint artifacts.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(SK_ColorWHITE);
canvas->DrawCircle(
contents_bounds.CenterPoint(),
std::min(contents_bounds.width(), contents_bounds.height()) / 2 - 1,
flags);
// Contents.
if (!contents_image_.isNull()) {
canvas->DrawImageInt(contents_image_, contents_bounds.x(),
contents_bounds.y());
}
}
void HoldingSpaceTrayIconPreview::OnDeviceScaleFactorChanged(
float old_device_scale_factor,
float new_device_scale_factor) {
InvalidateLayer();
}
void HoldingSpaceTrayIconPreview::OnImplicitAnimationsCompleted() {
if (!NeedsLayer()) {
container_->layer()->Remove(layer_.get());
layer_.reset();
}
// NOTE: Running `animate_out_closure_` may delete `this`.
if (animate_out_closure_)
std::move(animate_out_closure_).Run();
}
void HoldingSpaceTrayIconPreview::OnViewBoundsChanged(views::View* view) {
DCHECK_EQ(container_, view);
if (layer_)
UpdateLayerBounds();
}
void HoldingSpaceTrayIconPreview::OnViewIsDeleting(views::View* view) {
DCHECK_EQ(container_, view);
container_observer_.Reset();
}
void HoldingSpaceTrayIconPreview::OnHoldingSpaceItemImageChanged() {
contents_image_ = gfx::ImageSkia(
std::make_unique<ContentsImageSource>(item_->image().image_skia()),
GetPreviewSize());
InvalidateLayer();
}
void HoldingSpaceTrayIconPreview::CreateLayer(
const gfx::Transform& initial_transform) {
DCHECK(!layer_);
layer_ = std::make_unique<ui::Layer>(ui::LAYER_TEXTURED);
layer_->SetFillsBoundsOpaquely(false);
layer_->SetTransform(initial_transform);
layer_->set_delegate(this);
UpdateLayerBounds();
container_->layer()->Add(layer_.get());
}
bool HoldingSpaceTrayIconPreview::NeedsLayer() const {
return index_ && *index_ <= kHoldingSpaceTrayIconMaxVisiblePreviews;
}
void HoldingSpaceTrayIconPreview::InvalidateLayer() {
if (layer_)
layer_->SchedulePaint(gfx::Rect(layer_->size()));
}
void HoldingSpaceTrayIconPreview::AdjustForShelfAlignmentAndTextDirection(
gfx::Vector2dF* vector_2df) {
if (!shelf_->IsHorizontalAlignment()) {
const float x = vector_2df->x();
vector_2df->set_x(vector_2df->y());
vector_2df->set_y(x);
return;
}
// With a horizontal shelf in RTL, translation is a negative offset relative
// to the parent layer's right bound. This requires negation of `vector_2df`.
if (base::i18n::IsRTL())
vector_2df->Scale(-1.f);
}
void HoldingSpaceTrayIconPreview::UpdateLayerBounds() {
DCHECK(layer_);
// With a horizontal shelf in RTL, `layer_` is aligned with its parent layer's
// right bound and translated with a negative offset. In all other cases,
// `layer_` is aligned with its parent layer's left/top bound and translated
// with a positive offset.
const gfx::Size size = GetPreviewSize();
gfx::Point origin;
if (shelf_->IsHorizontalAlignment() && base::i18n::IsRTL()) {
origin = container_->GetLocalBounds().top_right() -
gfx::Vector2d(size.width(), 0);
}
gfx::Rect bounds(origin, size);
if (bounds != layer_->bounds())
layer_->SetBounds(bounds);
}
} // namespace ash